Scene Manipulation
A compiled map ships a node manifest: one deterministic id/name/path entry per mesh-bearing node, with the pivot and bounds the compiler baked. The scene.* commands act on that manifest at runtime, so a node can be turned off, hidden or removed while the world is running — no recompile, no relaunch.
Every one of these commands is server-authoritative and gated on world authority (the server console, the embedded host, or an admin). A plain remote client is rejected. Mutations flow through the world’s scene editor, which destroys or rebuilds the node’s baked physics colliders and replicates the new full state to every client.
Commands
Section titled “Commands”| Command | What it does |
|---|---|
scene.hierarchy [depth] | Lists the manifest down to depth path levels: node id, path, effective state, and live collider count (so a visual-only node is visible as such). |
scene.edits | Lists every node not in its default state. |
scene.select [target] | Selects a node, so clients draw the pivot gizmo on it. No target clears the selection. |
scene.disable <target> | Hides the node and makes it non-solid — its baked colliders are destroyed. |
scene.enable <target> | The reverse: shows the node and rebuilds its colliders. Also overrides an authored dh.removeObject removal for the session. |
scene.hide <target> | Hides the node visually only. Its colliders stay. See Hide versus disable. |
scene.show <target> | Reverses a visual hide. |
scene.delete <target> | Hides the node, destroys its colliders, and marks it not re-enableable this session. |
scene.save | Writes this map’s edits to the engine’s saved-delta store. See Persistence. |
scene.forget | Discards the saved delta, leaving the live session alone. |
scene.promote [path.dh-map] | Turns the session’s deleted nodes into authored dh.removeObject patches. See Promotion. |
A <target> is a manifest id (5), an exact path (buildings/block03/wall_north), or a name (wall_north). A name shared by several nodes is ambiguous and is rejected with the candidates listed — use the path.
Hide versus disable
Section titled “Hide versus disable”The two look identical on screen and are deliberately different underneath:
scene.disablemakes the node non-solid. Its colliders are destroyed on the server and excluded from the client’s prediction world. You can walk through it.scene.hideonly makes the node invisible. The renderer skips its geometry; physics never hears about it. You still collide with it.
Hiding is the tool for looking behind something without changing how the level plays. Because the two states are distinct, scene.show will not silently rescue a disabled node — it says so and points at scene.enable.
The renderer implements the skip by rebuilding the map mesh’s index buffer from the always-retained per-node geometry ranges, so a hidden node costs nothing to draw. The rebuild happens only when the hidden set actually changes.
The pivot gizmo
Section titled “The pivot gizmo”scene.select marks one node as the manipulation target. Every client draws a pivot gizmo at that node’s manifest pivot: three colored translate arms (red +X, green +Y, blue +Z) and three rotate rings, in the same visual vocabulary as the corner axis gizmo — dark halos under every stroke, depth as a shade rather than an opacity, drawn back to front.
Unlike the corner gizmo, this one is world-anchored: it is projected through the frame’s own view-projection, so it foreshortens, shrinks with distance, and bends with the Panini warp exactly as the geometry it annotates does. That is why its arm length is specified in meters, not pixels.
Selection is replicated, not client-local: it rides the same message the edits do, so everyone watching sees the same node called out. It is a pure view concept — selecting changes nothing about how a node renders or collides.
| Preference | Type | Default | What it controls |
|---|---|---|---|
client.scene.pivotGizmo | bool | true | Whether the selected node’s pivot gizmo draws at all. |
client.scene.pivotGizmoArm | float (meters) | 1 | Translate arm length. Tune it to the scale of the map you are authoring. |
client.scene.pivotGizmoThickness | float (pixels) | 2.5 | Arm stroke width. |
client.scene.pivotGizmoRings | bool | true | Whether the rotate rings draw alongside the arms. |
Persistence
Section titled “Persistence”Runtime edits are session-only until you ask for them to be kept. scene.save writes them to a per-map JSON document under the engine’s own user-data root — never your workspace. A saved delta is derived state about someone else’s map; keeping it out of the workspace means the workspace stays exactly what you put there, and a delta can be discarded at any time without touching a source file.
Only disabled, hidden and deleted persist. An enabled override is session-scoped by design (it un-does the map’s own authored baseline, which the map is free to change under the save), and an authored removal is the map’s to state, not the save’s.
Saving with nothing edited removes the file, so “save an empty edit set” and “forget” are the same gesture.
The manifest hash guard
Section titled “The manifest hash guard”Each save records the hash of the node manifest it was taken against, and every entry carries both the node id and the node’s path and name. On replay:
- Hash matches — the manifest is identical, so every saved id still means what it meant. Entries apply by id, exactly.
- Hash differs — the map was rebuilt and an id may now name different geometry. Ids are distrusted entirely; every entry re-resolves by its saved path, then by its name.
An entry that resolves to nothing (or ambiguously) costs one warning and is skipped. A mismatch is never an error and never aborts a map load — the same warn-and-skip semantics an authored dh.removeObject gets for a stale target. Refusing the whole save because one wall was renamed would break the feature exactly when a map is being iterated on, which is when it is used.
Promotion
Section titled “Promotion”scene.promote is the one-way step from “remembered by the engine” to “authored by the map”: it writes dh.removeObject entries into a source .dh-map’s patches array, so a promoted removal is indistinguishable from a hand-written one and survives a recompile.
- Only deleted nodes promote.
deletedis a statement of intent, which is exactly whatdh.removeObjectmeans.disabledandhiddenare working states — something toggled to look behind — and the map patch vocabulary has no entry that means either one. - The path is explicit. A compiled pallet does not record where its source lives, so the command will not guess: run it with no path to print the exact JSON it would add, and pass the
.dh-mappath when you want it written. - A commented source is refused, not rewritten. A
.dh-mapmay carry comments and a JSON round-trip cannot preserve them, so a file containing comments gets the paste-ready JSON in the console instead of a silent edit.
Recompile the pallet after a promotion for the change to take effect.