Files
xrwise-datastructure/data-structure-block-coding.md
T
luzieahrens 8ecb8c23d1 adjust doc
2026-07-01 13:01:33 +02:00

11 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" 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

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,
  "toggle": true
}
Field Type Description
id string Unique block identifier
effectProp "color" | "position" | "rotation" | "scale" | "visibility" 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")
toggle boolean When true, each firing alternates between current state and target value instead of always applying the target. Not available for "visibility", and not shown/used when the parent script's trigger.kind is "source" with mode: "value"

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.

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

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 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 fields existed are missing trigger.kind (and may be missing other new fields). normalizeScript() in the panel fills in defaults for any missing/invalid field (defaulting trigger.kind to "scene"), so old saves keep loading without a migration step.
  • TypeScript types: defined inline in components/creator/graphical-coding/graphical-coding-panel.tsx as SceneTriggerBlock, SourceBlock, ScriptTrigger (their union), EffectBlock, and BlockScript.
  • Runtime status: as of this writing, only the creator's editing UI and data model support SourceBlock. The xrwise-viewer scene runtime (lib/block-runtime.ts) does not yet poll REST endpoints, evaluate condition/threshold, or apply incoming values to effects — that wiring is a follow-up.