feat: vertical building model — stored level heights, wall inversion, decks#526
Merged
Conversation
…le + level height schema Extract pointInPolygon/computeWallSlabSupport and friends into a cycle-free packages/core/src/systems/slab/slab-support.ts (severs level-height -> spatial-grid-manager -> use-scene), add deriveLegacyLevelHeight as the pure mesh-free equivalent of the viewer's stacked level height, and add the optional LevelNode.height field plus the storey service (getStoredLevelHeight, getLevelElevations per-building prefix sums). No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each floating-level-selector row shows its storey height; clicking opens a popover with 2.5/3.0/3.5 presets and a free slider writing level.height. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ompaction, wall-top classification Pass 3 in migrateNodes: derive and store each legacy level's exact stacked height (never snapped), compact ordinals per building anchored at zero so basements stay basements, classify wall tops against the derived plane (|plane - top| < 0.20 strictly -> plane-bound, else explicit height materialized), and drop the blind totalRise 2.5 stair default on legacy scenes only. Epsilon and strictness validated by a prod census. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ly vertical truth Wall-top inversion: a wall without an explicit height now tops out at its storey plane (resolveWallTop); slabs lift only the base. Window/door caps resolve the real top through the same slab election instead of Infinity. All level stacking (viewer, elevator, first-person, stair openings, MCP scene queries) reads stored LevelNode.height; the four divergent live derivations and level.metadata.height are deleted. Stair totalRise becomes optional and derives from the storey height when absent. MCP create-level stops writing its elevation param into the ordinal. Level creation sites write explicit heights; templates carry their true derived heights. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fallbacks; wall Top control Slab elevation writes clamp to plane − MIN_WALL_HEIGHT when plane-bound walls elect the slab (pure clampSlabElevationForWalls + registry handle bounds + shared panels for 2D/3D parity); ceiling heights clamp under the plane and auto-ceilings derive from resolved wall tops. Every remaining wall.height ?? 2.5 fallback resolves through resolveWallTop / resolveWallEffectiveHeight (panels, overlays, measurements, quantities, spatial grid, MCP reports); template walls matching their storey become plane-bound. Wall panel gains a Top control (Follows storey / Custom height) derived purely from height presence; the store update path now deletes keys passed as explicit undefined so plane-binding round-trips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n, rendered-polygon unification Floor-placed nodes and walls gain a nullable supportSlabId. Elections surface the winning slab (getSlabSupportForItem, candidates query) and prefer a still-valid persisted host, falling back silently when the host is gone or reshaped away; deleting the host strips references in the same undo commit. Item-side support now tests the rendered slab polygon (like walls) through a per-level cache invalidated by the spatial-grid sync. Also adds the resolveStairTotalRise unit tests from the stage-1 gates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erywhere Floor-placed commits (draft pipeline, per-kind creation tools, registry move tool) and wall create/move/endpoint commits persist supportSlabId via shared resolveSupportSlabPatch helpers — only when overlapping supports disagree on elevation, clearing it otherwise or when the node leaves the floor. WallSlabSupport surfaces electedSlabId; every wall support read site passes wall.supportSlabId as the preferred host. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cess intent slab.elevation stays the walking surface; new thickness grows downward so the solid occupies [elevation − thickness, elevation]. Migration writes thickness := elevation for solids (byte-identical intervals, including degenerate zero) and recessed: true for legacy negative pools. Geometry branches on recessed instead of the elevation sign; presets keep today's intervals; free elevation edits move the body without coupling thickness (the deck semantic). Dead viewer SlabSystem component deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
getLevelAbove + getCoveringSlabUndersideAt give the first cross-level query; ceiling writes clamp to min(storey plane, lowest covering underside) − 0.01, and the space-detection reconcile now clamps manual ceilings down (never up) when a deck above intrudes — a flush deck reactively lowers the ceiling below it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One-gesture composites over the kernel: draw a deck footprint and commit deck slab + railings + stair (mezzanine) or deck + railings (balcony) in a single undo step. Fences gain supportSlabId and lift onto their host deck; railing runs split around the stair mouth; edges near wall centerlines are treated as closed. Stairs target the deck via explicit totalRise with no level-to-level opening sync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dragging a grounded slab's top stretches it (elevation and thickness move together — gaps impossible); floating decks move with thickness preserved and land grounded at zero; pools keep the drag-through-zero gesture. User-facing copy says level, not storey (Follows level, Level height). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rtical reactivity getWallPlaneTop samples the wall span against the level above's slabs, so a thick or flush upper floor shortens the walls below it instead of colliding (automatic attach, no dialog). Level-height edits now dirty the level's walls, stairs, ceilings, and fences; covering-slab changes dirty the level below. The effective-height helper triplicated across editor overlays moved to core. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ceiling.height becomes optional — absent means the ceiling tracks min(level top, covering slab underside) − 0.01 live, so level-height edits no longer require ceiling fixes. Ceiling panel gains the same Follows level / Custom height control as walls; auto-from-walls ceilings are created height-less and their height-derivation machinery is deleted; migration drops stored heights within 0.20 of the bound on legacy scenes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Auto slabs derive their polygon from wall centerlines, so covering-clamp samples sat exactly on the boundary where ray-cast point-in-polygon is side-dependent (min edges in, max edges out) — walls clamped or not by orientation. getWallPlaneTop now clips the wall's thickness band against the covering polygon (boundary-inclusive, arc-aware) and the ceiling bound's point sampling gained an explicit on-boundary test. Verified against the reported repro scene. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stairs gain deckSlabId — rise follows the deck's elevation live (straight flights re-converge via a write-sync mirroring auto-openings), the panel shows a unified To destination with Follows deck / Custom rise, and the mezzanine tool attaches instead of baking a stale rise. Item placement under an elevated deck no longer flickers: grid events fed a feedback loop (the grid plane rode the ghost's elected height), so the support election is now capped at the surface the pointer ray actually hits, with a ground sentinel keeping under-deck commits deterministic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Decks ship as catalog presets instead; the kernel the presets rely on (fence deck-hosting, stair deck attachment, clone remaps, pointer-decided placement) all stays. The tool code lives at e30042d for reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…es then unsticks Floating decks keep their drawn polygon (and stop being seam candidates for grounded neighbors) instead of growing into nearby walls. Panel elevation edits are pure placement; the viewport drag stretches a grounded slab up to 0.4 m then unsticks it into a thin deck. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rise now subtracts the stair's own elected base (same election the visual lift uses), so base + rise always equals the deck walking surface; the auto-sync defers a microtask so it reads a settled spatial grid and re-converges on both deck and base-slab moves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wall and fence draw tools now publish the pointed surface, so the draw plane rides the deck top (no more perspective-skewed floor hits) and previews sit on the deck. Fences gain real support election: a pure resolveFenceSupportSlabPatch persists the deck host at draw and reshape commits; wall commits thread the pointer cap so aiming under a deck elects the floor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Level, ceiling, and slab preset buttons show clean values per display system (8/9/10 ft storeys, 8'-9' ceilings, whole-inch slab steps) instead of converted metric labels; metric presets unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s the cutout syncStairRises now converges every follows-mode straight stair (level or deck) plus deck-attached custom rises — detaching a stair from a deck re-derives its height, and ordinary stairs finally track level-height changes. Attaching via the panel writes slabOpeningMode none and hides the cutout controls; detaching restores the destination cutout and clears the stale explicit rise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hop was hysteresis: the election consumed the riding grid plane's perspective-skewed hit, giving two self-consistent fixed points for one pointer ray. getPointedSupportSurface now returns the ray's crossing of the pointed surface and both the support cap and the cursor XZ derive from that single computation, so items stay on the surface the pointer aims at and sit exactly under the cursor across storeys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stair tool subscribed to both node clicks (synthesized on pointerup) and the native-click grid event with none of the guards sibling tools carry — one physical click over any node surface committed twice. A commit gate + follow-up click swallow fix the double dispatch, and the stair and column tools now restore select mode on exit instead of leaving the dead build-mode-without-tool state that ignored every click. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Selecting multiple nodes now docks the collapsed-by-default panel on the right: N selected header, kind breakdown, and Duplicate/Delete mirroring the floating pill. A new multiSelectionFooter slot lets the host app dock actions below it, exactly like inspectorFooter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a6bb29f. Configure here.
|
|
||
| return { | ||
| supportSlabId: candidateElevations.size >= 2 ? (support.electedSlabId ?? undefined) : undefined, | ||
| } |
There was a problem hiding this comment.
Walls miss ground host under decks
High Severity
resolveWallSupportSlabPatch doesn't persist GROUND_SUPPORT_ID when a pointer-capped election targets the ground beneath an overlapping deck. This causes walls to incorrectly lift onto the deck in subsequent uncapped elections. Note that computeWallSlabSupport also lacks a read path for GROUND_SUPPORT_ID.
Reviewed by Cursor Bugbot for commit a6bb29f. Configure here.
…l-building-model use-draft-node conflict resolved by combining both sides: the branch's supportSlabId commit patches and elevation cap keep their structure, and main's placement-preview set/clear calls land at their intended points. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 tasks
wass08
added a commit
that referenced
this pull request
Jul 21, 2026
…ows (#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>
wass08
added a commit
that referenced
this pull request
Jul 21, 2026
…anel The wall-panel lint error predates this branch (#526); fixed here to unblock the quality gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wass08
added a commit
that referenced
this pull request
Jul 21, 2026
* feat(walkthrough): unify first-person and baked walkthrough UI into a shared HUD The builder first-person overlay (crosshair, Exit Street View button, hints card) and the baked-GLB walkthrough HUD were two divergent UIs. Extract the GLB-style HUD (reticle, floor/room labels, Esc pill, interact prompt) into a shared WalkthroughHud in packages/editor, feed it from FirstPersonControls via a small useFirstPersonHud store (interact target each frame, floor/zone labels sampled from the camera), and align FOV/projection handling with the baked controller. The now-unused WalkthroughControls glide controller is removed from packages/viewer (WALKTHROUGH_FOV moves to the GLB controller module). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(viewer-ui): shared ViewerControlsBar + ViewerSceneHeader for preview and embedders Preview mode's ViewerOverlay was an older copy of the community viewer UI (separate scan/guide/camera buttons, own render/theme/edges menus, 4-state wall mode). Extract the community design into shared prop-driven components: ViewerControlsBar (visibility, level/wall modes, display menu, walkthrough, orbit/top view) and ViewerSceneHeader (back, project info, optional stats slot, breadcrumb, levels card). ViewerOverlay is now a thin composition of them. The display menu gains the edges submenu everywhere; the vestigial translucent wall mode is dropped (a stale value renders as cutaway). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(walkthrough): hold-Ctrl crouch + P screenshot pause in both controllers Crouch swaps the capsule for a short one (shrinks around the centre, so a mid-jump crouch lowers the head and raises the feet — enough to thread window openings), lowers the eye with a short lerp, and slows movement; standing back up is gated on headroom via an upward raycast against the collider world. Tuning constants live in the GLB controller module and are shared with the editor first-person controller. P releases the pointer lock without leaving the walkthrough so the cursor is free for an OS screenshot (macOS region capture needs a movable pointer); clicking the canvas re-locks and resumes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(walkthrough): crouched profile fits ~1 m openings The float gap counts toward the effective obstacle height — the capsule rides floatHeight (0.5 m) above the ground, so the old crouch spanned 0.5–1.3 m and a 1.14 m opening still blocked it. Crouching now also lowers the float gap (0.25 m) and uses a shorter capsule (0.7 m), for an effective 0.25–0.95 m span; the stand-up headroom check grows to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(walkthrough): seamless screenshot pause — auto-release pointer lock on ⌘ Replace the P shortcut: macOS swallows the full ⇧⌘4 but the ⌘-down keystroke still reaches the page, so the moment ⌘ (or PrintScreen) goes down while locked the cursor is released without leaving the walkthrough — the native screenshot flow just works, no user education. The HUD pill flips to "Click to resume" (click-through, so the resuming click lands on the canvas) via a new walkthroughSuspended flag on the viewer store, reset on lock/exit/unmount. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(walkthrough): screenshot pause is P again, advertised in the HUD The ⌘ auto-release fired on every command combo and felt broken. Back to an explicit P toggle, now discoverable: the HUD bottom shows a "P free cursor" pill next to "Esc to exit", and while paused it flips to "Click or P to resume · Esc to exit" (click-through so the resuming click lands on the canvas). P re-locks as well as releasing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(walkthrough): freeze crouch during cursor pause; no editor hints in first person While the P pause is active, Ctrl no longer toggles crouch — ⌃⇧⌘4 (clipboard screenshot) was crouching the player mid-capture; the held state stays frozen until resume. HelperManager now renders nothing in first-person mode, so the Ctrl multi-select hint no longer pops over the walkthrough HUD (Ctrl is crouch there). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: biome formatting + pre-existing useOptionalChain fix in wall panel The wall-panel lint error predates this branch (#526); fixed here to unblock the quality gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


What does this PR do?
Implements the vertical building model kernel decided in the private repo's
plans/editor-vertical-building-model-research.md, plus five rounds of hands-on refinement. 24 commits, reviewable stage by stage:Stage 1 — stored level heights + wall-top inversion.
LevelNode.heightbecomes the only vertical truth (per-building prefix sums; six divergent level-height derivations deleted, including MCPmetadata.heightand the first-person copy). Default walls have no height: their top is pinned to the level plane (resolveWallTop), so slabs lift only the base and top-of-level holes are impossible by construction.wall.heightsurvives as the explicit exception with a "Follows level / Custom height" panel control derived purely from field presence. Load migration writes exact derived heights (never preset-snapped), compacts ordinals per building anchored at zero (basements stay basements), and classifies wall tops against the plane at ε = 0.20 strict<(calibrated by a 4,000-project prod census). Level rows get a height badge + presets popover (unit-aware: 8/9/10 ft imperial).Stage 2 — persisted support hosts. Floor-placed nodes and walls remember which slab supports them (
supportSlabId, written at commit only when overlapping supports disagree); elections prefer a still-valid host and silently re-elect on loss; deletion strips references undo-safely. Item support unified onto rendered slab polygons with a per-level cache.Stage 3 — slab placement/thickness split.
elevationstays the walking surface; newthicknessgrows downward (solid occupies[e−t, e]); migration is byte-identical for every legacy slab; pools become explicitrecessedintent keeping the drag-through-zero gesture. Ceilings mirror walls (optional height = follows the level top live, minus covering-slab intrusion) and clamp to covering-slab undersides via the first cross-level query; plane-bound wall tops clamp to those undersides too (Revit's attach-to-floor-bottom, automatic). Wall-face adoption is gated to grounded slabs so decks keep their drawn polygon. Adaptive vertical editing: panel = placement; viewport drag stretches to 0.4 m then unsticks into a thin deck.Stage 4 — decks as product surface. Stairs attach to decks (
deckSlabId: rise follows the deck live, flights re-converge via a write-sync, flush landing accounting for the stair's own elected base, cutout auto-disabled). Fences host on decks and walls/fences draw on them (pointer-decided placement surface — the same ray-derived cap that fixed under-deck and stacked-slab placement). The one-gesture mezzanine/balcony tools were built, then removed in favor of catalog presets (code preserved at e30042d); the kernel they validated stays. A compact multi-selection panel ("N selected" + kind breakdown + Duplicate/Delete) with amultiSelectionFooterhost slot supports the community app's save-selection-as-room flow.Also fixes latent bugs surfaced along the way: double stair per validation click (node-click + native grid:click double dispatch), the dead
mode:'build'+tool:nullselection lock (stair + column tools), and the grid-plane feedback loops behind placement flicker.How to test
bun dev, open a scene with a multi-level building — it loads through the migration: level rows show real stored heights (a legacy default storey reads 2.55 m), no holes at the top of any level.bun test— full suite; migration/census/clamp/election behaviors are unit-covered (≈250 files).Screenshots / screen recording
N/A here — verified against real scenes via headless capture during development; happy to attach a clip on request. The lab explainer at the community app's
/lab/vertical-building-modelvisualizes the model this implements.Checklist
bun devbun checkto verify)mainbranch🤖 Generated with Claude Code
Note
High Risk
Large changes to vertical placement, wall/slab/ceiling height resolution, and scene sync affect core editor behavior and persisted node fields across many types; regressions could show as wrong elevations, clipping, or stale geometry until dirty paths run.
Overview
Adds pointer-decided slab support so placement and drag use a ray-derived walking surface (
getPointedSupportSurface) and an optionalmaxElevationcap, fixing stacked deck/floor flicker and election at stable plan XZ instead of event-plane hits.Introduces
supportSlabIdcommit/read paths for floor-placed nodes, walls, and fences (resolveSupportSlabPatch,resolveWallSupportSlabPatch,resolveFenceSupportSlabPatch), plusGROUND_SUPPORT_IDwhen a capped commit chooses level base under an overlapping deck. Item/wall support now uses rendered slab polygons with a per-level cache invalidated on slab/wall changes.Floating decks keep their drawn footprint (no wall-band adoption); grounded/recessed room floors still seam and adopt. Spatial-grid sync marks dependents when level height, covering slabs above/below, deck elevation, or wall geometry changes.
Auto ceilings are created height-less (follow level top); manual ceilings clamp down under covering-slab undersides from the level above, with reactive re-clamp in space-detection sync. Schema adds
thickness/recessedon slabs, optionalLevelNode.heightandStairNode.deckSlabId, and documentssupportSlabIdacross floor-placed kinds. Wall/slab overlap logic moves tosystems/slab/slab-supportwithelectedSlabIdand pointer caps on wall election.Reviewed by Cursor Bugbot for commit a359ecd. Bugbot is set up for automated code reviews on this repo. Configure here.