Compare commits
10 Commits
0486ea4b63
...
14de604ad2
| Author | SHA1 | Date | |
|---|---|---|---|
| 14de604ad2 | |||
| c73a82943f | |||
| ec173c519e | |||
| 5c3349dd28 | |||
| 8ecb8c23d1 | |||
| c3d487b095 | |||
| 8354cd57a3 | |||
| fed0fbe5bc | |||
| 6ee148c160 | |||
| ec44709645 |
@@ -0,0 +1,437 @@
|
|||||||
|
# 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-<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`
|
||||||
|
|
||||||
|
```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" \| "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](./data-structure-database.md#logic)) 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 `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.
|
||||||
|
- 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](#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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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`:
|
||||||
|
|
||||||
|
```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.
|
||||||
@@ -48,6 +48,7 @@ type SceneItem = {
|
|||||||
|
|
||||||
// --- interactions ---
|
// --- interactions ---
|
||||||
grabable?: boolean
|
grabable?: boolean
|
||||||
|
locked?: boolean // blocks move gizmo + deletion in the editor
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -68,12 +69,14 @@ type SceneItem = {
|
|||||||
| `Camera` | `position`, `target` | `zoom`, `rotation` |
|
| `Camera` | `position`, `target` | `zoom`, `rotation` |
|
||||||
| `Text` | `position`, `text` | `color`, `fontSize`, `fontWeight`, `dimensions`, `rotation` |
|
| `Text` | `position`, `text` | `color`, `fontSize`, `fontWeight`, `dimensions`, `rotation` |
|
||||||
| `Custom` | `position`, `file` | `dimensions`, `rotation` |
|
| `Custom` | `position`, `file` | `dimensions`, `rotation` |
|
||||||
|
| `Area` | `position` | `dimensions` (X/Z footprint only — Y is fixed), `rotation` |
|
||||||
|
|
||||||
### Common optional field (all types)
|
### Common optional field (all types)
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `grabable` | `boolean` | Whether the item is interactable in-experience |
|
| `grabable` | `boolean` | Whether the item is interactable in-experience |
|
||||||
|
| `locked` | `boolean` | When `true`, hides the move gizmo for the item and blocks deletion until unlocked from the menubar |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -89,6 +92,27 @@ IDs are assigned sequentially when items are spawned and do not change.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## `SceneGroups`
|
||||||
|
|
||||||
|
Used by the Grouping feature to let several items (or nested groups) be selected, moved, and managed as one.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type GroupChild =
|
||||||
|
| { kind: "item"; id: number }
|
||||||
|
| { kind: "group"; id: number }
|
||||||
|
|
||||||
|
type SceneGroup = {
|
||||||
|
name: string
|
||||||
|
children: GroupChild[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type SceneGroups = Record<number, SceneGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
Group IDs are assigned sequentially from the store's `nextGroupId` counter, independent of item IDs. A group's `children` reference item/group IDs directly — there is no `groupId` field on `SceneItem` itself.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## `SceneStore` (Zustand)
|
## `SceneStore` (Zustand)
|
||||||
|
|
||||||
The complete editor state. Fields marked 💾 are persisted to `localStorage` under the key `scene-storage`.
|
The complete editor state. Fields marked 💾 are persisted to `localStorage` under the key `scene-storage`.
|
||||||
@@ -102,16 +126,48 @@ The complete editor state. Fields marked 💾 are persisted to `localStorage` un
|
|||||||
| `customItems` | `CustomItem[]` | `[]` | ✅ | User-uploaded GLB assets |
|
| `customItems` | `CustomItem[]` | `[]` | ✅ | User-uploaded GLB assets |
|
||||||
| `showLights` | `boolean` | `true` | ✅ | Light helper visibility |
|
| `showLights` | `boolean` | `true` | ✅ | Light helper visibility |
|
||||||
| `showWalls` | `boolean` | `true` | ✅ | Wall visibility |
|
| `showWalls` | `boolean` | `true` | ✅ | Wall visibility |
|
||||||
| `showDummy` | `boolean` | `false` | ✅ | Stickman dummy visibility |
|
| `showAreas` | `boolean` | `true` | ✅ | Area Collider visibility (Canvas Toggles) |
|
||||||
| `cameraTarget` | `number[]` | `[0,0,0]` | ✅ | OrbitControls look-at point |
|
| `cameraTarget` | `number[]` | `[0,0,0]` | ✅ | OrbitControls look-at point |
|
||||||
| `cameraRotation` | `number[]` | `[0,0,0]` | ✅ | Editor camera rotation |
|
| `cameraRotation` | `number[]` | `[0,0,0]` | ✅ | Editor camera rotation |
|
||||||
| `cameraQuaternion` | `[x,y,z,w]` | `[0,0,0,0]` | ✅ | Editor camera quaternion |
|
| `cameraQuaternion` | `[x,y,z,w]` | `[0,0,0,0]` | ✅ | Editor camera quaternion |
|
||||||
|
| `graphicalCoding` | `{ nodes, connections } \| null` | `null` | ✅ | Legacy node-graph coding state (superseded by `blockCoding`) |
|
||||||
|
| `blockCoding` | `{ scripts: unknown[] } \| null` | `null` | ✅ | Block Coding scripts — see [Block Coding data structure](./data-structure-block-coding.md) |
|
||||||
|
| `backgroundSound` | `string \| null` | `null` | ✅ | Uploaded ambient sound storage path (World Settings → Sound) |
|
||||||
|
| `groups` | `SceneGroups` | `{}` | ✅ | All item/group hierarchies |
|
||||||
|
| `nextGroupId` | `number` | `1` | ✅ | Next group ID to assign |
|
||||||
|
| `selectedGroup` | `number \| null` | `null` | — | Currently selected group ID |
|
||||||
|
| `multiSelectedItems` | `number[]` | `[]` | — | Item IDs in a multi-select (shift-click) |
|
||||||
|
| `multiSelectedGroups` | `number[]` | `[]` | — | Group IDs in a multi-select |
|
||||||
|
| `past` / `future` | `HistorySnapshot[]` | `[]` | — | Undo/redo stacks — see [Undo/Redo](#undoredo) below |
|
||||||
| `message` | `string \| null` | `null` | — | Transient toast message |
|
| `message` | `string \| null` | `null` | — | Transient toast message |
|
||||||
|
| `hovered` | `number \| null` | `null` | — | Item ID currently hovered in canvas or panel |
|
||||||
|
| `focusItem` | `number \| null` | `null` | — | Item ID the camera should focus on |
|
||||||
| `isDragging` | `boolean` | `false` | — | Pointer drag state |
|
| `isDragging` | `boolean` | `false` | — | Pointer drag state |
|
||||||
| `isTyping` | `boolean` | `false` | — | Text input focus state |
|
| `isTyping` | `boolean` | `false` | — | Text input focus state |
|
||||||
| `orbitControls` | `OrbitControlsImpl \| null` | `null` | — | Live OrbitControls ref |
|
| `orbitControls` | `OrbitControlsImpl \| null` | `null` | — | Live OrbitControls ref |
|
||||||
| `api` | `ScreenshotAPI \| null` | `null` | — | Screenshot renderer ref |
|
| `api` | `ScreenshotAPI \| null` | `null` | — | Screenshot renderer ref |
|
||||||
|
|
||||||
|
💾 reflects the `partialize` allowlist in `lib/SceneStore.ts` — `selectedGroup`, `multiSelectedItems`, `multiSelectedGroups`, `past`, `future`, and the transient/ref fields below them are intentionally excluded, so undo history and multi-selection do not survive a page refresh.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Undo/Redo
|
||||||
|
|
||||||
|
`past`/`future` hold up to `MAX_HISTORY` (50) `HistorySnapshot`s:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type HistorySnapshot = {
|
||||||
|
items: SceneItems
|
||||||
|
groups: SceneGroups
|
||||||
|
nextGroupId: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Mutations that go through `setItems`, `updateItem`, `batchUpdateItems`, or any group action (`createGroup`, `createGroupFromSelection`, `disbandGroup`, `renameGroup`) push the *pre-change* state onto `past` and clear `future`.
|
||||||
|
- Pushes are coalesced: rapid successive edits (e.g. dragging a gizmo) within 400ms of the last push are merged into one history entry, so undo steps back per gesture rather than per frame.
|
||||||
|
- `undo()`/`redo()` swap `items`/`groups`/`nextGroupId` between `past`/`future` and clear the current selection state (`selectedGroup`, `multiSelectedItems`, `multiSelectedGroups`).
|
||||||
|
- History is in-memory only — it is not part of `partialize` and does not round-trip through save/load or the database JSON.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## `ITEMS_CONFIG` spawn defaults
|
## `ITEMS_CONFIG` spawn defaults
|
||||||
@@ -137,3 +193,4 @@ When an item is added to the scene, it is initialized with these values from `it
|
|||||||
| `Camera` | `[1,1,1]` | `#444444` | `[0,0,0]` | `4` | `zoom: 1, target: [0,0,0]` |
|
| `Camera` | `[1,1,1]` | `#444444` | `[0,0,0]` | `4` | `zoom: 1, target: [0,0,0]` |
|
||||||
| `Text` | `[0.5,0.5,0.5]` | `black` | `[0,0,0]` | `2` | `text: "Text", fontWeight: "regular", fontSize: 5` |
|
| `Text` | `[0.5,0.5,0.5]` | `black` | `[0,0,0]` | `2` | `text: "Text", fontWeight: "regular", fontSize: 5` |
|
||||||
| `Custom` | `[1,1,1]` | `#ffffff` | `[0,0,0]` | `1` | — |
|
| `Custom` | `[1,1,1]` | `#ffffff` | `[0,0,0]` | `1` | — |
|
||||||
|
| `Area` | `[10,0.1,10]` | `#ffffff` | `[0,0,0]` | `0` | — |
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ This document describes the JSON format saved to and loaded from the database.
|
|||||||
"skybox": "string | null",
|
"skybox": "string | null",
|
||||||
"sky-color": "#rrggbb",
|
"sky-color": "#rrggbb",
|
||||||
"jsonversion": 2.0,
|
"jsonversion": 2.0,
|
||||||
"items": [ ...RoomItem ]
|
"items": [ ...RoomItem ],
|
||||||
|
"groups": { ...SceneGroups },
|
||||||
|
"background_sound": "string | null"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -23,6 +25,8 @@ This document describes the JSON format saved to and loaded from the database.
|
|||||||
| `sky-color` | `string` | Background hex color |
|
| `sky-color` | `string` | Background hex color |
|
||||||
| `jsonversion` | `number` | Schema version (`2.0`) |
|
| `jsonversion` | `number` | Schema version (`2.0`) |
|
||||||
| `items` | `RoomItem[]` | All objects in the room |
|
| `items` | `RoomItem[]` | All objects in the room |
|
||||||
|
| `groups` | `SceneGroups` (optional) | Item/group hierarchy created via the Grouping feature. Omitted entirely when there are no groups |
|
||||||
|
| `background_sound` | `string \| null` (optional) | Storage path of the uploaded ambient sound file, set from World Settings → Sound |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -32,6 +36,7 @@ Every item in the `items` array shares this base shape:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"id": 0,
|
||||||
"position": { "x": 0.0, "y": 0.0, "z": 0.0 },
|
"position": { "x": 0.0, "y": 0.0, "z": 0.0 },
|
||||||
"rotation": { "x": 0.0, "y": 0.0, "z": 0.0 },
|
"rotation": { "x": 0.0, "y": 0.0, "z": 0.0 },
|
||||||
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 },
|
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 },
|
||||||
@@ -44,6 +49,8 @@ Every item in the `items` array shares this base shape:
|
|||||||
|
|
||||||
> **Coordinate system note:** The X-axis is flipped on save (`x` = `-creator_x`) and un-flipped on load. Y-axis rotation is also negated.
|
> **Coordinate system note:** The X-axis is flipped on save (`x` = `-creator_x`) and un-flipped on load. Y-axis rotation is also negated.
|
||||||
|
|
||||||
|
> **`id`:** Stable numeric identifier assigned when the item is created in the Creator (the key of the in-memory `SceneItems` record). It is preserved across save/load rather than derived from array position, so group membership and block-coding references (`sourceItemId`/`targetItemId`) keep pointing at the right item even after items are added or removed. Rooms saved before this field existed fall back to array index on load.
|
||||||
|
|
||||||
### `ItemArg`
|
### `ItemArg`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -54,6 +61,32 @@ All values are serialized as strings.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## `SceneGroups`
|
||||||
|
|
||||||
|
Top-level `groups` field grouping items (and other groups) so they can be selected/moved together in the Creator. Keyed by numeric group ID, parallel to `items` being keyed by item ID.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"1": {
|
||||||
|
"name": "Group 1",
|
||||||
|
"children": [
|
||||||
|
{ "kind": "item", "id": 4 },
|
||||||
|
{ "kind": "item", "id": 7 },
|
||||||
|
{ "kind": "group", "id": 2 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `name` | `string` | Display name, editable in the Item List |
|
||||||
|
| `children` | `GroupChild[]` | Members of the group — either an item (`{ "kind": "item", "id": <RoomItem.id> }`) or a nested group (`{ "kind": "group", "id": <group id> }`) |
|
||||||
|
|
||||||
|
Group membership references stable `RoomItem.id`s (see the `id` note above), so groups keep pointing at the right items across save/load. The `groups` field is omitted from the export entirely when the room has no groups.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Per-type `resourcename` and custom args
|
## Per-type `resourcename` and custom args
|
||||||
|
|
||||||
### Basic shapes
|
### Basic shapes
|
||||||
@@ -93,6 +126,14 @@ All values are serialized as strings.
|
|||||||
| `Presentation` | `PresentationWall` | `file`, `widthcrop`, `heightcrop`, `controls` _(❌ not in Creator)_ |
|
| `Presentation` | `PresentationWall` | `file`, `widthcrop`, `heightcrop`, `controls` _(❌ not in Creator)_ |
|
||||||
| `Camera` | `CCTVCamera` | `target` (`"x,y,z"`), `fov` (derived: `zoom * 60`) |
|
| `Camera` | `CCTVCamera` | `target` (`"x,y,z"`), `fov` (derived: `zoom * 60`) |
|
||||||
|
|
||||||
|
### Logic
|
||||||
|
|
||||||
|
| Creator type | `resourcename` | Custom args |
|
||||||
|
|---|---|---|
|
||||||
|
| `Area` | `Area` | `color`, `opacity` _(serialized but not user-editable; the area is always rendered as an orange glow in the editor)_ |
|
||||||
|
|
||||||
|
> The Area Collider is an editor-only helper zone with no Unity-side visual — its footprint is defined by `scale.x`/`scale.z`. It is picked as the `sourceItemId` of an `"in_area"` Block Coding trigger (see [Block Coding data structure](./data-structure-block-coding.md)).
|
||||||
|
|
||||||
### Other / Props
|
### Other / Props
|
||||||
|
|
||||||
| Creator type | `resourcename` | `type` field | Custom args |
|
| Creator type | `resourcename` | `type` field | Custom args |
|
||||||
@@ -115,6 +156,9 @@ All values are serialized as strings.
|
|||||||
| Arg | Type | Description |
|
| Arg | Type | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `grabable` | `"true" \| "false"` | Whether the item can be grabbed in-experience |
|
| `grabable` | `"true" \| "false"` | Whether the item can be grabbed in-experience |
|
||||||
|
| `locked` | `"true" \| "false"` | Whether the item is locked against moving/deleting in the Creator. Editor-only concern — Unity/runtime does not read this arg |
|
||||||
|
|
||||||
|
> `grabable` and `locked` are both written by the shared `buildCustomArgs()` helper, so they are present on every type **except** `Text`, `Billboard`, and `Entrance`, which build their `item-custom-args` array by hand and currently omit both.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -122,6 +166,7 @@ All values are serialized as strings.
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"id": 0,
|
||||||
"position": { "x": 0, "y": 5, "z": 0 },
|
"position": { "x": 0, "y": 5, "z": 0 },
|
||||||
"rotation": { "x": 0, "y": 0, "z": 0 },
|
"rotation": { "x": 0, "y": 0, "z": 0 },
|
||||||
"scale": { "x": 1, "y": 1, "z": 1 },
|
"scale": { "x": 1, "y": 1, "z": 1 },
|
||||||
@@ -140,6 +185,7 @@ All values are serialized as strings.
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"id": 1,
|
||||||
"position": { "x": 0, "y": 4, "z": 0 },
|
"position": { "x": 0, "y": 4, "z": 0 },
|
||||||
"rotation": { "x": 0, "y": 0, "z": 0 },
|
"rotation": { "x": 0, "y": 0, "z": 0 },
|
||||||
"scale": { "x": 1, "y": 1, "z": 1 },
|
"scale": { "x": 1, "y": 1, "z": 1 },
|
||||||
@@ -157,6 +203,7 @@ All values are serialized as strings.
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"id": 2,
|
||||||
"position": { "x": -4.0, "y": 1.0, "z": 0.0 },
|
"position": { "x": -4.0, "y": 1.0, "z": 0.0 },
|
||||||
"rotation": { "x": 0.0, "y": 0.0, "z": 0.0 },
|
"rotation": { "x": 0.0, "y": 0.0, "z": 0.0 },
|
||||||
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 },
|
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 },
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
# Graphical Coding — Database Save Format
|
||||||
|
|
||||||
|
This document describes the JSON structure stored in the Supabase `rooms` table under the `graphical_coding` column.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Top-level shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [ ...GraphNode ],
|
||||||
|
"connections": [ ...Connection ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The entire graph is serialized as a single JSON blob — no normalization, no references across rows.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `GraphNode`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "gc-1-1234567890",
|
||||||
|
"kind": "source | trigger | effect",
|
||||||
|
"label": "My Node",
|
||||||
|
"position": { "x": 100, "y": 200 },
|
||||||
|
"inputs": [ ...PortDef ],
|
||||||
|
"outputs": [ ...PortDef ],
|
||||||
|
"config": { ...kind-specific config }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `id` | `string` | Unique node identifier, format `gc-<index>-<timestamp>` |
|
||||||
|
| `kind` | `"source" \| "trigger" \| "effect"` | Node category |
|
||||||
|
| `label` | `string` | User-visible name |
|
||||||
|
| `position` | `{ x: number, y: number }` | Canvas position in pixels |
|
||||||
|
| `inputs` | `PortDef[]` | Input ports |
|
||||||
|
| `outputs` | `PortDef[]` | Output ports |
|
||||||
|
| `config` | `object` | Kind-specific configuration (see below) |
|
||||||
|
|
||||||
|
### `PortDef`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "id": "gc-1-1234567890", "name": "fired", "dataType": "boolean" }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `id` | `string` | Port identifier (shares format with node IDs) |
|
||||||
|
| `name` | `string` | Display name, e.g. `"fired"`, `"value"`, `"trigger"` |
|
||||||
|
| `dataType` | `string` | `"number"`, `"string"`, `"boolean"`, `"object"`, or `"any"` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `Connection`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "conn-1234567890",
|
||||||
|
"fromNode": "gc-1-111",
|
||||||
|
"fromPort": "gc-1-111-o0",
|
||||||
|
"toNode": "gc-3-333",
|
||||||
|
"toPort": "gc-3-333-i0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `id` | `string` | Unique connection identifier |
|
||||||
|
| `fromNode` | `string` | Source node ID |
|
||||||
|
| `fromPort` | `string` | Source port ID (output port) |
|
||||||
|
| `toNode` | `string` | Target node ID |
|
||||||
|
| `toPort` | `string` | Target port ID (input port) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Node `config` by kind
|
||||||
|
|
||||||
|
### `source` — REST API data feed
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"endpoint": "https://api.example.com/sensor",
|
||||||
|
"condition": ">",
|
||||||
|
"threshold": 42
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `endpoint` | `string` | REST API URL polled for data |
|
||||||
|
| `condition` | `">" \| "<" \| "==" \| "!=" \| ">=" \| "<="` | Comparison operator applied to the API response value |
|
||||||
|
| `threshold` | `number` | Value compared against the API response |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `trigger` — User interaction in the scene
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sourceItemId": 7,
|
||||||
|
"triggerEvent": "clicked",
|
||||||
|
"cooldown": 500
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `sourceItemId` | `number \| null` | Scene item ID to watch (`null` = any item) |
|
||||||
|
| `triggerEvent` | `"clicked" \| "proximity_enter" \| "proximity_exit"` | Interaction type |
|
||||||
|
| `cooldown` | `number` | Minimum milliseconds between trigger firings |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `effect` — Scene modification
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"inputMode": "trigger",
|
||||||
|
"targetObjectType": "item",
|
||||||
|
"targetItemId": 3,
|
||||||
|
"effectProperty": "color",
|
||||||
|
"targetColor": "#ff0000"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `inputMode` | `"trigger" \| "stream"` | Whether the effect fires on a trigger pulse or follows a continuous stream value |
|
||||||
|
| `targetObjectType` | `"item" \| "sky"` | What to modify |
|
||||||
|
| `targetItemId` | `number \| null` | Scene item ID (only relevant when `targetObjectType` is `"item"`) |
|
||||||
|
| `effectProperty` | `"color" \| "position" \| "rotation" \| "scale" \| "visibility"` | Which property to change |
|
||||||
|
|
||||||
|
Additional fields depend on `effectProperty`:
|
||||||
|
|
||||||
|
**`color`**
|
||||||
|
```json
|
||||||
|
{ "targetColor": "#ffffff" }
|
||||||
|
```
|
||||||
|
|
||||||
|
**`position`**
|
||||||
|
```json
|
||||||
|
{ "positionAxis": "x", "targetPositionValue": 2.5 }
|
||||||
|
```
|
||||||
|
|
||||||
|
**`rotation`**
|
||||||
|
```json
|
||||||
|
{ "rotationAxis": "y", "targetRotationValue": 90 }
|
||||||
|
```
|
||||||
|
`targetRotationValue` is in degrees.
|
||||||
|
|
||||||
|
**`scale`**
|
||||||
|
```json
|
||||||
|
{ "targetScale": 2.0 }
|
||||||
|
```
|
||||||
|
|
||||||
|
**`visibility`**
|
||||||
|
```json
|
||||||
|
{ "targetVisibility": false }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Full example
|
||||||
|
|
||||||
|
A graph with one trigger and one effect, connected:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "gc-1",
|
||||||
|
"kind": "trigger",
|
||||||
|
"label": "On Click",
|
||||||
|
"position": { "x": 120, "y": 200 },
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{ "id": "gc-1-o0", "name": "fired", "dataType": "boolean" }
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"sourceItemId": null,
|
||||||
|
"triggerEvent": "clicked",
|
||||||
|
"cooldown": 500
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gc-2",
|
||||||
|
"kind": "effect",
|
||||||
|
"label": "Skybox Color",
|
||||||
|
"position": { "x": 480, "y": 200 },
|
||||||
|
"inputs": [
|
||||||
|
{ "id": "gc-2-i0", "name": "trigger", "dataType": "boolean" }
|
||||||
|
],
|
||||||
|
"outputs": [],
|
||||||
|
"config": {
|
||||||
|
"inputMode": "trigger",
|
||||||
|
"targetObjectType": "sky",
|
||||||
|
"targetItemId": null,
|
||||||
|
"effectProperty": "color",
|
||||||
|
"targetColor": "#3a0f8c"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": [
|
||||||
|
{
|
||||||
|
"id": "conn-9",
|
||||||
|
"fromNode": "gc-1",
|
||||||
|
"fromPort": "gc-1-o0",
|
||||||
|
"toNode": "gc-2",
|
||||||
|
"toPort": "gc-2-i0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Persistence notes
|
||||||
|
|
||||||
|
- **Column:** `graphical_coding` (JSONB) in the Supabase `rooms` table. Not yet reflected in `supabase-types.ts`.
|
||||||
|
- **Save:** triggered manually via the "Save Project" button; the whole object is JSON-stringified and sent as a `FormData` field to `PATCH /api/save-room/[id]`.
|
||||||
|
- **Load:** the value is read from `roomData.graphical_coding` in `creator.tsx` and hydrated into the Zustand `SceneStore`.
|
||||||
|
- **Local cache:** Zustand's `persist` middleware also writes the graph to `localStorage` under the key `"scene-storage"`, so edits survive a page refresh before an explicit save.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Area
|
||||||
|
|
||||||
|
### Type
|
||||||
|
pre-defined
|
||||||
|
|
||||||
|
### 3D
|
||||||
|
The Area Collider is an editor-only helper zone, not a runtime-visible object. In the Creator it renders as a flat plane that glows orange so its footprint is visible while editing; it has no representation in Unity/the published room. Its footprint is `scale.x` x `scale.z` (default `10 x 10`); `scale.y` is fixed at a thin `0.1` and not editable.
|
||||||
|
|
||||||
|
### Custom Args:
|
||||||
|
| Key | Type | Implemented | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| color | hexColor | ❌ | Serialized (spawn default `#ffffff`) but not user-editable and not used for rendering — the area is always shown with the fixed orange glow |
|
||||||
|
| grabable | `"true" \| "false"` | ✔️ | Whether the item can be grabbed in-experience (inherited from the shared item args; not meaningful since the area has no runtime presence) |
|
||||||
|
| locked | `"true" \| "false"` | ✔️ | Whether the item is locked against moving/deleting in the Creator |
|
||||||
|
|
||||||
|
### Special Notes:
|
||||||
|
- Category `Logic` in the Creator sidebar — helper items that mark trigger zones for Block Coding rather than decorating the room.
|
||||||
|
- Hidden in the editor canvas via the "Areas" Canvas Toggle (`showAreas` in the scene store) without affecting the saved data.
|
||||||
|
- Referenced by the Block Coding `"in_area"` trigger event (`SceneTriggerBlock.sourceItemId`) — see [data-structure-block-coding.md](../data-structure-block-coding.md).
|
||||||
|
|
||||||
|
### Example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"position": { "x": 0.0, "y": 0.0, "z": 0.0 },
|
||||||
|
"rotation": { "x": 0.0, "y": 0.0, "z": 0.0 },
|
||||||
|
"scale": { "x": 10.0, "y": 0.1, "z": 10.0 },
|
||||||
|
"type": "pre-defined",
|
||||||
|
"resourcename": "Area",
|
||||||
|
"item-custom-args": [
|
||||||
|
{ "argument": "color", "value": "#ffffff" }
|
||||||
|
],
|
||||||
|
"item-custom-args-adv": null
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -1,23 +1,27 @@
|
|||||||
# Directional Light
|
# Directional Light
|
||||||
|
|
||||||
### Type
|
### Type
|
||||||
|
|
||||||
pre-defined
|
pre-defined
|
||||||
|
|
||||||
### 3D
|
### 3D
|
||||||
|
|
||||||
No visible mesh. Represents a scene-wide directional light source (like the sun).
|
No visible mesh. Represents a scene-wide directional light source (like the sun).
|
||||||
The `target` arg defines the direction the light points toward.
|
The `target` arg defines the direction the light points toward.
|
||||||
Default spawn height is y=5.
|
Default spawn height is y=5.
|
||||||
|
|
||||||
### Custom Args:
|
### Custom Args:
|
||||||
|
|
||||||
| Key | Type | Implemented | Description |
|
| Key | Type | Implemented | Description |
|
||||||
|---|---|---|---|
|
| --------- | -------------------- | ------------------------------ | ------------------------------------------------------------------- |
|
||||||
| color | hexColor | ⚠️ (Creator tbd) | Color of the light |
|
| color | hexColor | ⚠️ (Creator tbd) | Color of the light |
|
||||||
| intensity | float | ⚠️ **See note below.** | Light intensity (default tbd). |
|
| intensity | float | ✔️ | Light intensity (default 1). |
|
||||||
| target | string `"x,y,z"` | ⚠️ (Unity only reads rotation) | World-space look-at point the light aims at |
|
| target | string `"x,y,z"` | ⚠️ (Unity only reads rotation) | World-space look-at point the light aims at |
|
||||||
| grabable | `"true"` / `"false"` | ⚠️ | Whether the item can be grabbed in-experience (common to all types) |
|
| grabable | `"true"` / `"false"` | ⚠️ | Whether the item can be grabbed in-experience (common to all types) |
|
||||||
|
|
||||||
### Special Notes:
|
### Special Notes:
|
||||||
**Key name discrepancy:** The database save format uses `intensity` as the argument key, but `LightManupulationBehaviour.cs` reads `brightness`. Light intensity args may not apply at runtime unless this is resolved.
|
|
||||||
|
**Key name discrepancy:** The database save format uses `intensity` as the argument key. `LightManupulationBehaviour.cs` read `brightness` and "intensity".
|
||||||
|
|
||||||
### Example:
|
### Example:
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
# Point Light
|
# Point Light
|
||||||
|
|
||||||
### Type
|
### Type
|
||||||
|
|
||||||
pre-defined
|
pre-defined
|
||||||
|
|
||||||
### 3D
|
### 3D
|
||||||
|
|
||||||
No visible mesh. Represents an omnidirectional point light source that emits light equally in all directions.
|
No visible mesh. Represents an omnidirectional point light source that emits light equally in all directions.
|
||||||
Default spawn height is y=2.
|
Default spawn height is y=2.
|
||||||
|
|
||||||
### Custom Args:
|
### Custom Args:
|
||||||
|
|
||||||
| Key | Type | Implemented | Description |
|
| Key | Type | Implemented | Description |
|
||||||
|---|---|---|---|
|
| --------- | -------------------- | ---------------- | ------------------------------------------------------------------- |
|
||||||
| color | hexColor | ⚠️ (Creator tbd) | Color of the light |
|
| color | hexColor | ⚠️ (Creator tbd) | Color of the light |
|
||||||
| intensity | float | ⚠️ **See note below.** | Light intensity (default 1). |
|
| intensity | float | ✔️ | Light intensity (default 1). |
|
||||||
| grabable | `"true"` / `"false"` | ⚠️ | Whether the item can be grabbed in-experience (common to all types) |
|
| grabable | `"true"` / `"false"` | ⚠️ | Whether the item can be grabbed in-experience (common to all types) |
|
||||||
|
|
||||||
### Special Notes:
|
### Special Notes:
|
||||||
**Key name discrepancy:** The database save format uses `intensity` as the argument key, but `PointLightItemBehaviour.cs` reads `brightness`. Light intensity args may not apply at runtime unless this is resolved.
|
|
||||||
|
**Key name discrepancy:** The database save format uses `intensity` as the argument key. `LightManupulationBehaviour.cs` read `brightness` and "intensity".
|
||||||
|
|
||||||
### Example:
|
### Example:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user