* fix: zone grid snapping, shelf position panel, deselect on item placement
- Zone tool reads the editor's gridSnapStep (0.5/0.25/0.1/0.05) instead
of a hardcoded 0.5 for both cursor move and click snapping.
- Shelf inspector gains a Position group (vec3 X/Y/Z sliders), matching
the item panel.
- Item catalog clears the viewer selection before arming placement so
shortcuts (rotate & co) don't hit both the ghost and the selected node.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: move level display naming into @pascal-app/core
getDefaultLevelName / getLevelDisplayName ("Ground Floor" / "Floor N" /
"Basement N") lived in packages/editor's internal lib, so viewer-only
surfaces couldn't reach them and fell back to hand-rolled "Level N"
labels. The helpers are pure domain logic, so they move to core and
export from its barrel; the editor package's seven call sites now import
them from there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix: guard preset placement against double commit from duplicate click events
A single physical click reaches commitAtCursor twice: node clicks are
synthesized on pointerup (use-node-events) while grid:click rides the
native click event from a canvas DOM listener (use-grid-events) that
deliberately ignores stopPropagation. The second pass found the fresh
draft already deleted and took the orphan re-create path, minting a
hidden ghost copy, replaying the placement SFX, and abandoning the
re-armed clone as a second ghost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: show the compass in all view modes via the viewer-area container
The floorplan compass now portals onto the always-visible viewer area
instead of living inside the (display:none in 3D) floorplan pane. The
2D/3D navigation poses already sync through navigationSyncPose, so the
needle and align-to-north stay correct in 2d, 3d, and split alike.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The v0.9.0 GitHub release declared the trio at 0.9.0 but the npm publish
never ran (npm still serves 0.8.0). Aligning manifests so the release
workflow's bump=patch lands everything at the announced-next 0.9.1
(mcp 0.3.0 -> 0.3.1, ifc-converter 0.1.0 -> 0.1.1 keep their own lines,
matching the per-package version table style of previous releases).
Inter-package peer/dev refs move to ^0.9.0 so the workspace stays
self-linked during the release run; the workflow re-syncs them to the
final versions after bumping.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Allow template forks to preserve scans
* Add room preset save trigger
* Show room preset action for selected zones
* Use standard inspector for selected zones
* Support room preset editor workflows
* Preview room clear-underneath items
- next 16.2.7 -> 16.2.6 in both apps. 16.2.7 regresses Turbopack dev route
matching for API routes nested below a dynamic segment (probed in the
private repo: /api/items/[id]/fork, /api/scenes/[id]/events 404 with the
router's HTML not-found; production builds unaffected; bisected
16.2.6 good / 16.2.7 bad). 16.2.6 carries all the 16.2.5/16.2.6
security fixes — only the bugfix backports are forgone.
- Remove @react-three/uikit-lucide from @pascal-app/editor dependencies:
zero imports in this repo or private-editor; dead weight in every npm
consumer's install.
- Pin @visual-json/react "latest" -> "^0.4.0" (current resolution;
"latest" in a published package's deps is unreproducible and was
inconsistent with private-editor's ^0.4.0).
- Add root override three: 0.184.0, mirroring private-editor's dedupe —
drops the nested stats-gl three@0.170.0 so the lockfile resolves a
single three (the stated single-instance invariant now holds here too).
Verified: build, check-types, biome check, 931 package tests green;
lockfile resolves one next (16.2.6) and one three (0.184.0);
oxide/lightningcss optionalDependency pins still match resolutions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Range edits:
- next 16.2.1 -> 16.2.7 in apps/editor + apps/ifc-converter. 16.2.5/16.2.6
fixed six high-severity advisories (middleware bypass, DoS, SSRF, XSS,
RSC cache poisoning); 16.2.7 is bugfix backports. No API/config changes.
- typescript 6.0.2 -> 6.0.3 everywhere, including aligning the 5.9.3
stragglers (@pascal-app/mcp, @repo/ui, @repo/eslint-config) that date
from the original scaffold and were never intentionally held back.
- @types/node in @pascal-app/mcp: ^25.5.0 -> ^22.19.20 — mcp was the only
workspace typed against non-LTS node 25; 22 matches the release
workflow's node and every other workspace.
- @number-flow/react ^0.5.14 -> ^0.6.0 (packages/editor, apps/editor);
the only breaking change (removed --number-flow-char-height CSS var)
is unused here.
- agentation ^2.3.2 -> ^3.0.2 (apps/editor devtool; v3 is a drop-in for
the props-less <Agentation /> usage, additions are opt-in).
- Root overrides: @types/react 19.2.14 -> 19.2.17,
@types/three 0.184.0 -> 0.184.1 (types-only fixes).
Lockfile refresh within existing ranges picks up react 19.2.7 (pairs with
next 16.2.7 — 19.2.6 had a server-action FormData regression), motion
12.40.0, three-mesh-bvh 0.9.10, @react-three/uikit-lucide 1.0.73, the
June radix wave, lucide-react 1.17.0, zustand 5.0.14, tailwind-merge
3.6.0, geist 1.7.2 (fixes Geist Mono ligature regression), react-grab
0.1.44, biome 2.4.16 + ultracite 7.8.2, turbo 2.9.17.
Held: three stays 0.184.0 (npm latest; single-instance constraint),
tailwindcss 4.3.0 + lightningcss 1.32.0 already match the pinned
optionalDependencies native binaries, eslint 10 major not taken
(@repo/ui is unconsumed legacy scaffold).
Verified: turbo build, check-types, biome check, and 931 package tests
green; bun.lock stays lockfileVersion 1 (CI bun 1.3.0 compatible);
single next/three resolution confirmed.
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
* Snapshot: ridge vent locks to segment ridge during placement, free sliders after
Placement tool clamps the cursor to the segment's ridge line for all roof
types (gable, hip, shed, gambrel, dutch, mansard; flat rejected). Renderer
re-derives Y from the live surface plus a small lift, and treats stored
position[1] / position[2] as user offsets so the inspector sliders move
the vent after placement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(roof): contribute outer silhouette to alignment-guide candidates
Roof has no centred-box footprint (it's the union of its `roof-segment`
children) so the capability bridge needs the resolved AABB directly. We
build it from the children's corners in roof-local space, then transform
to world coords. Roofs only contribute as static candidates — the
move-roof tool drives them by origin, so the relocatable-box path never
applies.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(floorplan): upright text geometry that stays horizontal under scene rotation
Floor-plan text labels were rotating with the 90° default scene rotation,
making zone names read sideways. Add an `upright` flag to text geometry:
when set, the registry layer counter-rotates the label by sceneRotationDeg
around its anchor so it reads horizontally on screen. Zone labels opt in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): redesign material-paint cursor badge and gate floor-plan paint mode
Paint cursor swaps the inline icon+label chip for a stacked badge — a
glowing accent stem pointing down to the hit point with the paint icon
above — so the cue reads as "pointer" rather than "tooltip". Switches
the accent to indigo. The 2D floor-plan now also shows the paint-icon
overlay and routes pointer interactions to the painter when in
material-paint mode.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): 90° rotate action on the floating building menu
Adds a Rotate button to the building action menu (and a generic `onRotate`
slot to NodeActionMenu). Each click rotates the building 90° CW around
its world-bbox center, mirroring the offset compensation
`MoveBuildingContent` uses during R/T-rotate drags — so the building
spins in place instead of orbiting its (often off-centre) origin.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(column,shelf): show cursor sphere during placement preview
Matches the placement affordance other floor-placed tools already have
so users see the snap point as a discrete dot alongside the ghost mesh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(ifc-converter): update next-env types path to .next/dev/types
Next.js moved generated route types under `.next/dev/types/` — regenerate
the reference so the type-check resolves them again.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ridge-vent): track parent segment's live overrides for real-time follow
While dragging a roof-segment resize handle (width / depth / pitch /
wallHeight / rotation), the new dimensions stream through
useLiveNodeOverrides and only flush to the store on release — so the
ridge vent was snapping to the new ridge only after the drag ended.
Merge segment overrides into the renderer's segment view so the vent
rides the resize in real time.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): step building rotation in 15° increments
Cuts the floating building menu's rotate-step from 90° to 15° so each
click nudges the building rather than slamming it to the next cardinal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): world-frame alignment guides and grid snap under building rotation
Brings the 2D floor-plan alignment + grid behaviour in line with 3D so
guides and snap follow the world XZ grid regardless of how the active
building is rotated.
Core: adds resolveAlignmentInBuildingWorld + snapWorldXZToBuildingLocal,
plus a shared BuildingPose type, so callers can resolve in world space
while staying in building-local coords downstream.
Editor: introduces packages/editor/src/lib/world-grid-snap.ts as the
single entry point for tools that need world-frame alignment / grid
snap (resolveAlignmentForActiveBuilding, getActiveBuildingPose,
snapWorldXZForActiveBuilding, snapBuildingLocalToWorldGrid). Honours
useLiveTransforms and resolves the active building via level.parentId
first so the helper stays in sync with the floor-plan panel.
Alignment store unification: applyFloorplanAlignment and the registry
move overlay now publish guides in world XZ — the same frame the 3D
tools already use — so the shared useAlignmentGuides store carries one
consistent coordinate system. The 2D layer renders world XZ via the
fixed world → SVG transform (a 90° rotation around the building's
world position; independent of building rotation), so guides come out
parallel to the world-axis-aligned floor-plan grid in every case.
Drafting + move sessions across walls, fences, slabs, ceilings,
columns, items, shelves, roofs, stairs, zones and elevators are
re-routed through the new helpers so cursor placement, grid snap,
draft endpoints, and move commits all land on the world XZ grid
even when the building has been rotated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* revert(editor): drop building drag-rotate and grid building-XZ chase
Two behaviours added recently were causing the world grid to slide
around during building edits. Reverts both, keeping the rest of the
world-frame alignment work intact.
- floating-building-action-menu / node-action-menu: roll the rotate
button back to a single click that increments rotation by 15°
around the bbox center (state at 3eb02fde). The drag-rotate from
b50c0d5a streamed live position updates per frame, which the
floor plan and grid both followed.
- grid: stop chasing the active building's world XZ each frame
(pre-c00a2469 behaviour). The grid stays anchored at world (0, 0)
so it doesn't visibly sweep when the building origin moves —
during a drag, an R/T rotation in the move tool, or a click
rotate that re-anchors the origin to the bbox-pivot. Y still
lerps to the active level's floor height.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): consistent building rotation pivot + 2D position sync
Several connected fixes to building rotate/move so the 3D mesh, the
2D floor plan, and alignment guides all stay visually consistent
across rotations and drags.
Rotation-invariant pivot
- New `getBuildingLocalBboxCenter` helper walks descendant mesh
geometry bounds in the building's LOCAL frame, returning a
centroid that doesn't depend on the building's current world
rotation. The previous approach derived the pivot from the world
AABB at the current rotation, which made the local pivot subtly
drift each time the building was rotated.
- Floating menu click-rotate and move-tool R/T rotation now seed
their pivot from this helper, so successive rotations stay
pinned at the same world point.
Move-tool stability
- R/T rotation always pivots around the current world bbox center,
even before the cursor has moved onto the grid (previously it
fell back to rotating around the origin in that case).
- Commit (`onGridClick`) now writes the mesh's actual on-screen
pose instead of recomputing it from the click event's grid
coords, removing a one-cell snap on release.
Alignment resolver
- Tie-break is now perp-first, primary-second. The previous order
was reliable when wall faces shared an exact axis-aligned coord
(pre-rotation), but post-rotation float deltas meant primary
never tied and the resolver could lock onto the far corner of a
candidate. Perp-first picks the visually closest point.
2D ↔ 3D sync
- The floor-plan SVG scene group now applies a live `translate(...)`
derived from `(committedBuildingPosition → live buildingPosition)`,
in addition to the existing live rotation. Building-local content
slides in lockstep with the 3D mesh during a drag.
- The alignment-guide layer projects from world XZ using the
COMMITTED building position, so guides stay locked to their world
coordinates while the building slides past them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Merge remote-tracking branch 'origin/main' into fix/fir-5-june
* feat(wall): alt-click commits a single wall instead of chaining
Holding Alt on the click that completes a segment now stops drafting
instead of starting a new segment from the just-placed endpoint. Same
behaviour wired in both the 3D wall tool and the 2D floor-plan
placement hook (which already passed `singleWall: event.altKey` —
the 2D handler was ignoring the flag).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* revert(editor): drop 15° rotate from floating building action menu
Removes the rotate button on the floating building menu and the
`getBuildingLocalBboxCenter` pivot helper that only existed to support
it. The selected-group rotate handle remains the way to rotate a
building.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(ifc-converter): point next-env at .next/types instead of .next/dev/types
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): unify 3D move grip with floating-menu Move via tap-to-engage
The translate gizmo on the 3D bounding box now hands the node to the
registry move tool (same path as the floating action menu's Move
button) instead of running its own plane-drag. One move flow — green
bounding box, cursor follow, alignment guides, R/T rotation,
click-to-commit — drives both entry points.
Adds a `dragBounds` capability on the node registry so kinds whose
rendered mesh contains extras (per-level landing assemblies on an
elevator, etc.) can declare the box the user thinks of as "the thing
being dragged." Elevator and stair opt in; everything else still
auto-measures. `MoveRoofTool` now shows the green box for whole-stair
and whole-roof moves, and aligns roofs by their footprint corners
(previously only stairs did). Slab floor-plan moves clear
`autoFromWalls` on commit so space-detection doesn't snap the slab
back, via a new `extraCommitFields` hook on the polygon-centroid
move target.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): 2D alignment guides in building-local frame + world-grid wall snap
The 2D floor-plan alignment layer now mounts inside the rotated scene
<g> and reads from an editor-local `useAlignmentGuides` store —
guides are stored in building-local meters and follow the SVG
transform that already carries the rest of the floor-plan geometry.
Pill labels are counter-rotated by the scene rotation so they stay
upright under building rotation. The 3D pipeline keeps its own
world-frame store; the two surfaces no longer share frames.
Wall drafting picks up an optional `gridSnap` override so the 2D
floor-plan can snap drafts onto the world-XZ grid even when the
active building is rotated — without it, a rotated building's
drafted wall would chase the local grid and miss the visible lines.
`snapPointTo45Degrees` and `snapWallDraftPointDetailed` route through
the override when supplied; otherwise the prior local-axis grid snap
is unchanged. The floor-plan panel wires `snapBuildingLocalToWorldGrid`
through the placement hook and exports `WALL_GRID_STEP` to support it.
Also fixes the minor/major grid stroke palette swap so minor lines
render with the minor palette and majors with the major palette.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(editor): drop dead onRotate slot, memoize dragBounds snapshot
Removes the unreachable `onRotate` prop, `RotateCw` import, and
rotate button from `NodeActionMenu` — its only caller was the
floating building menu's 15° rotate, which was removed in d65fd5fe.
Memoizes `MoveRegistryNodeTool`'s `dragBounds` lookup so it captures
the scene snapshot once per node instead of re-reading
`useScene.getState().nodes` on every render. Bounds depend only on
`node` (locked for the tool's lifetime) and start-time sibling state
(elevator shaft height from the level set), so a one-shot snapshot is
the right semantics — and it avoids implying a live subscription that
isn't there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: biome check --write (import order, formatting, unused import)
Auto-fixes from `bun check:fix`:
- import-order normalization in editor/nodes/core
- unused `snapToGrid` / `useLayoutEffect` imports dropped
- minor whitespace/wrapping tweaks
No behavioural change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update site labels after camera swaps
* fix: gate scene display until viewer is ready
* fix: render scene loader on first paint
* fix: keep loader during pending scene graph
* feat: add wasd camera panning
* feat: add x delete mode shortcut
* feat: add floorplan north compass
* fix: move floorplan compass to lower corner
* fix: progressively rebuild heavy scene geometry
* fix: simplify noisy ifc wall output
* 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
* editor: per-door-type floor-plan symbols
Render a distinct, static plan symbol for each door type in the
registry floor-plan builder (`packages/nodes/src/door/floorplan.ts`),
independent of the door's live open/close animation:
- single / hinged: fixed 90° swing with a dashed quarter-circle arc
- double / french: two mirrored half-width leaves + dashed arcs
- folding / bifold: static zigzag accordion (~80% span) on the wall face
- sliding: bypass — two overlapping panels on parallel tracks + arrow
- pocket: thin white leaf, ~60% closed, sliding into the solid wall
- barn: surface-mounted panel parked over the wall, dashed closed-ghost
+ slide arrow
The swing arc is dashed in screen-pixel units (the renderer uses
non-scaling-stroke). Symbols are oriented by hingesSide / swingDirection
/ slideDirection as appropriate.
Also includes pre-existing working-tree changes unrelated to the door
symbols: group move/rotate transform and box-select tweaks, and a
regenerated ifc-converter next-env.d.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix recessed ceiling fixtures and draw safety
* feat(editor): magnetic wall-snap with per-kind beacon (2D + 3D)
Snap the wall draft / endpoint-move point onto existing wall geometry —
corners, midpoints, wall–wall intersections, and along-wall edges — and
show a beacon at the snap point whose glyph encodes what it caught
(square = corner, triangle = midpoint, ✕ = intersection, circle = edge).
- Pure snap geometry extracted to wall-snap-geometry.ts (unit-tested).
- Ephemeral useWallSnapIndicator store drives a 3D pillar+glyph beacon
and a 2D SVG glyph beacon, both indigo to match the alignment guides.
- Gated by a new persisted "Magnetic snap" toggle in the Display menu
(useEditor); honored by draw + commit + endpoint-move in both views.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* editor: garage and open-doorway floor-plan symbols
Extend the per-door-type plan symbols in the registry floor-plan
builder (packages/nodes/src/door/floorplan.ts):
- open doorway (openingKind === 'opening'): bare gap, no leaf/arc/panel
(mirrors the 3D system, which renders only the cutout for openings)
- garage sectional: closed leaf + side tracks into the garage + dashed
parked ghost at the inner end
- garage roll-up: closed leaf + coil barrel (capsule) with a coil hint
- garage tilt-up: closed leaf + dashed parked panel + dashed curved
up-and-over swing path
- gate the swing arc to actual swing doors (hinged/double/french) so
other types fall back to the plain footprint
Garage mechanisms sit on the interior (door-local -z) side to match the
3D garage builders, independent of swingDirection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* arch: enforce layer boundaries — registry dispatch, store relocation, shared helper
Three architectural fixes to bring the branch into full compliance:
1. **ceiling-system kind check → CeilingCutCapability**
Replace `child.type === 'item'` branch in `ceiling-system` with registry
dispatch. Add `CeilingCutCapability` type to `packages/core` registry types,
implement `buildCeilingHole` on `itemDefinition`, and rewrite
`collectRecessedItemHoles` → `collectCeilingHoles` to dispatch through
`nodeRegistry` — viewer never again inspects a node's kind directly.
2. **useAlignmentGuides + useWallSnapIndicator → packages/editor**
These stores are editor-only UI (snap beacons, alignment guides). Move them
from `packages/core/src/store/` to `packages/editor/src/store/`, re-export
from `packages/editor`, and update all 34 consumer files across
`packages/editor` and `packages/nodes` to import from `@pascal-app/editor`.
3. **findLevelAncestorId extracted to core**
`item-light-system` had a private `resolveNodeLevelId` that duplicated
level-ancestor traversal logic. Extract it as `findLevelAncestorId` in
`packages/core` (spatial-grid-sync), export it, and replace the local copy.
All four packages typecheck cleanly (zero errors).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: fix lint, untrack .claude/launch.json
Run bun check --write to clear 8 Biome errors (formatting + import order
+ one unused import). Untrack .claude/launch.json and add it plus
.claude/settings.local.json to .gitignore so local IDE/agent configs
stop landing in commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Two gaps in the first-person walkthrough collider world:
- The visible default ground is the site node's mesh, but `site` is a
`site`-category node and excluded from the generic collider sweep. The
per-level fallback floor only fires for a level without a slab, so a
spawn on the bare ground (no slab) had no floor and fell through. Add a
dedicated site-ground collider derived from node data (not the rendered
mesh, so it's immune to geometry-mount timing) covering the scene
footprint at the site's ground plane.
- Ceilings are `structure`-category and were swept in as colliders, so the
player was held up as if standing on a floor slab. Exclude nodes whose
registry `surfaceRole` is 'ceiling' so the player passes through them
(they're a transparent mount surface for lights/fans), while walls,
slabs and stairs keep colliding.
Adds tests for both behaviors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(editor): shelf deletion plays item-delete SFX; refresh delete sounds
Shelves are furniture-like placeable objects, so deleting one should use
the lighter item-delete cue instead of the structure-delete one. Add a
shared emitDeleteSFX(nodeType) helper that maps item-like node types
(item, shelf) to item-delete and everything else to structure-delete,
and route the three delete sites (sledgehammer, floating action menu,
keyboard) through it.
Also refresh the item_delete/structure_delete audio assets and reduce
their pan jitter for a steadier delete cue.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(editor): update item_delete SFX asset
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* wip sfx
* wip audios
* feat(editor): structure build-start/end cues + menu hover/click SFX
Sound design pass for the build/items experience:
- Split structure-build into a start cue (draft begins) and end cue
(commit). Wall/fence fire start on the first click; slab/ceiling/zone
tick start on every non-closing vertex; roof on the first corner.
- Doors and windows now use the structure-end cue on place + move;
shelves use item-place instead.
- New menu_hover (round-robin retired in favour of a single sample) and
menu_click cues wired to the Build/Items main categories, item-catalog
tiles, the icon rail / tab bar, and the bottom action menu (via the
shared ActionButton).
- Refresh item_pick / item_place / item_rotate samples; drop the
now-unused numbered menu_hover variations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(editor): silence forEach return lint in updateSFXVolumes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(editor): convert SFX wavs to mp3 to shrink assets
Re-encode all SFX samples as 128kbps mp3 (~94% smaller, transparent for
short UI cues) and point the player at the .mp3 sources. Drops the .wav
originals; the freshly converted item_pick/place/rotate.mp3 override the
older committed mp3s.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: resolve lint and type errors to enable CI
- Remove stale biome-ignore suppressions in bootstrap.ts, r3f.d.ts,
and parametric-node-renderer.tsx (rules no longer fire).
- Replace forEach callbacks that return values with for...of loops
in node-actions.ts and build-collider-world.ts
(lint/suspicious/useIterableCallbackReturn).
- Rewrite assign-in-expression guards in ceiling/tool.tsx to explicit
if-statements (lint/suspicious/noAssignInExpressions).
- Hoist useMemo/useCallback above early return in chimney/panel.tsx
(lint/correctness/useHookAtTopLevel).
- Add `^build` to turbo check-types dependsOn so packages/core dist is
up-to-date before type checking — this resolves all 32 type errors
which were caused by stale dist, not missing symbols.
* ci: add lint and typecheck workflow on push/PR
Adds a quality gate that runs `bun run check` (Biome) and
`bun run check-types` (TypeScript) on every push and PR to main.
Uses concurrency groups to cancel stale runs. Closes#147.
---------
Co-authored-by: Pascal <open@pascal.app>
Slab and pool side walls assumed a CCW contour (the unflipped quad's right-hand normal is outward only for CCW), but outsetPolygon and the slab tool preserve the drawn winding. A CW-drawn slab therefore got inward-facing side-wall normals that FrontSide culling dropped, so the slab read as see-through from the camera-facing side (~half the time, depending on draw direction). Normalize the contour to CCW (reverse when signed area < 0) before building both the positive-slab and pool geometry.
Caps are unaffected (Earcut normalizes the outer ring); holes are already double-emitted + winding-normalized; ceilings use flat ShapeGeometry (no extruded walls); autoFromWalls slabs are already CCW so this is a no-op for them.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two invariants weren't captured by the wiki or the review-architecture
skill, so a PR could break them and pass review:
- node-schemas.md documented no backward-compat rule, even though
migrateNodes() runs at load. Add a Schema Evolution section (new
fields need a Zod default; renames/removals/retypes need a
migrateNodes entry) plus a Rules bullet.
- review-architecture skill only checked the narrow host-children
case for migrations and had no editor-layer check at all. Add a
general schema-migration item (§B) and an editor-overlay-layer
item (§E, overlay meshes must set EDITOR_LAYER or they leak into
thumbnails/snapshots and the ink/SSGI buffers).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
- Live slab-stacking Y previews for all floor-placed kinds (item/shelf/spawn/column/stair) during placement + both move pathways, via a shared core resolver; canonical positions unchanged.
- Unified 3D handle system (one drag pipeline + one visual primitive) with forgiving invisible hit-areas on every handle, kept on EDITOR_LAYER so they don't poison the MRT scene pass.
- Hover + click-to-edit slab holes in 3D (manual hole -> hole editor; stair/elevator hole -> select owner); generic cross-arrow polygon-move grip; normalized handle interaction colors.
- NaN-safe node mutations + non-finite shadow-light bounds guard.
- Built on #373 (level-scoped alignment / registry slab tool); #373 owns X/Z alignment, this owns Y floor-stacking.
* feat(editor): level-scoped alignment, reference-floor registry symbols, registry slab tool
- Scope alignment candidates to the active level so a node directly below
on another floor no longer snaps (alignment is XZ-only); building-scoped
nodes like elevator shafts stay in the pool across floors.
- Derive the elevator alignment footprint from its outer shaft (not the
inset cab), so its guide actually surfaces within the snap threshold.
- Extract shared stair footprint geometry (rotateXZ, segment transforms,
stairFootprintAABB) so opening-sync and alignment anchors derive the
chain identically; stairs now contribute plan bbox anchors.
- Render reference-floor stairs/roofs/elevators/shelves/spawns through
their registry floorplan builders for pixel-identical symbols.
- Move slab creation to the registry-driven slab tool (parity with
ceiling); 2D handlers only maintain draft state.
- Lift the 3D alignment guide ribbon to the active level's Y each frame.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(core): registry-drive elevator/stair alignment footprints
Address architecture review of the level-scoped alignment work: the core
anchor bridge (`alignment-anchors.ts`) hardcoded `if (node.type === 'elevator')`
and `if (node.type === 'stair')` branches to derive their plan footprints.
Move that onto the kinds themselves via a new `alignmentFootprint` capability
so the bridge dispatches generically — matching the registry composition model.
- Add `Capabilities.alignmentFootprint`: returns a `box` (rotatable rect
centred on position, relocatable for movable kinds) or an `aabb` (already
resolved, for non-rectangular plan shapes). Elevator uses `box` (its outer
shaft, and it's movable); stair uses `aabb` (segment chain / annular sector,
moves by origin).
- Drop both hardcoded branches; the bridge now consults the capability via
`floorFootprint` (box) and a unified `alignmentAABB` (box ∪ aabb).
- Export `stairFootprintAABB` from core so the stair definition consumes it.
- Tests register synthetic defs carrying the capability (the bridge no longer
knows elevator/stair by name), reproducing the production glue from the same
core helpers.
- Document why `REFERENCE_REGISTRY_KINDS` is a deliberate editor-local
curation, not an auto-derived set (most floorplan-builder kinds shouldn't
appear as standalone reference symbols, and "reference floor" is an editor
concept core must not know).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(editor): hide node arrow handles during thumbnail capture
The arrow-handle rig lives on SCENE_LAYER (so its chevrons read as proper
3D plates), but the thumbnail camera only filters EDITOR_LAYER + GRID_LAYER —
so a node selected at capture time would leak its arrows into the snapshot.
Hide the rig on `thumbnail:before-capture` and restore it on
`thumbnail:after-capture`, the same emitter handshake SelectionManager uses.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <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
* feat(editor): 3D alignment guides for item/wall/fence move + placement
Bring Figma-style alignment guides into the 3D editor, reusing the shared
pure resolver (`resolveAlignment`) and ephemeral guide store
(`useAlignmentGuides`) that previously only drove the 2D floor plan.
Core:
- `alignment-anchors.ts`: node→anchor adapters (footprint AABBs, corner
anchors, wall/fence segment anchors) + `refineGuidesToGap` so a guide's
line and distance read to the candidate's nearest edge, not the far side.
- `bboxCornerAnchors` + corner-only footprint anchors so alignment locks to
item edges, never centrelines.
- `resolvePointSnap` (point-coincidence variant; kept for future use).
Editor:
- `Alignment3DGuideLayer`: dashed ribbon + flat floor dots + distance pill,
in the project's indigo accent, mounted inside ToolManager's building-local
group so guides render in the cursor's frame.
- Producers wired in the item move tool, item placement coordinator, and the
wall + fence endpoint tools; walls and fences cross-align.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(editor): align guides snap to nearest real anchor, drop bbox re-span
The 3D alignment guide could place its end dot in empty space: a diagonal
wall (or any rotated / non-rectangular object) has bounding-box corners that
don't lie on the object, and `refineGuidesToGap` re-spanned the guide to
exactly those AABB edges — so the dot floated "along the coordinate" rather
than on the item.
- `resolveAlignment` now tie-breaks to the candidate anchor NEAREST on the
perpendicular axis (after the tightest axis match). Anchors are real points
(corners / endpoints / midpoints), so the guide always connects to the
closest actual point — which also yields the facing-edge gap distance.
- All four producers (item move, item placement, wall + fence endpoints) now
publish the raw resolver guides; the AABB nearest-edge re-span is gone.
- Removed the now-dead `refineGuidesToGap` and `resolvePointSnap` helpers
(and their tests / exports).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(editor): group move handle + shared group-transform core, move-tool polish
Add a group-move gizmo alongside the existing group-rotate handle, both
driven by a new shared `group-transform-shared` module (participant
classification, group-box + corner math, connected wall/fence component
expansion so attached structure transforms rigidly as one piece).
- core: refactor alignment-anchors collection + tests, extend handle registry
- editor: group-move-handle, group-transform-shared; rotate handle reuses them;
node-arrow-handles gains click-swallow guard; box-select + placement tweaks
- nodes: move-tool updates across ceiling/column/slab/roof/registry; door math
and panel adjustments; item definition cleanup
- nodes(fence): play `sfx:grid-snap` ticker on endpoint move, matching the
wall endpoint tool (fixes missing audio feedback on fence side drag)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(editor): keep autosave alive across page unload
The autosave debounces writes by 1s and relies on a `beforeunload` flush
for anything still pending. That flush fired a plain `fetch` PUT, which
the browser cancels the instant the page unloads — so refreshing right
after an edit (e.g. painting a roof material) silently dropped the change
and the reload showed the last persisted scene.
Thread a `{ keepalive }` option through the save callback and set it on
the unload flush so the request survives the unload. Also listen for
`pagehide` (fires where `beforeunload` does not, e.g. mobile Safari /
bfcache) and clear the dirty flag up front so the two listeners don't
double-send. Normal debounced saves omit `keepalive` (its 64KB body cap
only constrains the best-effort unload flush, not regular saves).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(editor): paint eraser + reset-all, drop roof cross-role bleed
Material paint gains an eraser (clear a surface back to its default) and a
"Reset all" action that defaults every painted surface on a node — for a
roof that includes each child segment — via a generic
`buildResetSurfaceMaterialUpdates` that nulls catch-all and role-specific
material fields without per-kind knowledge.
Also stop a single painted roof surface from bleeding onto the others:
`getEffectiveRoofSurfaceMaterial`, `getRoofMaterialArray`, and the segment
renderer no longer cross-fall-back between top/edge/wall. An unset role
resolves only to the legacy catch-all (back-compat) or the theme default,
so painting just the shingle, trim, or soffit stays on that surface.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(editor): alignment guides + drag bounding box across tools
Extend the Figma-style 3D alignment guides to the placement and move
tools for columns, elevators, roofs, stairs, ceilings, slabs, fences,
walls, doors, and windows: each collects alignment anchors from the
scene, resolves a snap within the shared threshold, and drives the
`useAlignmentGuides` overlay. Wall openings (doors/windows) only snap
along their host wall via the new `wall-opening-alignment` helper.
Add a shared `DragBoundingBox` overlay (exported from the editor barrel)
that renders the dragged object's bounds during a move, wired into the
column move tool alongside the alignment snap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(editor): axis-stable resize-arrow drag plane + slimmer gizmo handles
Build the linear resize-arrow's drag plane so it always contains the
handle's axis (view direction minus its along-axis component) instead of
a plane that merely faces the camera. The old camera-facing normal
collapsed when the axis pointed toward the viewer — screen motion barely
changed the axis component, so the resize crawled or stopped tracking the
cursor. Also slim the extruded arrow/handle geometry (shared by the node
arrows, wall side handles, and polygon editor) for a lighter gizmo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(editor): fill-block wall opening highlight + selected frameless openings
Draw the selected-wall opening highlight as a translucent block filling
the cutout volume (front-side culled) instead of a single vertical pane,
so it reads as an occupied slot from any angle — including a top-down
floorplan view where an edge-on pane was invisible. Also highlight a
directly-selected frameless opening (a `door` with openingKind
`'opening'`), which otherwise renders no geometry of its own, and reflect
live drag overrides via `useLiveNodeOverrides`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(viewer): double-side slab hole side-walls
Build the slab in 3D rather than via ExtrudeGeometry so each hole-wall
quad is emitted twice with opposite winding. The slab material is forced
to FrontSide (DoubleSide poisons the MRT scene pass), under which
ExtrudeGeometry's single-sided hole walls get back-face culled and you
see straight through the cut. The doubled quads keep the cut's inner
thickness visible from any angle without z-fighting.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(editor): box-select building-scoped nodes like elevators
Building-scoped selectable nodes (e.g. elevators) are children of the
building, not the active level, so the level walk never reached them.
Also walk the level's building children and box-test any registry-
selectable kind by its rendered bounds, matching the column/stair/shelf
path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(editor): shelf placement alignment + column placement ghost
Shelf placement now snaps to Figma-style alignment guides by its
footprint edges (layered on grid snap, Alt bypasses), matching the
existing 3D move tool. Guides refresh after each drop and clear on
teardown.
Column placement migrates to the registry `def.tool` path so it can
render a translucent column ghost at the cursor (like the shelf build
tool) instead of a bare cursor sphere — the editor package can't import
the column geometry, so the tool now lives in packages/nodes:
- extract `ColumnBody` from the renderer and add a `ColumnPreview`
(cloned translucent material, raycast disabled, origin-positioned)
- new `column/tool.tsx` registry placement tool with the same
footprint-edge alignment as shelf / column move
- wire `def.tool` + tool hints; drop the now-unreachable legacy
editor-side `ColumnTool` and its dead file
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(editor): floor-plan alignment, pivot moves & placement ghosts
Bring the 3D editor's Figma-style alignment experience to the 2D floor
plan across every node kind, fix pivot semantics on move, and add 2D
placement ghosts.
Alignment
- Wall anchors now include ±thickness/2 face corners so columns/items/etc.
snap flush to wall faces (fixes pillar↔wall); shared by 2D and 3D.
- Shared apply-alignment helper (applyFloorplanAlignment /
alignFloorplanDraftPoint, with excludeIds) used by move sessions,
structural drafting (wall/fence/slab/zone/ceiling/roof), and wall/fence
endpoint drags.
- Door/window/wall-item moves get along-wall edge-to-edge snapping.
- Generic free-translate move path aligns by edges (corner anchors).
Pivot moves (2D)
- Polygon kinds (slab/ceiling/zone) move by centroid→cursor via a shared
polygon-centroid mover; stair moves by origin→cursor; matching 3D.
- Shelf/column move targets write position directly (single source of
truth) so the 3D group no longer sticks on commit.
Placement ghosts (2D)
- usePlacementPreview store + FloorplanPlacementPreviewLayer render a
kind's def.floorplan footprint following the cursor; wired for column
and elevator.
Fixes
- Elevator placement no longer deselects the active floor plan
(preserve levelId through setSelection's hierarchy guard).
- Guides clear on every commit/cancel/unmount path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(editor): address architecture review for floor-plan work
- Move usePlacementPreview store from core to editor: placement ghosts
are an editor/tool concern the read-only viewer never needs. Rewire the
column tool (via the @pascal-app/editor public surface) and the
editor-internal elevator tool + preview layer (relative imports).
- FloorplanPlacementPreviewLayer: read scene lazily in ctx.resolve instead
of bulk-reading the nodes map during render.
- wiki/architecture/tools.md: refresh the stale useLiveTransforms-per-kind
note to reflect item/shelf/column (world-plan) + slab/ceiling/zone (delta).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(nodes): recreate window draft on wall:move when null after placement
After a successful click-to-place, the click handler deletes the
transient draft and relies on the wall-rebuild → R3F pointer-enter
cascade to create a fresh draft for the next placement. If that cascade
doesn't fire synchronously (e.g. async geometry rebuild) the next
wall:move receives a null draftRef and bails — requiring leave/re-enter
to place again.
Fix: in onWallMove, when draftRef.current is null but we're hovering a
valid wall, recreate the draft immediately (same WindowNode.parse +
createNode path as onWallEnter). This is idempotent: if wall:enter does
fire first, destroyDraft() in onWallEnter cleans up cleanly.
Preserves parity with door multi-place behaviour, matching #367's intent.
* fix(nodes): recreate door draft on wall:move when null after placement
Mirror of the window fix one commit back: after click-to-place the
DoorTool deletes its transient draft and relies on the wall-rebuild \u2192
R3F pointer-enter cascade to spawn a fresh draft for the next placement.
When that cascade doesn't fire synchronously, the next wall:move sees a
null draftRef and bails \u2014 forcing a leave/re-enter to place again.
Recreate the draft in onWallMove when null and over a valid wall on the
current level. Idempotent with onWallEnter (destroyDraft cleans up if
both fire).
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Pascal <open@pascal.app>
Replace the seven separate display-control buttons in the canvas toolbar
(render, theme, edges, grid, shadows, units, camera) with a single
"Display" dropdown, matching the simplification already shipped in the
hosted editor. Grid, shadows, camera and units are direct toggles that
keep the menu open; render, edges and theme are submenus.
Portal DropdownMenuSubContent so the side-opening submenus escape the
parent content's overflow-x-hidden clip, and re-export the submenu
primitives (Sub/SubTrigger/SubContent/Separator) from @pascal-app/editor.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(nodes,viewer): window preset live wall preview + shaped-cutout move tracking
MoveWindowTool marked the moving window `isTransient` unconditionally, but
WindowSystem only rebuilds the host wall's cutout for non-transient windows —
so a window *preset* (isNew) showed no live hole on the wall and couldn't be
placed consecutively without leaving/re-entering. Guard the transient mark on
`!isNew`, matching MoveDoorTool.
Separately, shaped openings (arch / rounded / `opening`) rebuild their cutout
brush from `node.position`, which a same-wall move doesn't write (it mutates the
mesh directly and publishes to `useLiveTransforms`). The wall-system's
`getEffectiveNode` only merges `useLiveNodeOverrides` (resize arrows), so shaped
cutouts lagged the move while rectangular ones (rebuilt from the live mesh
matrixWorld) tracked. Fold `useLiveTransforms` into door/window children before
collecting cutouts so shaped holes follow the live move too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(editor,viewer,nodes): smooth item placement on shelves
Three issues made hosting an item on a shelf janky (item-on-item was already
smooth because plain items have no `def.geometry` to rebuild):
- Jitter: the draft mesh intercepted the cursor ray, so the shelf-row hit was
re-derived from the moving item each frame. Disable raycasting on the draft
during placement (incl. async GLB children, reconciled per frame) so the ray
passes through to the surface beneath — mirrors MoveRegistryNodeTool.
- Reparent/vanish at the edges: `onShelfLeave` flipped state to floor without
reparenting the draft off the shelf, so the floor strategy's level-local
position rendered compounded with the shelf transform. The grid handler now
owns the shelf→floor transition.
- In/out oscillation: reparenting the draft onto the shelf dirtied the shelf,
and GeometrySystem disposed+rebuilt its boards, making r3f fire a spurious
shelf:leave→enter that thrashed placement between the row and the floor. Add
an opt-in `def.geometryKey` so GeometrySystem skips the rebuild when geometry
inputs are unchanged (shelf boards don't depend on hosted children). Keep the
item sticky by testing the cursor ray against the shelf's bounding box: a ray
that slips through a gap and lands on the floor behind the shelf still counts
as "on the shelf"; only a ray that misses the shelf detaches to the floor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(editor-app): add Shelf to the standalone editor Build tab
The shelf kind is fully wired (def.tool, presentation icon, StructureTool id)
but was absent from the standalone editor's Build palette. Add it between
Column and Spawn Point. Community has its own build-tab and is left untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(mcp): biome-format coordinate-conventions-demo.json
The committed example violated Biome formatting (expanded polygon arrays),
which broke the `mcp-ci` Biome check on main (#356) and every branch since.
Format it so CI is green. Pure formatting — no content change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Legacy/unparsed door nodes can miss schema-defaulted fields (segments, columnRatios, dividerThickness, panelInset/panelDepth …) and crash the door geometry build (EDITOR-AM). `updateDoorMesh` re-applies the Zod defaults once at entry via `DoorNodeSchema.safeParse` (with a drop-bad-segments retry and a full-defaults fallback), so every downstream read sees populated data — including the shaped-top divider path #334 added. Also normalizes schema-valid legacy doors at load in `migrateNodes`, mirroring the existing stair normalizer, so the door panel and window system see defaulted data too.
Reimplemented against main: the original used `segments ?? []` (wrong — legacy doors rendered an empty frame) and missed the #334 `dividerThickness` sites. Verified: `bun run check-types` clean, biome clean on touched files.
Three exporters (STL/OBJ/GLTF) crash when traversing a Mesh with missing/disposed/empty geometry (EDITOR-6H/6G/79). `prepareSceneForExport()` clones the scene and removes ONLY meshes whose position attribute is missing or zero-count — collected in a single traverse and removed afterward (no mutation during traversal). Lines, points, groups, cameras, lights, bones and valid skinned meshes are preserved, so OBJ/GLB output is no longer corrupted. STL/OBJ still throw and GLTF still rejects on other errors — no silent no-op.
Reimplemented against main without the original PR's all-non-mesh pruning, mutate-during-traverse loop, and error-swallowing try/catch. Verified: `tsc -p apps/editor` clean, biome clean.
Guards every `cursorGroupRef.current` dereference in the placement coordinator against the null window where mitt listeners are live but the `<group>` is unmounted (mount/teardown race) — the EDITOR-BC/BD crash family.
`getContext()` falls back to the draft's rotation, so the validation/revalidate path (Shift keys, onKeyUp) is safe; only the cursor *writes* are guarded, so Escape/right-click cancel, Shift reset, leave-state cleanup and transition state still run unconditionally. Also guards `wallPreviewRef` inside `WallTool.stopDrafting()` (the double-click/cancel path that the earlier diff missed).
Reimplemented against current main (the original branch conflicted with #366 and used over-broad handler guards that silently dropped cancel/Shift). Verified: `tsc -p apps/editor` clean, biome clean on touched files.