16 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 + optional loop + effects) with no cross-script references.
BlockScript
{
"id": "bl-1-1234567890",
"trigger": { ...SceneTriggerBlock | SourceBlock },
"loop": null,
"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 |
loop |
LoopBlock | null |
Optional block between the head and the effects, controlling how many times they fire. null = fires once per head firing (default) |
effects |
EffectBlock[] |
Ordered list of effect blocks stacked below the head block (and below loop, if present) |
A script's head block can be one of two kinds:
SceneTriggerBlock(kind: "scene") — fires from a scene interaction (click, proximity, gaze).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
}
| 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") |
"looked_at" fires when a visitor looks directly at sourceItemId — it reuses sourceItemId like "clicked" and does not use radius/exitRadius. There is no user-configurable cooldown field anymore; the panel no longer exposes it and a fixed ~100ms minimum delay between firings is assumed for when the runtime is implemented.
Effect blocks in a script headed by a "looked_at" trigger — and only those with effectProp: "visibility" — additionally show Toggle mode (normally hidden for "visibility"); 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).
LoopBlock
An optional block that sits between the head block and the effects list. Instead of applying the effects once per head firing, it repeats them.
{
"id": "bl-11-1234567890",
"kind": "seconds",
"durationSeconds": 5,
"times": 3,
"pauseMs": 500
}
| Field | Type | Description |
|---|---|---|
id |
string |
Unique block identifier |
kind |
"seconds" | "times" | "forever" |
Which repeat mode is active, chosen via a dropdown on the block |
durationSeconds |
number |
How long to keep repeating, in seconds (used when kind is "seconds") |
times |
number |
Fixed number of repetitions (used when kind is "times") |
pauseMs |
number |
Delay in milliseconds between each repetition, used for all three kinds |
All three fields (durationSeconds, times, pauseMs) are always present regardless of kind — the ones not relevant to the active kind are stored but ignored, same convention as EffectBlock.
When a script has a loop, every EffectBlock in it has its toggle forced to true and the Toggle mode control is hidden in the panel — see EffectBlock.toggle 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. See toggle visibility rules below |
When Toggle mode is shown/used in the panel (all conditions independent, evaluated per effect):
- Hidden for
effectProp: "visibility"— except when the parent script'strigger.kindis"scene"withevent: "looked_at", where it is shown and means: the effect activates while the item is looked at, and deactivates when the visitor looks away. - Hidden whenever the parent script's
trigger.kindis"source"withmode: "value"(the effect is driven by the incoming value instead). - Hidden whenever the parent script has a non-null
loop— in that casetoggleis forced totrueon every effect in the script instead of being user-controlled. - For
"proximity_enter"/"proximity_exit"triggers, Toggle mode is shown with a mode-specific meaning (activates on enter/deactivates on leave, or vice versa) rather than the generic "alternate on each firing" explanation.
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
},
"loop": null,
"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,
"targetGlowColor": "#ffffff",
"targetGlowIntensity": 1,
"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,
"targetGlowColor": "#ffffff",
"targetGlowIntensity": 1,
"toggle": true
}
]
}
]
}
Scene trigger with a Repeat block
A "When entering proximity" trigger whose glow effect repeats every 500ms for 10 seconds once a visitor enters range. toggle is forced true because the script has a loop:
{
"scripts": [
{
"id": "bl-11-1700000000011",
"trigger": {
"kind": "scene",
"id": "bl-12-1700000000012",
"event": "proximity_enter",
"sourceItemId": 4,
"radius": 5,
"exitRadius": 5
},
"loop": {
"id": "bl-13-1700000000013",
"kind": "seconds",
"durationSeconds": 10,
"times": 3,
"pauseMs": 500
},
"effects": [
{
"id": "bl-14-1700000000014",
"effectProp": "glow",
"targetObjectType": "item",
"targetItemId": 4,
"targetColor": "#ffffff",
"positionAxis": "x",
"targetPositionValue": 0,
"rotationAxis": "x",
"targetRotationValue": 0,
"targetScale": 1,
"targetVisibility": true,
"targetGlowColor": "#ffcc00",
"targetGlowIntensity": 2,
"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
},
"loop": null,
"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,
"targetGlowColor": "#ffffff",
"targetGlowIntensity": 1,
"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
},
"loop": null,
"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,
"targetGlowColor": "#ffffff",
"targetGlowIntensity": 1,
"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/kind/loop/glow/looked_atfields existed are missing them.normalizeScript()in the panel fills in defaults for any missing/invalid field (defaultingtrigger.kindto"scene",looptonull, glow fields to white/1), so old saves keep loading without a migration step. A straycooldownfield from older saves is simply ignored — it is no longer part ofSceneTriggerBlock. - TypeScript types: defined inline in
components/creator/graphical-coding/graphical-coding-panel.tsxasSceneTriggerBlock,SourceBlock,ScriptTrigger(their union),LoopBlock,EffectBlock, andBlockScript. - Runtime status: as of this writing, only the creator's editing UI and data model support
SourceBlockandLoopBlock. Thexrwise-viewerscene runtime (lib/block-runtime.ts) does not yet poll REST endpoints, evaluatecondition/threshold, repeat effects perloop, or apply incoming values to effects — that wiring is a follow-up.