68e5c6ca67d9e2b77bd002f28cb9ff892cacee04
184
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a0d3d9c701 |
feat(editor): door/window placement-feel polish + placement-state refactor (#411)
Round 8 of the opening-placement UX work. Make door/window placement feel
physical and predictable, and unify the validity/placement logic behind one
shared decision.
UX:
- Window default sill 0.5 m (DEFAULT_WINDOW_SILL_M) so fresh windows float
slightly above the floor; existing windows keep their own sill.
- Dev-only floor "shadow" projection for windows during placement/move
(footprint + dashed drop-line) so an elevated window's plan spot is legible.
- Move SFX: one soft grid-snap click per grid step — identical free-following
over floor or sliding on a wall (keyed on the raw cursor; per-frame + step
dedup), no separate snap cue (that was a "double"). Mirrored into the 2D
floorplan-move so 2D and 3D match.
- Shift = force-place over a collision (commit allowed; ghost stays a red
warning) + free-place (lands at the raw cursor but keeps the alignment guides
visible). Tint flips green/red live when Shift is pressed/released stationary.
- On-wall preview is now the tinted ghost (green placeable / red colliding),
matching the free-follow ghost, instead of a pale solid mesh + thin wireframe.
- R-flip fixes: always toggles (no initial no-op needing a second press),
e.repeat filtered, ghost rebuilds with the live `side`, and the ghost's
on-wall world yaw uses `itemRotation - wallAngle` so it faces exactly what
commit places (cursorRotation was π off for the asymmetric ghost). R ownership
follows the current pointer pane (capture-phase + stopImmediatePropagation in
the 2D overlay) so 3D and 2D never double-flip or go dead.
Refactor / quality:
- New `resolveOpeningPlacement({collides,forcePlace}) -> {placeable,tint}` in
shared/wall-attach-target.ts — the single source of truth the ghost tint AND
the commit gates both consume, so they can't disagree under Shift.
- Consolidated the byte-identical `hasWallChildOverlap` into one shared impl
(door-math/window-math re-export it).
- applyGhost gained a green "valid" tint.
- Removed dead `cursorRotation` from the move-tool targets after the yaw fix.
Docs: "2D <-> 3D behavioral parity" principle in wiki/architecture/tools.md
(+ README + AGENTS.md) — applicable behaviors must exist in both views.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
3d5a36c73d |
feat(render-modes): items show real materials in colored mode (not flat clay)
In colored mode (textures on), every item now renders its authored material — textures, vertex colours, and default colours — instead of being stripped to the uniform off-white baseMaterial. AI-generated and vertex-coloured items show their real appearance; slot resolution (override -> curated -> authored) is unchanged. Monochrome (textures off) still collapses to the furnishing clay role colour (the escape hatch). Drops the now-unused isAuthored marker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c9121d322c |
Merge origin/main (#407 placement restructure) into opening-proximity-guides
#407 ("Always-visible placement ghosts + true-nearest 2D opening snap") restructured the door/window placement tools: it split the old create-in-resolve into a pure resolveWallPlacement() + side-effecting applyWallTarget(), added an off-host floating ghost (fallbackPose / showGhostAt), unified wall hover into onWallHover, and extracted commit{Door,Window}AtWall. Conflict resolution (door/tool.tsx, window/tool.tsx): - Re-homed the single publishOpeningGuidesForWallEvent() call into applyWallTarget (after the draft update + updateCursor), using that scope (wall, getSlabElevationForWall(wall)); door includeVertical:false, window true. - Routed clearOpeningGuides3D() through showGhostAt so every off-host fallback path clears; kept clears in hideCursor, commit helpers, onRoofHover, teardown. - Made the window sill snap (resolvePlacementY) event-free and call it from the pure resolveWallPlacement, so hover + click both get sill/centre/top snapping; Shift bypasses, the moving draft is excluded via ignoreId. - Dropped the branch's inline onWallClick in favour of #407's onWallClick + commitWindowAtWall (no behavior lost). - Reconstructed both files' import blocks, which the auto-merge had truncated to stubs (only tsc caught it). All other conflicts auto-merged (registry types, floorplan-registry-layer, both move-tools). Verified: typecheck 9/9, biome clean, nodes 169 + core 594 tests pass, editor `bun run build` 7/7. Merge resolution reviewed by Codex (adversarial): no semantic regressions; all #407 behavior preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
69aa272004 |
perf(editor): stabilize 3D opening-guide rendering, remove per-tick GPU churn
Reuse one THREE.Line + preallocated position buffer per guide slot, mutating endpoints in place each drag tick instead of rebuilding the geometry, line, and two Vector3s and re-uploading the GPU buffer every frame. Key guides by a stable semantic id (sill / head / gap:side / vertical / spacing:i) so a slot that persists keeps its React element and drei <Html> pill mounted as the guide set churns, rather than remounting under shifting index keys. Also: make useOpeningGuides.clear() a no-op when already empty so the common no-guide hover frame doesn't push a fresh [] and re-render to the same nothing; dispose the move-tool cursor EdgesGeometry on unmount; and memoize the placement-tool cursor EdgesGeometry (static fallback dims) so it isn't reallocated and orphaned on every render during placement. Reviewed by Codex (peer + adversarial): no correctness, hook-order, or GPU-leak regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1628b728fa |
Always-visible placement ghosts + true-nearest 2D opening snap (#407)
* feat(editor): always-visible translucent placement ghost for openings + roof accessories When a host-surface placement tool is armed, the node's real geometry now follows the cursor everywhere as a translucent ghost: tinted invalid (red) and unconfirmable off-host, snapping onto its host surface (wall/roof) with the existing valid/invalid affordances when near. This replaces the old red wireframe box (door/window) and red DragBoundingBox (roof accessories), so the armed tool is visible before the cursor reaches a placeable surface. - New shared `applyGhost` helper (nodes/src/shared/ghost-materials.ts): clones materials, disables raycast (avoids cursor-ray starvation), tints invalid; cleanup disposes only the clones. - New door/window preview components built from the real geometry via new `buildDoorPreviewMesh`/`buildWindowPreviewMesh` viewer exports; tools float the ghost via a `fallbackPose` that is mutually exclusive with the on-host draft + wireframe outline. - `RoofAttachmentFallbackPreview` gains a `ghost` prop; all 11 roof-accessory tools pass their real preview (invalid-tinted) instead of a box `size`. Snapping behavior is unchanged (no proximity snap yet). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): magnetic proximity wall-snap for door/window placement The door/window ghost now follows the cursor over the floor like a moving item and magnetically snaps onto the nearest wall within range (1.5 m), then releases back to free-follow when the cursor moves away — instead of only attaching on a direct wall-mesh ray hit. A grid-snap sound plays each time it snaps onto a new spot, so it reads as moving a physical object that can only land on walls. - Plan-space proximity via the existing `findClosestWallInPlan` (the same helper the 2D floor-plan move uses): level-scoped, skips curved walls, returns wall + along-wall localX + side + wall-local rotation. - `grid:move` drives the snap and `grid:click` commits when proximity-snapped; a direct wall-mesh hover (wall:enter/move) still owns the precise face side. Both paths share `applyWallTarget` (create the draft once, reparent only on an actual wall change) and a shared commit that refreshes alignment anchors. - Disambiguation without a stuck flag: a per-pointermove `timeStamp` gate (R3F + the grid raycast share the source DOM event) plus a `cameraDragging` guard and stale-`hostKind` reset, so a missed wall:leave during a camera orbit can't strand the draft. - Window keeps its sill height on the floor path (the floor cursor carries no wall-face Y) — defaults to a ~0.9 m sill, mirroring the 2D move. - Shift bypasses the along-wall grid/alignment snap but still attaches to the nearest wall, matching the 3D-hover and 2D-move conventions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): door/window preset placement follows the cursor over open floor The community preset/catalog flow places doors and windows through the isNew move path (MoveDoorTool / MoveWindowTool), which had no free-follow: the fresh clone was parented to the level at the origin and only became visible once the cursor reached a wall, so over empty floor nothing tracked the cursor. Now the move tools mirror the def.tool placement behaviour: - Off-wall, the real node rides the cursor like an item (reparented to the level, positioned at the building-local cursor) so it's obvious what's being placed before it attaches. - Within range of a wall it magnetically snaps on via findClosestWallInPlan (the same plan-space helper the 2D move uses), releasing back to free-follow when the cursor moves away, and plays the grid-snap sound on each new snap. - grid:click commits only when snapped (open floor is a no-op — a door/window needs a wall); the wall/roof mesh-hover paths are unchanged and still own their own click. A per-pointermove timeStamp gate + cameraDragging guard keep the floor handler from fighting a wall/roof hover. - Windows default to a ~0.9m sill while off-wall (fresh preset clones carry position [0,0,0], which buried half the window below the floor). The wall/roof commit body is extracted into a shared commitToWall so the mesh-click and proximity-click paths stay identical. Existing-node moves are fully restored on cancel/unmount (the node stays isTransient through free-follow). Standalone-editor def.tool placement already had this in a prior commit; this brings the community move path to parity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): 2D floorplan ghost follows the cursor for door/window placement Mirrors the 3D free-follow in the top-down floor plan: while placing a door or window, a loose footprint rectangle now follows the cursor over open floor so it's obvious what's being placed before it snaps to a wall. The instant the cursor nears a wall, the existing synthesized wall:enter/move path takes over and the real on-wall door/window symbol (swing arc, etc.) replaces the ghost. - The opening-placement pointer-move handler in floorplan-panel sets a new `openingGhostPoint` on the off-wall (findClosestWallPoint miss) branch and clears it on a wall hit; a loose width × 0.1m rectangle renders at that point inside the floor-plan scene group (same world→SVG transform as every glyph). - Width comes from the moving node or the kind default (door 0.9 / window 1.5). - The ghost clears when opening placement ends (tool/mode change, cancel, commit) and on level change, so no stale rectangle lingers. Deliberately a plain rectangle, not the full swing-arc symbol: off-wall there's no host to orient the swing to. The shared door/window def.floorplan builders are untouched — overloading them with a wall-less fallback would make roof-hosted doors (parent is a roof segment, builder returns null today) draw stray rectangles in plan. Keeping the preview in the editor layer avoids that. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): faithful 2D door/window ghost, 2D wall-slide, R-flip during placement; 3D snap only on hover Three placement fixes plus a snapping revision, for both the standalone def.tool path and the community isNew move path (door + window): - 2D faithful ghost: the off-wall placement ghost now renders the real blueprint symbol (door swing arc / window panes) following the cursor, not a bare rectangle. Done by publishing a transient opening on a synthetic wall to usePlacementPreview (extended with a `parentNode` fed as the builder's ctx.parent) so the real def.floorplan builder draws it. Cleared on wall-hit, on commit, on placement-inactive, and on level change. - 2D slide-along-wall: the floor-plan registry layer ignored useLiveTransforms for door/window (only floor-placed + slab/ceiling/zone), so a same-wall slide updated the 3D mesh but left the 2D symbol frozen. It now merges the wall-local live position/rotation onto the node (keeping parentId) so the 2D symbol slides with the cursor. - R-flip during placement: pressing R now flips a door/window's facing (front ↔ back, rotation += π) before commit — the placement tools own R while placing (the global selection-based R/T handler stands down via isPlacingOpening so it can't double-fire). No-op on roof faces (front-only). - 3D snapping zero-padding: removed the 1.5 m proximity magnet; in 3D the opening free-follows the cursor over open floor and snaps only when the cursor ray actually hovers a wall/roof mesh (big raycast targets). 2D keeps its 0.5 m findClosestWallPoint padding since plan walls are thin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): make FloorplanRegistryMoveOverlay the sole 2D owner of door/window placement Community door/window placement (movingNode + metadata.isNew) had TWO 2D paths running at once: the floorplan-panel synthesized wall:*/grid:* events (driving the 3D MoveDoorTool) AND FloorplanRegistryMoveOverlay via def.floorplanMoveTarget. They fought — R didn't flip the 2D symbol and clicks didn't commit in 2D, while 3D worked. The overlay + floorplanMoveTarget is the purpose-built 2D owner (faithful def.floorplan symbol, plan-space CTM coords, Figma snap, single-undo commit), so it now owns 2D opening placement when movingNode is set: - floorplan-panel: the opening pointer-move branch + the registry grid catch-all + the background-click catch-all all now exclude the door/window MOVE case (`!isOpeningMoveActive`), so the synthesized events no longer fire for it (they still drive pure raw-build placement, which has no movingNode). Without the catch-all exclusions the move case fell through to grid:move/grid:click, which re-drove the 3D tool's free-follow and consumed the commit click. - R-flip in 2D: `FloorplanMoveTargetSession` gains optional `flipSide()`; door/window floorplan-move implement it (XOR the wall-derived side + π rotation, re-running the last apply). The overlay's keydown calls `session.flipSide()` on R — gated on `hasMovedSinceStart` so it only fires when the 2D pane is the active mover (the 3D MoveDoorTool owns R in 3D/split; this prevents a double flip / double cue on one R press). - Commit in 2D now flows solely through the overlay's pointerup (no competing synthesized wall:click), so click-to-place commits. The global use-keyboard R/T already stands down during opening placement (isPlacingOpening), so a selected node can't also flip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): 2D door/window move free-follows the cursor off-wall and commits only on a wall Moving an existing door/window in the 2D floor plan: the move target's `apply` early-returned off-wall (`if (!hit) return`), so the opening stayed frozen on its old wall instead of following the cursor between walls (3D free-follows), and an off-wall confirm click committed the stale last-wall position — looking like the placement failed. Now `doorFloorplanMoveTarget`/`windowFloorplanMoveTarget` mirror the 3D move: - Off-wall, `apply` free-follows the cursor — hides the real node and floats the faithful door/window symbol at the cursor via a synthetic wall published to `usePlacementPreview` (the same preview layer fresh placement uses). The real node is `visible:false` so the registry layer skips it (no double symbol). - Back on a wall, it clears the ghost, reveals the real node, and snaps as before. - `canCommit` returns false while off-wall, so an open-floor click reverts to the pre-move snapshot (door returns to its wall) instead of committing in mid-air — matching the 3D move, where clicking open floor commits nothing. On a wall the commit lands normally. The overlay's snapshot revert (cancel / invalid commit) and the on-wall `apply`'s `visible:true` restore both guarantee the node is never left hidden after a move. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): 2D opening snap picks the true nearest wall, with a tighter radius The 2D door/window snap felt too aggressive and could grab a wall further away than the one the cursor was actually nearest. Root cause in `findClosestWallInPlan`: it compared a candidate's true segment distance against the previous best's `perpDistance` (signed offset to the wall's infinite line, not the clamped segment distance). Near a wall end those diverge, so a closer wall could be rejected / a farther one kept. - Track the best segment distance and keep the strict minimum — the wall chosen is now always the single closest segment to the cursor (true nearest), which resolves correctly when many walls sit close together. - Tighten the snap radius from 1.5 m to 0.4 m: plan walls are thin, so the old radius snapped from far away. The opening now free-follows the cursor until it's genuinely near a wall. Only the 2D move/placement targets use this helper (3D snaps on raycast hover); wall-attached items share the same improved nearest-wall behaviour. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): true-nearest 2D opening snap with dev hit-area overlay Round 7 of the placement-ghosts work. Make the 2D door/window wall snap always pick the wall nearest the cursor, fix fresh-placement snapping/slide in 2D, and add a dev-only overlay that visualises each wall's snap region. - Extract the plan-space nearest-wall-segment math to core (`lib/wall-distance.ts`: collectLevelWallSegments / closestOnSegment / nearestWallSegment / WALL_SNAP_DISTANCE_M). `findClosestWallInPlan` delegates to it, so the snap and the debug overlay share one source of truth. WallHit contract unchanged. - door/window 2D move now resolves the host level via the shared `getOpeningHostLevelId` (wall-hosted, roof-hosted, AND fresh-placement parented straight to the level — the last case previously resolved to the building, so a new opening never snapped in 2D). - Cursor resolver switched to absolute mode: query the snap with the true cursor, not the original-wall position + grab delta, so it picks the cursor-nearest wall (matching the 3D move) instead of a far wall across a thin gap. - 2D move clears any stale `useLiveTransforms` entry for the node each apply: the registry layer renders door/window from the live transform in preference to the scene node, so a leftover entry from the 3D tool froze the 2D slide for fresh / re-armed openings. - Fresh window defaults to a 0.9 m sill in 2D (was sitting half-below floor at y=0), matching the 3D MoveWindowTool. - New dev-only FloorplanVoronoiLayer + `show2dVoronoi` editor flag: draws each wall's snap hit area as an analytic capsule (no grid sampling), gated on a developer-menu toggle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d52cbe6757 |
feat(paint-slots): read pascal_material glTF extras as curated slot default
Insert the plan's middle resolution step for item slots: a curated default baked into the GLB as a per-material 'pascal_material' extra (three's GLTFLoader copies material extras to userData). Resolution per slot is now: node.slots[slotId] override -> pascal_material curated default -> authored GLB material. Bare ids resolve as library:<id>; unresolved refs fall through. Runtime reads only the GLB (no DB). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6d5f041b48 |
fix(walls): bound wall miters, heal corrupt scenes, follow grid snap
Three related editor fixes surfaced while debugging a captured house project that rendered an infinite wall and failed to load. Infinite wall (core/systems/wall/wall-mitering.ts): Junction miters are line-line intersections, so the joint point sits ~halfThickness/sin(theta) from the junction. The only guard was an exact-parallel check (det < 1e-9), so two walls meeting at a shallow angle (a room-preset preview dragged onto an existing wall, or a wall drawn nearly collinear to its neighbour) produced a joint point far away — an infinite spike. Add a miter limit: reject joints farther than 10x half-thickness from the junction and fall back to a square joint, exactly like the parallel case. Scene load failure (core/utils/heal-scene-graph.ts + validate-build-json + use-scene migrateNodes): Capture wall-merge could leave a `children: [null]` entry (see the matching merge-walls.ts fix in private-editor) and zero-length walls. `null` children fail wall schema validation, so the whole scene fails to load. Add a shared heal step — strip non-string child refs, drop childless zero-length walls — run on every load path: import validation now repairs instead of hard-failing (with a warning), and setScene heals on the prod project-load path too. Grid snap (nodes slab/ceiling/spawn tools): These tools hardcoded a 0.5 m snap (Math.round(x*2)/2) and ignored the editor's grid-snap setting, so the cursor jumped by 0.5 while later vertices already followed the configured step. Route them through snapPointToGrid / snapScalar with gridSnapStep. Adds unit tests for the miter limit and the heal step. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
86e9b3c8bf |
feat(editor): placement-time + resize-time opening guides
Extend the opening proximity guides to two more interactions:
- PLACEMENT: the door/window placement tools publish the same 3D guides
(sill/head, edge proximity, sill alignment, equal-spacing) while a NEW
opening is being dropped; window placement also snaps its sill to a
neighbour's sill/centre/top (Shift bypass) — so "two windows aligned" reads
during placement, not just move.
- RESIZE: a new `onDrag` hook on the linear-resize handle descriptor lets the
door/window width/height arrows publish live guides for the edge being
resized — proximity to neighbours as the width grows, and the live sill/head
as a window's height changes. The generic LinearArrow stays kind-agnostic;
only door/window declare the hook.
Refactor (Codex review follow-up): one `publishOpeningGuidesForWallEvent`
wrapper now backs all four wall-event publish sites (door/window move +
placement) over a shared `makeWallToWorld`; window placement's repeated
sill-snap is a single `resolvePlacementY` helper. Opening guides clear on
commit / leave / cancel / roof-hover / unmount (mirroring the alignment-guide
lifecycle) and on resize end.
Codex-reviewed — no blockers; lifecycle/leaks, coordinate frame, sill-snap
precedence, and resize disposal confirmed. Typecheck + biome + 23 core + 170
nodes tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7afb286e47 |
feat(paint-slots): authored item materials + unified slot painting
Phase 1 + paint unification of the paint-slots plan. - core: scene-material data layer (materials map mirroring collections, undo/partialize/setScene full-graph support), SceneMaterial schema, scene:/library: MaterialRef helpers + parseMaterialRef, slot id helpers (deriveSlotId/slotLabelFromId), slots map on ItemNode, hitObject on PaintResolveArgs, optional PaintCapability.commit. - viewer: resolveMaterialRef (library:/scene: -> three material, null on dangling). - nodes(item): renderer keeps authored GLB materials for slot-authored assets and applies per-slot overrides per-instance (never mutates the shared cached GLB); textures-off still collapses to furnishing role; non-authored items unchanged. Item paint capability + registration. - editor: item joins the unified (nodeId, slotId) paint dispatch; item paint target + slot reset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6caba97f1b |
feat(editor): 3D viewport proximity + sill + equal-spacing guides for openings
Wire the opening-guides service into the 3D door/window move tools and render the
wall-plane guides as the spatial twin of the 2D plan guides:
- sill / head height (floor → bottom edge, top edge → wall top) — windows only
- edge-to-edge proximity dimensions to the nearest neighbour each side
- a sill-alignment line + SNAP when a window shares a neighbour's sill / centre
/ top (competes with the 0.5m grid, Shift bypasses) — the chosen
"snap + guide" behaviour
- Figma-style equal-spacing "=" badges across a run of openings
Adds `useOpeningGuides` (editor store) + `OpeningGuides3DLayer` (raw THREE.Line
overlays + Html pills, mounted beside Alignment3DGuideLayer) and a thin
`opening-guides-runtime` helper (collect siblings / sill snap / publish / clear)
called from the door + window move-tools at their per-tick `applyPreview` hook;
guides clear on commit / cancel / leave / roof-hover / unmount.
Guides render in the move cursor's building-local frame (reuses `wallLocalToWorld`)
so they track the dragged opening exactly. Codex-reviewed (roof-hover stale-guide
clear, collapsed-dimension suppression). Placement-time guides reuse the same
helper and are the next step.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
3ac6b27eca |
feat(editor): 2D plan proximity + equal-spacing guides for openings
Route the door/window floor-plan placement dimensions through the new
opening-guides service:
- edge-to-edge clearance to the nearest neighbour (or wall end) on each
side, now with overlap suppression (previously nearest-only, ad-hoc).
- Figma-style equal-spacing — a "=" badge per gap on the wall centreline
whenever the moving opening is part of a run of 3+ (near-)equally-spaced
openings.
Adds the `equal-spacing-badge` FloorplanGeometry primitive, its 2D renderer
(distinct pink accent), and overlay registration. Shown while placing/moving.
Sill height + vertical alignment are 3D-only (a top-down plan has no vertical
axis) and land in the next phase.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
1f829d52ed |
feat(editor): draggable move handle for wall-hosted doors & windows
Doors and windows could only be moved via the floating action menu — their 3D handle rig declared width/height resize arrows but no move grip, and Ctrl/Meta-drag was a no-op for them. Add a press-drag move cross and make direct-drag work for every bespoke-mover kind. - door/window: add a `tap-action` `move-cross` handle (plane node-normal, portal grandparent, `engageMoveDrag`) mirroring the item wall grip. It routes through the existing per-kind move tool (3D `affordanceTools.move`, 2D `floorplanMoveTarget`) — wall-bound slide + re-host onto another wall — so the grip, the floating Move button, and the 2D plan's move dot share one pipeline. Grab-drag-release commits without a second click. - canDirectMoveNode: gate Ctrl/Meta-drag on `movable || affordanceTools.move` (the 3D-mountable move paths) instead of `movable` only, so doors/windows/ walls/slabs/stairs/… are draggable in 3D as they already are in 2D. Floorplan-only movers (zone) stay excluded — no 3D tool mounts. The floating helper auto-syncs (it reads canDirectMoveNode). - TapActionArrow: honor `plane: 'node-normal'` by tilting the move cross [π/2,0,0] into the wall face — previously ignored, so the item wall grip rendered flat too. Now door/window/wall-item crosses lie in the wall. - use-node-events: split the drag-suppression gate. `inputDragging` still suppresses SELECTION events (the synthesized release-click would re-select), but no longer suppresses SPATIAL events (enter/move/leave) — a surface-following move tool runs with `inputDragging` set and needs wall:move to track the cursor. General consumers that must ignore drags (viewer hover, box-select) already self-gate on `inputDragging`; the editor's select-hover and paint-preview enter handlers now gate on it too. - handle-arrow: make handle hit areas inert while `placementDragMode` is set, so a move grip riding the dragged node can't intercept the ray and starve the move tool's surface raycast. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cf24b62c44 |
fix(viewer): UI flicker on camera move from interactive overlays + dirty-mark leaks (#401)
* chore: sync bun.lock with 0.9.1 workspace versions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(viewer): stop interactive overlays from starving frames and flickering the UI Every interactive item mounted a drei <Html occlude> overlay unconditionally — invisible (opacity 0) when no zone was selected, but still alive. With `occlude` as a bare boolean, drei raycasts the entire scene per overlay on every camera-move frame, and rewrites each element's z-index while toggling display when the occlusion flips. On scenes with hundreds of interactive items (recessed lights, ceiling fans) this starved the frame budget and made the whole DOM UI blink during camera moves while the WebGPU canvas stayed healthy. Overlays now mount only while a zone is selected and the item sits inside its polygon, fade in/out over 300ms (the child components stay rendered so the exit transition can play before the <Html> unmounts), and drop `occlude` entirely. eps=-1 works around a drei mount bug: its mount path writes the element transform without the distanceFactor scale, and with a static camera the eps guard never re-applies it, so freshly mounted overlays stayed mis-scaled until the camera moved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(viewer): break down dirty nodes by kind in the perf overlay DIRTY now reads e.g. "29 (12 wall, 9 ceiling, 8 item)" — sorted by count, only non-zero kinds, with a "missing" bucket for dirty ids whose node no longer exists. Makes dirty-mark leaks attributable at a glance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(core): skip dirty marks for kinds with no dirty consumer dirtyNodes is consumed by GeometrySystem (def.geometry), FloorElevationSystem (capabilities.floorPlaced), and the legacy per-kind viewer systems. Site, building, level, zone, and guide match none of those, so their marks were never cleared: they accumulated for the whole session (every child create/delete dirties its parent), permanently defeated every consumer's empty-set early exit each frame, and polluted the perf overlay's DIRTY readout. NodeDefinition gains an explicit dirtyTracking?: boolean opt-out (default tracked — no derivable predicate exists since wall's dirty consumption lives in the viewer while zone/guide/level declare def.system for unrelated per-frame work). markDirty consults the registry; the five structural kinds opt out. Also fixes a second leak: deleteNodesAction never removed deleted ids from the dirty set, and every consumer skips missing nodes without clearing them, so marks on deleted nodes lived forever. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style: replace concise-arrow forEach with for...of in deleteNodesAction biome's useIterableCallbackReturn rejects forEach callbacks that implicitly return a value (Set.add / clearDirty). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5411f5abc8 | fix(editor): improve guided manipulation and snap affordances | ||
|
|
aab48e053f |
fix(editor): harden editor interactions and WebGPU rendering
Fix editor bug sweep regressions, WebGPU CSG/material crashes, Shift snap bypass behavior, arrow handle drag projection, and the wall preview null guard covered by the Sentry follow-up PRs. |
||
|
|
ee7bb39605 |
style: biome format/import-order for refactored tool files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
aa3b0ef758 |
refactor: release-review cleanup for roof wall openings
Dual review pass (Claude multi-angle + Codex release-quality). One correctness fix and the agreed do-now cleanups: - fix: clone-scene-graph remaps roofSegmentId like wallId in both clone paths — duplicated scenes/levels kept pointing roof-hosted children at the original segments. - extract the settled, stateless roof target/cursor math shared by the four door/window tools into shared/roof-wall-opening-placement.ts (resolveRoofWallOpeningTarget + getRoofWallOpeningCursorPose + worldToSelectedBuildingLocal); tools keep the stateful lifecycle (drafts, undo/temporal, commit field lists). −199 net lines. - rename host-generic state: currentWallId→currentHostId, markWallDirty→markHostDirty (they hold segment ids too); capability cascadesViaHostSegment→dirtyHandledByOwnSystem (behavior-facing, before the public API hardens). - drop getRoofAccessoryKinds from core's public API — its only caller was the standalone Build tab, which now enumerates the registry inline with its app-specific filter. - window move-tool uses the shared stripPlacementMetadataFlags; stale "segment-local" comment fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b8dfa90762 | Merge remote-tracking branch 'origin/main' into feat/editor-ux-rendering-placement | ||
|
|
7879b83df3 |
feat: face-frame hosting for roof wall children + items on roof walls
Wall-mounted items join doors/windows on roof-segment wall faces, and the storage model moves to FACE-LOCAL coordinates so hosted children track segment edits live. - Children store roofFace + position [u, v, z-from-mid-plane] with rotation 0 in-frame — the exact wall-child conventions (the wall volume's mid-plane lands on the nominal footprint). A shared <RoofFaceHostFrame> derives segment pose + face frame from the live-override-merged segment: children follow resize handle drags in real time and never jump on commit. No re-anchor cascade needed — position is authoritative, the frame is derived. migrateNodes converts branch-era segment-local data. - Items: roofWallStrategy + roof:* handlers in the placement coordinator (surface 'roof-wall'), Shift free-place normalized with walls, ItemSystem wall-side push extended to segment hosts, correct 2D plan glyphs via face→segment→roof pose composition. The roof hit resolver + overlap guard moved to @pascal-app/editor (the coordinator lives there; nodes already depends on editor). - Cuts: subtractAccessoryCuts extracted and applied in BOTH the merged-shell and per-segment CSG paths (full edit mode / painted segments used to lose every hole), built from the CURRENT host geometry and live-effective children so holes follow segment and opening drags. - Handle rig: the grandparent portal now maps the node's world pose into the portal frame instead of composing parent+node registry poses — correct for any nesting (the face-frame group broke the old assumption), identical for walls. - Host-field hygiene: useDraftNode.commit/adopt and the window panel duplicate forward roofSegmentId/roofFace/wallId; every roof↔wall re-anchor clears and every revert restores them. Codex-reviewed (design consultation, adversarial rounds on the replaced cascade and on this refactor); frame conventions locked by unit tests. Record: private-editor plans/editor-roof-wall-openings.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5fd9be05df | Fix spawn floorplan move handle | ||
|
|
d36e8fcc78 |
fix: zone grid snapping, shelf position panel, deselect on item placement (#394)
* 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> |
||
|
|
11135b608b | fix: respect imperial units in wall panel (#321) | ||
|
|
bdfee058bd |
feat: doors and windows on roof-segment wall faces
Openings now host on the walls a roof segment generates — the base walls under the roof and the coplanar gable/shed/gambrel end faces, so a window can sit in a gable pediment. - core: roof-segment-walls.ts models the four vertical faces as 2D frames (u along face, v height) with convex profile polygons that mirror the wall volume getRoofSegmentBrushes builds; rect-in-profile clamping and anchored resize limits via half-plane algebra. - schemas: optional roofSegmentId on door/window; position is the segment-local wall mid-plane center, rotation[1] the face yaw. - cut: reuses capabilities.roofAccessory.buildCut; new cutScope: 'wall' subtracts from the wall brush only. cascadesViaHostSegment keeps the roof-merge loop from consuming door/window dirty marks (their own systems cascade via parentId). - tools: roof:* handlers in door/window tool + move-tool (the Build-tab preset path), with roofSegmentId cleared/restored across every roof<->wall re-anchor and revert; shared hit resolver normalizes normals through world space (merged mesh vs painted segment frames). - fix: RoofSystem no longer rebuilds per-segment CSG in accessory-reveal mode — the uncut rebuild used to draw over the merged shell's fresh opening until deselect. - fix: the walkthrough collider world now prunes by renderer-effective visibility; stale uncut segment CSG inside the hidden segments-wrapper blocked the player at openings the merged shell had cut through. Known gap: painted segments render per-segment CSG without accessory cuts (pre-existing, also affects skylight/dormer). Twice Codex-reviewed; details in private-editor plans/editor-roof-wall-openings.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9bfd75c2c7 |
chore(release): align core/viewer/editor to 0.9.0 ahead of v0.9.1
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> |
||
|
|
e81dc63b28 | feat: sync surface polygon editing | ||
|
|
8d8052297c | Merge remote-tracking branch 'origin/main' into chore/deps-2026-06 | ||
|
|
92078741cd | feat: add ceiling wall snap feedback | ||
|
|
50304d7d10 |
Support template scans and room preset workflows (#390)
* 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 |
||
|
|
f3c1407cae | fix: sync ceiling handles during polygon edits | ||
|
|
265acdb2be | fix: improve ceiling handle feedback and WebGPU placeholders | ||
|
|
9d24e260c3 |
chore(deps): June 2026 dependency refresh
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> |
||
|
|
555cf06c12 |
editor: building rotation, alt-click single wall, world-grid alignment, tap-to-engage move (#388)
* 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> |
||
|
|
b33059ef81 | feat: add persistent site boundary handles | ||
|
|
ce6f999310 |
arch: enforce layer boundaries — ceiling dispatch, store relocation, shared helper (#382)
* 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> |
||
|
|
73cd6e8614 | fix(editor): strip placement metadata on commit | ||
|
|
8dc602caa9 | Improve editor manipulation flows | ||
|
|
6b8dc33b62 | fix: restore walkthrough collisions and spawn controls | ||
|
|
be0f491bbd | Fix relative move drag offsets | ||
|
|
03f57b1f4f | Fix stair opening previews and slab cutouts | ||
|
|
21b79c0d62 | Snap roof segment resize handles to grid | ||
|
|
f84910848b | Improve roof node editing | ||
|
|
3750679d48 | Add wall placement axis angle guides | ||
|
|
16e09d778d | Fix auto ceiling height sync | ||
|
|
1419709131 |
editor: structure build-start/end cues + menu hover/click SFX (#378)
* 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> |
||
|
|
8f2e5678e8 |
ci: add lint and typecheck workflow (closes #147) (#369)
* 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> |
||
|
|
0b338cf647 |
feat(editor): live floor-stacking, unified handle system, slab-hole editing + interaction polish (#375)
- 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. |
||
|
|
d1b40aa98d |
editor: level-scoped alignment, reference-floor symbols & registry slab tool (#373)
* 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> |
||
|
|
46f94b97b3 |
editor: alignment guides + floor-plan move/placement parity (#372)
* 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> |
||
|
|
ee98c55b65 |
Polish: window preset preview + smooth shelf item placement + Shelf build tool (#367)
* 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> |
||
|
|
cefcb013d9 |
fix(editor): null-safe cursor-group access in placement coordinator (#323)
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. |