diff --git a/data-structure-block-coding.md b/data-structure-block-coding.md index ea15ef8..5d7e36e 100644 --- a/data-structure-block-coding.md +++ b/data-structure-block-coding.md @@ -58,13 +58,15 @@ A script's head block can be one of two kinds: |---|---|---| | `kind` | `"scene"` | Discriminant identifying this as a scene-event trigger | | `id` | `string` | Unique block identifier | -| `event` | `"clicked" \| "proximity_enter" \| "proximity_exit" \| "looked_at"` | The interaction event to watch for | +| `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. --- @@ -170,6 +172,7 @@ All fields are always present regardless of `effectProp`. Fields that are irrele - 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. @@ -296,6 +299,49 @@ A "When entering proximity" trigger whose glow effect repeats every 500ms for 10 } ``` +### 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`: diff --git a/data-structure-creator.md b/data-structure-creator.md index 70e4f90..34971a2 100644 --- a/data-structure-creator.md +++ b/data-structure-creator.md @@ -48,6 +48,7 @@ type SceneItem = { // --- interactions --- grabable?: boolean + locked?: boolean // blocks move gizmo + deletion in the editor } ``` @@ -68,12 +69,14 @@ type SceneItem = { | `Camera` | `position`, `target` | `zoom`, `rotation` | | `Text` | `position`, `text` | `color`, `fontSize`, `fontWeight`, `dimensions`, `rotation` | | `Custom` | `position`, `file` | `dimensions`, `rotation` | +| `Area` | `position` | `dimensions` (X/Z footprint only — Y is fixed), `rotation` | ### Common optional field (all types) | Field | Type | Description | |---|---|---| | `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 +``` + +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) 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 | | `showLights` | `boolean` | `true` | ✅ | Light helper 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 | | `cameraRotation` | `number[]` | `[0,0,0]` | ✅ | Editor camera rotation | | `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 | +| `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 | | `isTyping` | `boolean` | `false` | — | Text input focus state | | `orbitControls` | `OrbitControlsImpl \| null` | `null` | — | Live OrbitControls 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 @@ -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]` | | `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` | — | +| `Area` | `[10,0.1,10]` | `#ffffff` | `[0,0,0]` | `0` | — | diff --git a/data-structure-database.md b/data-structure-database.md index aec5aa3..106c2c2 100644 --- a/data-structure-database.md +++ b/data-structure-database.md @@ -12,7 +12,9 @@ This document describes the JSON format saved to and loaded from the database. "skybox": "string | null", "sky-color": "#rrggbb", "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 | | `jsonversion` | `number` | Schema version (`2.0`) | | `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 | --- @@ -57,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": }`) or a nested group (`{ "kind": "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 ### Basic shapes @@ -96,6 +126,14 @@ All values are serialized as strings. | `Presentation` | `PresentationWall` | `file`, `widthcrop`, `heightcrop`, `controls` _(❌ not in Creator)_ | | `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 | Creator type | `resourcename` | `type` field | Custom args | @@ -118,6 +156,9 @@ All values are serialized as strings. | Arg | Type | Description | |---|---|---| | `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. --- diff --git a/objects/Area.md b/objects/Area.md new file mode 100644 index 0000000..e9db30d --- /dev/null +++ b/objects/Area.md @@ -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 +} +```