Compare commits

...

10 Commits

Author SHA1 Message Date
luzieahrens 14de604ad2 update dcs 2026-07-04 09:03:58 +02:00
luzieahrens c73a82943f update doc 2026-07-03 12:41:07 +02:00
luzieahrens ec173c519e update 2026-07-03 10:02:06 +02:00
luzieahrens 5c3349dd28 update glow 2026-07-02 10:19:51 +02:00
luzieahrens 8ecb8c23d1 adjust doc 2026-07-01 13:01:33 +02:00
luzieahrens c3d487b095 describe data structure blocks 2026-07-01 09:49:09 +02:00
Luzie Ahrens 8354cd57a3 change node IDs 2026-06-29 18:17:43 +02:00
luzieahrens fed0fbe5bc Merge branch 'main' of https://github.com/XRwise/creator-datastructure 2026-06-29 18:15:13 +02:00
luzieahrens 6ee148c160 add graphical coding doc 2026-06-29 18:14:46 +02:00
TJ ec44709645 brightness/intensity fixed in unity 2026-06-11 16:12:17 +02:00
7 changed files with 830 additions and 20 deletions
+437
View File
@@ -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.
+58 -1
View File
@@ -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` | — |
+48 -1
View File
@@ -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 },
+226
View File
@@ -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.
+35
View File
@@ -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
}
```
+14 -10
View File
@@ -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 | | --------- | -------------------- | ------------------------------ | ------------------------------------------------------------------- |
| intensity | float | ⚠️ **See note below.** | Light intensity (default tbd). | | color | hexColor | ⚠️ (Creator tbd) | Color of the light |
| target | string `"x,y,z"` | (Unity only reads rotation) | World-space look-at point the light aims at | | intensity | float | | Light intensity (default 1). |
| grabable | `"true"` / `"false"` | ⚠️ | Whether the item can be grabbed in-experience (common to all types) | | 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) |
### 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:
@@ -25,13 +29,13 @@ Default spawn height is y=5.
{ {
"position": { "x": 0.0, "y": 5.0, "z": 0.0 }, "position": { "x": 0.0, "y": 5.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 },
"type": "pre-defined", "type": "pre-defined",
"resourcename": "Directional Light", "resourcename": "Directional Light",
"item-custom-args": [ "item-custom-args": [
{ "argument": "color", "value": "#ffffff" }, { "argument": "color", "value": "#ffffff" },
{ "argument": "intensity", "value": "20" }, { "argument": "intensity", "value": "20" },
{ "argument": "target", "value": "0,0,0" } { "argument": "target", "value": "0,0,0" }
], ],
"item-custom-args-adv": null "item-custom-args-adv": null
} }
+12 -8
View File
@@ -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 | | --------- | -------------------- | ---------------- | ------------------------------------------------------------------- |
| intensity | float | ⚠️ **See note below.** | Light intensity (default 1). | | color | hexColor | ⚠️ (Creator tbd) | Color of the light |
| grabable | `"true"` / `"false"` | | Whether the item can be grabbed in-experience (common to all types) | | intensity | float | | Light intensity (default 1). |
| 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:
@@ -23,11 +27,11 @@ Default spawn height is y=2.
{ {
"position": { "x": 0.0, "y": 2.0, "z": 0.0 }, "position": { "x": 0.0, "y": 2.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 },
"type": "pre-defined", "type": "pre-defined",
"resourcename": "Point Light", "resourcename": "Point Light",
"item-custom-args": [ "item-custom-args": [
{ "argument": "color", "value": "#ff0000" }, { "argument": "color", "value": "#ff0000" },
{ "argument": "intensity", "value": "50" } { "argument": "intensity", "value": "50" }
], ],
"item-custom-args-adv": null "item-custom-args-adv": null