bbe0e2b0b52fb3d6d820b96377e4d667b9388e35
856
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f51cea9a85 |
feat(editor): Items category bar as icon-grid with tooltips (#349)
Render the function-tree category roots as a Build-style square-tile grid (icon when available, otherwise a two-letter abbreviation) with a hover tooltip for the full name, so the Items and Build panels read the same. Share one TooltipProvider with disableHoverableContent + the icon-grid tooltip class to avoid the flicker when moving between adjacent tiles. Legacy fallback panel category bar wraps instead of scrolling horizontally. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
986d75026f |
viewer: fit shadow frustum to building geometry instead of the camera (#348)
Following the camera look-at broke when zoomed out (fixed ±50 frustum too small to cover the scene) and when zoomed into an empty corner (frustum centred on nothing). Instead, fit the directional light's ortho shadow camera to the building: union the registered scene-node bounds (excluding the site/ground plane), fit a sphere, and size + place the shadow camera to cover that sphere plus a margin. Bounds are refreshed on a short interval since they only change while editing. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
37a3aba672 |
viewer: center shadow frustum on focus so it tracks the view (#347)
The directional shadow light was placed at focus + theme offset (only ~17-32 units), while the ortho shadow camera ran near=1/far=100. That left the focus near the front of a long frustum whose far end swung around as the look-at moved, so shadows appeared not to follow the view. Park the light at a fixed distance along its (preserved) direction and bracket near/far around that distance so the focus stays centered in the frustum depth. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
b8d94a6436 |
editor: tool-defaults seeding + drawTool capability (fence presets) (#346)
* feat(editor): tool-defaults seeding + drawTool capability; fence consumes it Adds a generic, transient `useEditor.toolDefaults` slice keyed by tool, set via `setToolDefaults(tool, params)`. A draw tool's create path merges its entry when minting a node and clears it on deactivation, so a host app can prime the next-drawn node's parameters — placing a saved preset of a drawn kind, or a future "small / medium / large" dimension picker for wall / slab / ceiling. Marks the kind with `capabilities.drawTool` (helper `isDrawnViaTool`) so host apps know to route placement through `setToolDefaults(type) + setTool(type)` instead of cloning a finished instance. Wires fence end-to-end: it declares `drawTool: true`, its create path merges `toolDefaults.fence`, and the draft preview (bar geometry, cursor, HUD label heights) reflects the seeded height/thickness so the ghost matches what will be built. The tool clears its own defaults on unmount. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(editor): restore dropped useEditor import in fence tool The toolDefaults-seeding commit lost the `useEditor` import (formatter stripped it), shipping a runtime ReferenceError when FenceTool mounts. Re-add it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a144502c04 |
feat(editor): hierarchical function-tree Items browse (#345)
* feat(editor): hierarchical function-tree Items browse Add an optional DB-driven taxonomy browse to the Items panel. When the embedder supplies a `functionTree`, the panel renders roots as category tabs and child tags as a secondary chip row, filtering items by the selected node and any descendant slug; otherwise it falls back to the legacy hardcoded category path untouched. Adds `AssetInput.functionTags` so items can carry their function-axis tag slugs, and exports `FunctionTreeNode` for embedders to type the tree. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(editor): sync bun.lock (@pascal-app/mcp 0.3.0) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
8a5f08b842 |
editor: refine preset capture framing + zoom limits (#344)
Tunes the auto-framing and camera clamps used by `setCaptureMode({ mode:
'preset', isolated })`:
- Frame from a 3/4 view of the node's *front* face. Reads the isolated
root's yaw, picks `+Z` (local forward, the face authored for catalog
shots), offsets the camera by 35° to the side so both the front and
one adjacent face are visible. 25° elevation keeps the top visible
without going isometric. Multi-isolate falls back to world `+Z`.
- Bump the bounds-fit multiplier from 1.6 → 2.4 (floor 3 → 4) so the
subject lands with ~25-30% padding inside the locked square crop —
the user can recompose without immediately needing to zoom out.
- Relax `minDistance` from 10 → 0.5 while in preset capture mode (a
0.3–2m preset can't be framed close-in against the editor's general
10m floor). Reverts to 10 on exit so general editing keeps the
looser navigation guardrails.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0ad0ac1591 |
chore(editor): delete legacy door/window presets UI (#339) (#343)
Retires the door/window-only `PresetsPopover`, its `presets-context` adapter, and the `PresetThumbnailGenerator` ahead of the unified preset system landing via the items catalog. Drops the public exports (`PresetsPopover`, `PresetsAdapter`, `PresetsTab`, `PresetsProvider`, `usePresetsAdapter`) and the matching `presetsAdapter` prop / provider wrappers on `<Editor>`, plus the `preset:generate-thumbnail` / `preset:thumbnail-updated` event types in core. Door and window panels now render the regular parametric inspector with no popover trigger; `materialPreset` stays untouched. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1fd59dd9cd |
feat(editor): preset-system primitives (#340) (#341)
* feat(editor): preset-system primitives — presettable, sceneApi subtree round-trip, isolate + setCaptureMode enum, headless exports
Per pascalorg/editor#340 (redesigned: single live canvas, no Viewer scene prop).
Core
- `capabilities.presettable` on `NodeDefinition` + `isPresettable` /
`isPresettableKind` helpers. Explicit `false` on level / building /
site / zone / spawn / guide / scan / item; implicit `true` for any
kind with `def.parametrics`.
- `sceneApi.getSubtreeSnapshot(rootId)` + `materializeSubtree(subtree,
position, parentId?)` for round-tripping a node subtree through
catalog storage. Strips id / parentId / absolute root position /
host refs (`wallId`, `wallT`); fresh IDs minted at materialize time;
child ordering preserved (FIFO walk).
Viewer
- `<Viewer isolate>` prop + `ViewerHandle.setIsolated(ids | null)`.
Walks `sceneRegistry`, hides every registered group not in the
isolated set's ancestor + descendant closure. Building block for
preset capture + future focus-mode UX.
Editor
- `useEditor.captureMode: CaptureMode` discriminated union
(`idle` | `standard` | `preset`). `isCaptureMode` stays as a derived
boolean for the existing read sites; `setCaptureMode` accepts both
the boolean shape (back-compat) and the enum.
- `preset` capture mode in `SnapshotCaptureOverlay`: drag locked to a
square, mode-picker hidden, transparent flag forwarded through the
`camera-controls:generate-thumbnail` emitter event.
- Headless exports: `Inspector` (alias of `ParametricInspector`),
`FloatingMenu` (alias of `FloatingActionMenu`), `ToolbarLeft` /
`ToolbarRight` (aliases of `ViewerToolbarLeft` / `ViewerToolbarRight`),
`useSelection` hook returning `{selectedIds, selectedNode, building/
level/zone}`, plus re-exports of `useScene` / `useViewer` from core /
viewer so consumer shells (community, embedders) need only one import.
Out of scope by design (see issue #340 "Out of scope"): a separate
offscreen Viewer rendering an arbitrary subtree. The unified preset
modal captures inside the live canvas via isolation + the existing
snapshot pipeline — no `useScene` factory / React context refactor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(editor): split snapshot/materialize into pure getSubtree + cloneNodesInto; add def.hostRefFields; auto-stage preset capture square
Per pascalorg/editor#340 redesign discussion: the editor's scene API
should expose *pure* primitives and let the host (community modal,
embedders) own storage shape, position stripping, and host-ref
re-derivation policy.
Editor API delta
- `sceneApi.getSubtreeSnapshot(rootId)` → `sceneApi.getSubtree(rootId)`
Returns the live subtree verbatim (BFS via `children[]`, no clones,
no stripping). Callers deep-clone if they need persistence.
- `sceneApi.materializeSubtree(subtree, pos, parent?)`
→ `sceneApi.cloneNodesInto(nodes, { rootId, parentId?, position? })`
Generic clone-and-insert. Deep-clones via JSON, mints fresh ids
preserving the prefix, rewires parent/children, stamps position +
parent if supplied. Host-ref-agnostic — `wallId`/`wallT` etc are
preserved verbatim.
- New `capabilities.hostRefFields?: string[]` on `NodeDefinition`.
Declares per kind which schema fields are placement-derived so the
host strips them at preset-save time. Declared on door (`['wallId']`),
window (`['wallId']`), item (`['wallId', 'wallT']`).
- New `getHostRefFields(def)` exported from `@pascal-app/core`.
Removed the intermediate token-based payload format (`NodeSubtree`,
`buildSubtreeSnapshot`, `materializeSubtree`, `SubtreeNode`).
UX polish
- `<SnapshotCaptureOverlay>` in `preset` mode now auto-stages a centered
square crop sized to ~75% of the shorter viewport dimension. The
user can pan / move / resize within square-aspect, but doesn't have
to drag from scratch — clicking the capture button works
immediately on entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): lock preset capture frame; allow item presets
- SnapshotCaptureOverlay: in `preset` mode, the auto-staged centered
square is now fully locked — corner handles hidden, the dim layer is
click-through (no drag-to-move, no drag-to-resize). The user just
adjusts the camera (orbit / pan / zoom) and clicks capture. The
letterbox + dashed border stay visible as a cosmetic frame.
- `item.capabilities.presettable` removed (implicit `true` via
`def.parametrics`). Enables compositions like "table-with-plants",
"shelf-with-books" where the preset root may be an item and other
items ride along as descendants. The GLB-kind item catalog is
unchanged; presets become siblings of GLB rows under the same
`items` table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): auto-frame camera on preset capture entry; restore on exit
`<CustomCameraControls>` now watches `useEditor.captureMode` and, when
preset capture mode begins, flies the camera to a pose that fits the
union bounds of the isolated subtree inside the locked square crop —
no more hunting for the subject after opening the modal. The
pre-capture pose is stashed and restored on exit so the user lands
exactly where they were.
The user can still pan / orbit / zoom from the auto-staged pose if
they want a different angle before snapping.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
a984410b3f | release: add ifc-converter to release workflow; bump @pascal-app/mcp to 0.3.0 | ||
|
|
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> |
||
|
|
814d94336b |
core(space-detection): add pause/resume refcount primitive (#336)
initSpaceDetectionSync subscribes to every scene mutation and auto-derives slabs/ceilings from wall topology. When a host app drives explicit slab/ceiling creation (e.g. the community editor's AI create_room flow), the auto-sync races those nodes and the polygon-signature de-dupe is fragile enough that duplicates leak through. Expose pauseSpaceDetection / resumeSpaceDetection / isSpaceDetectionPaused mirroring the existing pauseSceneHistory refcount in store/history-control. While paused, the subscriber rolls previousSnapshots forward so resume does NOT trigger a backfill that would re-introduce the very duplicates the host paused to avoid. No behavior change for callers that don't touch the new functions. |
||
|
|
bb5ce68254 |
Viewer render modes: Solid/Rendered + textures + surface-role clay + scene themes + edges (#332)
* viewer: add Phase 1 render-modes foundation (shading/textures/colorPreset state, defaultRender prop, SSGI gating) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: Phase 2 render-modes material-class switch (Lambert in solid, Standard in rendered) Shading-aware material factories (cached per class), reactive selection in renderers via the useViewer(shading) pattern, and dirty-rebuild on toggle for geometry/door systems. Rendered mode output unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: Phase 3a render-modes surface roles + clay palette foundation Adds surfaceRole token to core NodeDefinition, per-kind default roles, ColorPreset palettes + resolveSurfaceColor/createSurfaceRoleMaterial (glazing stays translucent), and the textures-off recolor path for def.geometry kinds (slab/fence/shelf via GeometrySystem.applyDefaultSurfaceRole) + wall. Renderer- based kinds (roof/window/stair/item/column/door/ceiling/elevator) wired in 3b. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: Phase 3b render-modes textures-off recoloring for renderer/system kinds Wires clay role coloring (textures=off) for roof/roof-segment, window, stair/ stair-segment, door, item, column, ceiling, elevator via createSurfaceRoleMaterial, reactive on textures/colorPreset. Per-surface roles: roof top+edge=roof / underside=ceiling; window frame=joinery / glass=glazing; stair+door+elevator= joinery; ceiling=ceiling; column=wall; item=furnishing. textures=on unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * editor: Phase 4a render-modes UI — Solid/Rendered toggle + per-context persistence Per-context shading via renderContext discriminator + shadingByContext (persisted); <Viewer> renderContext prop seeds per-context on mount. Solid/Rendered toggle in the editor action bar + standalone toolbar + command palette. Editor mounts default to renderContext=editor / shading=solid. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: fix window-system glassMaterial type to allow clay glazing reassignment The let was inferred as MeshLambertNodeMaterial from the imported glass constant, so reassigning createSurfaceRoleMaterial('glazing') (returns THREE.Material) failed under tsc --build. Widen the annotation to THREE.Material. Surfaced by the build (check-types had replayed a stale turbo cache). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: Phase 5 lighting — add theme-driven hemisphere light, trim fill directionals 3->2 Adds a sky/ground hemisphere fill (theme-lerped) and drops the second fill directional; the hemisphere covers the shadow-side fill it provided, at one fewer per-fragment directional term (shared by Solid + Rendered). Ambient lowered since the hemisphere now carries soft fill. Intensities are a starting point — tune visually on the gpu-perf overlay. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: tune Solid lighting for more form — stronger hemisphere sky/ground contrast, lower ambient Darker hemisphere ground (#d8d6cf -> #aaa49a) + higher hemisphere intensity and lower ambient so directional shading reads as form and undersides ground without AO. Keeps Solid free of any post-processing pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: biome format render-mode files (lefthook pre-commit) Formatting-only — import wrapping, dep-array wrapping, single-line ternaries — across Phase 2-4a files that weren't biome-clean. No logic changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: scene-theme system — named environment themes (studio/paper/sunset/night/...) New SceneTheme registry (lib/scene-themes.ts) drives lights, background, and tone mapping; lights.tsx refactored data-driven (N directionals + hemisphere + ambient). sceneTheme state (persisted) + cycle-button picker in editor bar + standalone toolbar, importing the registry from the viewer barrel (single source). Default 'studio' reproduces the prior look exactly; app light/dark 'theme' untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * editor: Phase 7 — popover-dropdown pickers for render mode + scene theme (editor bar) Replace the shading + scene-theme cycle buttons in viewer-overlay.tsx with DropdownMenu pickers: render mode shows 2 rows (Solid/Rendered) with one-line detail; scene theme lists all themes with a derived color-swatch strip + active check. Imports the registry from the viewer barrel (single source). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * editor: Phase 7 — dropdown pickers in standalone toolbar + export DropdownMenu from barrel Replicate the render-mode + scene-theme dropdown pickers (with swatch strip + active check) to apps/editor's compact toolbar, matching viewer-overlay.tsx. Export DropdownMenu* from the @pascal-app/editor barrel for the standalone app. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer+editor: Phase 8 — crisp geometry edge overlay (off/soft/strong/sketchy) EdgeOverlaySystem draws EdgesGeometry LineSegments over node-backed building meshes (scoped via sceneRegistry, skips zone-layer/hitbox/overlay meshes), rebuilt on geometry-uuid or mode change, line color follows scene-theme background luminance; sketchy = static TSL vertex jitter. New 'edges' state (persisted, default off) + Edges dropdown in editor bar + standalone toolbar. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer+editor: edge overlay — thick lines via Line2, drop sketchy mode Switch EdgeOverlaySystem from LineBasicNodeMaterial (1px hardware cap) to LineSegments2 + Line2NodeMaterial so edges have real screen-space width (soft 1.5px / strong 3px); resolution tracks viewport. EdgeMode is now off/soft/strong (sketchy removed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: fix edge overlay crash — webgpu Line2NodeMaterial has no settable resolution material.resolution is undefined under WebGPU (the node material reads the viewport internally); optional-chain the .set() call so it no-ops there instead of throwing. Thickness still applies via linewidth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: edge overlay — weld vertices to fix CSG spiderweb edges + lighter strong Position-only mergeVertices before EdgesGeometry so coplanar triangles from CSG-cut walls (doors/windows) share vertices and their interior edges are suppressed — only opening outlines + silhouettes remain. Strong linewidth 3 -> 2px (was too heavy). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: edge overlay — crease-only extractor to fix CSG spiderweb Replace EdgesGeometry (which always draws unpaired boundary/T-junction edges) with buildCreaseEdges: weld positions, keep only edges shared by exactly two faces whose dihedral exceeds the threshold, drop everything unpaired. CSG-cut walls/slabs are watertight so real corners + opening outlines survive while the interior triangulation fans (coplanar or T-junction) are removed. Open meshes (bare ground plane, billboard leaves) shed their boundary clutter too. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: crease edges — coarser weld (0.1mm->1mm) to recover CSG/extrude seam edges The cap<->side-wall top edge of ExtrudeGeometry walls drifts past 0.1mm after CSG, so it stayed unpaired and was dropped. Weld at ~1mm to pair it into a real crease while staying far below feature size (wall thickness, openings). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: replace geometry edge overlay with screen-space ink (SketchUp look) Port the prototype's screen-space ink into the post-processing pipeline: depth + normal Sobel reading the scene-pass MRT. Crease term (normalized normals, center-vs-neighbour) + distance-independent depth-step term (raw Laplacian / (1-d)² with a noise gate so flat ground stays clean). Topology-agnostic, so it finally handles CSG-cut walls/openings without the spiderweb or missing-edge problems of EdgesGeometry. Driven by the existing edges off/soft/strong mode; MRT now builds when SSGI OR ink is on; ink colour tracks scene-theme luminance. Removes EdgeOverlaySystem + crease-edges (geometry approach). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: soft/strong ink modes + keep editor overlays out of the ink Two adjustments to the screen-space ink pass: 1. Soft vs strong now visibly differ. The edge masks saturate, so the old `intensity` gain did nothing once a line was detected. Replace it with a sample radius (line thickness) + opacity: soft = 1px / 50%, strong = 2px / 100%. `inkedEdges` takes `radius` + `opacity` instead of `intensity`. 2. Editor overlays (gizmos, move handles, tool previews, grid) no longer get inked. The scene pass that feeds the depth/normal MRT now renders only SCENE_LAYER; overlays render in a dedicated pass on OVERLAY_LAYER and are composited on top after the ink + outlines, so they read as crisp UI and never get inked or AO'd. New OVERLAY_LAYER constant in viewer; editor's EDITOR_LAYER re-exports it so the two stay in lockstep. Also moves WallMoveSideHandles (the wall/fence move arrows) onto EDITOR_LAYER — it was the one overlay still on SCENE_LAYER. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: render the grid in the scene pass so geometry occludes it The depth-gate fix couldn't help the grid: its material is depthWrite:false, so it never wrote overlay-pass depth and the "didn't write depth -> keep on top" term forced it on top — hence the floor grid bleeding through walls and objects. A full-floor plane can only be occluded correctly by living in the same depth context as the scene, so move the grid onto its own GRID_LAYER which the scene pass renders (alongside SCENE_LAYER). It's flat and depth-non-writing, so the screen-space ink still ignores it; gizmos/handles stay on OVERLAY_LAYER. The grid camera layer is enabled in custom-camera-controls and disabled on the thumbnail camera so thumbnails stay grid-free, matching EDITOR_LAYER. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: strong ink — match soft's 1px thickness, differ by darkness only Strong at radius 2 read too thick. Soft's 1px line is the nice one, so use it for both modes and let strong distinguish itself purely by being fully solid (opacity 1) vs soft's lighter 50%. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: shadow frustum follows the view + a shadows on/off setting The directional light's ortho shadow camera only covers ±50 around the light target, which was pinned at the origin — so zones far from origin received no shadows no matter where the camera moved. Recentre each shadow-casting light (position + target together, preserving direction) on the view focus every frame: the orbit-controls target when available, else the camera's ground projection. The shadow area now tracks wherever the user looks. Also add a persisted `shadows` toggle (default on) to the viewer store and a "Shadows" switch in the editor settings panel — the dedicated shadows control the render-modes plan deferred. Lights gate castShadow on it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * editor: shadows toggle in the standalone toolbar + a touch more shadow strength The shadows switch I added only lived in the cloud settings panel's Visibility section, which is hidden in the local/standalone editor (no projectId). Add a ShadowsToggle button next to the grid toggle in the standalone toolbar so it's reachable there, matching how Show Grid is exposed in both places. Also push shadow strength partway toward the aesthetic prototype (which runs near-black, no blur): bump the bright-key shadow-intensity cap 0.4 -> 0.55 and tighten shadow-radius 2 -> 1.5. Still softer than aesthetic by design. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * nodes: site ground receives shadows (lit material instead of unlit Basic) The site ground fill used MeshBasicMaterial — unlit, so it could never show the directional shadow, and shadows visibly truncated at the slab edge. Swap it for a lit MeshLambertNodeMaterial with receiveShadow on the mesh; the geometry is the site polygon (slab footprints punched out), so shadows now extend across the whole site and stop at its boundary, which is the desired bound. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * themes: per-theme clay palettes + 2x2 swatch in the theme pickers Each scene theme now carries a clayTints map (wall/floor/ceiling/roof/glazing) giving it a per-surface-role palette — e.g. Mediterranean's blue roof + warm walls. The theme pickers (standalone toolbar + community overlay) now render the aesthetic-style 2x2 swatch of those role tints over the theme background instead of the old 3-colour strip. Data + UI only; wiring the tints into the textures-off surface materials is a separate change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * editor: slimmer theme switcher + cloud-sun icon Shrink the scene-theme toolbar button (w-[8.5rem] -> w-28) so it stops reserving space for "Mediterranean"; the label truncates when it overflows. Swap the palette icon for cloud-sun (atmosphere/lighting, distinct from the app light/dark Sun-Moon toggle) in both the standalone toolbar and the community overlay. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * editor: theme switcher icon -> swatch-book Swap the scene-theme icon from cloud-sun to swatch-book in both the standalone toolbar and the community overlay. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * themes: colour untextured building surfaces by the active scene theme Untextured walls/roof/slab/ceiling now take the active theme's per-role colour (theme.clayTints[role], falling back to the colour preset) in BOTH textures modes. The textures toggle only governs surfaces that actually have an explicit material/preset — those still show their texture when textures are on. This is what makes e.g. Mediterranean read as a blue roof + warm walls instead of the old hardcoded white/grey defaults. - materials.ts: resolveSurfaceColor / createSurfaceRoleMaterial take an optional sceneThemeId (theme tint ?? preset palette); theme folded into the cache key. - wall-materials, roof-materials, slab/geometry, ceiling/renderer: the untextured fallback now resolves to the themed role colour instead of white/grey, in both modes; theme threaded into each builder + material cache key. - wall-cutout: now reads textures/colorPreset/sceneTheme and re-applies wall materials when any change (previously it ignored textures/colorPreset entirely). - geometry-system: threads sceneTheme into the generic surface-role path + rebuild effect. Renderers/preview call sites thread sceneTheme through. Doors/windows/stairs/columns/items still use their existing defaults — a follow-up pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(slab): recessed (negative-elevation) slabs extrude downward again The registry geometry builder created the slab mesh at Y=0 without applying the negative-elevation offset, so recessed slabs rendered above the floor plane (pool geometry is built locally with its floor cap at Y=0 and walls rising to Y=|elevation|, so the mesh must be shifted down by `elevation` to recess). The runtime slab-system already did this; the static builder path didn't. Mirror it: shift mesh.position.y by elevation when negative. Positive elevation unchanged. Unrelated to render modes — bundled into this branch's PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(roof): legacy roofs render again — migration wrote invalid wallHeight 0 Legacy roof nodes (old format, no `children`) were migrated with a hardcoded `wallHeight: 0`. With zero wall height the eave height (`wallHeight - autoDrop`) went negative in getRoofSegmentBrushes, producing geometrically invalid brushes; three-bvh-csg then spammed "TriangleClipper: Coplanar clip not handled" every frame and emitted NaN positions, so the merged roof geometry failed computeBoundingSphere and never rendered. - core/use-scene migration: wallHeight 0 -> 0.5 (the RoofSegmentNode schema default), so migrated segments have a valid wall height. - roof-system: clamp eave height to >= 0.01 so an intentional wallHeight 0 can never yield a negative eave, and guard updateMergedRoofGeometry so a CSG result with NaN positions is discarded (keep the last good mesh, warn once per roof) instead of poisoning the buffer + spamming the console. Unrelated to render modes — bundled into this branch's PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(column): columns cast and receive shadows Column meshes (box, beam, cylinder, sphere, torus) rendered without castShadow/ receiveShadow, so columns neither dropped a shadow nor caught one — unlike walls, slabs and roofs. Set both on all column shape meshes. Unrelated to render modes — bundled into this branch's PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * viewer: default edges to soft Editor defaults are now solid shading / studio theme / soft edges / shadows on. Shading (solid, via EDITOR_DEFAULT_RENDER), theme (studio) and shadows (on) were already the defaults; edges was 'off' — make 'soft' the default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * themes: fold light/dark into the scene theme (remove the separate toggle) The viewer had two overlapping appearance controls: a light/dark `theme` toggle AND scene themes (which already drive the 3D background + lights). They conflicted — e.g. Night/Twilight are dark themes, but the light/dark toggle was an independent axis still tinting the 2D scene chrome. Unify on the scene theme: add an explicit `appearance: 'light' | 'dark'` to each SceneTheme (twilight/night = dark, the rest = light) and drive everything the old toggle drove off it — canvas backdrop, grid line colours, measurement-label/ cursor/site-edge contrast, the site ground fill, the ground occluder, and the mobile viewer bg. The editor UI chrome is unaffected (always dark via a fixed body class). Removes the `theme`/`setTheme` store state (+ persistence) and every light/dark toggle UI: the standalone toolbar Sun/Moon button, the community overlay theme switch, the command-palette command, and the ifc-converter preview toolbar button. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * themes: per-theme ground colour + add the "Verdant" nature theme - Add a `ground` colour to every SceneTheme and drive the site ground fill + the infinite ground-occluder off it (instead of the binary isDark ? #1f2433 : #fafafa). Dark themes now get a lit mid-tone ground (twilight #4a4566, night #2b3247) so the ground reads as ground rather than going near-black. - Add a new green/nature scene theme "Verdant": soft green sky + lit, with a green roof clay tint and mossy ground. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * wiki: document the surface-colour / theme system The colour-per-node/renderer/system model from the render-modes work was undocumented. Add wiki/architecture/materials-and-themes.md covering surface roles, colour presets, the textures axis, scene themes (appearance / ground / clay tints), and the "untextured surfaces are theme-coloured in both modes" invariant + where each kind wires it. Also fix two pages that the same work made stale: - node-definitions: geometry builders receive (shading, textures, colorPreset, sceneTheme); document the `surfaceRole` token + applyDefaultSurfaceRole. - layers: OVERLAY_LAYER (1, viewer) with EDITOR_LAYER now its alias, the new GRID_LAYER (3, rendered in the scene pass for depth occlusion), and the overlay pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * nodes: wire #330 roof-accessory kinds into the surface-role/theme colours PR #330's new kinds (chimney, dormer, skylight, solar-panel, ridge-vent, box-vent) use custom renderers, so the generic textures-off recolour path never reached them — they fell back to hardcoded colours. Wire each renderer into the render-modes system: read shading/textures/colorPreset/sceneTheme and resolve untextured surfaces via createSurfaceRoleMaterial (and force the role colour when textures are off), matching column/ceiling. Roles: chimney body→wall / cap→roof; dormer wall→wall, roof→roof, glass→glazing, frame→joinery; skylight glass→glazing / frame→joinery; ridge-vent + box-vent→roof; solar-panel frame→roof (the dark product-specific cell face is left as-is). Each definition also gets its dominant `surfaceRole` token. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(roof): legacy-roof migration must use a non-zero wallHeight The render-modes/#330 merge left the legacy roof→roof-segment migration writing `wallHeight: 0`. With #330's pitch model that builds a flat, zero-volume wall CSG brush, which three-bvh-csg can't clip ("Coplanar clip not handled") and yields NaN positions — so the migrated old roof never renders. Use the schema default 0.5 (what new roofs use), giving a valid wall. The eave clamp + merged- geometry NaN guard added earlier stay as defense-in-depth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(roof): guard slope frame against missing/NaN pitch (no more NaN geometry) getSegmentSlopeFrame used `pitch <= 0` to detect flat/zero-pitch, but an undefined or NaN pitch (a segment from an older migration that set `roofHeight` instead of `pitch`, or stale persisted data) slips past that check and computes Math.tan(NaN) → NaN tanTheta/activeRh → NaN segment geometry → the merged-roof CSG spews "Coplanar clip not handled" and NaN positions, so the roof never renders. Use `!(pitch > 0)` so any non-positive/non-finite pitch resolves to the flat frame. Self-heals bad data regardless of how the segment was produced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(roof): migration guarantees a positive pitch for every roof-segment Segments saved with neither a valid pitch nor a roofHeight (older/partial saves, e.g. landing home-graph) fell through the legacy roofHeight->pitch branch, leaving pitch undefined. The slope-frame guard then resolved them to a flat frame, so the roof rendered as a slab instead of pitched. Branch 2b now normalises any segment lacking a valid pitch: derive from roofHeight when present, else fall back to the schema default (40deg). The migration result is cast (not zod-parsed), so this is the only place the default lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
3cb318e445 |
ifc-converter: drop test script until tests exist (#328)
The package has no test files yet. `bun test` exits 1 ("No tests found!")
and fails CI; --pass-with-no-tests isn't honored by the bun 1.3.0 pinned
in CI. Match the convention used by @pascal-app/viewer (no test script):
turbo skips the test task for packages without one. The script comes back
when real tests land.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
82b45ea84d |
ifc-converter: pass test script with no tests yet (#327)
The package has no test files yet, so `bun test` exits 1 ("No tests
found!") and fails CI. Add --pass-with-no-tests so the task stays green
until real tests land, at which point they run normally.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
7d9c89c612 |
feat(ifc-converter): real IFCWALL dims + profile-typed columns
Two converter accuracy refinements: - Recover plain IFCWALL height/thickness from geometry. These carry Brep/mapped geometry that getBodyExtrusionData can't read, so measure the mesh in the wall's own axis frame (along/across/vertical) — a rotation-invariant projection rather than a world-space AABB (which conflated a rotated wall's length and thickness). Gate on the measured length matching the known wall length, and free web-ifc geometry handles via try/finally on every path. - Type columns from their IFC swept profile: IfcCircleProfileDef → round + radius, IfcRectangleProfileDef → rectangular + width/depth, falling back to the width/depth ratio when the profile type is unknown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0df51219d0 |
feat: IFC → Pascal converter (package + app)
Brings the IFC-to-Pascal converter into the monorepo as a pure-logic package plus a Next.js app, replacing the standalone repo that consumed published @pascal-app/* packages (and drifted from their schemas). packages/ifc-converter — pure conversion. Parses IFC via web-ifc and maps elements onto @pascal-app/core node schemas (workspace-linked, so no more version drift). Builds doors/windows, walls, slabs, columns, roofs, stairs, sites/buildings/levels. Validates each node via the real Zod schemas at build time (tryParse) and strips undefined metadata. apps/ifc-converter — the UI: drop zone, example picker, element search, JSON download, and a 3D preview rendered through the real @pascal-app/viewer (registry bootstrap + read-only scene) with a custom toolbar (camera/level/wall/grid/theme), level selector with camera-focus, auto-fit, and selection bridged to an inspector. Conversion specifics worth noting: - Door/window vertical centering (height/2; windows + sill). - Nearest-wall hosting fallback for files lacking IFCRELFILLSELEMENT, preferring walls long enough to contain the opening and clamping the along-wall position so cutouts can't overflow and break wall CSG. - Plain IFCWALL (Brep/mapped geometry) falls back to default height/thickness instead of collapsing to zero-height slivers. - Columns convert as plain structural shafts (no decorative base/capital) sized from the IFC profile. - Beams + items are skipped for now (no Pascal beam type; items need a catalog asset) — counted in the conversion summary. Large example IFCs are fetched from a public bucket at runtime; the four small ones are committed. web-ifc.wasm is copied into public/ on install/dev/build. README flags early-alpha + invites contributions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3e8b972b8f |
fix(mcp): templates use string IDs for SiteNode.children
PR #320 changed SiteNode.children from embedded BuildingNode/ItemNode objects to flat string[] IDs. PR #325 updated the runtime call sites but missed the three scene templates, which still mutated the site node's children array to embed full building objects after building the flat dict. This caused AnyNode.safeParse to fail for site_empty, site_2br, and site_garden in bun test --cwd packages/mcp. Remove the obsolete mutation blocks; each template already initialises site.children with the correct string id (e.g. ['building_empty']). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
01639e42e4 |
fix: update mcp and nodes for SiteNode.children string[] schema change
Fixes breakage introduced by #320 which changed `SiteNode.children` from embedded `BuildingNode | ItemNode` objects to flat `string[]` IDs. - `packages/mcp/src/lib/rehydrate-site-children.ts`: replace now-obsolete re-embedding logic with a no-op passthrough (call-site compatible) - `packages/mcp/src/tools/variants/generate-variants.ts`: drop the inline copy of the same function and its call - `packages/nodes/src/site/renderer.tsx`: cast `childId as AnyNodeId` since `SiteNode.children` is now `string[]`, not `AnyNodeId[]` Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
ffc497c67a |
editor: gate Load Build behind a verification dialog
Loading JSON previously called setScene blindly and crashed when the file held schema-invalid nodes (e.g. items missing `asset`). The new dialog parses the file, runs validateBuildJson in core, and surfaces structure counts (site/building/levels/walls/doors/ windows/items/slabs/ceilings/zones/scans), floor area, and a per-node Schema Details list grouped by type. Import is blocked when any hard error exists. Two schema bugs the validator surfaced are fixed here too: - SiteNode.children is now an id array like every other node (was a discriminatedUnion of full objects; three readers carried a string-or-object ternary that's now dropped). migrateNodes flattens legacy nested-object children on load. Default-scene seed and photo-to-scene MCP builder updated to pass `building.id`. - LevelNode.children now includes shelf — the editor allowed it but the schema didn't. The schema-vs-registry-as-source-of-truth discussion is captured in plans/editor-node-registry.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d218cb7f26 |
fix: restore fence tool handlers dropped by null-guard commit
|
||
|
|
251af2ce1d | fix: add null guards for cursorRef in fence and wall tool click handlers | ||
|
|
3adae8acea | style: apply biome lint fixes after portforward | ||
|
|
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 | ||
|
|
2bf824cc91 |
floor-elevation: restore stripped import + fix column rotation type
Two follow-up fixes after the FloorElevationSystem landed: - `viewer/src/components/viewer/index.tsx`: the auto-format hook stripped the `FloorElevationSystem` import a second time, leaving the JSX mount unresolved. - `packages/nodes/src/column/definition.ts`: column stores Y rotation as a scalar `number`, but `floorPlaced.footprint` types the rotation field as the full Euler tuple. Wrap as `[0, column.rotation, 0]`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e3f2343456 |
nodes: restore floorPlaced imports stripped by the formatter
The previous commit's `floorPlaced` capability used `getScaledDimensions` in `item/definition.ts` and `ColumnNodeType` in `column/definition.ts`, but the editor's auto-format hook ran between the import edit and the body edit and removed both as "unused" — breaking `bun dev`. Re-add the imports. |
||
|
|
9a481f2511 |
registry: generic FloorElevationSystem driven by a floorPlaced capability
Symptom: a shelf placed on a level with a raised slab underneath visually clipped through it — `ItemSystem` lifted items onto slabs, but shelves (and other floor-placed registry kinds) had no equivalent path. Fix: lift the slab-elevation logic out of `ItemSystem` into a generic `<FloorElevationSystem>` keyed off a new `capabilities.floorPlaced` config. Any kind that opts in declares a `footprint(node)` (dimensions + rotation used to query overlapping slabs) and an optional `applies` predicate (skips items whose `asset.attachTo` is wall / ceiling). The new system runs at frame priority 1 so its `mesh.position.y` override lands before `ItemSystem` / `GeometrySystem` (priority 2) clear the dirty mark. The spatial-grid sync's `markNodesOverlappingSlab` also dropped its hardcoded `item` branch in favour of an iteration over every registered kind that declares `floorPlaced` — so any new floor-placed kind picks up slab-driven re-elevation automatically. Tagged kinds: - `item` — `footprint = getScaledDimensions`, `applies = !asset.attachTo` - `shelf` — `footprint = (w, h, d)` - `column` — `footprint = (w, h, d)` - `spawn` — `footprint = (0.6, 1.8, 0.6)` (marker) `ItemSystem` retains only the wall-side z-offset block (`mesh.position.z = wallThickness / 2`). The elevation block + its `getScaledDimensions` / `resolveLevelId` / `spatialGridManager` imports moved to the generic system. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
78e3ed13d8 |
floorplan-panel: prune dead constants, drag-state types, and unused helpers
Final sweep after the layers, handlers, and state were dismantled — pruning the remaining orphan types and constants that only the dead code referenced. Removed: - 27 dead constants: `FLOORPLAN_*_HOVER_*_STROKE_WIDTH` for wall / item / endpoint (consumed by the deleted legacy SVG layers), `FLOORPLAN_WALL_OUTER_MEASUREMENT_*` + `FLOORPLAN_WALL_INNER_ MEASUREMENT_*` + `FLOORPLAN_OPENING_MEASUREMENT_*` palette constants (no live measurement consumer), `FLOORPLAN_ITEM_ CLEARANCE_*` thresholds, `FLOORPLAN_MEASUREMENT_LABEL_*` / `LINE_ OUTLINE_*` + `FLOORPLAN_ACTION_MENU_OFFSET_Y` / `FLOORPLAN_NODE_ FOOTPRINT_*` / `FLOORPLAN_SPAWN_*` / `FLOORPLAN_TRACE_*_FILL_ OPACITY` + several `FLOORPLAN_WALL_*_STROKE_WIDTH` variants. - Dead drag-state types: `SlabBoundaryDraft`, `SlabHoleBoundaryDraft`, `SlabVertexDragState`, `SlabHoleVertexDragState`, `SlabHoleMoveDraft`, `CeilingBoundaryDraft`, `CeilingVertexDragState`, `CeilingHoleBoundaryDraft`, `CeilingHoleVertexDragState`, `CeilingHoleMoveDraft`, `ZoneBoundaryDraft`, `ZoneVertexDragState`, `WallFaceLine`. - Dead helpers: `getWallMeasurementOverlay` + `getLinearMeasurementOverlay` (both defined but never called now that the measurement layers are gone). Floor-plan panel: 9,006 → 8,651 lines (-355). Cumulative reduction vs `main`: 17,913 → 8,651 (-9,262 lines, ~52%). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a3dc48a503 |
floorplan-panel: prune dead drag state, useEffects, and measurement helpers
After the legacy SVG layers and handler callbacks were dismantled, the backing drag-state useStates, mirror useRefs, and watch-effects became orphans, along with a handful of top-level math helpers whose callers were already gone. Removed: - 12 dead `useState` declarations: `slabBoundaryDraft` / `slabVertexDragState` / `slabHoleBoundaryDraft` / `slabHoleVertexDragState` / `slabHoleMoveDraft` / `ceilingBoundary Draft` / `ceilingVertexDragState` / `ceilingHoleBoundaryDraft` / `ceilingHoleVertexDragState` / `ceilingHoleMoveDraft` / `zoneBoundaryDraft` / `zoneVertexDragState`. All written only by the dead vertex-drag handlers and read only by the dead `clear*BoundaryInteraction` callbacks and `transientFloorplanFit` checks (which collapse to `false` once their inputs are gone). - 5 dead `useRef` mirrors of the above (`slabBoundaryDraftRef` etc.) plus their mirror `useEffect` writes. - 5 dead `clear*BoundaryInteraction` callbacks (slab, slabHole, ceiling, ceilingHole, zone) — only called by deleted useEffects and by `clearDraft`, where the call became a no-op. - 7 dead `useEffect` watchers for `slabVertexDragState`, `ceilingVertexDragState`, `slabHoleVertexDragState`, `slabHoleMoveDraft`, `ceilingHoleVertexDragState`, `ceilingHoleMoveDraft`, `zoneVertexDragState` (each early-returns because state is always null, so all listener wiring + commit / cancel paths inside were unreachable). - 5 dead `shouldShow*BoundaryHandles` flags + the 5 `useEffect`s that called `clear*BoundaryInteraction` when they flipped. - `selectedSlabEditingHoleIndex` / `selectedSlabEditingHole` / `selectedCeilingEditingHoleIndex` / `selectedCeilingEditingHole` — derived from now-`null` selectedX entries. - Dead drag-state guards in `handlePointerMove`, `handleSvgPointer Move`, and the floor-plan fit `useMemo`. - Dead top-level helpers: `getRaySegmentIntersection`, `getSlabHandlePolygon`, `getSlabVisualOffsets`, `getDraftSlabVisualPolygon`, `WallMeasurementFaceContext` type, `getWallMeasurementFaceContext`, `getAdjacentOpeningBounds`, `getSelectedWallMeasurementOverlays`, `getItemDimensionMeasurementOverlays`, `polygonCentroid`. Floor-plan panel: 10,466 → 9,006 lines (-1,460). Cumulative reduction vs `main`: 17,913 → 9,006 (-8,907 lines, **~50%**). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |