docs(wiki): vertical-model architecture page; drop stale SlabSystem rows (#528)
Documents the shipped vertical building model (#526): stored truth table, resolution helpers, clamp rules, pointer-decided placement, the load migration that lives in migrateNodes indefinitely, and the gotchas. The systems tables stop listing the deleted SlabSystem and point at the registry geometry path instead. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
adc1ec89f4
commit
de13119f86
@@ -21,6 +21,7 @@ Canonical rules for code that touches `packages/core`, `packages/viewer`, `packa
|
||||
| [scene-registry](scene-registry.md) | Global node ID → Object3D map and `useRegistry` |
|
||||
| [spatial-queries](spatial-queries.md) | Placement validation (`canPlaceOnFloor`/`Wall`/`Ceiling`) for tools |
|
||||
| [node-schemas](node-schemas.md) | Zod schema pattern for node types, `createNode`, `updateNode` |
|
||||
| [vertical-model](vertical-model.md) | Stored level heights, plane-bound wall/ceiling tops, slab placement + thickness, support hosts, clamp rules, and the load migration |
|
||||
| [events](events.md) | Typed event bus — emitting and listening to node and grid events |
|
||||
| [creating-rules](creating-rules.md) | How to add or update a page in this folder |
|
||||
|
||||
|
||||
@@ -17,13 +17,14 @@ Pure logic: no rendering, no Three.js objects. They read nodes from `useScene`,
|
||||
| System | Responsibility |
|
||||
|---|---|
|
||||
| `WallSystem` | Wall mitering, corner joints |
|
||||
| `SlabSystem` | Polygon-based floor/roof generation |
|
||||
| `CeilingSystem` | Polygon-based ceiling generation |
|
||||
| `RoofSystem` | Pitched roof shape |
|
||||
| `DoorSystem` | Placement constraints on walls |
|
||||
| `WindowSystem` | Placement constraints on walls |
|
||||
| `ItemSystem` | Item transforms, collision |
|
||||
|
||||
Slab geometry has no dedicated system: it renders through the registry `def.geometry` (`packages/nodes/src/slab/geometry.ts`, calling the pure generators in `packages/viewer/src/systems/slab/slab-system.tsx`) with a small `def.system` for dirty tracking.
|
||||
|
||||
### Viewer Systems — `packages/viewer/src/systems/`
|
||||
|
||||
Access Three.js objects (via `useRegistry`) and manage rendering side-effects.
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Vertical Model
|
||||
|
||||
*How buildings stack: stored level heights, plane-bound wall/ceiling tops, slab placement + thickness, support hosts, and the clamp rules that keep it all coherent.*
|
||||
|
||||
Applies to: anything that reads or writes vertical geometry — levels, walls, slabs, ceilings, stairs, fences, floor-placed items.
|
||||
|
||||
The invariant, in one sentence:
|
||||
|
||||
> Wall tops are pinned to the level plane; floors and platforms move what stands on
|
||||
> them, never the walls or the storey above; anything that doesn't fit is clamped,
|
||||
> never asked.
|
||||
|
||||
**Sources**: `packages/core/src/services/storey.ts`, `packages/core/src/systems/wall/wall-top.ts`, `packages/core/src/systems/slab/slab-support.ts`, `packages/core/src/systems/stair/stair-rise.ts`, `packages/core/src/store/use-scene.ts` (migration Pass 3)
|
||||
|
||||
## Stored truth
|
||||
|
||||
| Field | Meaning | Absent means |
|
||||
|---|---|---|
|
||||
| `level.height` | Storey height in meters, floor-to-floor. Level world Y = per-building prefix sum of stored heights, ordered by the `level` ordinal (`getLevelElevations`). | Unmigrated legacy data (never seen post-load; the migration writes it). Consumers fall back to `DEFAULT_LEVEL_HEIGHT` (2.5). |
|
||||
| `wall.height` | Explicit custom height (half wall, parapet). Top = elected base + height. | **Plane-bound** (the default): the top follows `getWallPlaneTop` — `min(level height, lowest covering-slab underside over the span)`. Slabs lift only the base. |
|
||||
| `ceiling.height` | Explicit custom height, write-clamped to the bound. | **Follows the level**: resolves live to `getCeilingClampBound` = `min(level height, covering underside) − 0.01`. |
|
||||
| `slab.elevation` | The walking surface (top), level-local. | Default 0.05. |
|
||||
| `slab.thickness` | Grows **downward**: the solid occupies `[elevation − thickness, elevation]`. | Default 0.05. |
|
||||
| `slab.recessed` | Pool intent: open shell, floor at (negative) `elevation`, inner walls up to the plane. Excluded from "covering" queries and wall-face adoption. | Solid slab. |
|
||||
| `supportSlabId` | Persisted support host on walls and all floor-placed kinds. Written at commit **only when overlapping supports disagree on elevation**; `'ground'` sentinel pins bare ground under a deck. | Support is elected per query (coverage election for walls, footprint max for items). |
|
||||
| `stair.deckSlabId` | Destination deck: rise follows `deck.elevation − the stair's own elected base` live; cutout sync disabled while attached. | Destination is a level. |
|
||||
| `stair.totalRise` | Explicit custom rise (wins over everything). | Follows: derived from the deck or the containing level; `syncStairRises` converges straight-stair segments to the resolved rise. |
|
||||
|
||||
Two schema rules protect these semantics:
|
||||
|
||||
- **No Zod defaults on meaning-bearing fields.** `level.height`, `wall.height`, `ceiling.height`, `stair.totalRise` are `.optional()` with no `.default()` — absence is data. Creation sites write values explicitly; `migrateNodes` output is cast, not parsed, so a schema default would never materialize on legacy load anyway.
|
||||
- **The store deletes explicit-`undefined` keys.** `updateNode(id, { height: undefined })` removes the key (see `mergeNodeUpdate` in `node-actions.ts`); that is how "Follows level/deck" mode switches work. UI mode controls derive state from field presence — no persisted mode enums.
|
||||
|
||||
## Resolution helpers (use these, never `?? 2.5`)
|
||||
|
||||
| Helper | Home | Resolves |
|
||||
|---|---|---|
|
||||
| `getStoredLevelHeight`, `getLevelElevations`, `getLevelAbove/Below` | `services/storey.ts` | Level heights, per-building stacking, neighbors |
|
||||
| `getWallPlaneTop` | `services/storey.ts` | A plane-bound wall's top: level height clamped to covering-slab undersides, span-sampled with boundary-inclusive band overlap |
|
||||
| `resolveWallTop`, `resolveWallEffectiveHeight`, `MIN_WALL_HEIGHT` | `systems/wall/wall-top.ts` | A wall's top / effective height given plane + elected base |
|
||||
| `getWallEffectiveHeightForNodes` | spatial-grid manager | The above with the real slab election, for UI overlays |
|
||||
| `getCeilingClampBound`, `getCoveringSlabUndersideAt` | `services/storey.ts` | Ceiling bound; the cross-level covering query (level above, non-recessed slabs) |
|
||||
| `resolveCeilingHeight` | `services/level-height.ts` | A ceiling's effective height (explicit or follows) |
|
||||
| `resolveStairTotalRise`, `syncStairRises` | `systems/stair/stair-rise.ts` | Stair rise precedence + straight-flight convergence |
|
||||
| `computeWallSlabSupport`, `getSlabSupportForItem`, `getSupportCandidatesForFootprint` | `systems/slab/slab-support.ts` + spatial-grid manager | Support election (rendered polygons, host-preferring, optional `maxElevation` cap) |
|
||||
| `clampSlabElevationForWalls`, `applySlabTopChange`, `SLAB_UNSTICK_THRESHOLD` | slab-support + `nodes/slab/elevation-limit.ts` | Slab edit clamps and the adaptive drag/panel rules |
|
||||
|
||||
## Clamp rules (clamp, never ask)
|
||||
|
||||
- A slab under plane-bound walls clamps its elevation to `level height − MIN_WALL_HEIGHT` (0.5).
|
||||
- Ceilings clamp (at write time, and reactively downward via space-detection) to `min(level top, covering-slab underside) − 0.01`.
|
||||
- Plane-bound wall tops clamp to covering-slab undersides — a thick or flush upper-level slab shortens the walls below it (Revit's attach-to-floor-bottom, automatic). Explicit-height walls are exempt.
|
||||
- Slab vertical editing is adaptive: the panel moves placement (thickness untouched); the viewport drag stretches a grounded slab (elevation and thickness together) up to `SLAB_UNSTICK_THRESHOLD` (0.4), then unsticks it into a 0.05-thick deck; floating decks move with thickness preserved and re-ground at underside 0; pools keep the drag-through-zero gesture.
|
||||
- Wall-face adoption in `getRenderableSlabPolygon` applies only to grounded slabs (`elevation − thickness ≤ 0.01`, not recessed) — floating decks keep their drawn polygon and are skipped as seam candidates.
|
||||
|
||||
## Pointer-decided placement
|
||||
|
||||
Grid events intersect a plane that rides the ghost's elevation, so any stacked-surface decision must come from the true camera ray, not the plane hit: `getPointedSupportSurface` returns the nearest slab plane the ray crosses inside its rendered polygon plus the crossing point, and both the support-election cap (`maxElevation`) and the cursor XZ derive from that single computation. Pointing under a deck elects the floor; pointing at the deck top elects the deck; commits persist the capped winner (or `'ground'`). 2D floorplan placement has no camera ray and keeps max-election.
|
||||
|
||||
## Load migration (lives in `migrateNodes` Pass 3, indefinitely)
|
||||
|
||||
Because community autosave only persists after the first post-load edit, the migration must remain in `migrateNodes`:
|
||||
|
||||
- Writes each legacy level's **exact** derived height (a default legacy storey stores 2.55 = 0.05 slab + 2.5 wall) — never snapped to presets.
|
||||
- Compacts `level` ordinals per building, anchored at zero (non-negatives → 0,1,2…; negatives → −1,−2… — basements stay basements). Runs every load; idempotent.
|
||||
- Classifies wall tops against the derived plane: `|plane − top| < 0.20` **strict** → plane-bound (height key removed); else explicit (materializing 2.5 on absent-height short walls). ε calibrated by a prod census: intentional 0.20-short walls exist and must not snap.
|
||||
- Ceilings within ε of the bound (and all `autoFromWalls` ceilings) drop their height → follows mode; stairs drop the legacy blind `totalRise: 2.5`. Both gated on the scene being legacy (some level lacked `height`).
|
||||
- Slabs get `thickness := elevation` (byte-identical occupied interval, including degenerate zero); negative-elevation pools become `recessed: true` with elevation unchanged.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Ordinals are semantic.** `level < 0` renders "Basement N"; `level === 0` is the ground-floor lookup. Never renumber without the zero anchor.
|
||||
- **Boundary geometry.** Auto slabs derive polygons from wall centerlines, so wall/ceiling clamp samples sit exactly on polygon edges — always use the boundary-inclusive band-overlap helpers (`wallOverlapsSlabFootprint`, `slabCoversPoint`), never raw ray-cast point-in-polygon on those paths.
|
||||
- **Straight stairs build from stored segment heights**, not the resolved rise — any rise change must go through `syncStairRises` (applied by `StairOpeningSystem`, history-paused, one microtask after store updates so the spatial grid has settled).
|
||||
- **Reactivity is explicit.** A `level.height` change dirties that level's walls/stairs/ceilings/fences; a slab change dirties the level below's walls/ceilings and deck-attached stairs (`spatial-grid-sync.ts`). If a new consumer reads these bounds, wire its dirty rule there.
|
||||
- **Host lifecycle.** Deleting a slab strips `supportSlabId`/`deckSlabId` from survivors in the same undo commit; a host merely reshaped away falls back silently and resumes if the slab returns.
|
||||
- **Clone paths differ.** `clone-scene-graph.ts` remaps `supportSlabId`/`deckSlabId`; the editor clipboard (`scene-clipboard.ts`) intentionally does not (it re-elects); room placement remaps them (fixed in the private repo's `room-placement.ts`). When adding a new clone/instantiation path, remap both fields.
|
||||
|
||||
## Deferred by decision (see the private repo's plan archive)
|
||||
|
||||
Persistent Room identity, partial-storey navigation, slab reference-face enums, suspended ceilings, and a site datum for sloped terrain all have named gates in `plans/` — none block this model. Decks ship as catalog rooms/presets; the one-gesture mezzanine/balcony tools were removed (code preserved at editor `e30042db`).
|
||||
Reference in New Issue
Block a user