12 KiB
Block Coding — Database Save Format
This document describes the JSON structure stored in the Supabase rooms table under the graphical_coding column for the block coding system (replacing the old node-graph format).
Top-level shape
{
"scripts": [ ...BlockScript ]
}
The entire block coding state is one JSON blob — a flat array of scripts. Each script is self-contained (trigger/source + effects) with no cross-script references.
BlockScript
{
"id": "bl-1-1234567890",
"trigger": { ...SceneTriggerBlock | SourceBlock },
"effects": [ ...EffectBlock ]
}
| Field | Type | Description |
|---|---|---|
id |
string |
Unique script identifier, format bl-<index>-<timestamp> |
trigger |
SceneTriggerBlock | SourceBlock |
The single head block at the top of the script. Discriminated by kind |
effects |
EffectBlock[] |
Ordered list of effect blocks stacked below the head block |
A script's head block can be one of two kinds:
SceneTriggerBlock(kind: "scene") — fires from a scene interaction (click, proximity).SourceBlock(kind: "source") — fires from, or streams a value from, a REST API endpoint.
SceneTriggerBlock
{
"kind": "scene",
"id": "bl-2-1234567890",
"event": "clicked",
"sourceItemId": 3,
"radius": 5,
"exitRadius": 5,
"cooldown": 500
}
| Field | Type | Description |
|---|---|---|
kind |
"scene" |
Discriminant identifying this as a scene-event trigger |
id |
string |
Unique block identifier |
event |
"clicked" | "proximity_enter" | "proximity_exit" | "looked_at" |
The interaction event to watch for |
sourceItemId |
number | null |
Scene item ID to watch (null = no item selected yet) |
radius |
number |
Proximity enter radius in metres (only used when event is "proximity_enter") |
exitRadius |
number |
Proximity exit radius in metres (only used when event is "proximity_exit") |
cooldown |
number |
Minimum milliseconds between consecutive firings |
"looked_at" fires when a visitor looks directly at sourceItemId — it reuses sourceItemId/cooldown like "clicked" and does not use radius/exitRadius. Effect blocks in a script headed by a "looked_at" trigger never show Toggle mode (see EffectBlock.toggle below).
SourceBlock
An alternative head block that connects to a REST API endpoint instead of a scene event. Depending on mode, it either fires the script like a trigger, or streams its fetched value into the script's EffectBlocks.
{
"kind": "source",
"id": "bl-2-1234567890",
"url": "https://api.example.com/sensor",
"mode": "trigger",
"condition": "received",
"threshold": 0
}
| Field | Type | Description |
|---|---|---|
kind |
"source" |
Discriminant identifying this as a REST API source |
id |
string |
Unique block identifier |
url |
string |
REST API endpoint to call |
mode |
"trigger" | "value" |
"trigger" fires the script's effects on a condition; "value" streams the fetched value into the effects instead |
condition |
"received" | ">" | "<" | "==" | "!=" | ">=" | "<=" |
Comparison applied to the fetched value when mode is "trigger". "received" fires on any response, ignoring threshold |
threshold |
number |
Value compared against the response when condition is not "received" |
When mode is "value", condition/threshold are stored but unused — every EffectBlock in the script is expected to use the fetched value in place of its own static target value (see Incoming values below).
EffectBlock
All fields are always present regardless of effectProp. Fields that are irrelevant to the chosen property are stored but ignored at runtime.
{
"id": "bl-3-1234567890",
"effectProp": "color",
"targetObjectType": "item",
"targetItemId": 7,
"targetColor": "#ff3366",
"positionAxis": "x",
"targetPositionValue": 0,
"rotationAxis": "x",
"targetRotationValue": 0,
"targetScale": 1,
"targetVisibility": true,
"targetGlowColor": "#ffffff",
"targetGlowIntensity": 1,
"toggle": true
}
| Field | Type | Description |
|---|---|---|
id |
string |
Unique block identifier |
effectProp |
"color" | "position" | "rotation" | "scale" | "visibility" | "glow" |
Which property this block modifies |
targetObjectType |
"item" | "sky" |
Whether to target a scene item or the sky color |
targetItemId |
number | null |
Scene item ID (only relevant when targetObjectType is "item") |
targetColor |
string |
Target hex color (used when effectProp is "color") |
positionAxis |
"x" | "y" | "z" |
Axis to move along (used when effectProp is "position") |
targetPositionValue |
number |
Target position on the chosen axis in world units |
rotationAxis |
"x" | "y" | "z" |
Axis to rotate around (used when effectProp is "rotation") |
targetRotationValue |
number |
Target rotation angle in degrees |
targetScale |
number |
Uniform scale multiplier (used when effectProp is "scale") |
targetVisibility |
boolean |
true = visible, false = hidden (used when effectProp is "visibility") |
targetGlowColor |
string |
Target glow hex color (used when effectProp is "glow"). Always a static value — never source-bound, similar to positionAxis/rotationAxis |
targetGlowIntensity |
number |
Glow intensity multiplier (used when effectProp is "glow"). This is the field replaced by an incoming Source value |
toggle |
boolean |
When true, each firing alternates between current state and target value instead of always applying the target. Not available for "visibility"; not shown/used when the parent script's trigger.kind is "source" with mode: "value"; and not shown when the parent script's trigger.kind is "scene" with event: "looked_at" |
EffectBlock has no field referencing a source — the binding is implicit via the parent script's head block. There is no per-effect opt-in; if a script's head is a SourceBlock in "value" mode, every effect in that script is driven by the incoming value. For multi-field effects (position, rotation, glow) only the single "value" field is replaced by the incoming value — the axis selector (positionAxis/rotationAxis) and the glow color (targetGlowColor) stay static/manually set.
Incoming values (Source, mode "value")
The expected shape of the REST response value depends on effectProp:
effectProp |
Expected response format |
|---|---|
color |
Hex color string, e.g. "#ff0000" or "ff0000" |
position |
Number, in meters, e.g. 2.5 |
rotation |
Number, in degrees, e.g. 90 |
scale |
Number, as a multiplier, e.g. 1.5 |
visibility |
Boolean or 0/1, e.g. true, false, 1, 0 |
glow |
Number, as a glow intensity multiplier, e.g. 1.5 (applies to targetGlowIntensity only — targetGlowColor stays static) |
This mapping is UI-only today (shown as a hint in the panel) — no runtime in xrwise-viewer currently polls SourceBlock.url or applies these values; see Persistence notes.
Full examples
Scene trigger
A script that changes a cube's color and moves it along X when a visitor clicks it, using toggle mode so each click alternates between states:
{
"scripts": [
{
"id": "bl-1-1700000000001",
"trigger": {
"kind": "scene",
"id": "bl-2-1700000000002",
"event": "clicked",
"sourceItemId": 4,
"radius": 5,
"exitRadius": 5,
"cooldown": 300
},
"effects": [
{
"id": "bl-3-1700000000003",
"effectProp": "color",
"targetObjectType": "item",
"targetItemId": 4,
"targetColor": "#c05580",
"positionAxis": "x",
"targetPositionValue": 0,
"rotationAxis": "x",
"targetRotationValue": 0,
"targetScale": 1,
"targetVisibility": true,
"toggle": true
},
{
"id": "bl-4-1700000000004",
"effectProp": "position",
"targetObjectType": "item",
"targetItemId": 4,
"targetColor": "#ffffff",
"positionAxis": "x",
"targetPositionValue": 3.0,
"rotationAxis": "x",
"targetRotationValue": 0,
"targetScale": 1,
"targetVisibility": true,
"toggle": true
}
]
}
]
}
REST API source — trigger mode
Fires the script's effects whenever the endpoint reports a value greater than 20:
{
"scripts": [
{
"id": "bl-5-1700000000005",
"trigger": {
"kind": "source",
"id": "bl-6-1700000000006",
"url": "https://api.example.com/temperature",
"mode": "trigger",
"condition": ">",
"threshold": 20
},
"effects": [
{
"id": "bl-7-1700000000007",
"effectProp": "visibility",
"targetObjectType": "item",
"targetItemId": 9,
"targetColor": "#ffffff",
"positionAxis": "x",
"targetPositionValue": 0,
"rotationAxis": "x",
"targetRotationValue": 0,
"targetScale": 1,
"targetVisibility": true,
"toggle": true
}
]
}
]
}
REST API source — value mode
Streams the endpoint's fetched value directly into the scale of item 12 (the static targetScale below is stored but ignored — the panel shows an "⚡ incoming value" badge in its place):
{
"scripts": [
{
"id": "bl-8-1700000000008",
"trigger": {
"kind": "source",
"id": "bl-9-1700000000009",
"url": "https://api.example.com/loudness",
"mode": "value",
"condition": "received",
"threshold": 0
},
"effects": [
{
"id": "bl-10-1700000000010",
"effectProp": "scale",
"targetObjectType": "item",
"targetItemId": 12,
"targetColor": "#ffffff",
"positionAxis": "x",
"targetPositionValue": 0,
"rotationAxis": "x",
"targetRotationValue": 0,
"targetScale": 1,
"targetVisibility": true,
"toggle": true
}
]
}
]
}
Persistence notes
- Column:
graphical_coding(JSONB) in the Supabaseroomstable — same column as the old node-graph format; the schema is distinguished by the presence ofscripts(block coding) vsnodes/connections(legacy). - Save: the Zustand
blockCodingstore value is JSON-stringified and sent asgraphical_codingin theFormDataofPATCH /api/save-room/[id], which writes it directly to thegraphical_codingcolumn. - Load:
roomData.graphical_codingis read increator.tsxand hydrated intosetBlockCoding(gc)in the Zustand store. - Local cache: Zustand's
persistmiddleware also writes the value tolocalStorageunder the key"scene-storage", so edits survive a page refresh before an explicit save. - Backward compatibility: scripts saved before the
SourceBlock/kindfields existed are missingtrigger.kind(and may be missing other new fields).normalizeScript()in the panel fills in defaults for any missing/invalid field (defaultingtrigger.kindto"scene"), so old saves keep loading without a migration step. - TypeScript types: defined inline in
components/creator/graphical-coding/graphical-coding-panel.tsxasSceneTriggerBlock,SourceBlock,ScriptTrigger(their union),EffectBlock, andBlockScript. - Runtime status: as of this writing, only the creator's editing UI and data model support
SourceBlock. Thexrwise-viewerscene runtime (lib/block-runtime.ts) does not yet poll REST endpoints, evaluatecondition/threshold, or apply incoming values to effects — that wiring is a follow-up.