* Add roof surface placement support for items
Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.
- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fixed conflict
* perf(editor): reduce floorplan annotation navigation work
* perf(editor): reduce floorplan camera update work
* perf(editor): isolate floorplan pose updates
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(viewer,editor): extract snapshot pipeline, add hero framing + BakeThumbnail
Move the offscreen SSGI capture pipeline out of ThumbnailGenerator into
viewer's snapshot-pipeline (adding the missing scene-referred GRADE, uniform-
driven), add hero-pose per-node-box corner framing helpers, and a BakeThumbnail
component so the community bake page can render a publish-quality hero shot
headlessly. Auto-save thumbnails now re-pose onto the same hero angle instead
of copying the user's mid-edit camera; user-driven captures keep the exact
viewport pose.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(viewer): tune hero framing — 19° elevation, tighter fit
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(viewer,editor): ink edges in snapshot captures
Captures now run the viewport's AO -> ink -> grade order with the same
soft/strong edge derivation (edgeColorFor/edgeOpacityScaleFor), uniform-gated
so one cached pipeline serves all modes. Radius scales with render height so
supersampled captures keep the viewport's line weight. Preset/item
(transparent) captures stay ink-free.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(viewer): lower hero elevation to 13°, exact fit — match reference framing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(viewer): hero framing — site plate in frame, building-centered aim, facade-relative 45°
Three framing fixes from review: the intentionally-shaped site plate joins the
fit constraints (from scene data — the site Object3D carries the ±400m horizon
disc and can't be measured), the aim centers the building (plate+structure on
XZ, structure alone on Y) so outlying boxes take asymmetric margin instead of
shifting the subject, and the azimuth sits 45° to the dominant wall axis
(length-weighted fold-4 vector sum) so rotated plans still read as a proper
corner shot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(core): elect wall slab support from the carrying profile, not face coverage
An elevated deck drawn against a house wall covers the wall's outer face
line end-to-end (boundary contact counts), so the max-across-polylines
election handed the wall origin to the deck: every wall-hosted window/door
rode along whenever the deck height changed, and placement local-Y was
clamped above the deck top. Elect from the carrying profile instead (per
arc segment: highest support per face, min across supported faces), with
the pointer cap applied inside the profile so a capped-away deck still
falls back to the floor that carries the wall. Also pass curveOffset/
thickness/supportSlabId at the window/door tool query sites so their
cursor agrees with the rendered wall frame.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(core): honor live node overrides in spatial-grid support queries
Group drags publish translated slab polygons and wall endpoints to
useLiveNodeOverrides only; the committed spatial index made floor items
and walls re-elect support against the pre-drag slab footprint, so
multi-selection moves and room-preset placement jumped vertically until
the validating click committed the batch. Support queries now read
live-effective slab/wall records and bypass the rendered-polygon cache
while a slab or wall on the level has an override; the committed cached
path stays the fast path otherwise.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(editor): give the room-preset stamp tool a polygon snap context
The host app's room stamp drives placement with tool='room', which has no
registry entry, so snapContextOf resolved null: Shift never cycled the
snapping mode and the HUD chip stayed hidden during room preset placement.
A tool-level context map hands the stamp the no-angle polygon set.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(editor): only synthesize walkthrough fallback floors for ground levels
Every slab-less visible level got a >=30x30m opening-free fallback floor
box at its elevation, so a walkthrough spawned on an upper level (the
no-spawn-node fallback ray picks the highest surface) stood on a phantom
plane it could never descend from. Match the baked-GLB viewer policy:
only the lowest level of each building (derived baseY === 0) gets the
fallback; upper levels rely on their real slabs, whose stair openings
are cut into the geometry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(core): fold useLiveTransforms slab deltas into support queries
The slab move tool and the room-preset stamp publish a translation DELTA
to useLiveTransforms (no polygon override), so the spatial index still
elected support against the slab's committed footprint: furniture riding
a room-preset preview (or sitting on a dragged deck) dropped to ground
under the visually-moved deck until the validating click. Effective slab
records now apply the live delta to polygon/holes/elevation, mapped
exactly once at each query's loop entry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(viewer): reapply floor lift every frame for nodes with live previews
FloorElevationSystem only wrote mesh Y for dirty nodes, but the React
commit that rebinds a dragged node's base-Y group position can land
between frames, after priority-2 systems consumed the dirty mark — the
lift then vanished until the next pointer tick re-dirtied the node,
blinking the Y of items dragged over elevated slabs. Nodes holding a
live override or transform now get the lift reapplied every frame.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(editor): stop the entry camera swap from insta-cancelling walkthrough
Entering walkthrough with a persisted orthographic camera swaps it to
perspective, which recreates the interaction callbacks and re-ran the
pointer-lock effect: its cleanup called exitPointerLock, and the unlock
handler read that as "user left walkthrough" — instantly cancelling the
fresh entry and arming the browser's ~1.25s re-lock cooldown (hence
needing multiple button presses). The effect is now mount-stable (the
changing callback rides a ref, deps down to [gl]), and the entry lock
request swallows async cooldown rejections like the P-resume path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(core): don't block build-JSON import over plugin-node children
Exports from projects with plugins carry nodes like trees:tree whose ids
sit in level.children; the import validator parsed parents against the
static children id union, so one tree id hard-failed the level schema and
blocked the whole import — while the same data loads fine from the DB
(setScene never runs this gate). Parents are now validated against a copy
with non-static-schema child ids filtered out; those nodes keep surfacing
through the unknown-types warning and the imported payload is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(core): treat registered plugin kinds as first-class in import validation
Nodes of runtime-registered plugin kinds (trees:tree, trees:grass) were
lumped into the unknown-types warning even when the plugin is loaded.
The validator now consults the node registry: registered kinds validate
against their own registered schema (corrupt plugin nodes still block),
count under stats.pluginTypes, and raise no warning — only genuinely
unregistered types do, which stays correct for hosts without the plugin.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style: appease biome (format + forEach block body)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
camera-controls fires controlstart for every pointerdown — including
buttons mapped to ACTION.NONE (plain left click in edit mode). Since
#535 that set cameraDragging=true with no rest/sleep ever following to
clear it, so every canvas click (selection, wall placement) was
suppressed once the camera was at rest.
Only flag dragging when currentAction actually drives the camera, and
clear the flag on controlend for mapped-button taps with zero movement
(no wake -> no rest/sleep).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Add roof surface placement support for items
Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.
- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fixed conflict
* fix(editor): defer floorplan annotation collision layout
* fix(viewer): suspend outlines during camera movement
* fix(editor): keep floorplan zoom off the render hot path
* fix(editor): keep floorplan pan off render hot path
* fix(editor): keep floorplan rotation off render hot path
* fix(editor): keep rotation backdrop white
* fix(editor): finalize floorplan navigation safely
* fix(editor): keep navigation interaction state current
* fix(editor): stop floorplan animation before pan
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Wall/fence drafting already drew an X/Z axis cross at the segment start.
Now the moving endpoint gets a single long guide line perpendicular to the
draft segment (a second cross would collide with the start cross on
axis-aligned segments), fences gain the same guides they lacked entirely,
and the 2D floor plan renders the equivalent SVG guides (cross at start,
perpendicular line at end) fed by the floorplan draft preview store.
The guide components are extracted from wall/tool.tsx into
nodes/shared/draft-axis-guides.tsx so both tools (and the fence's formerly
duplicated arc/label helpers) share one implementation.
* fix(wall): stop endpoint drag snapping back to the stale junction corner
Walls attached to the moving corner cascade with the drag, but the snap
pipeline reads the scene store, which keeps their pre-drag coordinates
until commit. Their stale corners recreated the old junction as a snap /
alignment target, so inside the connect radius (5cm, 70cm magnetic) the
endpoint could never land closer than that to where it started — sub-5cm
corrections (e.g. squaring a scan-imported 91° junction) were impossible
in every snapping mode.
Both endpoint-move paths (3D tool + 2D floorplan affordance) now exclude
the walls linked at the moving corner from snap candidates and alignment
anchors while attached; Alt-detach keeps them (they stay put, so they're
live geometry). The 2D path previously over-excluded: walls linked at the
FIXED corner don't move, and their anchors are exactly what lets the
dragged corner align back onto a true axis.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(wall): re-run full snap pipeline on Alt toggle during endpoint drag
The snap/alignment candidate set now depends on Alt (stale-junction
exclusion), so a keyboard detach/re-attach must re-resolve from the raw
cursor point instead of re-applying the previously snapped one — matching
what the 2D dispatcher already does by re-invoking apply() with the raw
planPoint on modifier changes. Flagged by Bugbot on #533.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Bake v2 exporter side: library/preset, legacy scene-material, and item-GLB
textures get a validated pascalTextureRef stamped at load (origin-checked,
env-derived). Material-ish sources resolve to 'library-material' (storage
bucket) or 'app-material' (static catalog on the assets CDN) by URL shape.
exportSceneToGlb grows a textures: 'embed' | 'reference' option — reference
mode swaps stamped textures for 1x1 canvas-backed placeholders (skipping the
WebGPU blit) and writes the ref to both texture and image extras via a
GLTFExporter writeTexture plugin. Download GLB stays embed; the bake page
passes reference.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* 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>
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>
* fix(viewer): guard TextureNode updates against null textures
three's override-material passes (shadow, prepasses) copy per-object texture
slots onto shared materials whose cached per-mesh node graphs can disagree
about a slot's presence; when a graph with a TextureNode pulls a null slot,
the exception kills the whole render pass and the scene goes black. Patch
TextureNode.prototype.update to skip null values (with a throttled warning)
so the slot just renders textureless for a frame and recovers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* revert(viewer): drop the always-on neutral displacement texture
Giving every standard material a displacement texture put displacement
TextureNodes into the shared shadow/prepass override graphs; in scenes mixing
preset materials with GLB item materials (no displacementMap), an item's
shadow render can hit a cached graph that expects the texture and pull null
— build-order dependent, which is why it broke many-but-not-all projects at
open right after the release. The TextureNode fallback guard covers the
original paint-hover crash (black texel = zero displacement), so the broad
neutral-texture approach is retired.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style: sort guard imports
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(materials): dynamic library registry, picker source tabs, create-material entry point
Core gains a runtime material registry (registerLibraryMaterials/
unregisterLibraryMaterials/subscribeLibraryMaterials) so embedders can
feed user/community materials; library: refs to registered materials
resolve in the viewer unchanged. MaterialCatalogItem carries an optional
source (pascal|community|mine|workspace). MaterialPicker gets a source
filter row and an optional onCreateMaterialRequest '+ New material'
tile, threaded through MaterialPaintPanel.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(materials): underline source tabs in picker, matching catalog browse surfaces
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(materials): add 'other' category for uncategorized library materials
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(viewer): rebuild node material when a texture slot is cleared
Reused cached WebGPU materials keep a compiled TextureNode per slot; nulling
the slot for a cold texture load (or a preset without the map) without
needsUpdate leaves the node's per-frame material reference pulling null,
crashing the render pass in TextureNode.update. Cold loads are the norm for
freshly generated library materials, whose maps aren't in the texture cache.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(viewer): standard materials always carry a displacement texture
three's WebGPU shadow pass copies each object's displacementMap onto one
shared per-light shadow material while caching per-mesh shadow node graphs
against that shared override. A mesh whose shadow graph was built with a
displacement TextureNode (painted with a generated material — the only
presets carrying height maps) crashes the render pass with "null (reading
'matrix')" the moment a material without a displacement texture is swapped
onto it, which is exactly what the paint hover preview does. Give every
MeshStandardNodeMaterial a shared 1x1 black displacement texel (zero offset)
when it has no real height map, so the copied slot is never null in either
direction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(core): stored storey heights groundwork — pure slab-support module + 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>
* feat(editor): storey height badge + edit popover on level rows
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>
* feat(core): vertical-model load migration — stored heights, ordinal compaction, 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>
* feat: pin wall tops to the storey plane; stored heights become the only 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>
* feat: clamp slabs/ceilings under the storey plane; sweep wall-height 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>
* feat(core): persisted support hosts — schema, host-preferring election, 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>
* feat: persist support hosts at commit; thread wall host preference everywhere
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>
* feat: split slab into placement + thickness; pools become explicit recess 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>
* feat: clamp ceilings to covering-slab undersides across levels
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>
* feat: mezzanine and balcony build-tab tools
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>
* feat: adaptive slab vertical editing; level vocabulary in UI copy
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>
* feat: clamp plane-bound wall tops to covering-slab undersides; fix vertical 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>
* feat: ceilings follow the level top by default
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>
* fix: wall plane clamp missed max-side boundary walls
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>
* feat: stairs attach to decks; pointer-decided placement surface
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>
* chore: remove the mezzanine/balcony composite tools
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 e30042db for reference.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: gate wall adoption to grounded slabs; panel moves, drag stretches 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>
* fix: deck-attached stairs land flush with the deck surface
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>
* feat: draw walls and fences on elevated decks
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>
* feat: unit-aware height presets
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>
* feat: stairs converge to their resolved rise; deck attachment disables 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>
* fix: stacked-slab move hopping — one ray, one surface, one XZ
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>
* fix: single stair per click; tools restore select mode on exit
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>
* feat: compact multi-selection panel with host footer slot
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>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* add devcontainer setup and change default port to 3002
* fix(devcontainer): enforce frozen install
---------
Co-authored-by: victor <victor@elalemanyo.de>
* fix(mcp): keep store receiver bound in scene event operations
appendSceneEvent and listSceneEvents detached the store methods before
invoking them (const append = this.requireStore().appendSceneEvent;
append(options)), so `this` was undefined inside store implementations
that rely on it — SqliteSceneStore.withWriteTransaction in particular.
Every save_scene, create_from_template, and live-sync mutation routed
through the facade failed with "undefined is not an object (evaluating
'this.withWriteTransaction')" even though the SQLite write succeeded.
Invoke the methods on the store instance instead, and add facade-level
regression tests covering both the bound-receiver path and the
stores-without-scene-events fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(mcp): close scene store after facade tests
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com>
* fix(core): guard material-ref parsers against non-string values (Sentry MONOREPO-EDITOR-EM)
getLibraryMaterialIdFromRef and getSceneMaterialIdFromRef only guarded
against null/undefined, then called .startsWith(). When a non-string
material ref reaches them (legacy/malformed wall material slot ref),
.startsWith is undefined -> TypeError: e.startsWith is not a function.
Narrow with typeof !== 'string' -> return null, so a bad ref degrades to
'no library/scene material' instead of throwing during wall material
resolution (packages/viewer wall-materials.ts -> parseMaterialRef).
* test(core): cover malformed material refs
---------
Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com>
* Add roof surface placement support for items
Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.
- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fixed conflict
* fix wall treatment miter geometry
* fix cabinet group resizing and corner alignment
* fix cabinet corner depth resizing
* fix(cabinet): stabilize modular preset changes
* fix(cabinet): stabilize corner resizing and wall alignment
* fix(nodes): stabilize wall cabinet depth resizing
* fix(editor): hide cabinet arrows for module selection
* feat(cabinet): add individual width resize handles
* feat(cabinet): improve wall cabinet editing
* fix(cabinet): harden wall cabinet resizing
* fix(cabinet): correct wall drag and depth handles
* fix(cabinet): refine individual depth resizing
* fix(cabinet): preserve context-aware corner depth behavior
* fix(editor): respect snapping modes for resize handles
* fix(editor): harden cabinet resize interactions
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com>
* feat: add persistent measurement tools
* feat: make measurements associative
* fix: finish measurements with Escape
* feat: improve measurement snapping guides
* feat: clarify measurement axis feedback
* feat: smart measure lens, zone reports, and direct measurement editing
- Smart measurement lens: registry-owned wall/slab/zone hover reports with a
single top-center HUD, click-to-pin, latest-event back pressure, and no
scene writes
- Conservative derived zone quantities (footprint, perimeter, proven
enclosure, gross wall/floor surface, flat-room volume) with the
selected-zone blueprint panel
- Direct editing of committed measurements via selected-only 2D/3D vertex
affordances with midpoint insertion, cancellation, and one-write history
- Shared measurement surface-query session; 2D tracing joins the
slab/ceiling magnetic pipeline with registered-corner snapping
- Angle arcs on the smaller angle, indigo active/black resting hierarchy,
screen-sized normal-aligned contact rings
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: make measurement snapping always magnetic
Measurement drafting and committed-edit paths gated wall, semantic, and
axis magnetism on isMagneticSnapActive(), which is only true in the
'lines' snapping mode — in the default 'grid' mode corners and wall
intersections barely attracted (3D association fell to the 0.012 m
verify tolerance, 2D wall radii to the 0.05 m connect stick).
Measurement is an analysis tool whose anchors exist to bind real
geometry, so its snapping no longer consults the construction
snapping-mode chip: 2D/3D drafting and committed vertex edits are always
magnetic, Alt is the temporary bypass in both views (releasing the axis
pull, wall magnetism, and the 2D projected-geometry pull, and shrinking
association to contact tolerance). A discrete 2D wall snap (endpoint /
midpoint / crossing) now outranks the locked axis pull, and committed 2D
edits route the fallback through the raw pointer so free drags no longer
quantize to the construction grid. Volume extrusion height keeps its
mode-driven grid quantize.
Codex adversarial review confirmed the diagnosis and plumbing; its 2D
Alt-depth and grid-quantize findings are applied. New
surface-plan-snap tests pin the magnetic override seam.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(measurement): stabilize area surface intent
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(viewer): tune shadow biases to stop acne without detaching shadows
normalBias 0.02 was too small a texel offset for the building-fit 1024
shadow map and brought back self-shadowing acne. Settle on normalBias 0.08
(0.07 and below acnes, 0.1 reads detached) plus depth bias -0.0005 to
suppress the residual acne that a normal bias alone couldn't clear.
Also adds a `?debug=shadowcamera` diagnostic that draws a CameraHelper for
each shadow camera so the fitted frustum can be inspected while tuning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(editor): polygon editor handle UX — focus, cursors, visibility
- vertex/midpoint cylinders ignore scene depth like the edge arrows so
they stay visible through walls and slabs; vertex radius 0.1 -> 0.08,
midpoint 0.06 -> 0.05, midpoints use the brighter arrow shade at rest
- during any drag only the active handle stays mounted (other arrows,
vertices, edge bars and the cross disappear) so the gesture reads clearly
- handles set pointer cursors: move for vertices/midpoints/cross, and a
screen-space direction-aware resize cursor for the edge arrows
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(editor): keep ceiling affordances quiet during any interaction
The ceiling corner brackets' hit boxes caught drag-time hover (spatial
events keep firing during host drags by design), set hoveredId to the
ceiling and flashed the ceiling grid mid-gesture — e.g. while dragging a
slab polygon vertex. Unmount the brackets while ANY interaction scope is
active, and gate CeilingSystem's hover-driven grid reveal on idle scope +
no inputDragging as a second layer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(core): stop room detection resurrecting deleted slabs on load
Two holes in the wall-driven auto slab sync:
- initSpaceDetectionSync started with an empty baseline, so scene
hydration (one atomic setScene) read as 'every wall changed' and ran a
full detection pass on every load, recreating auto slabs the user had
deleted. Seed the baseline from the store at init and treat a level's
first snapshot as baseline — detection now only reacts to in-session
wall edits.
- matchesManualFootprint required mutual coverage, so a single manual
slab spanning multiple rooms never suppressed those rooms' auto slabs
(only a fraction of it lies inside each room). Suppression now only
asks whether the ROOM is substantially covered by the union of manual
slabs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>