267dac7b1aef8ecab806d34aa300b20c77cdb03b
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5551500d98 |
feat: HVAC ductwork + DWV plumbing systems (#402)
Adds two new MEP node families (HVAC ductwork, DWV plumbing) built on a shared port-connectivity model. Co-authored by @sudhir9297. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
eb1f2d12c2 |
feat(nodes): roof accessories — gutters, downspouts & vents + gizmos (#355)
* Add roof surface placement support for items Items (e.g. solar panels) can now be placed on sloped roof surfaces. The placement system computes euler rotation from the roof surface normal so items sit flush on the slope instead of going inside. - Add roofStrategy to placement-strategies with enter/move/click/leave - Wire roof:enter/move/click/leave events in the placement coordinator - Add calculateRoofRotation in placement-math using surface normals - Support full 3D cursor rotation for sloped surfaces - Items on roofs are parented to the level with world-space rotation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fixed conflict * fix(editor): ceiling-attached item placement from 2D floor plan The 3D viewer drives ceiling-item placement via ceiling:enter/move/click raycast events on the ceiling mesh. The floor plan has no such mesh, so ceiling-attached items (lights, fans) never transitioned out of surface: 'floor' — the draft sat at floor height while the 2D cursor moved freely, reading as a 2D/3D sync bug. Synthesise the same ceiling events from 2D plan points by hit-testing ceiling polygons on the active level, and publish the building-local cursor (not world-space) to useLiveTransforms so the floorplan registry override renders the draft under the cursor regardless of building position / rotation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(editor): chevron arrows render on SCENE_LAYER so ink-edge shader outlines them Node arrow handles and polygon-editor edge arrows were tagged for EDITOR_LAYER, which hides them from the post-processing scenePass — the ink-edge shader reads the depth/normal MRT from that pass, so the chevrons rendered flat with no outlined edges. Drop the EDITOR_LAYER tagging on both, matching the wall-height arrow which already stays on SCENE_LAYER for the same reason. Pair with depthWrite: true on the chevron materials so their silhouettes enter the depth buffer; depthTest stays off to keep the chevron drawn on top of underlying geometry. Without depthWrite, only the normal-discontinuity branch of the ink shader can detect the chevron, and the lines drop out when faces align with whatever sits behind them in screen space. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(editor): ceiling grid overlay no longer blocks selecting items under it Two issues kept the ceiling grid overlay covering the room after the user moved on from a ceiling-related action: 1. CeilingSystem treated any selected descendant of a ceiling as "reveal the grid" — so after placing a ceiling light and the new item became selected, the grid stayed on and its mesh intercepted every subsequent 3D click, re-selecting the ceiling instead of the items below. Restrict the reveal to directly-selected ceilings. 2. The ceiling top material used the opaque surface-role material, so a top-down camera lost view of everything under the ceiling the moment the overlay turned on. Swap the top material for the transparent grid-pattern material (bottom stays opaque so the in-room view still reads as a solid surface). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(core): curve- and thickness-aware wall/slab overlap detection `wallOverlapsPolygon` and `getSlabElevationForWall` previously treated a wall as an infinitely thin chord from start to end. Two failure modes fell out of that: 1. Curved walls whose chord lies outside the slab but whose centerline bows into the slab interior were missed entirely. The wall stayed at Y=0 while the slab elevation moved, and `markNodesOverlappingSlab` never re-dirtied it when the slab Y changed. 2. Perimeter walls of a room — whose centerline sits exactly on (or just outside) the slab's polygon edge — also missed detection, because pointInPolygon on the boundary is unreliable. Half the wall's body is inside the slab; it should follow the slab elevation. Switch `wallOverlapsPolygon` to a wall-shaped input (start/end + optional curveOffset + thickness), sample the centerline for curved walls, and add a ±halfThickness perpendicular test for straight walls. Threaded through `getSlabElevationForWall`, the wall system, and the `markNodesOverlappingSlab` pass. Legacy chord-only call shape preserved for callers that don't yet have a wall in hand. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(editor): show Move button for legacy-movable kinds in floating action menu `isRegistryMovable` only sees kinds wired through `capabilities.movable`, `floorplanMoveTarget`, or `affordanceTools.move`. The legacy tail of `MoveTool` (tools/item/move-tool.tsx) still handles roof, roof-segment, stair, stair-segment, building, and elevator, but the floating action menu was hiding the Move button for them because the registry check returned false. The mover worked once invoked — the entry point was missing. Add a `LEGACY_MOVABLE_KINDS` set alongside the registry check so those kinds get the Move button until they migrate onto kind-owned affordances; drop a kind from the set once it does. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(nodes): in-world registry handles for roof accessories + axis-stable surface basis Adds the registry-driven chevron / tracker / rotate-gizmo handle set to skylight, solar-panel, chimney, and roof-segment so every roof-mounted kind has consistent in-world manipulation, and fixes the underlying renderer + surface-basis bugs that made those handles land on the wrong spot or behave inconsistently across mirrored slopes. Handles - `LinearResizeHandle` gains `shape: 'arrow' | 'tracker'`. `'tracker'` renders a dashed vertical leader from the surface up to a draggable cube, reusing the linear-resize drag pipeline. Roof-segment's wall-height handle adopts it. - Roof-segment: width chevrons split into two asymmetric handles (each grows its own edge, opposite stays world-fixed via `apply` recomputing `position`). - Skylight: width × 2 (asymmetric), height × 2 (asymmetric), curb-height tracker, rotate gizmo (corner, lifted off surface), and a diagonal frame-thickness chevron at the -X+Z corner. - Solar panel: same six handles operating on total array dimensions (back-solving `panelWidth` / `panelHeight` from `columns` / `rows`), plus a frame-depth chevron above the array. - Chimney: registry handle set following the same idioms. Skylight / solar-panel renderer - Collapse the previously nested `position → surfaceQuat → rotation-y → rotation-x` groups into a single registered transform group whose local `position` + composed `quaternion` carry the full pose in segment frame. The registry handles read this Object3D's local matrix (via `portal: 'grandparent'`), and a split tree exposed only the bottom group's local pose so handles landed at the segment origin on the roof floor. Surface basis (solar-panel/geometry.ts) - `surfaceQuatFromNormal` builds `right` by projecting world +X onto the surface plane instead of `up × normal`. The cross-product version flipped sign when the normal's Z component flipped (e.g. the two slopes of a gable roof), so hosted children's local +X pointed in opposite world directions across the ridge and asymmetric chevrons anchored the wrong edge. Projecting +X keeps the basis stable across mirror-image slopes. Skylight move-tool ghost - Switch from the raycast normal (`event.normal × normalMatrix`) to the analytical normal (`getAnalyticalNormal`) on every pointer move, and mirror the placement tool's transform stack: `position → yaw (roof + segment) → surfaceQuat → skylight rotation → preview`. Re-engaging Move from the floating action menu now shows the same correctly oriented ghost the first-placement tool does. * feat(nodes): split roof-segment depth chevron into asymmetric front/back arrows Brings the depth handle in line with the width handles: one chevron on each Z edge, each anchored to the opposite edge so dragging only moves its own side. `apply` recomputes `position` along the segment's local +Z arm (yaw-aware) so the anchored edge stays world-fixed. Depth also feeds the slope-frame math via `getActiveRoofHeight`, so a naïve depth change would also raise/lower the peak (constant pitch across a larger run). We hold the peak fixed by back-solving a new `pitch` for the new depth via `getPitchFromActiveRoofHeight`, clamped to the schema's pitch range — the segment grows along the deck plane without ramping up. * feat(nodes): in-world handles for dormer + window-bottom clipping check Dormer joins chimney / roof-segment with chevron handles on the selected node. Five body handles (width L/R, depth, wall-height tracker, rotate) plus four window-opening handles (width L/R, height top/bottom) — the window handles re-emit windowOffsetX / windowOffsetY in `apply` so the anchored edge stays put as the dragged edge follows the pointer. Handle visibility on roof accessories needed an editor-side assist: the host segment's mesh registers inside RoofRenderer's `<group segments-wrapper visible={false}>`, which hides anything portaled into it. Chimney's `portal: 'grandparent'` escape trips a WebGPU "Color target has no corresponding fragment stage output" pipeline error on dormer (likely an MRT interaction with the window-assembly's transparent glazing), so RoofEditSystem now flips the wrapper visible whenever ANY accessory hosted on a segment of this roof is selected — and resets each segment mesh to an empty 4-group placeholder on the transition so stale per-segment CSG from a prior edit doesn't double-render on top of the merged shell. Window clipping was using wall-top-above-slope as the exposure threshold, but the window sits in the skirt well below the eave — so a dormer whose eave barely cleared the host roof rendered a fully-buried window. `getDormerExposedFaces` now gates on window-bottom-above-slope; both the CSG cut decision and the window-assembly render path feed off the same number. The in-world window chevrons resolve the host segment via sceneApi and flip to whichever face is currently exposed, so dragging the dormer across the ridge moves the chevrons to the visible gable. Also fixes the BoxGeometry vs ExtrudeGeometry mismatch in `buildDormerFallbackGeometry` — body was indexed, roof was not, so `mergeGeometries` rejected the pair and spammed the console on every height-drag frame. Body is now `.toNonIndexed()` before the merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(viewer): glazing role uses FrontSide to avoid MRT back-face pipeline error DoubleSide on a NodeMaterial inside the MRT scene pass makes WebGPU compile a back-face shader variant that doesn't declare outputs for every MRT target — the validator rejects it and poisons the render context with "Color target has no corresponding fragment stage output". The warning was already documented on `glassMaterial` (materials.ts:77), but `createSurfaceRoleMaterial` was still forcing DoubleSide for the glazing role. Manifested on scene open as soon as a dormer was present: the dormer's window-assembly mounts the glazing material on both gable faces on the first frame, so the back-face pipeline gets compiled immediately. Glazing now resolves to FrontSide; the dormer's back gable group flips 180° so its FrontSide normals point outward (the sill no longer needs its per-face Z mirror since the group rotation handles it). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(nodes): in-world handles for box-vent + ridge-vent; freeze non-active arrows during drag Adds 5 chevron handles to box-vent (width L/R, depth, height, rotate) and ridge-vent (length L/R, width, height, rotate). Box-vent's renderer now composes slope tilt + yaw onto the registered ref's quaternion (mirrors solar-panel) so handle placements use vent-mesh-local coords directly; ridge-vent's registered ref was already at the vent frame. Ridge-vent renderer now merges `useLiveNodeOverrides` so the mesh updates in-flight during a handle drag instead of freezing until commit. Box-vent / dormer / chimney already did this; ridge-vent was the only roof accessory not subscribed. `NodeArrowHandles` now tracks the active drag descriptor + a pre-drag store snapshot. Non-active arrows render against the snapshot with a node-local freeze offset that cancels the mesh's `position` drift — asymmetric resize (width / length L+R) recomputes position to anchor the opposite edge, and without the freeze every other chevron would slide along with the moving mesh center. The active arrow's freeze offset is null, so it tracks the cursor as before. Rotation drags collapse the offset to zero (position doesn't change), so non-active chevrons naturally rotate with the mesh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(nodes): gutter accessory — eave-mounted rain channel with three profiles New `gutter` node kind hosted on a roof-segment. Placement tool snaps to the eave line of the segment under the cursor (segment-local `Z = +depth/2`, `Y = wallHeight`); the back wall of the gutter then sits flush against the fascia and the trough hangs outward (+Z). Three cross-section profiles share the same outer-outline-minus-cavity extrude recipe and only differ in the outline curve: - `k-style`: ogee fascia (S-curve) — default residential look - `half-round`: semicircular trough — colonial / classical feel - `box`: rectangular u-channel — commercial / industrial Three in-world chevron handles via the registry: - length L + R (asymmetric — drag one end, the other stays world-fixed) - size (anchor='max', drops the trough downward as the cursor pulls) Wiring touches every node-kind ledger: schema (core/schema/nodes/ gutter.ts), AnyNode union, schema barrel, material targets, roof-segment hosted-accessory comment, event bus (`gutter:*`), nodes barrel + registry, plus the per-kind file set under `packages/nodes/src/gutter/` (geometry, schema re-export, parametrics, renderer, preview, tool, definition, index). V1 ships gutters only — downspouts deferred so the eave-snap + cross-section pipeline can be eyeballed before stacking the downspout-corner placement logic on top. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(gutter): roof-panel entry, eave-edge snap, open-top U-channel, move tool UI surface area: - Roof inspector "Elements" section now lists existing gutters + an "Add Gutter" button. Adds `'gutter'` to the StructureTool union so setTool('gutter') typechecks. - Sidebar tree-node map gets a GutterTreeNode entry; selecting a gutter in the outline focuses it just like other roof accessories. - Floating action menu now shows a Move button on a selected gutter via the new `affordanceTools.move`. MoveGutterTool ghost-follows the cursor, eave-snaps on each frame, and commits to the new segment + side on click. Mirrors the ridge-vent move flow. Geometry fix: - Three cross-sections were authored as a closed outline + inset hole, which extrudes as a sealed box with a tunnel through it (top sealed). Real gutters need an OPEN top. Each profile now traces a single U-shape polygon around the channel material: outer wall down -> bottom -> outer wall up -> front rim -> inner wall down -> inner bottom -> inner wall up -> back rim. The interior of the U is empty space, not a hole inside a closed shape. Placement fix: - Snap now lands on the OUTER drip edge of the roof, not the wall line. Segment-local Z = sign * (depth/2 + overhang - 4 cm tuck), Y = wallHeight - overhang * tan(pitch) + 4 cm tuck. Sign of the cursor's localZ picks the near eave; back eave uses rotation = pi so the trough hangs outward in both directions. The 4 cm tuck offsets keep the gutter visually attached to the fascia rather than floating at the very tip of the overhang. Hook order fix (regression from the previous commit): - `NodeArrowHandlesForNode` had its new useState/useMemo hooks AFTER the `if (!portalObject ...) return null` guard. The registry-resolve useEffect flips portalObject from null to object one frame later, so the guard passed on render N+1 and three new hooks suddenly appeared in the hook list. Moved them above the early return. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(viewer/nodes): drop DoubleSide NodeMaterial MRT landmines across slab, vents, gutter, window DoubleSide on any NodeMaterial inside the MRT scenePass makes WebGPU compile a back-face shader variant whose fragment outputs don't cover every MRT target — the validator rejects the pipeline and poisons the render context with "Color target has no corresponding fragment stage output but writeMask is not zero", manifesting as renderPipeline_NNN invalid on scene open. The pattern was already documented at materials.ts:77 and fixed for glazing in 9400f1c5, but several roof / floor renderers still requested DoubleSide on `createSurfaceRoleMaterial` (which returns a `MeshLambertNodeMaterial`) and on user-supplied materials (which may also be NodeMaterials): - slab/geometry.ts — fired on every untextured floor; the live culprit on scene reload after the gutter renderer was switched to FrontSide. - gutter/renderer.tsx — the U-channel cross-section is traced as a single closed polygon around the material, so ExtrudeGeometry already produces outward-facing normals on every visible face; DoubleSide was speculative. - box-vent / ridge-vent renderers — DoubleSide was deliberate (to keep back faces of thin extrudes visible from below); now a known visual tradeoff. Build the geometry as a closed solid in `geometry.ts` if the underside-view becomes noticeable; do not bring DoubleSide back. - viewer/lib/materials.ts `DEFAULT_WINDOW_MATERIAL` — same fix on the fallback window material. Local `defaultMaterial` constants in box-vent / ridge-vent / gutter also lose their `side: DoubleSide` for consistency (those are `MeshStandardMaterial`, hit only when a preset ref fails to resolve, but the same landmine pattern). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(editor): Shift-snap rotate gizmos to 15° increments Holding Shift while dragging a whole-node rotation arrow now snaps the delta to π/12 (15°) steps. Scoped to `descriptor.shape === 'rotate'` so curved-stair sweep handles keep their continuous feel. * feat(nodes): stair railings track live segment-drag overrides `StairRailings` was reading each child segment from zustand only, so width/length/height drag handles (which publish to `useLiveNodeOverrides` and only flush on release) left the railing frozen at the pre-drag values until release. Subscribe to the override map and merge each child's override onto its zustand snapshot so the railing rebuilds every frame during the drag. * chore(ifc-converter): next-env routes path moves under .next/dev/types Auto-generated next-env.d.ts update from the local Next.js dev server — the routes type now lives under `.next/dev/types/routes.d.ts` rather than `.next/types/routes.d.ts`. * fix(gutter): roofType-aware eave snap — 4-way on hip/flat, low side on shed Gutter place + move tools were hard-coded to snap to ±Z eaves, working for gable / gambrel / mansard / dutch but missing: - Hip / flat: 4 eaves, not 2. Clicks on the side slopes (±X eaves) collapsed back onto ±Z, so users couldn't place a gutter on a hip's side eave at all. - Shed: only one real eave (the low side at +Z). Clicking on the high wall side used to snap to -Z, which is the rake / high end with no fascia to hang from. New shared `eave-snap.ts` module: - `resolveEaveSnap(segment, localX, localZ)` returns `{ eaveX, eaveY, eaveZ, rotation, side }`. - Hip / flat picker uses `max(|lx|/halfW, |lz|/halfD)` — same discriminator `analyticalSurfaceY` uses for hip — to pick which of the four slopes the cursor is on, then signs +/-. - ±X eaves rotate the gutter ±π/2 so its outward axis points away from the building. - Shed always returns +Z (the low side). - Gable / gambrel / mansard / dutch unchanged (±Z). Both tools collapsed their duplicated tuck constants + inlined resolver — the "keep these in sync" comment became a landmine once the resolver grew non-trivial. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(gutter): end caps + corner mitre + ghost-placed parity - Ghost/move previews mirror the GutterRenderer transform chain (roof → segment → snap) and use FrontSide. Removes drift between the placement ghost and the gutter that lands on click. - New endCapLeft / endCapRight booleans (default true) slice a solid-outer plug into the extrusion at each enabled end. Inspector exposes both toggles; caps subtract from node.length so the user-set span is preserved. - corner-mitre.ts detects sibling gutters meeting within 5 cm on the same segment and returns per-end mitre angles. The end-face skew holds back walls at the inner corner while front rims extend to the outer eave intersection; cap on a mitred end is force- suppressed so the L-junction stays open. Renderer pulls siblings via useShallow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(gutter): drag-snap corners, live eave Y, hangers - Length L/R handles now snap to sibling gutter endpoints within 10 cm and pull BOTH gutters' lengths to the axis intersection — the geometric eave corner — so the 5 cm corner-mitre window fires reliably. Sibling adjustment writes through sceneApi.update; the drag pipeline's history pause batches it with the main commit into one undo step. - Renderer derives eave Y live from segment.wallHeight, overhang, and pitch via the new shared computeEaveY() — instead of trusting node.position[1] from placement time. Subscribes to the segment's useLiveNodeOverrides entry too, so a wall-height drag on the segment moves the gutter on every frame (not just at commit). - Hangers: new hangerStyle (strap / none) + hangerSpacing fields. buildHangers() lays thin 25mm × 3mm × rim-width box straps across the rim at the configured spacing, inset by 5 cm from each end and skipping any cap slabs. BoxGeometry is converted to non-indexed before merge — ExtrudeGeometry isn't indexed, and mergeGeometries rejects mixed-index sets. Inspector exposes both fields in a new "Hangers" group. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(gutter): downspout outlet with real CSG-drilled hole New schema fields: outletSide ('none' | 'left' | 'right'), outletInset, outletDiameter. Default 'none' so existing gutters don't sprout outlets on schema upgrade. Geometry adds a solid cylindrical stub at bore + 3 mm wall radius descending 6 cm from the trough floor, profile-aware Z midpoint (k-style 0.4·size, half-round size, box size/2), X clamped between the caps. After merging into the channel + caps + hangers, a three-bvh-csg SUBTRACTION drills a bore-wide cylinder vertically through the floor and stub — so the result is a real hole in the trough floor with a hollow tube hanging through it. Drill overshoots floor + stub by 1 cm each side to keep cut planes from coinciding with mesh faces (csg-evaluator produces degenerate output on coplanar cuts). CSG only runs when outletSide ≠ 'none' — the existing merge path is the fast path for capped-only gutters. Inspector exposes the three fields under a new 'Outlet' group. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(downspout): new node + gutter inspector list section DownspoutNode lives next to the other roof accessories. Scene-graph parent is the same roof-segment the host gutter sits on; logical attach is via `gutterId`. Schema: length (default 2.5 m), diameter (default 0.07 m / 3″ — matches gutter outlet default), material. Renderer mounts a CylinderGeometry under the same transform chain the gutter uses (segment → gutter-mesh-local → outlet); pulls `computeEaveY` from the live + drag-override segment so wallHeight / overhang / pitch changes track on the same frame as the gutter. `resolveGutterOutletPlacement` (gutter/outlet-lookup.ts) is the shared helper both the downspout renderer and the inline Add path use to compute (x, y, z, bore) in gutter-mesh-local space. Two arrow handles on a selected downspout: - length: tracker shape (dashed leader + draggable cube). Anchored to the outlet, cube at the pipe bottom — readable even when the cube ends up below ground. - diameter: symmetric `z`-axis chevron sitting at a fixed −20 cm Y below the outlet with 25 cm of outward clearance past the worst- case k-style rim, so it stays inside the gutter's camera frame instead of floating mid-pipe. Inspector UX: new optional `trailingSection` slot on ParametricDescriptor — a lazy-loaded React subsection rendered between groups and the Actions section. Gutter's slot loads a `downspouts-panel` that lists every attached downspout (button per item, click → select), and an "Add Downspout" button below that immediately creates a new one parented to the gutter's segment — matches the roof inspector's gutter list pattern. Disabled with a helper line when `outletSide === 'none'`. Multiple downspouts per gutter are allowed. `StructureTool` union picks up 'downspout' so the placement tool remains addressable (used by the legacy roof-panel button before the inspector list took over). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: pending floorplan alignment-guide work-in-progress Snapshot of in-flight alignment-guide files that have been sitting in the working tree (not authored in this session). New service + zustand store wire up the data; the floorplan layer + overlay are the visible consumers. Committed as-is to clear the tree. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(core): gutter multi-outlet model + downspout routing options Replace the gutter's single outletSide/outletInset/outletDiameter triple with an `outlets` array of `{ id, offset, diameter }` so one run can host several downspouts on independent drops instead of stacking on one. Each downspout links to an outlet by `outletId`. Add downspout routing/styling fields: `standoff` (gap proud of the wall), `shape` (auto/round/rect), `strapStyle`/`strapSpacing`, and `terminal` (splash/kickout/straight). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(editor): in-world rotate & move gizmos with live-drag dimension pill Add two new handle-descriptor capabilities to the core registry — a free `translate` handle (ground-plane or wall-normal move cross) and `rotationPlane` for arc-resize (yaw vs spin-flat-against-wall) — plus `measureLabel`, which routes a resize handle's readout to a floating dimension pill instead of its inline chip, and `overrideTarget`, a cross-node redirect for handles that edit a sibling's value. node-arrow-handles implements all four, merging the in-flight drag into `useLiveNodeOverrides` so the mesh moves in real time and commits only on release. Item gains in-world rotate + move gizmos (floor items: world-Y rotate + floor-plane move; wall items: wall-normal spin + wall-face move). Add the shared MeasurementPill (H · L · T) and formatMeasurement, wired into the floating action menu (live wall/fence height drag) and the wall / fence endpoint move tools. Side handles merge live overrides so every affordance tracks the height mid-drag. Item duplicate now drag-to-places (no auto-insert) and the placement coordinator rotates in 45° steps to match the R-key step. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(gutter): multi-downspout outlets with CSG drops, routing & profiles Build out the multi-outlet gutter against the new schema. Each outlet is drilled through the trough floor via CSG (profile-geometry shares the trough cross-section), and a downspout links to one outlet by id so several no longer stack on a single drop. The downspouts panel manages the outlet list; outlet-lookup resolves a downspout's mount from its gutter + outlet. Downspout gains real routing (routing.ts): offset elbows step the run back from the eave overhang to the wall (standoff escape hatch), auto/round/rect cross-section following the gutter profile, wall straps, and splash / kickout / straight terminals, with inspector-editors for the new fields. Length-snap now snaps only the dragged gutter to the geometric corner — never moving its corner-mate — so dragging one gutter can't reset another the user placed deliberately. Adds gutter floorplan (eave-line silhouette). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(floorplan): 2D footprints for roof nodes + rotate-handle angle wedge Add floorplan builders for box-vent, chimney, dormer, ridge-vent, roof, skylight, and solar-panel, and wire each into its definition, so roof-layer nodes finally draw a 2D footprint. Roof draws the merged silhouette of its child segments, and roof-segment now renders proper architectural roof linework (ridge / hip / break + shed downslope arrow) per shape instead of a bare rectangle. Add a `pivot` to the floorplan rotate affordance so the layer can sweep a live angle wedge + degree readout during a rotate drag — the 2D twin of the 3D rotate gizmo. Column / elevator / shelf / stair pass their pivot through. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(editor): select ceiling only via its corner handles, not the grid body Add `viaHandle` to NodeEvent and set it on ceiling corner-bracket clicks. The selection manager now ignores non-handle ceiling clicks without stopping propagation, so a top-down click on the revealed ceiling grid falls through to the item hosted beneath it instead of re-selecting the ceiling and swallowing the click. The corner brackets draw with depthTest off at a high render order so they stay visible and clickable through occluding geometry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(editor): highlight wall openings on a selected wall Add WallOpeningHighlights — an indigo accent frame + translucent pane drawn around each door / window opening of the selected wall, so editable children (including frameless openings with no visible geometry) are easy to locate. The accent is deliberately distinct from the white selection outline, and draws with depthTest off so it reads on top of the wall. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(solar-panel): derive surface frame live so panels re-seat on roof changes Compute the panel's Y and tilt from the parent roof-segment's finished (deck + shingle) surface every render via getRoofOuterSurfaceFrameAtPoint — the same helper skylights use — instead of reading the stored position[1]/surfaceNormal snapshot. Merging the segment's live overrides means the panel re-seats and re-tilts continuously during a wall-height / pitch drag rather than floating or burying until the value commits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(first-person): coerce imported item geometry attrs to Float32 for collider merge mergeGeometries requires every merged geometry to share the same typed-array constructor per attribute. Imported item GLBs using KHR_mesh_quantization or interleaved buffers broke the merge against Float32 wall/slab geometry, so decode each attribute into a plain non-normalized Float32 BufferAttribute before cloning into the collider world. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(editor): lower camera minimum zoom distance to 6m Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(mcp): bump @pascal-app/mcp to 0.3.0 in lockfile Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(nodes): registry-own move dispatch + hoist shared roof helpers Addresses the architecture-review findings on the gutter/downspout branch: - Move dispatch: port the bespoke roof/roof-segment/stair/stair-segment movers (MoveRoofTool) and the building mover (MoveBuildingContent) into @pascal-app/nodes (shared/move-roof-tool.tsx, building/move-tool.tsx) and declare them via `affordanceTools.move`. Delete both hardcoded dispatch lists — `LEGACY_MOVABLE_KINDS` in floating-action-menu and the roof/stair/ building arms of MoveTool. `onMove` is now purely `isRegistryMovable`. Editor internals the movers need are exported from @pascal-app/editor (adds clearRoofDuplicateMetadata); sfxEmitter.emit -> triggerSFX. elevator keeps its existing capabilities.movable path (its legacy arm is the lone remaining one, now documented). - Cross-kind imports: hoist resolveRoofSegmentHit (roof/segment-hit.ts) and the roof-surface normal math (getSurfaceY/getAnalyticalNormal/ surfaceQuatFromNormal, formerly in solar-panel/geometry.ts) into packages/nodes/src/shared/, so the 8 roof accessories + skylight/box-vent stop reaching into sibling kind folders. roof/index and solar-panel/index re-export from shared so public surfaces are unchanged. - Inspector: make ParametricInspector action `enabledIf` reactive by subscribing to its boolean result (ParamActionButton), matching the existing FieldRenderer/visibleIf pattern. Type-checked (tsc) and linted (biome) across editor + nodes. Move behaviour is preserved by construction but not yet runtime-verified in the editor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(nodes): add cupola + turbine-vent roof accessories; group-rotate gizmo; in-ring rotation readout New roof-accessory node types and editor gizmo work: - feat(nodes): cupola (roof lantern) and turbine-vent (whirlybird) node types — schema, geometry, parametrics, renderer, panel, move/placement tools, floorplan, and tests; registered in the node + core schema indexes with site-panel tree nodes. - feat(editor): group-rotate handle — a single rotation gizmo for 2+ movable nodes that spins the selection rigidly about its shared bbox center. - fix(editor): the live rotation readout (degree wedge + chip) now renders as a child of the node frame, concentric and coplanar with the guide ring, so it sits centered in the ring on pitched roofs instead of floating off to the side. Flat-ground gizmos and the group-rotate readout are unchanged. - refactor(nodes): rework box-vent / ridge-vent geometry + definitions and share roof helpers; tidy roof/box-vent panels. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(nodes): eyebrow-vent kind; ridge-vent tracks roof height; vent paint + tree-node go registry-driven New eyebrow-vent roof accessory plus two follow-on cleanups: - feat(nodes): eyebrow-vent — a louvered roof vent in three styles (`scoop` swept eyebrow, `half-round` D-vent, `slant-box` hooded box). Solid louver slabs set into a framed/contained front, double-sided geometry, and adjustable style / louver-count / dimensions / slant (`backRatio`). Full registry wiring: core schema, node dir (definition/geometry/renderer/tool/ move-tool/panel/floorplan/preview/tests), event type, palette button. - fix(nodes): ridge vent derives its Y from the segment's current surface (`getSurfaceY`) instead of a stored value, so lowering a roof drops the cap onto the new ridge automatically. - refactor(nodes,editor): migrate the vent family (box / ridge / turbine / cupola / eyebrow) to `capabilities.paint` via a shared single-surface capability, removing the hardcoded `node.type` paint arms from the editor's selection-manager + material-paint (matches chimney/dormer). - refactor(editor): replace the five identical vent tree-node components with one `def.presentation`-driven `RegistryTreeNode`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
c00a2469fd |
editor: in-world selection UI across wall / door / window / stair (#334)
* 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 * wall: 2D floor-plan move, side arrows, drag-to-move endpoints Bundles the in-progress wall editing work on this branch: - Wall corner endpoint drag in 3D (`floating-action-menu.tsx`, `wall/move-endpoint-tool.tsx`): press-and-drag on the floating endpoint button or the new 3D corner sphere, release to commit. Replaces the prior click-to-arm / click-to-place flow. - New 2D move side arrows on selected walls via a new `move-arrow` floor-plan geometry kind (core type + registry-layer renderer + wall floor-plan builder emission), mirroring the 3D `WallMoveSideHandles`. - 2D wall body move: new `wallFloorplanMoveTarget` translates the moving wall and cascades shared endpoints onto linked walls so L-corners stay connected through the drag. - `MoveWallTool` cleanup gains an external-commit guard so a 2D commit doesn't get clobbered by the 3D mover's cleanup restore. - HMR-safe `bootstrap.ts` no longer re-registers builtin kinds whose registry entry survived the closure reset. - Misc 2D polish: floor-plan auto-fit measures the painted scene via `getBBox`, wall dimension offset bumped, swallow-click guard in `handleSelect` so registry-driven selection holds through the post-pointerdown re-render. Floor-plan move-target / move-arrow code still carries diagnostic console logs for the cascade flow; keeping for debug on this branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * wall: 2D floor-plan move adopts 3D junction planner 2D wall drag now produces the same scene topology as 3D — linked corners cascade per `planWallMoveJunctions`, off-axis branches stay rectilinear with a bridge wall inserted between the original and new corner, and same-direction consumed walls collapse and delete. Previously the 2D handler did a naive endpoint-stretch cascade with no bridges or collapses, so dragging an L-corner in 2D vs 3D yielded different scenes. `FloorplanMoveTargetSession` gains an optional `commit` hook. The default overlay path snapshots affected nodes and writes a diff back on release — fine for kinds whose commit is a pure position update, but insufficient when commit needs to also create or delete nodes. When `commit` is present, the overlay reverts to baseline, resumes history, and delegates the atomic write; one Ctrl-Z rolls back the entire operation including bridge creates and collapsed deletes. Shared helpers (`planWallMoveJunctions` plan → updates, linked-wall snapshots, bridge synthesis) lifted to a new `packages/nodes/src/wall/ move-shared.ts` so both the 3D `MoveWallTool` and the 2D `wallFloorplanMoveTarget` import them. Net -163 LoC after dedup. Auto-slab live preview and ghost bridge previews mid-drag — visible in 3D today — remain 3D-only; 2D surfaces them at commit time through the normal scene reactions. Tracked as follow-up. Also drops three `// temp diagnostic` console.log blocks left over from the prior wall-move branch (2D setup, 2D canCommit, 3D cleanup). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * door/window: R flips side, E toggles open/close R previously toggled the open/closed state of operable doors and operable windows. It now flips the opening's side (front ↔ back, rotation += π) for both — same gesture as flipping a furniture item that knows about handedness. The open/close toggle moved to E, which was unbound for doors and windows before. T is now a no-op on doors and windows so it doesn't free-rotate a wall-bound node by π/4 (which made no architectural sense). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * door/window: keep 2D plan in sync during placement drag While drafting a door or window across the same host wall, the tool was bypassing the scene store and mutating the Three.js mesh directly. That kept 3D snappy but left the 2D floor plan reading the last committed position — drafts froze in place on the 2D side during a same-wall drag. Route same-wall moves back through \`updateNode\` so 2D and 3D both re-render from a single source. The reparent path (cross-wall drag) still uses \`updateNode\` with \`parentId\` and \`wallId\` — we only avoid forwarding those fields when the wall hasn't changed so the host wall's \`children\` array doesn't churn each tick and trigger a WebGPU "Vertex buffer slot 0 ... was not set" warning from the briefly re-rendered placeholder geometry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * floor-plan: live wall-draft measurement + opening placement event order Two changes to the floor-plan panel: 1. Length + angle labels render alongside the wall draft in 2D, matching the 3D \`WallTool\` feedback. Length sits at the segment midpoint with a plate that flips when its on-screen orientation would read upside-down; angle arcs anchor at each endpoint that meets an existing wall and label the deviation from that wall's direction. 2. The pointer-move handler ran the registry catch-all (\`isFloorplanGridInteractionActive\`) before the opening-placement branch. Door and window are registered kinds, so during their build mode the catch-all emitted \`grid:move\` and returned — starving the \`wall:enter\` / \`wall:move\` events the placement tools listen for. Reorder so opening placement runs first; the wall-build skip in the catch-all is preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * wall: pass geometry as prop on move arrow handles R3F's \`<primitive attach="geometry">\` path emits a \`Draw(0, 1, 0, 0)\` on the first frame because the host \`<mesh>\` briefly renders with the default empty \`BufferGeometry\` before the primitive child attaches. Combined with \`frustumCulled={false}\`, WebGPU flagged "Vertex buffer slot 0 ... was not set" every time a wall or fence was selected and the move arrows mounted. Pass \`arrowGeometry\` as a prop on the \`<mesh>\` so it's never mounted with the default placeholder. Same fix applied to both the wall and fence move-arrow handles. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ifc-converter: regenerate next-env.d.ts after Next route path move Next.js moved the generated routes typings from \`./.next/dev/types/routes.d.ts\` to \`./.next/types/routes.d.ts\` in the current version pinned by the workspace. Regenerated via \`next typegen\` so the project compiles against the right path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * move: track which view finalised so split-view cleanups don't race In split view, both the 2D move overlay and the 3D move tool mount for the same \`movingNode\` and each captures its own pre-drag snapshot. When one side finalises (commit or Esc), the other side unmounts because \`setMovingNode(null)\` propagates — and its effect cleanup had to *guess* whether the live scene was already-committed state (skip restore) or its own drag's uncommitted state (revert). Both cleanups did this via the same heuristic: diff snapshot fields against current scene state. Cheap, but it conflates "the other side committed" with "the user's apply() actually changed something" — and fails outright if a commit happens to land on the same numeric values as the snapshot. Replace the heuristic with an explicit \`movingNodeOrigin\` state field: '2d' | '3d' | null. The finalising side sets its origin before \`setMovingNode(null)\` runs; the other side's cleanup reads it. \`movingNodeOrigin\` is preserved across \`setMovingNode(null)\` (so it's still observable when the cleanup fires) and reset the next time a non-null \`setMovingNode\` starts a fresh drag. Wired on the wall move-tool (3D) and \`FloorplanRegistryMoveOverlay\` (2D) — the two real call sites today. Other 3D move tools can adopt the same flag incrementally as their own split-view races surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * wall: 2D drag publishes to useLiveNodeOverrides, zustand only on commit Side-arrow / corner-dot / curve-handle drags in the 2D floor plan now publish `{ start, end, curveOffset }` to `useLiveNodeOverrides` each tick instead of writing to `useScene`. WallSystem, the 2D registry layer, and the wall sidebar all merge the overrides in when reading endpoints, so the visual + slider preview tracks the cursor while zustand stays at the pre-drag values until pointer-up. Commit writes one tracked `applyNodeChanges` (junction-aware) and clears the overrides; Esc / pointercancel / mid-drag unmount also clear them. Also bundles the in-progress branch work this depends on: - FloorplanAffordanceSession gains optional `commit?()` mirror of the move-target hook; the dispatcher reverts → resumes → calls it when present (vs. its default snapshot-diff dance). - Selected wall body is now pointer-events-inert (polygon `pointerEvents: 'none'` + hit-line skipped) so only the arrows / endpoint dots / curve dot start a drag. - Move button removed from the 2D floating action menu and the wall sidebar inspector for walls — redundant with the side-arrows. - `useWallMoveGhosts` store + `FloorplanWallMoveGhostLayer` for the dashed bridge previews painted mid-drag. - WebGPU "Vertex buffer slot 0 ... was not set" fixes on grid + guide renderer + wall draft preview by passing geometry as a prop (same pattern as wall-move-side-handles). - Floor-plan wall-tool fallback: when the 3D wall tool's `grid:click` already committed the wall, treat `createWallOnCurrentLevel` returning null as "the 3D side handled it" and chain the next draft segment instead of clearing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * wall: new in-world selection UI — side arrows, height handle, corner pickers, ground menu 3D affordances for a selected wall, replacing the HTML floating pill: - side move arrows: thinner chevron+shaft silhouette (extruded, beveled); press-hold-drag-release commits on pointerup (MoveWallTool no longer uses grid:click) - height arrow above the wall midpoint, drags vertically against a camera-facing plane and updates wall.height live; new resizingWallHeight state gates camera orbit; commit plays sfx:item-place - corner picker per endpoint: billboarded hex disc at floor + dashed vertical leader cylinder; pointerdown routes to the existing movingWallEndpoint flow (works for 2D and 3D) - ground action menu (curve / duplicate / delete): three Lucide SVGs rendered as canvas-textured planes lying flat on the floor, anchored one wall thickness + clearance outside the camera-facing face; one rigid container moves them as a unit (auto-flips sides + rotates with the wall, on curved walls uses the t=0.5 curve frame) - floating action menu hidden for walls (replaced by the above) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * wall: smooth ground-menu side-flip with hysteresis + lerp The three floor icons appeared to "move one at a time" when orbiting: binary side decision flickered on grazing orbits, and the 180° rotation flip swapped curve/delete across each other while duplicate (offset 0) stayed put. Now lerps position+rotation toward target with a hysteresis dead-zone, so the menu swings around the wall as one unit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * wall: raise ground-action menu to 10 cm so icons clear floor textures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * wall: anchor 2D action menu and 3D height arrow at curve apex 2D menu centres on getWallMidpointHandlePoint and stays horizontal 32 px above the wall; 3D height arrow uses getWallCurveFrameAt(0.5) so the apex+tangent match the side handles on curved walls. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * door: in-world selection UI — side width arrows, height arrow, ground menu Side arrows resize width anchored at the opposite edge; top arrow drags height anchored at the floor. Ground menu mirrors the wall pattern with move + duplicate + delete icons that flip to the camera side. Handles portal into the level (not the wall mesh) and wrap in a per-frame transform mirror so wall hover outline doesn't pick them up. New viewer flag handleDragging gates node pointer events during in-world drags; pointerup also swallows the follow-up synthetic click so the PointerMissedHandler doesn't deselect the active item on commit. Wall height arrow, wall move arrow, and fence move arrow all opt in. Scale chevron arrows down to 65 % across wall + door so the family reads as one. Panel type grids (door, window, column, skylight) get matched breathing room (px-3 py-2.5, gap-2) so labels stop hugging the borders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * door: 2D width arrows + world-relative move dot, deterministic commits Side-arrow width drag in the 2D floor plan: doors now emit two width arrows at the wall-tangent edges when selected, routed through a new `resize-width` affordance that anchors at the opposite edge, clamps to wall bounds, and previews per-tick via scene writes so both the floor plan and the 3D viewer track the drag in real time. `move-arrow` kind gains optional `affordance` + `payload` so the same chevron primitive can route to either the move flow (walls) or an arbitrary affordance (door width-resize) without forking the renderer. Move-dot for the door is now world-anchored — it scales with zoom in place of the previous screen-constant size, matching the rest of the door's chrome. Both `doorWidthAffordance.commit()` and `doorFloorplanMoveTarget.commit()` own their atomic final write so the dispatchers take the deterministic revert → resume → commit path. The diff path was silently reverting when the post-apply state happened to match the snapshot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * window: in-world selection UI, 2D width arrows, deterministic commits Bring window 3D + 2D selection chrome to parity with door. Selecting a window in 3D now emits two side width arrows, top + bottom height arrows (top anchors at the sill, bottom anchors at the lintel and clamps to the wall floor), and an in-world action menu that rides just below the bottom arrow's tip so the column moves with the sill. 2D plan adds two `resize-width` arrows at the start / end edges, routed through the new `windowWidthAffordance` — same anchored-edge + wall-bounds clamp + per-tick scene-write preview the door uses. `windowFloorplanMoveTarget.commit()` is now self-owned: `apply()` snapshots the last valid placement and `commit()` re-applies it, so the dispatcher takes the deterministic revert → resume → commit path instead of the diff path that silently reverts when the post-apply state happens to match the snapshot. Mirrors the door fix. The HTML floating-action-menu skips windows now that the in-world ground menu owns those actions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * stair: in-world selection UI — side/length/height arrows, ground menu Bring stair-segment selection chrome to parity with wall / door / window. Selecting a stair segment in 3D now emits two side width arrows (each slides the opposite edge anchor under the user), a length arrow at the back face that extends the run, and — for stair-type segments — a height arrow on top. A ground action menu (duplicate / delete) sits beside the segment and flips sides as the camera orbits, with hysteresis + lerp so it doesn't dither. The handles portal into the stair's PARENT object (level / building / scene root) rather than the stair group itself: StairRenderer attaches `useNodeEvents` to the stair group, so any descendant pointer-over would bubble up and set `hoveredId = stairId`, which then makes the post-processing outline traverse the entire stair group and stroke our icons. Mirrors the door fix. A two-layer transform mirror (`stairPoseRef` + `segmentPoseRef`) keeps the handles aligned with the chained per-segment pose that StairSystem writes imperatively each frame. Duplicate forces `attachmentSide: 'front'` on the clone so it continues the chain end cleanly instead of inheriting the original's side and U-turning. New `resizingStairSegment{Width,Length,Height}` editor state lets `CustomCameraControls` suppress orbit/zoom while an arrow is dragging, matching the wall/door/window handle pattern. The HTML floating-action-menu skips stair-segments now that the in-world ground menu owns those actions. Stair-segment panel swaps its bespoke fill-to-floor toggle for the shared `ToggleControl` so it looks like the other panels and groups with the thickness slider under one `space-y-3` block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * stair: 2D move-revert fix, parent ground menu, curved/spiral in-world arrows - Route stair 2D moves through `floorplanMoveTarget` and honor `movingNodeOrigin === '2d'` in `MoveRoofTool` cleanup so the 3D tool's restore-from-snapshot no longer stomps the 2D commit. - Parent stair selection shows an in-world ground action menu (move / duplicate / delete) anchored beside the stair; the screen-space floating menu is suppressed for `type === 'stair'` to match door / window / segment. - Curved & spiral stairs gain in-world resize arrows: rise (centered on the pillar for spirals), width, inner radius, and two sweep handles (one per arc end) clustered beside the width arrow. - Camera controls pause during curved-stair drags. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * editor: reuse measurement-bar geometry; fix two post-merge dangling refs MeasurementBar was building a fresh BoxGeometry per render for every wall measurement bar, which the WebGPU backend flagged ("Vertex buffer slot N ... was not set") when walls moved. Hoist a unit cube and scale it instead. Two refs left dangling after the main-branch merge resolved its conflicts on GitHub: - floorplan-panel.tsx referenced a `theme` variable that no longer exists; the file already derives `isDark` from `getSceneTheme(state. sceneTheme).appearance === 'dark'` higher up. Use that. - grid.tsx applied `EDITOR_LAYER` but only imports `GRID_LAYER` (the new dedicated grid layer). Use the imported one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * editor: pin building bbox center to cursor during move The floating drag button anchors at the building's bbox center, but the move tool was teleporting the building's origin to the cursor — so the moment a drag started the building jumped by `bbox_center - origin`. Capture the local-space offset from origin to bbox center at mount and apply it on every grid move, grid click, and R/T rotation, so the bbox center stays pinned to the cursor through the whole drag. Also seed the cursor sphere at the bbox center instead of the origin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * stair: 2D resize affordances + dispose curved/spiral geometry on swap Floorplan view: emit `move-arrow` children alongside the existing chrome, mirroring the in-world arrows on selected stairs: - straight: per-segment side (left/right width) and front (length) - curved & spiral: width, inner-radius, and two sweep-end arrows Hidden during placement so they don't fight the cursor follow. Stroke widths on curved/spiral chrome converted to screen pixels (paired with `non-scaling-stroke`); the old world-metre values rendered as sub-pixel at every zoom. First step line is now also emphasised on curved stairs to match legacy chrome. Skip the straight-only direction-arrow polyline for curved/spiral — the arc-aligned arrow above already conveys "up" and `buildFloorplanStairArrow` produces a malformed polyline once the chain is wrapped around an arc. Renderer: extract `SpiralColumnMesh` and `SpiralStepSupportMesh` and add the same prop-+-dispose pattern used by `CurvedStepMesh` / guide/renderer.tsx. Without disposing the prior BufferGeometry on each resize tick, WebGPU keeps a stale pipeline reference and flags "Vertex buffer slot 0 ... was not set" mid-drag on Lambert. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * editor: revert in-world ground menus to HTML floating; align grid to building - Wall/door/window/stair/stair-segment selection menus return to the shared HTML floating menu; remove the in-world ground icons, SVG textures, hysteresis/lerp constants, and unused imports across wall/door/window/stair-segment handle files. - Drop Move from the floating menu (the in-world side arrows cover it); delete the now-unused handleMove. - Floating menu scales with camera zoom (ortho.zoom or 1/distance), clamped at MIN 0.5 / MAX 1 so zoom-in keeps the default pixel size and zoom-out shrinks to a readable floor. - Per-type y-offsets tuned: wall 0.5, opening 0.6, landing 0.5, flight 0.75, parent stair 0.2, structural 0.4, default 0.05. - Align wall/fence arrow materials with the door/window pattern (depthTest/depthWrite false, transparent: true) so they render on top of geometry consistently. - Grid cellSize now follows `gridSnapStep` via a small `SnapAwareGrid` wrapper, and the grid mesh anchors its world XZ to the active building's mesh — snapped wall endpoints (in building-local coords) now fall on visible grid lines instead of mid-cell. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * handles: registry-driven in-world resize arrows Adds a `handles?: HandleDescriptor[] | (node) => HandleDescriptor[]` field to `NodeDefinition` so each kind declares its in-world resize affordances as pure data instead of shipping a bespoke React component. - New `packages/core/src/registry/handles.ts` exposes a discriminated union: `linear-resize` (axis + center/min/max anchor), `radial-resize` (1:1 outward growth), plus stubs for `arc-resize` and `endpoint-move` for follow-up migrations. - New `packages/editor/src/components/editor/node-arrow-handles.tsx` reads `def.handles`, mounts arrows with shared drag plumbing (raycast plane, NDC, pointer listeners, SFX, history pause, handle-dragging guard). Portal modes: `'parent'` (column-like, single wrapper rides self pose) and `'grandparent'` (door/window-like, outer wrapper rides parent pose + inner group rides self pose so handles escape the parent's selection-outline traversal). `apply` receives the node-at-drag-start so edge-anchored resizes (door width re-centers position) compute their fixed anchor from pre-drag state. - Migrate column, door, window, stair-segment. Old per-kind handle files (`column-side-handles.tsx`, `door-side-handles.tsx`, `window-side-handles.tsx`) removed; `stair-segment-handles.tsx` retains `StairHandles` (parent stair curved/spiral arrows) pending the `arc-resize` migration. - Column: height + crossSection-aware footprint (radius / uniform width=depth / independent width+depth / brace width+depth for non-vertical supports). - Door / window: edge-anchored width (left + right) with wall-length max bound; bottom-anchored height (door) / top + bottom edges (window). - Stair-segment: width (chain auto-centers), length anchored at chain start, height for step flights only (landings skip it). Wall and parent-stair curved/spiral arrows stay on legacy components for now — they need `endpoint-move` + `arc-resize` descriptor variants and rotated-axis projection, which are their own focused sessions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * handles: migrate wall + parent stair to registry; add arc-resize Closes the wall + parent-stair gap on the registry-driven handle migration. Net −1177 lines (the per-kind handle files were 1500+ lines of duplicated drag plumbing; their replacements are ~50-line config blocks on each NodeDefinition). - `arc-resize` reworked to take a raw `delta` (radians) instead of `newValue` so two-field writes like curved-stair sweep (which updates `sweepAngle` AND `rotation` together to keep the non-dragged edge world-fixed) stay in the descriptor without awkward inverse-currentValue gymnastics. `currentValue` removed from arc-resize for the same reason — applies own their math. - New `ArcArrow` renderer in `node-arrow-handles.tsx`: raycasts a horizontal drag plane at the arrow's Y, measures the signed angle delta around the node's local origin (atan2 in world XZ, normalised to [-π, π] so wraparound doesn't flip mid-gesture), hands the delta to `descriptor.apply` along with the initial node. - Wall: height arrow migrated (linear-resize axis='y' anchor='min', placement uses curve apex for curved walls, chord midpoint for straight). Side-move arrows + corner pickers stay on the legacy `wall-move-side-handles.tsx` because they're tap-to-engage-mode affordances (move whole wall / move endpoint), not drag-resize — modelling them in the registry needs an editor-action descriptor variant which is a follow-up. - Parent stair: curved + spiral stairs declare 5 handles — rise (linear-resize axis='y' anchor='min'), width (linear-resize axis='x' anchor='min'), inner-radius (linear-resize that also writes width to keep outer rim fixed), and sweep start / end (arc-resize variants writing sweepAngle + rotation). Straight stairs declare nothing — their segment children own resize. - Old `stair-segment-handles.tsx` (1405 lines) deleted; all its arrows now flow through the registry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * handles: tap-action descriptor + EditorApi; finish wall + fence migration Closes the final gap in the registry-driven handle migration. Wall side- move + corner pickers + fence side-move were the last legacy handles because they're click-to-engage-mode affordances (hand the node to its move tool / start an endpoint drag), not drag-resize — `apply(node, value, sceneApi)` had no path to editor state. - New `EditorApi` interface in core (alongside `SceneApi`) exposes `engageMove(node)` + `engageEndpointMove(node, endpoint)`. Concrete implementation in `packages/editor/src/lib/editor-api.ts` casts through `useEditor`'s setters so the descriptor layer never imports editor internals. - New `TapActionHandle` descriptor variant: `placement` + `onActivate (node, sceneApi, editorApi)`. `shape` field picks the visual — defaults to the chevron arrow; `'corner-picker'` renders the dashed vertical leader + billboarded hex disc + ring (sized to `nodeHeight(node)`). - `TapActionArrow` renderer in `node-arrow-handles.tsx` wires up pointer-down → descriptor.onActivate. Pulled the chevron and corner visuals into `ArrowShape` / `CornerPickerShape` building blocks so future shapes can be added without touching the descriptor union. - Wall: front/back side-move (engageMove) + start/end corner pickers (engageEndpointMove). Joined by the existing height arrow on the same `def.handles` list. Old `wall-move-side-handles.tsx` (600 lines) deleted — wall now has zero per-kind handle component. - Fence: front/back side-move. The bespoke endpoint move buttons in the floating menu stay until they migrate to a tap-action too. Net for this commit: -620 +513. Combined with the prior two migration commits: -2287 +912 across the full registry migration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * wall: revert side-move arrows + corner pickers to legacy component The registry-driven tap-action path didn't render the four non-height wall handles, even with descriptors resolved and the wall mesh in sceneRegistry. Fence uses the same descriptor shape and renders fine, so the bug is wall-specific and not in the descriptor layer itself — left for a real diagnosis later. Restored the pre-5756f241 wall-move-side-handles.tsx (height arrow + front/back side-move + start/end corner leaders, 753 lines) and mounted it next to NodeArrowHandles in editor/index.tsx. Dropped the def.handles field on wallDefinition so the two paths don't race. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * editor: level naming helper + ambient floorplan render during building moves Two unrelated WIP fixes bundled: - Level names: extract \`getDefaultLevelName(n)\` / \`getLevelDisplayName(level)\` into \`packages/editor/src/lib/level-name.ts\` and swap in across rename inputs, command palette, floating selector, site panel, level-tree node, level-duplicate dialog, view toggles, and viewer-overlay breadcrumb. Default labels now read "Ground Floor" / "Floor N" / "Basement N" instead of the bare "Level N" string each caller was concatenating itself. - Building-move ambient floorplan: when a building is selected (or mid-move) without an explicit level, FloorplanRegistryLayer falls back to that building's level 0 (or lowest level) and renders it dimmed + non-interactive so the floor stays visible as context instead of disappearing. FloorplanPanel allows the SVG to mount in that case. MoveBuildingTool publishes per-frame pose to useLiveTransforms so the floor-plan follows the drag without reading from the Three.js mesh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * handles: 3D resize arrows publish to useLiveNodeOverrides, commit on release Door / window / wall height-arrow drags now stage the patch in \`useLiveNodeOverrides\` each frame and write to zustand exactly once on pointerup. The kind's system reads via \`getEffectiveNode\` and rebuilds the mesh imperatively, so the React tree never re-renders mid-drag and undo isn't polluted by per-frame writes. - \`packages/core\`: shared \`getEffectiveNode<T>(node)\` helper exported from \`@pascal-app/core\`; spreads any override fields onto the input, returns it unchanged when none. Replaces the inline merge wall-system had as \`getEffectiveWall\`. - \`DoorSystem\` / \`WindowSystem\`: subscribe to \`useLiveNodeOverrides.overrides\` (so override-only ticks re-run the component and pick up the latest dirtyNodes), merge via \`getEffectiveNode\` before \`updateXMesh\`. Parent-wall dirty cascade uses the effective node's parentId. - \`WallSystem.updateWallGeometry\`: door / window children are merged through \`getEffectiveNode\` before being passed to \`generateExtrudedWall\`, so cutouts track the in-flight resize. - \`LinearArrow\` (registry handle): onMove → override + markDirty; onUp → one tracked \`sceneApi.update(lastPatch)\` + clear; onCancel → clear + markDirty to revert geometry. - Legacy \`WallHeightArrowHandle\` in wall-move-side-handles.tsx switched to the same pattern (was the only inline-drag handle in that file — side-move + corner pickers hand off to other tools). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * floating menu: enable Move icon for wall / door / window Widens the \`onMove\` gate on \`NodeActionMenu\` so wall, door, and window join column in showing the Move chevron. \`handleMove\` calls \`setMovingNode(node)\` which dispatches through the existing \`affordanceTools.move\` path on each kind's definition (already present for all three). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * handles: live drag, guide rings, dimension chips; floating-menu Move on stairs - Window bottom height arrow: flip Z rotation so chevron points down when placement Y < 0. Door / column height arrows unaffected (still above the node). - Floating menu: raise stair-segment offsets (segmentType is 'stair' | 'landing', so the legacy 'stair-flight' key was dead); enable the Move icon for parent stair and stair-segment. - HandleDecoration on LinearResizeHandle + RadialResizeHandle. Generic GuideRing renders at node-local (0, y, 0) in the XZ plane when the arrow is hovered or dragging. Curved/spiral stair width arrow gets an outer rim ring, inner-radius arrow gets an inner pillar ring, and column radius arrow gets a footprint ring on round / octagonal / sixteen-sided shafts. - ArcArrow migrated to the live-override pattern (sweepAngle + rotation). NodeArrowHandles subscribes to useLiveNodeOverrides for the selected node and merges into the effective node, so arrow positions, decorations, and dimension chips all track the in-flight drag instead of freezing at pre-drag values. - StairRenderer and ColumnRenderer subscribe narrowly to their own override entry and render against the merged effective node, so the curved/spiral mesh and the column body update per pointer move without zustand churn. - DimensionLabel chip (<Html>) rendered next to every linear-resize / radial-resize arrow on hover or drag. Format follows the wall / fence label recipe (metric / imperial via useViewer.unit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fence + column: registry handles, brace spread arrows, per-style defaults - Fence: side-move arrows already on the registry path; add the height arrow (axis 'y' linear-resize, anchor min) + start/end corner pickers (tap-action, shape 'corner-picker' with dashed leader + billboarded hex). Move icon enabled on the floating menu; the menu floats above the height arrow via a fence-specific MENU_Y_OFFSET. Endpoint move buttons + Alt-detach plumbing removed from the floating menu — corner pickers cover that flow. Legacy wall-move-side-handles.tsx no longer branches into fence (dedupes the side-move arrows that were stacked). - Column: bottom + top spread arrows for non-vertical supports — anchor 'center' so dragging the right leg outward grows the full leg-to-leg span symmetrically. Conditionally added per supportStyle: - a-frame: both bottom and top spreads - y-frame / v-frame: top spread only Per-style preset map applied on supportStyle switch (panel.tsx) so every style snaps to its renderer's natural proportions (defaults lifted from each support's fall-through expressions); a customised A-frame switched to Y-frame no longer carries its 1.4 m bottom into state, and an X-brace gets equal parallel legs rather than inheriting A-frame's pinched 0.12 m top. - GeometrySystem: merge `getEffectiveNode(node)` before calling `def.geometry`. Smooths drags for every kind on the parametric path (fence, shelf, item, anything that ships `def.geometry`): live override mutates the mesh per pointer move, zustand only hears the commit. Mirrors WallSystem / DoorSystem / WindowSystem / StairRenderer / ColumnRenderer hookups. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * elevator + column: registry handles, rotation gizmo with curved arrow - Elevator: width / depth / cab-height arrows on the registry path (anchor='center' for width/depth so dragging outward grows the full span symmetrically; anchor='min' for cab-height with shaftTopY resolved through `resolveElevatorLevels` so the arrow lands above the full shaft on multi-level elevators, not just the cab top). Floating-menu Move icon enabled + a fence-style MENU_Y_OFFSET so the menu floats above the height arrow. - Whole-node rotation gizmo for both elevator and column. Uses arc-resize with `shape: 'rotate'` + a new `decoration` ring on ArcResizeHandle. Curved-arrow geometry is a two-headed icon (arc ribbon with chevron wings + tangential tip at each end), rendered in node-local XZ plane at mid-height. Guide ring traces the rotation circle (footprint-diagonal + small offset) on hover or drag. Position offsets along +Z only — sticks out the front of the node instead of diagonally at the corner. apply() negates the cursor angular delta (atan2(z,x) is opposite-handed from three.js Ry) so dragging CCW around the node rotates the node CCW. - ArcArrow renderer extended: tracks `isDragging` like LinearArrow, renders the optional ring decoration, and swaps geometry between the chevron (default, used by stair-sweep handles) and the new curved-arrow shape when `shape: 'rotate'` is set. - HandlePlacement.position / .rotationY now optionally take a `sceneApi` so descriptors that depend on cross-node state (the elevator's level-chain resolution) can compute placement against the live scene. SceneApi gains a `nodes()` accessor returning the full record. Test stubs in core (relations-resolver, drag-session, hosting) updated to satisfy the new shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * slab + ceiling + shelf: registry handles, live polygon preview, cursor polish - slab: per-edge resize chevrons in PolygonEditor (gated on `allowEdgeMove`, so site / zone editors are unaffected), height arrow via `def.handles`, and a floating-menu Move icon. Polygon drags now publish the in-flight polygon to `useLiveNodeOverrides` through a new `onPolygonPreview` prop; GeometrySystem rebuilds the slab mesh at pointer rate while the store stays untouched until the single commit on release. Hole editor wired the same way. Handle materials switched meshStandard → meshBasic so the blue corner / green midpoint cylinders read true colour instead of dimming in scene lighting. - ceiling: same Move icon, per-edge arrows, height arrow, and live preview through the boundary + hole editors. CeilingSystem now merges via `getEffectiveNode`, so polygon and height overrides flow through on every dirty tick. Height arrow placement is mesh-local (not `height + offset`) because CeilingSystem parks `mesh.position.y` on the height value. - shelf: width / depth / height arrows + a curved rotation gizmo with ring decoration. Move icon on the floating menu. Shelf stores rotation as a tuple, so the rotate `apply` reads back `[x, y, z]` and only mutates `y`. - LinearArrow: snapshot `rideObject.matrixWorld.invert()` at drag-start and reuse it in `onMove`. Kinds that park `mesh.position` on the field being dragged (ceiling `height`) used to chase a moving ride frame, so the local-Y delta collapsed and the value stalled / jittered. - ArcArrow: cursor is `'grab'` on hover and `'grabbing'` during the drag (was the misleading `'ew-resize'`); the `Cursor` type gains those two members. - ParametricNodeRenderer: merge `useLiveNodeOverrides` for position + rotation so the rotation gizmo shows live motion through the outer group — GeometrySystem already covered geometry-affecting fields. - floating menu offsets: slab 0.4 → 0.7, ceiling 0.4 → 1.0, shelf 0.6, so the menu floats above each kind's new height arrow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * roof-segment: registry handles, live override path, analytical-normal fix - roof-segment: width / depth / wall-height / pitch / rotation arrows; pitch drag back-solves the angle from peak-height via the slope frame - roof-system: getEffectiveNode + useLiveNodeOverrides so drags rebuild the segment + merged shell live, commit-on-release stays a single write - floating menu: Move icon for roof-segment; uniform EXTRA_MENU_LIFT - skylight / solar-panel / box-vent ghost: fix analytical normal — shed sign flip, mansard / dutch +X face direction, gambrel + mansard tier awareness; one (dx·tan, 1, dz·tan) formula across all roof types Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * floorplan: resize/rotate arrows for column, shelf, elevator, fence; rotation handedness fix - Column / shelf / elevator: per-cross-section resize arrows in the 2D floor plan, matching the 3D handle set (width / depth / uniform / radius / brace dims), plus a corner rotate-arrow. Body move stays on the move-handle dot via the registry overlay's generic translate. - Fence: floor-plan curve sagitta handle + side move-arrows + a body-move target (`fenceFloorplanMoveTarget`) with linked-fence endpoint cascade and ALT-detach. Commit strips `isNew` metadata and re-selects so the chrome stays visible at the new position. - Roof-segment: floor-plan resize + rotate arrows wired through new affordances; `resolveSegmentFrame` aligns with the builder's transform so handles stay glued to the rendered footprint. - Stair: in-world rotate gizmo bow orientation derived from the gizmo's position (was a stray `-π/4` that read as "pointing outward" on the spiral). StairSystem now merges the live override before the slab-elevation spatial query, so dragging the rotate gizmo no longer drops the group's Y when a segment swings off its pre-drag footprint. - Rotation handedness: floor-plan now plots column / shelf / roof-segment at `-rotation` so SVG's CW-with-y-down `rotate` visually matches Three.js Y-rotation (CCW from top-down). Same `rotation` value rotates the same direction in both views, and the same cursor gesture writes the same sign — `- delta` in every rotate affordance, lined up with the 3D handles. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * registry: floorplanScope + movable capability; cursor focus under mouse - Registry: duplicate-kind throws in production, warns in dev (HMR). New `kindsWithFloorplanScope('building')` and `isRegistryMovable` helpers; `resolveBuildingForLevel` extracted into spatial-grid-sync. - Floorplan registry layer: building-scoped kinds (elevator today) now dispatched via `def.floorplanScope === 'building'` instead of a hardcoded `node.type === 'elevator'` arm. - FloatingActionMenu: Move button gated by `isRegistryMovable(kind)`, replacing the 13-arm `node?.type === '…'` chain so adding a movable kind no longer touches this file. - 2D cursor indicator: render at the raw mouse position in all modes (drop the snapped `cursorAnchorPosition` machinery) so the badge always sits under the cursor. - 3D grid reveal ring: the shader's `positionLocal.xy` is in grid-mesh-local space, but the cursor uniform was in world coords — so the ring drifted by the building's world XZ. Store the last world cursor and re-derive the local uniform every frame after the mesh's XZ lerp, so the ring stays locked under the mouse including during the catch-up frames after a building rotation commits. - Roof system: tighten the merged-shell filter's type predicate so TS narrows `n` before `hasSegmentMaterialOverride(n)`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * wall: corner billboard, perpendicular grid snap, drop 45° from move/draft - Corner picker discs (3D move + wall corner leader) now solve `parentWorld⁻¹ · cameraWorld` so they face the camera even when an ancestor building/level has a rotation; the old `camera.quaternion` copy silently broke under any parent rotation. - Side-handle wall move snaps the wall centre's *absolute* perpendicular projection to grid lines, so axis-aligned walls land on real grid positions regardless of where they started. - Wall draft + endpoint move (3D and 2D) drop the 45°-from-start angle snap. It was useful for picking a direction during the very first draft, but during a perpendicular endpoint drag it pulls the cursor onto a 45° ray from the fixed corner instead of tracking the grid. - Shift now selects the fine grid step (`WALL_FINE_GRID_STEP = 0.05`) for precision placement in every wall snap call site, replacing the former "Shift = bypass angle snap" semantics with a consistent "Shift = finer snap" convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fence: drop 45° angle snap from draft + endpoint move, Shift = fine step Mirrors the wall convention shipped in e89a8228: - `snapFenceDraftPoint` gains an optional `step` override. - Fence draft (3D `tool.tsx` and 2D `floorplan-panel.tsx`, `use-floorplan-background-placement.ts`) snaps to the active grid step only — no 45°-from-start snap. Shift switches to `WALL_FINE_GRID_STEP` for precision placement. - Fence endpoint move (3D `actions/move-endpoint.ts` and 2D `floorplan-affordances.ts`) drops `start`/`angleSnap` so a perpendicular drag tracks the grid instead of pulling onto a 45° ray from the fixed endpoint. Shift switches to the fine step. Also fixes the matching wall click path in `use-floorplan-background-placement.ts:215` that was missed in e89a8228, plus its locally-injected `snapWallDraftPoint` signature. Side-handle perpendicular slide (`fence/move-tool.tsx`) was already grid-snap-only without 45°, so it's untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * floorplan menu: show Move button for selected walls Removes the `node.type !== 'wall'` exclusion that hid the Move icon on the 2D floor-plan floating menu for walls. The dispatcher already has a working path for walls — `def.affordanceTools.move` routes to `MoveWallTool` (perpendicular slide + linked-wall cascade) — so the menu just needs to expose the button. The original opt-out called the menu entry "redundant" because walls also have side-arrow handles, but the user wants the same icon walls get the same affordance as every other selected element in the floating menu. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * floorplan wall move: axis-lock to wall normal, match 3D MoveWallTool The 2D `wallFloorplanMoveTarget` was applying the raw cursor delta in XZ, so dragging a selected wall in the floor plan let it free-float sideways and lengthwise. The 3D `MoveWallTool` constrains the same drag to the wall's perpendicular axis (sideways slide only) — this brings the 2D path into parity. - Captures the wall's centre and the `getPerpendicularWallMoveAxis` normal at session start. - Each tick, projects `originalCentre + rawDelta` onto the axis, snaps that absolute scalar to the active grid step, and translates the wall by `axis * perpDelta`. Same math as the 3D tool. - Shift bypasses snap (raw projection), matching the 3D convention. - Degenerate zero-length walls fall back to free XZ motion (rare; they're already destined for deletion via the junction planner). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * floorplan move overlay: commit at last pointermove, not pointer-up The overlay used to re-run \`session.apply\` with the pointer-up coordinates before committing, on the assumption that pointer-up might fire without a preceding pointermove. Side effect: when the pointer-up coord crossed a grid-snap boundary relative to the last pointermove, the snap flipped to a different cell and the moved node visibly jumped at release from where the drag had painted it. Trust the last pointermove instead — modern browsers reliably emit a final pointermove right before pointerup, and "what you saw is what gets committed" is the UX users expect. The previous sub-pixel drift fix loses to the visible boundary-jump it caused. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * floorplan: door wall-hit placement, fence/stair move fixes, wall auto-ceiling sync - Door / window placement: registry layer entries no longer swallow pointer events while a door / window tool is active, so clicking ON a wall now triggers placement (previously only clicks NEAR a wall worked — the wall's registry-entry `<g>` was stopping the pointer event before it reached the SVG background handler that emits `wall:click`). - Fence floor-plan move: 3D `MoveFenceTool` now respects `movingNodeOrigin === '2d'` on unmount. Without the guard, the 2D overlay's commit would call `setMovingNode(null)`, unmounting the 3D tool, whose cleanup then ran `restoreOriginal()` and reverted the just-committed positions — the "fence reverts on commit" symptom. Mirrors the wall move-tool's existing guard. - Stair floor-plan move: anchored, delta-based motion (was position-jumps-to-snapped-cursor), and reads `getWallGridStep()` instead of hard-coded 0.5 so the stair snaps to the editor's current grid step in real time. Matches the 3D `MoveRegistryNode` commit position. - Stair segment length arrow: drop the placement `rotationY` — `axis: 'z'` already auto-rotates the chevron by `-π/2`, stacking another `-π/2` spun the tip to `-X` (sideways) instead of `+Z` (forward off the run). Matches shelf / roof-segment. - Stair segment system: merge `useLiveNodeOverrides` when rebuilding geometry, chain transforms, merged mesh, and slab elevation, so width / length / height drags show the live value on the mesh and the store only gets the final tracked write on commit. - Stair length arrow position: offset 0.06 m past the front edge so the head clears the stair fill and reads as pointing forward off the run rather than lying across the edge. - Stair default railing mode: `'both'` for new placements (was `'right'`). - Wall floor-plan move: anchored at first cursor sample (was raw centre) so the floating-menu drag-icon offset doesn't jump the wall to a different snap cell on grab. - Wall move: live auto-slab + auto-ceiling preview via `useLiveNodeOverrides`. The store stays at pre-drag values during the drag; commit writes the final plan in one atomic `applyNodeChanges` (creates / updates / deletes deferred from per-tick to commit so undo rolls the whole topology change back in one step). Adds `planAutoCeilingsForLevel` + `AutoCeilingSyncPlan` exports mirroring the existing auto-slab planner. - Wall draft: expose `WALL_ENDPOINT_SNAP_RADIUS` (0.7 m) for endpoint snap intent — strongest user intent (closing polygons, attaching to corners) wants a more generous radius than the generic join snap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * open-pr skill: refresh existing PR description instead of bailing Adds a 3b branch to the open-pr skill: when gh pr view finds an existing PR, regenerate the body from current branch commits/diff while preserving Screenshots verbatim and the user's checklist tick state, then apply via gh pr edit. Previously the skill would print the URL and exit, leaving stale descriptions on long-lived feature branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * floorplan: capability/hook dispatch, generic resize + endpoint state Removes per-kind `node.type ===` arms from the floorplan layer (door/window opening placement, wall live-override merge, building ambient context) in favour of new NodeDefinition capabilities and hooks. Collapses 12 `resizing*` editor-store fields into one `activeHandleDrag`, the wall/fence endpoint-move dispatch into a kind-keyed table, and renames now-shared wall utilities to segment-generic names. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 3d wall measurement: hide label for selected walls Item measurements still render; the wall branch is left in place so re-enabling is a one-line gate flip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * editor: set EDITOR_LAYER on arrow handles to exclude from thumbnails Arrow meshes portaled into the 3D scene were missing EDITOR_LAYER, so ThumbnailGenerator's camera (which calls cam.layers.disable(EDITOR_LAYER)) would render selection handles into captures. Add a useEffect in NodeArrowHandlesForNode that traverses the portal root group and sets EDITOR_LAYER on every child. The effect re-runs whenever descriptors change so newly created meshes (e.g. when handle count changes for the same selected node) get the layer tag immediately. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: open-pascal <open@pascal.app> |
||
|
|
87384cfbab |
feat(roof-system): six roof-accessory kinds (chimney, dormer, skylight, solar-panel, ridge-vent, box-vent) on the registry model (#330)
* 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(box-vent): port to packages/nodes registry shape Move box-vent from the legacy scattered layout (core schema + viewer/systems/renderers + editor/tools/panels/sidebar) into a single `packages/nodes/src/box-vent/` folder following the Phase 5 Stage E pattern. The kind now self-registers via the built-in plugin. - schema lives in `core/schema/nodes/box-vent.ts` (referenced by the hand-maintained AnyNode union) and re-exports from the kind folder. - `def.renderer` reads the parent roof-segment from useScene, applies the slope tilt + segment yaw + node rotation stack, and follows the segment's useLiveTransforms override during a parent drag. - geometry builder is pure and shared by renderer / preview / tool / unit tests. `computeBoxVentSlopeTilt` is lifted as a helper for future reuse by other roof-mounted kinds (skylight / solar-panel). - placement tool listens to `roof:*` events, snaps to the segment under the cursor, creates a new BoxVentNode parented to that segment. - BoxVentEvent + `NodeEvents<'box-vent', BoxVentEvent>` added to the event bus so `useNodeEvents(node, 'box-vent')` type-checks. Verified: workspace `bun run build` + `bun run check-types` pass; 13 new unit tests in `__tests__/{schema,geometry}.test.ts` pass. Worked example for porting the remaining roof-system kinds (ridge-vent, chimney, solar-panel, skylight, dormer) — see `.claude/PORT-CHEATSHEET.md`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ridge-vent): port to packages/nodes registry shape Same pattern as box-vent (`752ace83`): one folder under `packages/nodes/src/ridge-vent/`, schema in core, registration via the built-in plugin. No outside-the-folder edits beyond core schema/types, the event bus, and the plugin index. - pure geometry builder shared by renderer / preview / tool / tests, covering all three styles (curved cap / shingled / metal) and the optional end caps. - custom `def.renderer` reads the parent roof-segment, follows useLiveTransforms during a parent drag. No slope tilt — the ridge IS the high line of the segment so the transform stack is one level shallower than box-vent. - placement tool snaps the cursor to the ridge (segment-local Z=0) wherever the cursor lands on a segment, then commits on click with Z=0 baked into the new node's position. - RidgeVentEvent + NodeEvents<'ridge-vent', ...> added to the event bus. Verified: workspace build green, 9 new tests pass alongside the 13 box-vent tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(chimney): port to packages/nodes registry shape (Option C) Same shape as box-vent (`752ace83`) and ridge-vent (`10d489d6`). **Scope — Option C.** Chimney lands in the registry with solid geometry; the CSG-driven decoration (cap flue holes, body cavity, panels, bands, and the roof-trim that hides the chimney bottom inside the deck) is preserved in the schema but NOT rendered yet. These re-light when roof-segment migrates to Stage B and introduces a `roofCutout` capability the parent segment can read. Visual consequence: a placed chimney intersects the roof at the deck line instead of having a clean CSG-cut hole around it. Placement, move (via the legacy floating-vent-actions until the affordance tool is ported), paint, inspector edits, undo, and delete all work correctly. - pure builder returns `{ body, cap, flues, cricket }` so each piece carries its own material (body/top split matches the schema's `material` vs `topMaterial`). Body height derived from the parent segment's `wallHeight + (flat ? 0 : roofHeight) + heightAboveRidge`. - custom `def.renderer` reads the parent segment via `useScene`, follows `useLiveTransforms` during a parent drag. - placement tool listens to `roof:*` events, creates a new ChimneyNode parented to the targeted segment with segment-local coordinates. - ChimneyEvent + NodeEvents<'chimney', ChimneyEvent> added to the event bus. - ChimneyMaterialRole helper re-exported from core (used by the paint-mode picker — keeps the legacy multi-surface signature). Verified: workspace build green, 11 new tests pass (36 total across box-vent / ridge-vent / chimney). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(solar-panel): port to packages/nodes registry shape (Option C) Fourth roof-mounted kind, same shape as box-vent (`752ace83`), ridge-vent (`10d489d6`), and chimney (`45038713`). - pure builder generates the rows × columns cell grid as a single merged BufferGeometry with two render groups (frame + glass) so one mesh can take a `[frameMaterial, panelMaterial]` array. - analytical roof-surface helpers (`getSurfaceY`, `getAnalyticalNormal`, `surfaceQuatFromNormal`) live alongside the geometry builder and drive both the renderer (when `surfaceNormal` is absent from the node) and the placement preview/commit. - placement tool stores the analytical surfaceNormal on the new node so the runtime renderer and the placement preview produce the same orientation. - `solar-panel-presets.ts` moved into core (it was already imported from the schema there) and re-exported through `@pascal-app/core`. - inspector parametrics cover preset, grid, panel dims, mounting (flush/tilted with `tiltAngle` shown only when tilted), standoff, and frame. - SolarPanelEvent + NodeEvents<'solar-panel', ...> on the bus. **Option C still applies**: panels visually sit on the roof surface but the roof is NOT cut beneath them; the legacy renderer's useFrame-driven quaternion smoothing is replaced by a static quaternion computed once per render. Surface tracking under live parent rotation comes back when roof-segment migrates to Stage B. Verified: workspace build green, 16 new tests (52 total across the four ported kinds). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(skylight): port to packages/nodes registry shape (stub) Fifth roof-mounted kind. Schema is complete, but the geometry and animation surfaces are intentionally stubbed — this commit lands the registration so the kind is present in palette / inspector / sidebar / undo, and follow-up commits flesh out the type-specific geometry and the animation system. **Scope.** - Schema: every field from the archive ports verbatim (25 fields, five `skylightType` variants, opening/sliding state, lantern proportions, curb). - Geometry: frame + glass rendered as plain boxes regardless of `skylightType`. Lantern slope, opening swing tilt, and sliding panel offset from the archive are not yet rebuilt. - Animation: `operationState` and `slideFraction` round-trip via the inspector but don't drive geometry yet and don't interpolate over time. The legacy animation lived in `useInteractive.skylight Animations`, which doesn't exist on main — re-introducing that surface is a focused follow-up. - Inherits Option C from chimney: no CSG cutout into the roof; no frame CSG (4 box rails instead). **Why ship the stub now**: the framework wiring (schema in core, event bus entry, plugin registration, inspector descriptor, custom renderer with parent-segment lookup, placement tool) is the part that's reusable across all five `skylightType` variants. Wiring + box geometry takes the kind from "doesn't exist" to "place / move / paint / delete / undo all work" without committing to the harder type-specific geometry decisions. Follow-up commits: - type-specific geometry (lantern slope, opening tilt, sliding offset) - animation system + `useInteractive.skylightAnimations` extension Verified: workspace build green, 7 new tests (59 total across the five ported kinds). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dormer): port to packages/nodes registry shape (stub) Sixth and final roof-mounted kind. Schema complete; geometry stubbed as a house silhouette (box body + triangular gable). Same Option C inheritance as chimney, solar-panel, and skylight. **Key call: window is inlined, not a hosted child.** The archive's dormer carries its window opening as parametric fields on its own schema (`windowWidth`, `windowColumns`, `windowSill`, etc.) — not a hosted `WindowNode` child. So `relations.hosts` stays unset and the kind doesn't need a `children` field. The 17 window-* fields stay in the schema; geometry beyond the silhouette stub picks them up later. - per-surface material resolution (`getEffectiveDormerSurfaceMaterial`) ports verbatim into core with the same cross-fallback semantics (top → material, side ↔ wall, then legacy `material`). - placement tool follows the established pattern (`roof:*` events, segment-local commit, analytical surfaceNormal stored). - `RoofType` import resolved from the existing `roof-segment` schema on main (the archive's `./roof-type` file is consolidated there). - DormerEvent + NodeEvents<'dormer', DormerEvent> on the bus. **Stub scope.** Geometry renders gable-only regardless of `roofType`; no window opening cutout, no window frame, no sill, no roof trim where the dormer meets the host segment. The archive's geometry relies on `getDormerExposedFaces` + `generateDormerGeometry` from the legacy roof-system, neither of which exists in `packages/nodes`. Follow-up commits add per-roofType dormer roofs, the window opening+frame+sill, and the trim/CSG against the parent segment. Verified: workspace build green, 12 new tests pass (71 total across all six ported kinds; pre-existing spawn parity failures unrelated). All six roof-system kinds now live in the registry shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: porting cheat-sheet for the roof-system kind migrations Reference doc kept alongside the six kind ports (box-vent, ridge-vent, chimney, solar-panel, skylight, dormer) so future kind authors can follow the same shape. Captures: - the per-kind folder layout (13 files, what each one owns) - the three-checkbox composition model (`geometry` / `renderer` / `system`) - every `NodeDefinition` field with usage notes - the wiring touch-points outside the kind folder (`packages/nodes/src/index.ts`, `packages/core/src/events/bus.ts`, the AnyNode union, the core schema exports) - per-kind decisions for the six roof-system kinds (which checkboxes each one ticks, what gets stubbed, what's deferred) - pitfalls hit while porting (material-cache leaks, group-transform mutation, host-kind children fields, Path 1 vs Path 2 floorplan move) - a pre-PR checklist Kept under `.claude/` (not `wiki/`) since it's a working note for the in-flight migration, not authoritative project documentation. Move into `wiki/architecture/` later if it earns its keep. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: wire paint targets + skylight animation surface for new kinds Two small additions cherry-picked from roof-system-archive that the six kind ports depend on. Both are mechanical and unblock follow-up work without changing existing behavior. **Paint targets.** Add `chimney`, `skylight`, `dormer` to `MaterialTarget` enum so the paint picker surfaces these kinds. Wire `chimney` and `dormer` into the relevant material-library target arrays (WALL_TARGETS, SLAB_TARGETS, WALL_AND_SLAB_TARGETS, ROOF_TARGETS) so wall / slab / roof material catalog entries are offered when painting a chimney or dormer. Without this the new kinds' `material` / `materialPreset` fields can be set programmatically but the user-facing paint flow has nothing to target. **Skylight animation surface.** Port `SkylightInteractiveState` + `SkylightAnimationState` types, `skylights` / `skylightAnimations` store fields, and four actions (`setSkylightOpenState`, `removeSkylightOpenState`, `startSkylightAnimation`, `cancelSkylightAnimation`) onto `useInteractive`. Mirrors the existing door / window animation surfaces one-for-one. This is the prerequisite the skylight stub commit (`6dcee1ee`) called out — the follow-up commit that adds the skylight animation system component + wires `operationState` into the renderer's geometry now has something to consume. Neither change touches the six kind folders or their definitions — the kinds will pick up the new paint targets automatically and the skylight animation surface is dormant until a consumer ports forward. Verified: workspace build green, 71/71 kind tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(palette): surface the six roof accessories in StructureTools Closes the UX gap from the kind ports: box-vent / ridge-vent / chimney / solar-panel / skylight / dormer are registered in the registry with `def.tool` and `presentation`, but the top palette (`StructureTools`) is currently driven by a hand-coded `tools` array, not by the registry. So the new kinds existed in the codebase but had no entry point in the running editor — the user had no way to add them. - Extend `StructureTool` union in `use-editor.tsx` with the six new kind IDs so `setTool('chimney')` typechecks. - Add six entries to the `tools` array in `structure-tools.tsx`. All use the existing `/icons/roof.png` (a kind-specific icon set is a follow-up). The ToolManager already dispatches `nodeRegistry.get(tool)?.tool` (`tool-manager.tsx:28`), so clicking a new palette button activates the kind's registered `def.tool` automatically — no further wiring needed. Follow-up: a `parametrics.customPanel` on `roofDefinition` that surfaces inline "Add Chimney / Skylight / Dormer / ..." buttons in the roof inspector (matching the legacy `roof-panel.tsx` UX). For now, top palette is the entry point. Verified: workspace build green, 71/71 kind tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(roof): add inline "Add Element" section to the roof inspector When a roof is selected, the inspector now shows six quick-add buttons (Chimney, Dormer, Skylight, Solar Panel, Box Vent, Ridge Vent) in an "Add element" section between Position and Actions. Closes the discoverability gap from the kind ports — the user no longer has to hunt for the kind in the top palette. - Lives in `packages/nodes/src/roof/panel.tsx` (the roof's existing customPanel — it already escapes the auto-derived inspector to render Segments + Position + Actions). - Each button calls `useEditor.getState().setTool(kind)` to activate the kind's registered `def.tool`. The ToolManager dispatches via `nodeRegistry.get(tool)?.tool` (`tool-manager.tsx:28`), so this reuses the same code path as clicking the kind in the top palette. - Tools listen for `roof:*` events — after clicking "Add Chimney" the user clicks anywhere on a roof segment to commit the new node parented to that segment. Mirrors the legacy `roof-panel.tsx` UX (which had inline Add buttons that created hidden nodes + entered move mode); the registry-shaped equivalent activates the placement tool instead so the user sees a preview that follows the cursor. Verified: workspace build green, 71/71 kind tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ux(palette): remove roof accessories from the top palette Six kinds (box-vent / ridge-vent / chimney / solar-panel / skylight / dormer) only make sense in context of a selected roof segment — putting them in the top palette clutters it for users not actively editing a roof. They're entered through the roof inspector's "Add element" section instead (added in 275af8f4), which routes to the same registry-driven placement tools. - Remove the six entries from the `tools` array in `structure-tools.tsx`. - Keep `StructureTool` union additions in `use-editor.tsx` since `setTool('chimney')` etc. still need to typecheck from the roof panel's `activateTool` callback. Verified: workspace build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(placement): always resolve a roof segment on click so add commits Likely root cause of "Add Element clicks not adding anything." The six roof-mounted placement tools each had a private `resolveSegmentFromWorldPoint` that returned null when the click's segment-local (x, z) fell outside `width/2 × depth/2` — but the visible merged-roof mesh extends past those bounds by the segment's overhang. Clicks landing anywhere in the eave band, or beyond every segment's nominal footprint, silently no-op'd: `onClick` early-returned on `if (!hit) return` and no node was created. - Extract a shared `resolveRoofSegmentHit` into `packages/nodes/src/roof/segment-hit.ts`. - Bounds check now includes `seg.overhang` on each side, matching the visible roof mesh. - If no segment passes the exact check, fall back to the FIRST segment with the click point projected into its local frame. Same policy the legacy `roof-panel.tsx` used (it parented all add operations to `segments[0]` and let the user move afterward). - Rewire box-vent, ridge-vent, chimney, solar-panel, skylight, and dormer placement tools to use the shared helper. Drop the per-tool copies (and the now-unused `RoofSegmentNode` import in 5 of them). After this, clicking "Add Chimney" / etc. in the roof inspector followed by a click anywhere on the visible roof commits the new node every time. Verified: workspace build green, 71/71 kind tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(roof): mount accessory children of roof-segments so Add actually adds Closes "Add Element click adds nothing to the scene." After the click, the new chimney/skylight/dormer/box-vent/ridge-vent/solar-panel node was being created in `useScene.nodes` with `parentId: <segmentId>` — but nothing mounted it visually. Two missing pieces: 1. RoofSegmentNode had no `children` array. `createNodesAction` appends `newNode.id` to `parent.children` only when the parent declares the field (`node-actions.ts:355`). Without it the parent-side write was a no-op, so the accessory existed in the store but nothing ever fired its `<NodeRenderer>` mount. 2. Even with the schema field, `roof-segment/renderer.tsx` was a leaf `<mesh>` — no recursive `<NodeRenderer>` mount of `node.children`. Fix: - `core/src/schema/nodes/roof-segment.ts`: add `children: z.array(z.string()).default([])`. - `nodes/src/roof-segment/renderer.tsx`: emit a `<group>` alongside the placeholder mesh that iterates `node.children` and mounts each via `<NodeRenderer>`. The group carries the same transform as the mesh so accessories inherit the segment's local frame — matching the segment-local coordinates each accessory renderer assumes. - `nodes/src/roof/renderer.tsx`: drop the `visible={false}` segments wrapper. `RoofSystem` only fills the parent roof's `merged-roof` mesh (`viewer/systems/roof/roof-system.tsx:172` via `getObjectByName('merged-roof')`), so segment placeholder meshes stay empty and don't z-fight with the visible roof. Mounting segments inside a visible wrapper is what lets accessory grand- children render at all. Also unblocks the user's `roof/panel.tsx` accessory-list selectors (which loop `seg.children` for chimneys/dormers/skylights/etc.) by giving the schema the field they expect. Verified: workspace build green, 71/71 kind tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(roof-accessories): drop double-applied segment transform from renderers After the previous fix (segments host accessories via recursive NodeRenderer), each accessory was being positioned at *twice* the segment offset — the renderer's outer group still applied `segment.position` and `segment.rotation`, and the React parent (the segment's group) was already at that transform too. Result: chimneys, skylights, dormers, etc. landed in the scene graph but rendered far off-screen — invisible from any normal camera view. Fix the six accessory renderers (box-vent, ridge-vent, chimney, solar-panel, skylight, dormer) to assume the segment's transform is inherited from the React tree: - Drop the outer `<group position={segmentPosition} rotation-y={...}>` wrapper. - Apply `node.position` (segment-local) directly to the ref'd outer group, with the kind-specific tilt / quaternion / yaw on inner groups. - Drop `useLiveTransforms` lookup for the segment — React tree re-renders propagate parent transform changes automatically. - Keep the `useScene` segment lookup; it's still needed for kind- specific math (slope tilt, analytical surface normal, base Y from wallHeight) that reads segment fields beyond just the transform. Chimney's outer group sits at `[0, 0, 0]` because `applyNodeTransform` in `geometry.ts` already bakes `node.position` and `node.rotation` into the chimney's vertex positions (which also bake `baseY` from `segment.wallHeight`). No double application there either. After this, Add Element → click → place puts the accessory at the clicked spot on the roof, visible and selectable. Verified: workspace build green, 71/71 kind tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(scene): migrate existing roof-segments to add `children: []` Closes "Add Element click adds to side panel but not to scene graph." The previous fix added `children: z.array(z.string()).default([])` to RoofSegmentNode, but that default only applies when zod parses the segment fresh. Every roof-segment already in a loaded scene (saved before the schema change) carries no `children` field at runtime. `createNodesAction` (`node-actions.ts:355`) appends the new child id to `parent.children` only when `'children' in parent && Array.isArray(...)` is true. For un-migrated segments that check fails — the chimney / skylight / dormer / etc. is added to `useScene.nodes` (so it shows up in the sidebar tree) but the parent-side write is a no-op, so the segment's children array remains undefined, the segment renderer's recursive `<NodeRenderer>` finds nothing to mount, and nothing appears in the 3D scene. Mirror the existing shelf migration (`use-scene.ts:351`): when the scene loads, patch every roof-segment whose `children` isn't an array to `children: []`. Existing scenes get the field on next load; new segments get it from the schema default. After this the "Add Element" click commits visibly. Verified: workspace build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(chimney): port archive's bespoke tabbed panel as customPanel The auto-derived inspector worked but couldn't reproduce the archive's tabbed UI (Cap / Flues / Shoulder / Bands / Cricket / Panels) where each sub-section gets its own dedicated controls laid out as a grid of selectable cards. Users selecting a chimney got a flat parametric form instead of the dense bespoke editor. - Drop the legacy `packages/editor/src/components/ui/panels/chimney-panel.tsx` into `packages/nodes/src/chimney/panel.tsx` (the kind's customPanel slot). Rewrites: * Helper imports collapsed to one barrel from `@pascal-app/editor` (`ActionButton`, `ActionGroup`, `PanelSection`, `PanelWrapper`, `SegmentedControl`, `SliderControl`, `triggerSFX`). * `sfxEmitter.emit(...)` → `triggerSFX(...)` (same SFX, registry- safe export from `@pascal-app/editor`). * Inline a 3-line `cn` helper since editor doesn't re-export the legacy `lib/utils` one. * `ChimneyPanel` becomes `default export` so customPanel's lazy loader can pick it up. - Wire `chimneyParametrics.customPanel = () => import('./panel')` so the registry's parametric inspector defers to the bespoke component. - Keep `groups` in `chimneyParametrics` for MCP / fallback consumers (the parametric data is still authoritative). User-visible: clicking a chimney now opens the tabbed inspector with the exact category layout from the archive. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(chimney): CSG-trim the body against the roof so it carves cleanly Closes "chimney is not getting trimmed just like in roof-system." Previously the chimney mesh was rendered as solid geometry that intersected the roof shell visually at the deck line — Option C debt called out in `45038713`. Now the body is CSG-cut against the parent segment so only the portion above the shingles is visible, matching the archive's UX. - `packages/viewer/src/lib/csg-utils.ts`: port `csgEvaluator`, `csgGeometry`, `csgMaterials`, `computeGeometryBoundsTree`, `prepareBrushForCSG`, and the `Brush` / `SUBTRACTION` re-exports from `roof-system-archive`. Lives in viewer because `three-bvh-csg` + `three-mesh-bvh` are viewer-only deps. - `packages/viewer/src/index.ts`: expose the CSG primitives + the existing `getRoofSegmentBrushes` (which was already defined on main but not in the package surface). Adding `getRoofSegmentBrushes` to the export — internal already; this just opens it for kinds living in `@pascal-app/nodes`. - `packages/nodes/src/chimney/roof-trim.ts`: new helper `trimChimneyBodyAgainstRoof(body, segment, node)`. Wraps the body in a `Brush`, runs a two-pass `SUBTRACTION` (chimney - wallBrush - shinSlab), returns the trimmed `BufferGeometry`. Returns the input unchanged on any CSG failure so the chimney still renders. - `packages/nodes/src/chimney/renderer.tsx`: memoize a `trimmedBody` alongside the existing geo memo (keyed on the segment shape fields that drive the roof brushes) and pass it to the body mesh instead of `geo.body`. Disposal updated to release whichever buffer is actually live. Deferred (Option C still): bands and panels CSG. They were the same flow but operate on additional pieces; they re-light in a follow-up once the chimney's bands / panels geometry comes back online. Verified: workspace build green, chimney unit tests pass (14/14). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(skylight): port full v1 implementation (panel, 5-type 3D, CSG cut, animations) Brings the skylight node from a box-only stub up to feature parity with the roof-system branch. Mirrors the chimney port pattern established in 9fd42e33 and 65eec685. UI - packages/nodes/src/skylight/panel.tsx — bespoke tabbed inspector (type card picker + per-variant controls: lantern height/scale, opening angle/side/motor, sliding direction/track width, curb, frame, position, rotation). Wired via parametrics.customPanel. 3D - packages/nodes/src/skylight/{renderer,geometry,frame-csg}.tsx — full 5-variant geometry (flat / walk-on / lantern / opening / sliding) with frame ring CSG and type-specific glass (lantern pyramid + cylindrical frame bars; opening hinged glass with optional motor housing; sliding two-pane on tracks). - packages/nodes/src/skylight/preview.tsx — uses the real frame-csg builder so placement ghost matches the committed mesh. Placement / move - packages/nodes/src/skylight/tool.tsx — commits hit.localY so the skylight lands on the outer shingle surface, not the bare-rafter analytical Y (was sinking into the deck). - packages/nodes/src/skylight/move-tool.tsx — kind-owned drag wired via def.affordanceTools.move. Uses SkylightPreview as the ghost so drag and duplicate both show the real frame following the roof raycast. Reparents across segments and dirties old+new for CSG re-cut. CSG cutout - packages/viewer/src/systems/roof/roof-system.tsx — buildSkylightCutBrush added; the per-child loop in updateMergedRoofGeometry subtracts every skylight from shin/deck/wall in segment-local before the segment transform stacks on (matches v1). - Ported v1's getRoofOuterSurfaceFrameAtPoint helper (raycast against the actual outer-shingle module mesh) and made both the cut and the renderer read surface point + normal from it — keeps frame and cut aligned on every roof type incl. hip 4-faces, gambrel, mansard, dutch. - mergeVertices on the cut box before computeBoundsTree — without it three-bvh-csg silently no-ops on the BoxGeometry after applyQuaternion tilts the cut ~90° about the surface normal (hip short faces). - Renderer wraps content in an outer <group position={segment.position} rotation-y={segment.rotation}> so the frame inherits the same segment transform that applyTransform bakes into the cut brush (skylight is rendered under <group name="roof-elements"> at the roof level, not under the segment, so the renderer has to apply it explicitly). - Skylight dirty propagation in RoofSystem: edits/moves dirty the host segment so the parent roof rebuilds. - packages/viewer/src/index.ts — exposes getRoofOuterSurfaceFrameAtPoint, SurfaceFrame, getRoofSegmentBrushes, csg primitives so @pascal-app/nodes can compose roof-aware cuts without a layer violation. Animations - packages/editor/src/lib/skylight-interaction.ts — verbatim port of v1 (toggleSkylightOpenState, closeSkylightOpenState, isOperableSkylightType, SKYLIGHT_TOGGLE_ANIMATION_MS = 520). - packages/editor/src/hooks/use-keyboard.ts — R toggles, T closes operable skylights, mirroring door/window. - packages/nodes/src/skylight/system.tsx — SkylightAnimationSystem ported as def.system; advances skylightAnimations and writes operationState back to useInteractive.skylights. - Dropped the per-tick markSkylightDirty in the animation system. The renderer subscribes to useInteractive directly, so the glass swings/ slides via Zustand re-renders without dirtying the scene — the cut geometry doesn't depend on operationState, so re-CSG'ing the merged roof on every animation frame was pure waste (caused visible lag). - packages/core/src/index.ts — exports SkylightInteractiveState and SkylightAnimationState (interaction lib uses them). Drag / duplicate ghost - Floating action menu's setMovingNode → MoveTool → registry affordance now resolves to the kind-owned move tool. Duplicate already worked through structuredClone + def.schema.parse + setMovingNode; the new move-tool provides the ghost both flows use. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Persist roof segment materials across refresh * feat(solar-panel): full port from archive + placement/orientation fixes Solar panel — ported from roof-system archive to registry shape: **Inspector & presets** - Custom panel (panel.tsx) with preset card grid (Residential / Residential Large / Compact / Frameless); picking a preset now writes all four dim fields (panelWidth / panelHeight / frameThickness / frameDepth) so the inspector immediately reflects the selection. - Auto-fit to roof, Flip orientation, Custom label when no preset matches. - Live preview: renderer subscribes to useLiveNodeOverrides so slider drags update the mesh before the value is committed to the Zustand store. - Registered via parametrics.customPanel (same pattern as chimney). **Texture / materials** - Procedural cell texture (createSolarPanelTexture): chamfered cell shape, dark blue gradient, finger-line and busbar detail drawn on a 256×256 canvas, tiled per cell via stretched UVs in buildSolarPanelGeometry. - getDefaultPanelMaterial singleton uses MeshStandardNodeMaterial (WebGPU- native) so the material integrates with the MRT pass without triggering "Color target has no corresponding fragment stage output / writeMask not zero" GPU validation errors on segment reparent. - defaultFrameMaterial and move-tool previewMaterial also switched to WebGPU-safe types (MeshStandardNodeMaterial / MeshBasicMaterial). **Default grid size** - Schema defaults changed from 4 rows × 5 columns → 2 rows × 3 columns. - Placement ghost and move-tool ghost use a compact 2×3 footprint; committed panels also default to 2×3. **Placement tool** - Commit position uses raycast hit Y (hit.localY from segObj.worldToLocal) instead of analytical getSurfaceY so the panel lands exactly where the ghost was shown rather than sinking into the deck/shingle layers. - Ghost orientation uses the same analytical-normal + explicit-yaw pattern as the placement tool for correctness on rotated segments. **Move tool** - Rewrote ghost to use resolveRoofSegmentHit + getAnalyticalNormal (segment-local) + explicit rotation-y group, matching the placement tool's ghost layout exactly. Dropped unreliable event.normal / world- space quat path that gave wrong tilt on any segment with rotation ≠ 0. - Committed surfaceNormal is now segment-local (not world-space) so the renderer's surfaceQuat + outer segment.rotation group compose correctly without double-rotating the panel. - Uses shared resolveRoofSegmentHit (with surface-Y disambiguation) instead of the private copy, so segment hopping respects the correct face. - Reparents children arrays on segment hop. **Renderer** - Applies segment.position + segment.rotation explicitly (roof accessories are mounted under roof-elements group which has no transform, not under segment subtree). - Merges useLiveNodeOverrides so slider drags update the 3D mesh in real time (same pattern as elevator/skylight renderers). **Scene graph** - SolarPanelTreeNode added; registered in tree-node.tsx type map so panels appear under their parent roof-segment in the sidebar. **Segment-hit disambiguation** - resolveRoofSegmentHit now scores all bbox-passing candidates by |localY − analyticalSurfaceY(localX, localZ)| and picks the smallest, fixing the long-standing bug where hip/gable segments at the same roof origin all pass the axis-aligned bbox test and the first-match (always segments[0]) was returned regardless of which slope was clicked. Benefits all roof-accessory placement tools (chimney, box-vent, skylight, dormer, solar-panel). **Hip-roof normal fix** - getAnalyticalNormal for hip now uses slopeReach = min(w,d)/2 for the Y component on all four faces. The old code used depth/2 for front/back and width/2 for sides, which was only correct for square (w==d) hips; for any other aspect ratio the long-axis faces tilted the panel at the wrong angle. **Dormer, dormer move-tool, window-frame, ridge-vent, box-vent, skylight** - Assorted in-progress work: dormer window-frame geometry, move-tool port, panel refinements, ridge-vent / box-vent panel additions, skylight CSG frame refinements, roof-system geometry improvements, material-paint support, post-processing cleanup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * perf(chimney): fix hook order + share materials + memoise segment brushes - renderer.tsx: hoist `surfaceArray` useMemo above the `!segment || !geo` early return so hook call order stays stable across renders (the previous order would have crashed React the first time segment or geo flipped to null mid-session). - renderer.tsx: replace the module-scoped `bodyMaterial` / `topMaterial` singletons with per-instance fallback materials so a paint-mode or debug mutation on one chimney can't bleed into every other unpainted chimney on the scene; dispose them on unmount. - renderer.tsx: collapse the 36-field hand-maintained dep array on the `geo` useMemo (and the 10-field one on `trimmedBody`) down to the memoised `node` / `segment` references — adding a new schema field no longer risks stale geometry from a forgotten dep, and the `eslint-disable react-hooks/exhaustive-deps` lines are gone. - renderer.tsx + roof-trim.ts: memoise `getRoofSegmentBrushes(segment)` per-segment-shape in the renderer instead of rebuilding the four CSG-ready brushes inside `trimChimneyBodyAgainstRoof` on every call. A chimney slider drag changes `node.*` but not the segment, so the brushes now survive the entire drag instead of being rebuilt and disposed every frame. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(scene): pin reparent behavior for roof-mounted kinds under repeated A→B→A→B Investigating a reported crash where moving a vent across roof segments three times in a row crashes the scene. The hypothesis was duplicate IDs in the host segments' `children` arrays. These tests prove that's NOT the cause: at the store level, the auto-reparent inside `updateNodesAction` leaves children lists clean under repeated hopping for every roof-mounted kind (box-vent, chimney, skylight, dormer, solar-panel, ridge-vent), and even the redundant manual-then- auto pattern the vent move-tools use converges to the same correct state. Crash root cause still under investigation, but these pins prevent the obvious-and-tempting regression where someone "fixes" reparent by hand and accidentally lets duplicates through. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(chimney): give each sub-mesh its own name Body, cap, flues, cricket, and bands were all named `chimney-surface`, so hover/selection couldn't distinguish them and panel breadcrumbs couldn't say "Chimney cap" vs "Chimney body". Rename to `chimney-{body,cap,flues,cricket,bands}`. No code looked up the old literal, so this is a pure naming improvement. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(chimney): smooth round shading, radial cap UVs, cap reveal, flue pots Four visual upgrades to the chimney builder, all in `geometry.ts`: - **Round chimneys now render smooth, not faceted.** The previous `pushCylinderFaces` emitted unindexed triangles, so `computeVertexNormals()` baked per-triangle face normals into every vertex — the 24 polygon segments of a round body / cap / band were visible as flats. Round paths now build per-tier `THREE.CylinderGeometry` (indexed, side vertices shared across radial segments) and merge via `mergeGeometries`. Crisp rim edges are preserved because CylinderGeometry uses separate cap vertices. - **Radial cap UVs.** Old `pushCylinderFaces` pushed `(0,0)` for every vertex on the top/bottom fan, so any texture on a round chimney smeared to a point at the caps. CylinderGeometry gives proper radial UVs (0.5 ± 0.5·cos/sin) for free. - **Cap reveal.** The cap used to sit flush on the body, reading as glued on. New `CAP_REVEAL = 0.003` (3 mm) air gap above the body catches a shadow line and sells the cap as a separate stone / metal piece. `capTopY` (used for flue placement) updates so flues still sit on the actual cap top. - **Flue pots, not drainpipes.** Each flue was a single straight cylinder / box — visually a "drainpipe", not a chimney. New two-tier silhouette: a tall straight shaft topped by a short overhanging rim (12 % of height, capped at 4 cm; rim radius flares 12 %). Reads as a terracotta pot. Total height still equals `flueHeight`, so the bore cutter in `holes.ts` covers the whole envelope unchanged. Removed the now-unused `pushCylinderFaces` helper. Slab path unchanged — square chimneys keep their crisp 90° corners. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(chimney): cornerBevel for square bodies / caps / bands Square chimneys read as plastic boxes at any distance because every vertical edge is a perfect 90° corner that catches no light. New `cornerBevel` field (default 0 → existing scenes unchanged) replaces each corner with a 45° chamfer face. Real masonry chimneys often ship the same detail — a small bevel (~1-2 cm) breaks up the silhouette and reads as stone or chamfered brick. - Schema: add `cornerBevel: z.number().default(0)` to ChimneyNode. - Geometry: extend `pushSlabFaces` with an optional `bevel` param. When > 0, dispatch to a new `pushOctagonalSlabFaces` that emits an 8-vertex ring per y-level (axis-aligned faces + 45° chamfer faces) plus fan-triangulated octagonal caps. UVs follow the same physical-meter convention as the unchamfered path so a brick texture tiles at a consistent rate with and without bevel. - Thread `node.cornerBevel` through `buildBodyGeometry`, `buildCapGeometry`, and `buildBandsGeometry` (square paths only — round bodies have no corners to bevel). - Parametrics: expose under the Body group with `visibleIf` gating on square body for the MCP / fallback inspector. - Panel: add a "Corner Bevel" SliderControl in the Footprint section, same conditional visibility, clamped at `min(width, depth) / 2`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(chimney): style presets — Brick / Stone / Modern / Round The chimney panel exposes 30+ sliders; landing on a coherent silhouette (corbeled stone with a sloped cap and a cricket vs. a straight brick with a double band) takes a dozen edits even when you know what you want. New "Style" segmented control at the top of the panel applies a curated bundle of fields in one click. Presets only touch shape / silhouette / accessory fields: `bodyShape`, `shoulderStyle*`, `cap*`, `band*`, `cricket*`, `cornerBevel`, `panel*`, `flue*`. Dimensions (`width`, `depth`, `heightAboveRidge`), placement (`position`, `rotation`, `roofSegmentId`), and paint (`material*`, `topMaterial*`) are deliberately left alone — applying a preset to an already-sized, already-painted chimney resizes nothing and doesn't overwrite the user's material choices. - `presets.ts`: four preset bundles + `detectActiveChimneyPreset` helper for highlighting the matching preset in the segmented control. - `panel.tsx`: new "Style" PanelSection above Footprint, segmented control wired to `commitProp(chimneyPresets[key])`. Renders with no segment highlighted ("custom") when the current node doesn't match any preset exactly. - `__tests__/presets.test.ts`: round-trip each preset, confirm fresh-default chimneys are NOT detected as any preset, and confirm non-preset fields (dims / materials / placement) don't knock a chimney out of a preset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(chimney): drop the Stone preset Leaves Brick / Modern / Round. The parameterised round-trip test auto-adjusts via `CHIMNEY_PRESET_KEYS`; no test code change needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dormer): bugs, perf, UX, refactor + bundled pitch WIP Dormer review-backlog batch across five passes. Plus the previously unstaged pitch/roofHeight migration on RoofSegmentNode bundled in per session continuity. Dormer — bugs / dead code: - implement the windowSill (toggle was UI-only before) - ghost preview reads wallSkirtHeight and branches on roofType=flat - live-override slider drag swaps the heavy CSG for the fallback - consolidate arch/rounded shape builders between viewer CSG and frame - drop unused surfaceNormal field - collapse getEffectiveDormerSurfaceMaterial fall-through - confine the panel's updateWorldMatrix into a single useMemo - preserve position Y on panel commits (was being zeroed) Dormer — schema hygiene: - DORMER_DEFAULTS named constants replace inline magic numbers - collapse windowCornerRadius + windowRadiusMode + windowCornerRadii into the tuple alone; "All vs Individual" is derived UI state - drop the `as never` id casts; rely on objectId default factory Dormer — tactile UX: - R / Shift+R rotates the placement ghost by ±15° - auto-number new dormer names ("Dormer N", smallest free integer) - DORMER_PLACEMENT_SNAP_M + ROTATION_STEP constants extracted Dormer — code shape: - new use-dormer-placement hook dedupes tool + move-tool (~90% shared) - new <DormerWindowAssembly> isolates the frame/glass/sill JSX - panel.tsx 788 -> 295 lines; Position / Window / Actions sections extracted into per-file components Bundled pitch WIP (pre-existing, unrelated to dormer): - RoofSegmentNode.roofHeight removed; pitch (degrees) added - new helpers in roof-segment: getActiveRoofHeight, getPitchFromActiveRoofHeight, getSegmentSlopeFrame, ROOF_SHAPE_DEFAULTS - migration in use-scene.ts converts legacy roofHeight to pitch - consumers updated: chimney, box-vent, ridge-vent, solar-panel, roof-segment, roof, segment-hit, roof-tool, mcp construction-tools Verification: 12/12 dormer tests pass; targeted tsc on dormer files clean. Workspace bun build of nodes is also affected by the pitch WIP, which is included here per request. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(roof): per-segment top/edge/wall materials + restore legacy UV flow - RoofSegmentNode gains optional topMaterial / edgeMaterial / wallMaterial fields mirroring the parent roof. getEffectiveSegmentSurfaceMaterial resolves through segment-role → segment-legacy → parent fallback. - Segment renderer builds the 4-slot array per role with the parent's array as a fallback so paint at any level reaches the right surface. - Painting a segment directly (segment edit mode hover) writes to the segment's role fields via buildRoofSegmentSurfaceMaterialPatch — the parent roof and other segments are untouched. - Segments with any material override render as their own per-segment mesh inside a new always-visible 'painted-segments' group; the merged- roof CSG skips them (hasSegmentMaterialOverride) so we don't double- paint with the roof's default array. - Paint preview now dispatches to a segment-aware path (applyRoofSegmentPaintPreview) so hover effects land on the visible per-segment mesh instead of the hidden merged-roof. - Drop the createRoofUvGeometry post-CSG re-projection. UVs now flow through CSG (csgEvaluator.attributes includes 'uv') exactly as in the legacy roof-system branch. - Drop the stray [skylight-cut] console.log left in the hot path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(roof-system): registry capabilities for roof accessories, paint, and keyboard Replace kind-name branches in framework code with registry-driven dispatch. Three new NodeDefinition slots back the migration: - capabilities.roofAccessory — host roof cascade + optional CSG cut. Lets viewer's RoofSystem iterate dirty children and call buildCut on any kind that declares it, instead of switching on node.type. Dormer + skylight cut builders moved into packages/nodes/<kind>/. - capabilities.paint — resolveRole / buildPatch / applyPreview / getEffectiveMaterial. Chimney, dormer, and wall now route through it; per-kind arms deleted from selection-manager + material-paint. - keyboardActions — R / T handlers contributed by the kind. Skylight's open/close logic moved from editor/lib to nodes/skylight/interaction. Dormer + skylight kind code (geometry, fallback shape, exposed-face math, window-dim resolver, CSG cut builders) now lives under packages/nodes/ src/<kind>/ instead of packages/viewer/src/systems/roof/roof-system.tsx. The viewer keeps only roof-generic primitives (roof-segment brushes, surface-frame query, CSG dummy mats, material-slot remap). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * review: remove PORT-CHEATSHEET, document roofAccessory/paint/keyboardActions, fix double registry lookup - Remove .claude/PORT-CHEATSHEET.md (AI authoring aid, not for public repo) - Document three new NodeDefinition capabilities in wiki/architecture/node-definitions.md: roofAccessory, paint, keyboardActions - Fix double nodeRegistry.get() lookup in use-keyboard.ts: replace !.keyboardActions!.r!.run() with ?.keyboardActions?.r?.run() for both R and T arms * fix(item-placement): memoize preview/dimension callbacks to stop placement loop updatePreviewGeometry and updateDimensionGuides were declared as plain functions in the component body, so they got a fresh identity every render. Both sit in the placement setup effect's dependency array, which made React tear the effect down and re-run it on every commit — its teardown deletes the draft node while the setup re-creates it, producing an infinite delete/recreate loop ("Maximum update depth exceeded") when opening furnish mode. Wrap both in useCallback with empty deps (they only close over stable refs, module-level helpers, and the setDimensionBounds setter). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(item-placement): correct preview box dimensions and floor rotation Two issues surfaced after the placement-loop memoization removed the accidental every-render recompute that was masking them: 1. Preview box used stale (asset-default) dimensions at draft creation because nothing recomputed it once the imperative draft was made. Recompute the box from the freshly-created draft in `ensureDraft` and the chained next-draft path in `onGridClick`. 2. The green/red box (and the live transform the 2D floorplan mirrors) ignored the floor item's rotation: - `floorStrategy.move` returned a hardcoded `cursorRotationY: 0`; now returns the draft's rotation (`rotY`). - `onGridMove` never applied `result.cursorRotationY` to the cursor group; now it does, so box + floorplan track the draft on every move. - the init seed used the mesh world quaternion, which double-counts building rotation for floor items; floor now seeds from the node's local Y rotation (wall/ceiling keep the world-quaternion path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: open-pascal <open@pascal.app> Co-authored-by: Wassim SAMAD <wass08@gmail.com> |
||
|
|
d48be77f5d | Reduce wall rebuilds during window and door placement | ||
|
|
fe68dbeebd | Constrain wall moves along wall normals | ||
|
|
c31c08fd90 | feat: implement StairOpeningSystem to handle automated stair opening updates and adjust camera distance constraints | ||
|
|
8b73d7986b | feat: expand material library with new wood and flooring textures while cleaning up legacy assets | ||
|
|
a9d26239b7 | Refactor material catalog for roofing and flooring | ||
|
|
a3378a666d | Preserve elevator stop order and handle viewport resize | ||
|
|
d7a1ce89f3 | Scope first-person overlay and unify elevator colors | ||
|
|
5bd2f51d15 | Make fence drawing continue between segments | ||
|
|
5d53be26e5 | Refine wall chaining and door arch clipping | ||
|
|
023cd511e9 | Remove material strips from roof, stair, and fence panels | ||
|
|
4fc57a6fd9 | Enhance elevator placement preview | ||
|
|
89c62800a3 | Refine window panel by family | ||
|
|
2ee69e24e2 | Organize door panel controls by door family | ||
|
|
4a402756fc | Fix floorplan labels and door/fence geometry | ||
|
|
cdde88f9c0 | Clamp spiral stair openings at full sweep | ||
|
|
3800bd7fc8 | fix: increase stair opening buffer constraints and set default offset to 0.15 | ||
|
|
7cc11f0fd6 | feat: add fence move handles and duplication functionality to the floorplan editor | ||
|
|
5f17172638 | refactor: make onMove optional in FloorplanActionMenuEntry and remove unused wall move handler | ||
|
|
2b9bb769f3 | feat: add camera-aware wall move handles and UI layer for floorplan editor, and remove unused elevator icon. | ||
|
|
db5c743e1c | Render curved wall measurements along the wall | ||
|
|
6af3a0aa82 | Add draft angle arcs for walls and fences | ||
|
|
e009f3658b | Use wall face mitering for draft angle arcs | ||
|
|
77c55a2e7c | Reduce viewer rendering cost with lighter materials | ||
|
|
1ffa3d2d73 | Restore item controls and guard placement asset bounds | ||
|
|
aa6885e4c4 | Merge branch 'main' into feat/elevator-system | ||
|
|
ce69433a1a | Refine elevator sync and viewer rebuild handling | ||
|
|
9838d2cd4f | Refactor elevator runtime into core | ||
|
|
b782ce9cf2 |
Merge branch 'main' into feat/elevator-system
# Conflicts: # packages/editor/src/components/tools/item/move-tool.tsx # packages/editor/src/components/tools/tool-manager.tsx # packages/editor/src/components/ui/panels/panel-manager.tsx # packages/editor/src/store/use-editor.tsx # packages/viewer/src/components/renderers/site/site-renderer.tsx # packages/viewer/src/components/viewer/ground-occluder.tsx # packages/viewer/src/components/viewer/index.tsx # packages/viewer/src/components/viewer/post-processing.tsx |
||
|
|
70c85d7760 | Refine wall move previews and expose auto-slab planning | ||
|
|
0fd6e3d144 | Fix mirrored elevator indicators and arrows | ||
|
|
f3444d4275 | Add wall drag previews and fix move arrow handles | ||
|
|
0dcc72a57a | Merge remote-tracking branch 'origin/main' into fix/fri-8-may | ||
|
|
2fc754108d | Use community viewer toolbars in editor app | ||
|
|
564bfa229e |
Merge remote-tracking branch 'origin/main' into fix/fri-8-may
# Conflicts: # packages/editor/src/components/tools/fence/move-fence-endpoint-tool.tsx # packages/editor/src/components/tools/select/box-select-tool.tsx # packages/editor/src/components/tools/wall/move-wall-tool.tsx # packages/editor/src/components/ui/panels/column-panel.tsx # packages/editor/src/components/ui/panels/door-panel.tsx # packages/editor/src/components/ui/panels/window-panel.tsx # packages/viewer/src/components/renderers/column/column-renderer.tsx # packages/viewer/src/components/viewer/index.tsx # packages/viewer/src/systems/slab/slab-system.tsx |
||
|
|
d78308b366 | Hide inapplicable door and window panel settings | ||
|
|
be41511474 | Fix wall move junction ownership | ||
|
|
afadfb2ea8 | Guard viewer BVH against empty geometries | ||
|
|
4956de4910 | Refine column support selection and polygon handling | ||
|
|
92a9846193 | Refactor elevator systems across core, viewer, and editor | ||
|
|
909f96ef09 | Improve elevator floorplan resize and drag controls | ||
|
|
fdf08c091f | Add elevator floorplan overlay | ||
|
|
263228cbf0 | Add cab door-open control and align elevator buttons | ||
|
|
2480be91b1 | Optimize elevator editing performance with live previews | ||
|
|
0514363136 | Add column hit testing and align column defaults | ||
|
|
53393b7def | Add procedural support column variants | ||
|
|
c0a21058d4 | fix list | ||
|
|
d2d714ba68 | Smooth window animations and bay caps | ||
|
|
8f6a8fbe8b | Add first-person window interaction | ||
|
|
133d81e563 | Refine window corner shape support | ||
|
|
eef19804d1 |
Merge branch 'main' into feat/door-improvement
# Conflicts: # packages/core/src/events/bus.ts # packages/viewer/src/systems/door/door-system.tsx |
||
|
|
cef3f24dad | Move door animations into viewer system | ||
|
|
34c847620f | Fix floor item placement cursor height | ||
|
|
7e86e4628c | Fix item surface placement and remove item height annotations | ||
|
|
d403606b72 | Add floorplan side resizing handles | ||
|
|
70933adf9d | Fix floorplan player move and action menu anchoring | ||
|
|
0de07d6130 | Separate runtime door animation and add dual-side handles | ||
|
|
f5f80d0f3f | Centralize door animation state and operation mapping | ||
|
|
0ef2254e6b | Fix first-person door toggles and colliders | ||
|
|
6df4b4895c | Add garage door types and animated door controls | ||
|
|
1490d280d4 | Refine editor layer boundaries and selection handling | ||
|
|
762c9fc763 | Refactor guide events and column placement handling | ||
|
|
db765f1eb3 | Enable pillar duplication in floating action menu | ||
|
|
848529acb0 | Add column presets and procedural shaft controls | ||
|
|
46548c35e5 | Fix curved wall and fence angle measurements | ||
|
|
23eec44714 | Move render systems out of core | ||
|
|
eee7af9e25 | Add draggable level reordering to floating selector | ||
|
|
b3f7957076 | Refactor editor selection and viewer integration | ||
|
|
09bfb0b484 | Add frameless door openings and first-person collider support | ||
|
|
915393d649 | Add swing angle support for door rotation | ||
|
|
3df3d12f46 | Refactor guide UI state and slab rendering | ||
|
|
1dc9ea0ba8 | Replace spawn marker with box-based figure | ||
|
|
f375861439 | Merge branch 'main' of github.com:sudhir9297/editor into feat/improvement-and-fixes | ||
|
|
bbc638af22 | Handle unnamed reference floor levels | ||
|
|
25927dea3b | Add lower floor reference overlay | ||
|
|
8631622e60 | Refactor editor workflow and update UI state handling | ||
|
|
3d0000f93f | Disable pointer events on item placement labels | ||
|
|
411ae2e905 | Refine 2D door swing rendering | ||
|
|
3c185144e0 | Align outdoor tree footprints in 2D editor | ||
|
|
a05573b602 | Make spawn renderer editor-agnostic | ||
|
|
8c10b9280d | Exclude spawn points from level duplication | ||
|
|
e761084635 | Normalize root scene nodes and fix level duplication | ||
|
|
9ff657419c | Merge branch 'main' into feat/upgrade-and-bug-fix | ||
|
|
e688792c34 | Refactor first-person and spawn selection handling | ||
|
|
4796ed8262 | Use slider controls for stair geometry settings | ||
|
|
15daeaace6 | Standardize stair controls on slider inputs | ||
|
|
1c2746f538 | Merge branch 'main' into feat/upgrade-and-bug-fix | ||
|
|
bead8796d2 | Add spawn node support and refine stair and door cutouts | ||
|
|
025a9b5d28 | Add duplicate options to level menus | ||
|
|
a64d6b86cc | Fix floorplan preview update loop | ||
|
|
bc93f09304 | Merge branch 'main' into feat/2d-editor | ||
|
|
d00ba42b9f | Enable drag after duplicating roofs and stairs | ||
|
|
3ae8d6d0fa | Extract floorplan placement and hit-testing helpers |