Files
xrwise-datastructure/data-structure-block-coding.md
T
luzieahrens 14de604ad2 update dcs
2026-07-04 09:03:58 +02:00

18 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" | "in_area" 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.

"in_area" ("When in area") fires when a visitor enters/leaves the footprint of an Area Collider item (Creator type Area, see Database data structure) referenced by sourceItemId. It does not use radius/exitRadius — the area's own footprint (scale.x/scale.z) defines the trigger zone. The block coding panel's item picker restricts the choices to Area-type items when this event is selected, but sourceItemId is a plain item ID like any other trigger — the data shape itself does not enforce the item's type.

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'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.
  • For "in_area" triggers, Toggle mode is shown with the meaning: the effect activates when the visitor enters the area, and deactivates when they leave it.

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
        }
      ]
    }
  ]
}

Area trigger

A "When in area" trigger that reveals an item (visibility effect) while a visitor is standing inside Area Collider item 5, using Toggle mode so it hides again on exit:

{
  "scripts": [
    {
      "id": "bl-15-1700000000015",
      "trigger": {
        "kind": "scene",
        "id": "bl-16-1700000000016",
        "event": "in_area",
        "sourceItemId": 5,
        "radius": 5,
        "exitRadius": 5
      },
      "loop": null,
      "effects": [
        {
          "id": "bl-17-1700000000017",
          "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
        }
      ]
    }
  ]
}

radius/exitRadius are stored (every SceneTriggerBlock always carries all fields, same convention as EffectBlock) but ignored for "in_area" — the Area item's own footprint is what defines the zone.

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 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.