# 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 ```json { "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` ```json { "id": "bl-1-1234567890", "trigger": { ...SceneTriggerBlock | SourceBlock }, "loop": null, "effects": [ ...EffectBlock ] } ``` | Field | Type | Description | |---|---|---| | `id` | `string` | Unique script identifier, format `bl--` | | `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` ```json { "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 `EffectBlock`s. ```json { "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](#incoming-values-source-mode--value) 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. ```json { "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. ```json { "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's `trigger.kind` is `"scene"` with `event: "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.kind` is `"source"` with `mode: "value"` (the effect is driven by the incoming value instead). - Hidden whenever the parent script has a non-null `loop` — in that case `toggle` is forced to `true` on 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](#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: ```json { "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`: ```json { "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`: ```json { "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): ```json { "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 Supabase `rooms` table — same column as the old node-graph format; the schema is distinguished by the presence of `scripts` (block coding) vs `nodes`/`connections` (legacy). - **Save:** the Zustand `blockCoding` store value is JSON-stringified and sent as `graphical_coding` in the `FormData` of `PATCH /api/save-room/[id]`, which writes it directly to the `graphical_coding` column. - **Load:** `roomData.graphical_coding` is read in `creator.tsx` and hydrated into `setBlockCoding(gc)` in the Zustand store. - **Local cache:** Zustand's `persist` middleware also writes the value to `localStorage` under 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_at` fields existed are missing them. `normalizeScript()` in the panel fills in defaults for any missing/invalid field (defaulting `trigger.kind` to `"scene"`, `loop` to `null`, glow fields to white/`1`), so old saves keep loading without a migration step. A stray `cooldown` field from older saves is simply ignored — it is no longer part of `SceneTriggerBlock`. - **TypeScript types:** defined inline in `components/creator/graphical-coding/graphical-coding-panel.tsx` as `SceneTriggerBlock`, `SourceBlock`, `ScriptTrigger` (their union), `LoopBlock`, `EffectBlock`, and `BlockScript`. - **Runtime status:** as of this writing, only the creator's editing UI and data model support `SourceBlock` and `LoopBlock`. The `xrwise-viewer` scene runtime (`lib/block-runtime.ts`) does not yet poll REST endpoints, evaluate `condition`/`threshold`, repeat effects per `loop`, or apply incoming values to effects — that wiring is a follow-up.