diff --git a/data-structure-graphical-coding.md b/data-structure-graphical-coding.md new file mode 100644 index 0000000..aef7958 --- /dev/null +++ b/data-structure-graphical-coding.md @@ -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--` | +| `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-1000000000", + "kind": "trigger", + "label": "On Click", + "position": { "x": 120, "y": 200 }, + "inputs": [], + "outputs": [ + { "id": "gc-1-1000000000-o0", "name": "fired", "dataType": "boolean" } + ], + "config": { + "sourceItemId": null, + "triggerEvent": "clicked", + "cooldown": 500 + } + }, + { + "id": "gc-2-2000000000", + "kind": "effect", + "label": "Skybox Color", + "position": { "x": 480, "y": 200 }, + "inputs": [ + { "id": "gc-2-2000000000-i0", "name": "trigger", "dataType": "boolean" } + ], + "outputs": [], + "config": { + "inputMode": "trigger", + "targetObjectType": "sky", + "targetItemId": null, + "effectProperty": "color", + "targetColor": "#3a0f8c" + } + } + ], + "connections": [ + { + "id": "conn-9999999999", + "fromNode": "gc-1-1000000000", + "fromPort": "gc-1-1000000000-o0", + "toNode": "gc-2-2000000000", + "toPort": "gc-2-2000000000-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.