This commit is contained in:
luzieahrens
2026-07-03 10:02:06 +02:00
parent 5c3349dd28
commit ec173c519e
+106 -13
View File
@@ -12,7 +12,7 @@ This document describes the JSON structure stored in the Supabase `rooms` table
}
```
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.
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.
---
@@ -22,6 +22,7 @@ The entire block coding state is one JSON blob — a flat array of scripts. Each
{
"id": "bl-1-1234567890",
"trigger": { ...SceneTriggerBlock | SourceBlock },
"loop": null,
"effects": [ ...EffectBlock ]
}
```
@@ -30,11 +31,12 @@ The entire block coding state is one JSON blob — a flat array of scripts. Each
|---|---|---|
| `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 |
| `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).
- **`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.
---
@@ -48,8 +50,7 @@ A script's head block can be one of two kinds:
"event": "clicked",
"sourceItemId": 3,
"radius": 5,
"exitRadius": 5,
"cooldown": 500
"exitRadius": 5
}
```
@@ -61,9 +62,10 @@ A script's head block can be one of two kinds:
| `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).
`"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.
---
@@ -95,6 +97,34 @@ When `mode` is `"value"`, `condition`/`threshold` are stored but unused — ever
---
## `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.
@@ -133,7 +163,13 @@ All fields are always present regardless of `effectProp`. Fields that are irrele
| `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"` |
| `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.
@@ -171,9 +207,9 @@ A script that changes a cube's color and moves it along X when a visitor clicks
"event": "clicked",
"sourceItemId": 4,
"radius": 5,
"exitRadius": 5,
"cooldown": 300
"exitRadius": 5
},
"loop": null,
"effects": [
{
"id": "bl-3-1700000000003",
@@ -187,6 +223,8 @@ A script that changes a cube's color and moves it along X when a visitor clicks
"targetRotationValue": 0,
"targetScale": 1,
"targetVisibility": true,
"targetGlowColor": "#ffffff",
"targetGlowIntensity": 1,
"toggle": true
},
{
@@ -201,6 +239,55 @@ A script that changes a cube's color and moves it along X when a visitor clicks
"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
}
]
@@ -226,6 +313,7 @@ Fires the script's effects whenever the endpoint reports a value greater than `2
"condition": ">",
"threshold": 20
},
"loop": null,
"effects": [
{
"id": "bl-7-1700000000007",
@@ -239,6 +327,8 @@ Fires the script's effects whenever the endpoint reports a value greater than `2
"targetRotationValue": 0,
"targetScale": 1,
"targetVisibility": true,
"targetGlowColor": "#ffffff",
"targetGlowIntensity": 1,
"toggle": true
}
]
@@ -264,6 +354,7 @@ Streams the endpoint's fetched value directly into the scale of item `12` (the s
"condition": "received",
"threshold": 0
},
"loop": null,
"effects": [
{
"id": "bl-10-1700000000010",
@@ -277,6 +368,8 @@ Streams the endpoint's fetched value directly into the scale of item `12` (the s
"targetRotationValue": 0,
"targetScale": 1,
"targetVisibility": true,
"targetGlowColor": "#ffffff",
"targetGlowIntensity": 1,
"toggle": true
}
]
@@ -293,6 +386,6 @@ Streams the endpoint's fetched value directly into the scale of item `12` (the s
- **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.
- **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.