Spec-Driven Geometry, State-Driven Views: An Editable Digital Twin in React Three Fiber
One shared store and one shared scene component mean the editor and the big screen cannot drift apart, and the pre-allocated `iot` slot on every device turns a static demo into a live data twin by swapping `heatValue`. Any team shipping an R3F scene that must be built once and shown many ways gets a directly reusable pattern.
One React Three Fiber scene serves a sidebar editor and a big-screen visualization of the same teaching building. Floors are procedural, not imported: `makeFloorPlan` generates a basement garage, ground-floor lobby, N standard floors, and a roof mechanical room from a single count, `stackFloors` computes each floor's `baseY`, and the `Tower` component maps that list to `FloorGroup`s. Devices are plain data in a Zustand store, each with a `floorId`, position, rotation, scale, and an `iot` binding that pre-allocates protocol (none | mqtt | modbus | opcua | http), endpoint, tag, and points.
All view modes ride the same store fields. Explode lifts above-ground floor `level` by `explode * (level - 1) * 4.5`, clipping hides floors above `clipLevel`, solo keeps one floor and lifts basements above ground so the ground plane doesn't block them, and heat coloring drops a semi-transparent plane on the slab. Devices obey the same rules, so they follow floors through every mode, and dragging intersects a horizontal plane at the device's own world Y, so nothing can be pulled onto a different elevation. Persistence is `GET/PUT /api/dt/scene` with a local-storage fallback, and both the editor and the `/screen` route call `initFromServer` on mount. When a real IoT platform arrives, filling `iot.points` and swapping `heatValue` moves the scene from demo hashes to live data; the 3D interaction layer doesn't change.
Making the building procedurally generated rather than imported turns editing a floor height or adding a standard floor into a data change; the geometry recalculates from the same spec, which is the entire trick of the twin.
One Zustand store plus a shared scene component is what keeps the editor and the big screen consistent; with no sync layer, disagreement is impossible by construction.
The pre-allocated `iot` slot with protocol, endpoint, tag, and points is the deliberate extension point; until a platform connects, the twin runs on demo hashes, and the switch to live data is designed to be a one-field swap.
Deterministic demo heat values trade away realism to keep comparisons stable; the same floor and metric show the same color every time, which is what makes debugging heat bands tolerable.
How long did this take?
It took 5 days.