* Allow template forks to preserve scans
* Add room preset save trigger
* Show room preset action for selected zones
* Use standard inspector for selected zones
* Support room preset editor workflows
* Preview room clear-underneath items
* Add roof surface placement support for items
Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.
- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fixed conflict
* editor: per-door-type floor-plan symbols
Render a distinct, static plan symbol for each door type in the
registry floor-plan builder (`packages/nodes/src/door/floorplan.ts`),
independent of the door's live open/close animation:
- single / hinged: fixed 90° swing with a dashed quarter-circle arc
- double / french: two mirrored half-width leaves + dashed arcs
- folding / bifold: static zigzag accordion (~80% span) on the wall face
- sliding: bypass — two overlapping panels on parallel tracks + arrow
- pocket: thin white leaf, ~60% closed, sliding into the solid wall
- barn: surface-mounted panel parked over the wall, dashed closed-ghost
+ slide arrow
The swing arc is dashed in screen-pixel units (the renderer uses
non-scaling-stroke). Symbols are oriented by hingesSide / swingDirection
/ slideDirection as appropriate.
Also includes pre-existing working-tree changes unrelated to the door
symbols: group move/rotate transform and box-select tweaks, and a
regenerated ifc-converter next-env.d.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix recessed ceiling fixtures and draw safety
* feat(editor): magnetic wall-snap with per-kind beacon (2D + 3D)
Snap the wall draft / endpoint-move point onto existing wall geometry —
corners, midpoints, wall–wall intersections, and along-wall edges — and
show a beacon at the snap point whose glyph encodes what it caught
(square = corner, triangle = midpoint, ✕ = intersection, circle = edge).
- Pure snap geometry extracted to wall-snap-geometry.ts (unit-tested).
- Ephemeral useWallSnapIndicator store drives a 3D pillar+glyph beacon
and a 2D SVG glyph beacon, both indigo to match the alignment guides.
- Gated by a new persisted "Magnetic snap" toggle in the Display menu
(useEditor); honored by draw + commit + endpoint-move in both views.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* editor: garage and open-doorway floor-plan symbols
Extend the per-door-type plan symbols in the registry floor-plan
builder (packages/nodes/src/door/floorplan.ts):
- open doorway (openingKind === 'opening'): bare gap, no leaf/arc/panel
(mirrors the 3D system, which renders only the cutout for openings)
- garage sectional: closed leaf + side tracks into the garage + dashed
parked ghost at the inner end
- garage roll-up: closed leaf + coil barrel (capsule) with a coil hint
- garage tilt-up: closed leaf + dashed parked panel + dashed curved
up-and-over swing path
- gate the swing arc to actual swing doors (hinged/double/french) so
other types fall back to the plain footprint
Garage mechanisms sit on the interior (door-local -z) side to match the
3D garage builders, independent of swingDirection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* arch: enforce layer boundaries — registry dispatch, store relocation, shared helper
Three architectural fixes to bring the branch into full compliance:
1. **ceiling-system kind check → CeilingCutCapability**
Replace `child.type === 'item'` branch in `ceiling-system` with registry
dispatch. Add `CeilingCutCapability` type to `packages/core` registry types,
implement `buildCeilingHole` on `itemDefinition`, and rewrite
`collectRecessedItemHoles` → `collectCeilingHoles` to dispatch through
`nodeRegistry` — viewer never again inspects a node's kind directly.
2. **useAlignmentGuides + useWallSnapIndicator → packages/editor**
These stores are editor-only UI (snap beacons, alignment guides). Move them
from `packages/core/src/store/` to `packages/editor/src/store/`, re-export
from `packages/editor`, and update all 34 consumer files across
`packages/editor` and `packages/nodes` to import from `@pascal-app/editor`.
3. **findLevelAncestorId extracted to core**
`item-light-system` had a private `resolveNodeLevelId` that duplicated
level-ancestor traversal logic. Extract it as `findLevelAncestorId` in
`packages/core` (spatial-grid-sync), export it, and replace the local copy.
All four packages typecheck cleanly (zero errors).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: fix lint, untrack .claude/launch.json
Run bun check --write to clear 8 Biome errors (formatting + import order
+ one unused import). Untrack .claude/launch.json and add it plus
.claude/settings.local.json to .gitignore so local IDE/agent configs
stop landing in commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Guards building/ceiling/site/wall renderers with `(node.children ?? [])` so a node whose `children` array is missing (legacy/unparsed scene data) no longer crashes the renderer with "Cannot read properties of undefined (reading 'map')" (EDITOR-C0). Matches the existing guard in roof/renderer.tsx and the Array.isArray check in the parametric renderer.
Note: the schema declares `children: z.array(...).default([])`, so this can only be hit by data that bypasses Zod normalization on load. This is a defense-in-depth crash-stopper; the deeper fix is to normalize/parse legacy nodes in migrateNodes (use-scene.ts) so missing arrays are repaired before render — tracked as a follow-up.
* Add roof surface placement support for items
Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.
- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fixed conflict
* fix(editor): ceiling-attached item placement from 2D floor plan
The 3D viewer drives ceiling-item placement via ceiling:enter/move/click
raycast events on the ceiling mesh. The floor plan has no such mesh, so
ceiling-attached items (lights, fans) never transitioned out of
surface: 'floor' — the draft sat at floor height while the 2D cursor
moved freely, reading as a 2D/3D sync bug.
Synthesise the same ceiling events from 2D plan points by hit-testing
ceiling polygons on the active level, and publish the building-local
cursor (not world-space) to useLiveTransforms so the floorplan registry
override renders the draft under the cursor regardless of building
position / rotation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): chevron arrows render on SCENE_LAYER so ink-edge shader outlines them
Node arrow handles and polygon-editor edge arrows were tagged for
EDITOR_LAYER, which hides them from the post-processing scenePass — the
ink-edge shader reads the depth/normal MRT from that pass, so the
chevrons rendered flat with no outlined edges. Drop the EDITOR_LAYER
tagging on both, matching the wall-height arrow which already stays on
SCENE_LAYER for the same reason.
Pair with depthWrite: true on the chevron materials so their silhouettes
enter the depth buffer; depthTest stays off to keep the chevron drawn
on top of underlying geometry. Without depthWrite, only the
normal-discontinuity branch of the ink shader can detect the chevron,
and the lines drop out when faces align with whatever sits behind them
in screen space.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): ceiling grid overlay no longer blocks selecting items under it
Two issues kept the ceiling grid overlay covering the room after the
user moved on from a ceiling-related action:
1. CeilingSystem treated any selected descendant of a ceiling as
"reveal the grid" — so after placing a ceiling light and the new
item became selected, the grid stayed on and its mesh intercepted
every subsequent 3D click, re-selecting the ceiling instead of the
items below. Restrict the reveal to directly-selected ceilings.
2. The ceiling top material used the opaque surface-role material, so
a top-down camera lost view of everything under the ceiling the
moment the overlay turned on. Swap the top material for the
transparent grid-pattern material (bottom stays opaque so the
in-room view still reads as a solid surface).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(core): curve- and thickness-aware wall/slab overlap detection
`wallOverlapsPolygon` and `getSlabElevationForWall` previously treated a
wall as an infinitely thin chord from start to end. Two failure modes
fell out of that:
1. Curved walls whose chord lies outside the slab but whose centerline
bows into the slab interior were missed entirely. The wall stayed at
Y=0 while the slab elevation moved, and `markNodesOverlappingSlab`
never re-dirtied it when the slab Y changed.
2. Perimeter walls of a room — whose centerline sits exactly on (or just
outside) the slab's polygon edge — also missed detection, because
pointInPolygon on the boundary is unreliable. Half the wall's body is
inside the slab; it should follow the slab elevation.
Switch `wallOverlapsPolygon` to a wall-shaped input (start/end + optional
curveOffset + thickness), sample the centerline for curved walls, and
add a ±halfThickness perpendicular test for straight walls. Threaded
through `getSlabElevationForWall`, the wall system, and the
`markNodesOverlappingSlab` pass. Legacy chord-only call shape preserved
for callers that don't yet have a wall in hand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): show Move button for legacy-movable kinds in floating action menu
`isRegistryMovable` only sees kinds wired through `capabilities.movable`,
`floorplanMoveTarget`, or `affordanceTools.move`. The legacy tail of
`MoveTool` (tools/item/move-tool.tsx) still handles roof, roof-segment,
stair, stair-segment, building, and elevator, but the floating action
menu was hiding the Move button for them because the registry check
returned false. The mover worked once invoked — the entry point was
missing.
Add a `LEGACY_MOVABLE_KINDS` set alongside the registry check so those
kinds get the Move button until they migrate onto kind-owned
affordances; drop a kind from the set once it does.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(nodes): in-world registry handles for roof accessories + axis-stable surface basis
Adds the registry-driven chevron / tracker / rotate-gizmo handle set to
skylight, solar-panel, chimney, and roof-segment so every roof-mounted
kind has consistent in-world manipulation, and fixes the underlying
renderer + surface-basis bugs that made those handles land on the wrong
spot or behave inconsistently across mirrored slopes.
Handles
- `LinearResizeHandle` gains `shape: 'arrow' | 'tracker'`. `'tracker'`
renders a dashed vertical leader from the surface up to a draggable
cube, reusing the linear-resize drag pipeline. Roof-segment's
wall-height handle adopts it.
- Roof-segment: width chevrons split into two asymmetric handles
(each grows its own edge, opposite stays world-fixed via `apply`
recomputing `position`).
- Skylight: width × 2 (asymmetric), height × 2 (asymmetric), curb-height
tracker, rotate gizmo (corner, lifted off surface), and a diagonal
frame-thickness chevron at the -X+Z corner.
- Solar panel: same six handles operating on total array dimensions
(back-solving `panelWidth` / `panelHeight` from `columns` / `rows`),
plus a frame-depth chevron above the array.
- Chimney: registry handle set following the same idioms.
Skylight / solar-panel renderer
- Collapse the previously nested `position → surfaceQuat → rotation-y
→ rotation-x` groups into a single registered transform group whose
local `position` + composed `quaternion` carry the full pose in
segment frame. The registry handles read this Object3D's local
matrix (via `portal: 'grandparent'`), and a split tree exposed only
the bottom group's local pose so handles landed at the segment
origin on the roof floor.
Surface basis (solar-panel/geometry.ts)
- `surfaceQuatFromNormal` builds `right` by projecting world +X onto
the surface plane instead of `up × normal`. The cross-product version
flipped sign when the normal's Z component flipped (e.g. the two
slopes of a gable roof), so hosted children's local +X pointed in
opposite world directions across the ridge and asymmetric chevrons
anchored the wrong edge. Projecting +X keeps the basis stable across
mirror-image slopes.
Skylight move-tool ghost
- Switch from the raycast normal (`event.normal × normalMatrix`) to the
analytical normal (`getAnalyticalNormal`) on every pointer move, and
mirror the placement tool's transform stack: `position → yaw (roof +
segment) → surfaceQuat → skylight rotation → preview`. Re-engaging
Move from the floating action menu now shows the same correctly
oriented ghost the first-placement tool does.
* feat(nodes): split roof-segment depth chevron into asymmetric front/back arrows
Brings the depth handle in line with the width handles: one chevron on
each Z edge, each anchored to the opposite edge so dragging only moves
its own side. `apply` recomputes `position` along the segment's local
+Z arm (yaw-aware) so the anchored edge stays world-fixed.
Depth also feeds the slope-frame math via `getActiveRoofHeight`, so a
naïve depth change would also raise/lower the peak (constant pitch
across a larger run). We hold the peak fixed by back-solving a new
`pitch` for the new depth via `getPitchFromActiveRoofHeight`, clamped
to the schema's pitch range — the segment grows along the deck plane
without ramping up.
* feat(nodes): in-world handles for dormer + window-bottom clipping check
Dormer joins chimney / roof-segment with chevron handles on the
selected node. Five body handles (width L/R, depth, wall-height
tracker, rotate) plus four window-opening handles (width L/R,
height top/bottom) — the window handles re-emit windowOffsetX /
windowOffsetY in `apply` so the anchored edge stays put as the
dragged edge follows the pointer.
Handle visibility on roof accessories needed an editor-side assist:
the host segment's mesh registers inside RoofRenderer's
`<group segments-wrapper visible={false}>`, which hides anything
portaled into it. Chimney's `portal: 'grandparent'` escape trips a
WebGPU "Color target has no corresponding fragment stage output"
pipeline error on dormer (likely an MRT interaction with the
window-assembly's transparent glazing), so RoofEditSystem now flips
the wrapper visible whenever ANY accessory hosted on a segment of
this roof is selected — and resets each segment mesh to an empty
4-group placeholder on the transition so stale per-segment CSG from
a prior edit doesn't double-render on top of the merged shell.
Window clipping was using wall-top-above-slope as the exposure
threshold, but the window sits in the skirt well below the eave —
so a dormer whose eave barely cleared the host roof rendered a
fully-buried window. `getDormerExposedFaces` now gates on
window-bottom-above-slope; both the CSG cut decision and the
window-assembly render path feed off the same number. The
in-world window chevrons resolve the host segment via sceneApi and
flip to whichever face is currently exposed, so dragging the
dormer across the ridge moves the chevrons to the visible gable.
Also fixes the BoxGeometry vs ExtrudeGeometry mismatch in
`buildDormerFallbackGeometry` — body was indexed, roof was not, so
`mergeGeometries` rejected the pair and spammed the console on every
height-drag frame. Body is now `.toNonIndexed()` before the merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(viewer): glazing role uses FrontSide to avoid MRT back-face pipeline error
DoubleSide on a NodeMaterial inside the MRT scene pass makes WebGPU
compile a back-face shader variant that doesn't declare outputs for
every MRT target — the validator rejects it and poisons the render
context with "Color target has no corresponding fragment stage output".
The warning was already documented on `glassMaterial` (materials.ts:77),
but `createSurfaceRoleMaterial` was still forcing DoubleSide for the
glazing role.
Manifested on scene open as soon as a dormer was present: the dormer's
window-assembly mounts the glazing material on both gable faces on the
first frame, so the back-face pipeline gets compiled immediately.
Glazing now resolves to FrontSide; the dormer's back gable group flips
180° so its FrontSide normals point outward (the sill no longer needs
its per-face Z mirror since the group rotation handles it).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(nodes): in-world handles for box-vent + ridge-vent; freeze non-active arrows during drag
Adds 5 chevron handles to box-vent (width L/R, depth, height, rotate)
and ridge-vent (length L/R, width, height, rotate). Box-vent's renderer
now composes slope tilt + yaw onto the registered ref's quaternion
(mirrors solar-panel) so handle placements use vent-mesh-local coords
directly; ridge-vent's registered ref was already at the vent frame.
Ridge-vent renderer now merges `useLiveNodeOverrides` so the mesh
updates in-flight during a handle drag instead of freezing until commit.
Box-vent / dormer / chimney already did this; ridge-vent was the only
roof accessory not subscribed.
`NodeArrowHandles` now tracks the active drag descriptor + a pre-drag
store snapshot. Non-active arrows render against the snapshot with a
node-local freeze offset that cancels the mesh's `position` drift —
asymmetric resize (width / length L+R) recomputes position to anchor
the opposite edge, and without the freeze every other chevron would
slide along with the moving mesh center. The active arrow's freeze
offset is null, so it tracks the cursor as before. Rotation drags
collapse the offset to zero (position doesn't change), so non-active
chevrons naturally rotate with the mesh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(nodes): gutter accessory — eave-mounted rain channel with three profiles
New `gutter` node kind hosted on a roof-segment. Placement tool snaps
to the eave line of the segment under the cursor (segment-local
`Z = +depth/2`, `Y = wallHeight`); the back wall of the gutter then
sits flush against the fascia and the trough hangs outward (+Z).
Three cross-section profiles share the same outer-outline-minus-cavity
extrude recipe and only differ in the outline curve:
- `k-style`: ogee fascia (S-curve) — default residential look
- `half-round`: semicircular trough — colonial / classical feel
- `box`: rectangular u-channel — commercial / industrial
Three in-world chevron handles via the registry:
- length L + R (asymmetric — drag one end, the other stays world-fixed)
- size (anchor='max', drops the trough downward as the cursor pulls)
Wiring touches every node-kind ledger: schema (core/schema/nodes/
gutter.ts), AnyNode union, schema barrel, material targets, roof-segment
hosted-accessory comment, event bus (`gutter:*`), nodes barrel +
registry, plus the per-kind file set under `packages/nodes/src/gutter/`
(geometry, schema re-export, parametrics, renderer, preview, tool,
definition, index).
V1 ships gutters only — downspouts deferred so the eave-snap +
cross-section pipeline can be eyeballed before stacking the
downspout-corner placement logic on top.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gutter): roof-panel entry, eave-edge snap, open-top U-channel, move tool
UI surface area:
- Roof inspector "Elements" section now lists existing gutters + an
"Add Gutter" button. Adds `'gutter'` to the StructureTool union so
setTool('gutter') typechecks.
- Sidebar tree-node map gets a GutterTreeNode entry; selecting a
gutter in the outline focuses it just like other roof accessories.
- Floating action menu now shows a Move button on a selected gutter
via the new `affordanceTools.move`. MoveGutterTool ghost-follows
the cursor, eave-snaps on each frame, and commits to the new
segment + side on click. Mirrors the ridge-vent move flow.
Geometry fix:
- Three cross-sections were authored as a closed outline + inset
hole, which extrudes as a sealed box with a tunnel through it
(top sealed). Real gutters need an OPEN top. Each profile now
traces a single U-shape polygon around the channel material:
outer wall down -> bottom -> outer wall up -> front rim ->
inner wall down -> inner bottom -> inner wall up -> back rim.
The interior of the U is empty space, not a hole inside a
closed shape.
Placement fix:
- Snap now lands on the OUTER drip edge of the roof, not the wall
line. Segment-local Z = sign * (depth/2 + overhang - 4 cm tuck),
Y = wallHeight - overhang * tan(pitch) + 4 cm tuck. Sign of the
cursor's localZ picks the near eave; back eave uses rotation = pi
so the trough hangs outward in both directions. The 4 cm tuck
offsets keep the gutter visually attached to the fascia rather
than floating at the very tip of the overhang.
Hook order fix (regression from the previous commit):
- `NodeArrowHandlesForNode` had its new useState/useMemo hooks
AFTER the `if (!portalObject ...) return null` guard. The
registry-resolve useEffect flips portalObject from null to object
one frame later, so the guard passed on render N+1 and three new
hooks suddenly appeared in the hook list. Moved them above the
early return.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(viewer/nodes): drop DoubleSide NodeMaterial MRT landmines across slab, vents, gutter, window
DoubleSide on any NodeMaterial inside the MRT scenePass makes WebGPU
compile a back-face shader variant whose fragment outputs don't cover
every MRT target — the validator rejects the pipeline and poisons the
render context with "Color target has no corresponding fragment stage
output but writeMask is not zero", manifesting as
renderPipeline_NNN invalid on scene open. The pattern was already
documented at materials.ts:77 and fixed for glazing in 9400f1c5, but
several roof / floor renderers still requested DoubleSide on
`createSurfaceRoleMaterial` (which returns a `MeshLambertNodeMaterial`)
and on user-supplied materials (which may also be NodeMaterials):
- slab/geometry.ts — fired on every untextured floor; the live culprit
on scene reload after the gutter renderer was switched to FrontSide.
- gutter/renderer.tsx — the U-channel cross-section is traced as a
single closed polygon around the material, so ExtrudeGeometry already
produces outward-facing normals on every visible face; DoubleSide was
speculative.
- box-vent / ridge-vent renderers — DoubleSide was deliberate (to keep
back faces of thin extrudes visible from below); now a known visual
tradeoff. Build the geometry as a closed solid in `geometry.ts` if
the underside-view becomes noticeable; do not bring DoubleSide back.
- viewer/lib/materials.ts `DEFAULT_WINDOW_MATERIAL` — same fix on the
fallback window material.
Local `defaultMaterial` constants in box-vent / ridge-vent / gutter
also lose their `side: DoubleSide` for consistency (those are
`MeshStandardMaterial`, hit only when a preset ref fails to resolve,
but the same landmine pattern).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): Shift-snap rotate gizmos to 15° increments
Holding Shift while dragging a whole-node rotation arrow now snaps the
delta to π/12 (15°) steps. Scoped to `descriptor.shape === 'rotate'`
so curved-stair sweep handles keep their continuous feel.
* feat(nodes): stair railings track live segment-drag overrides
`StairRailings` was reading each child segment from zustand only, so
width/length/height drag handles (which publish to
`useLiveNodeOverrides` and only flush on release) left the railing
frozen at the pre-drag values until release. Subscribe to the override
map and merge each child's override onto its zustand snapshot so the
railing rebuilds every frame during the drag.
* chore(ifc-converter): next-env routes path moves under .next/dev/types
Auto-generated next-env.d.ts update from the local Next.js dev server —
the routes type now lives under `.next/dev/types/routes.d.ts` rather
than `.next/types/routes.d.ts`.
* fix(gutter): roofType-aware eave snap — 4-way on hip/flat, low side on shed
Gutter place + move tools were hard-coded to snap to ±Z eaves, working
for gable / gambrel / mansard / dutch but missing:
- Hip / flat: 4 eaves, not 2. Clicks on the side slopes (±X eaves)
collapsed back onto ±Z, so users couldn't place a gutter on a hip's
side eave at all.
- Shed: only one real eave (the low side at +Z). Clicking on the high
wall side used to snap to -Z, which is the rake / high end with no
fascia to hang from.
New shared `eave-snap.ts` module:
- `resolveEaveSnap(segment, localX, localZ)` returns
`{ eaveX, eaveY, eaveZ, rotation, side }`.
- Hip / flat picker uses `max(|lx|/halfW, |lz|/halfD)` — same
discriminator `analyticalSurfaceY` uses for hip — to pick which of
the four slopes the cursor is on, then signs +/-.
- ±X eaves rotate the gutter ±π/2 so its outward axis points away
from the building.
- Shed always returns +Z (the low side).
- Gable / gambrel / mansard / dutch unchanged (±Z).
Both tools collapsed their duplicated tuck constants + inlined
resolver — the "keep these in sync" comment became a landmine once
the resolver grew non-trivial.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gutter): end caps + corner mitre + ghost-placed parity
- Ghost/move previews mirror the GutterRenderer transform chain
(roof → segment → snap) and use FrontSide. Removes drift between
the placement ghost and the gutter that lands on click.
- New endCapLeft / endCapRight booleans (default true) slice a
solid-outer plug into the extrusion at each enabled end. Inspector
exposes both toggles; caps subtract from node.length so the
user-set span is preserved.
- corner-mitre.ts detects sibling gutters meeting within 5 cm on the
same segment and returns per-end mitre angles. The end-face skew
holds back walls at the inner corner while front rims extend to
the outer eave intersection; cap on a mitred end is force-
suppressed so the L-junction stays open. Renderer pulls siblings
via useShallow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gutter): drag-snap corners, live eave Y, hangers
- Length L/R handles now snap to sibling gutter endpoints within 10 cm
and pull BOTH gutters' lengths to the axis intersection — the
geometric eave corner — so the 5 cm corner-mitre window fires
reliably. Sibling adjustment writes through sceneApi.update; the
drag pipeline's history pause batches it with the main commit into
one undo step.
- Renderer derives eave Y live from segment.wallHeight, overhang, and
pitch via the new shared computeEaveY() — instead of trusting
node.position[1] from placement time. Subscribes to the segment's
useLiveNodeOverrides entry too, so a wall-height drag on the
segment moves the gutter on every frame (not just at commit).
- Hangers: new hangerStyle (strap / none) + hangerSpacing fields.
buildHangers() lays thin 25mm × 3mm × rim-width box straps across
the rim at the configured spacing, inset by 5 cm from each end and
skipping any cap slabs. BoxGeometry is converted to non-indexed
before merge — ExtrudeGeometry isn't indexed, and mergeGeometries
rejects mixed-index sets. Inspector exposes both fields in a new
"Hangers" group.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gutter): downspout outlet with real CSG-drilled hole
New schema fields: outletSide ('none' | 'left' | 'right'), outletInset,
outletDiameter. Default 'none' so existing gutters don't sprout
outlets on schema upgrade.
Geometry adds a solid cylindrical stub at bore + 3 mm wall radius
descending 6 cm from the trough floor, profile-aware Z midpoint
(k-style 0.4·size, half-round size, box size/2), X clamped between
the caps. After merging into the channel + caps + hangers, a
three-bvh-csg SUBTRACTION drills a bore-wide cylinder vertically
through the floor and stub — so the result is a real hole in the
trough floor with a hollow tube hanging through it. Drill overshoots
floor + stub by 1 cm each side to keep cut planes from coinciding
with mesh faces (csg-evaluator produces degenerate output on
coplanar cuts).
CSG only runs when outletSide ≠ 'none' — the existing merge path is
the fast path for capped-only gutters.
Inspector exposes the three fields under a new 'Outlet' group.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(downspout): new node + gutter inspector list section
DownspoutNode lives next to the other roof accessories. Scene-graph
parent is the same roof-segment the host gutter sits on; logical
attach is via `gutterId`. Schema: length (default 2.5 m), diameter
(default 0.07 m / 3″ — matches gutter outlet default), material.
Renderer mounts a CylinderGeometry under the same transform chain
the gutter uses (segment → gutter-mesh-local → outlet); pulls
`computeEaveY` from the live + drag-override segment so wallHeight /
overhang / pitch changes track on the same frame as the gutter.
`resolveGutterOutletPlacement` (gutter/outlet-lookup.ts) is the
shared helper both the downspout renderer and the inline Add path
use to compute (x, y, z, bore) in gutter-mesh-local space.
Two arrow handles on a selected downspout:
- length: tracker shape (dashed leader + draggable cube). Anchored
to the outlet, cube at the pipe bottom — readable even when the
cube ends up below ground.
- diameter: symmetric `z`-axis chevron sitting at a fixed −20 cm Y
below the outlet with 25 cm of outward clearance past the worst-
case k-style rim, so it stays inside the gutter's camera frame
instead of floating mid-pipe.
Inspector UX: new optional `trailingSection` slot on
ParametricDescriptor — a lazy-loaded React subsection rendered
between groups and the Actions section. Gutter's slot loads a
`downspouts-panel` that lists every attached downspout (button per
item, click → select), and an "Add Downspout" button below that
immediately creates a new one parented to the gutter's segment —
matches the roof inspector's gutter list pattern. Disabled with a
helper line when `outletSide === 'none'`. Multiple downspouts per
gutter are allowed.
`StructureTool` union picks up 'downspout' so the placement tool
remains addressable (used by the legacy roof-panel button before the
inspector list took over).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: pending floorplan alignment-guide work-in-progress
Snapshot of in-flight alignment-guide files that have been sitting in
the working tree (not authored in this session). New service +
zustand store wire up the data; the floorplan layer + overlay are
the visible consumers. Committed as-is to clear the tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(core): gutter multi-outlet model + downspout routing options
Replace the gutter's single outletSide/outletInset/outletDiameter triple
with an `outlets` array of `{ id, offset, diameter }` so one run can host
several downspouts on independent drops instead of stacking on one. Each
downspout links to an outlet by `outletId`.
Add downspout routing/styling fields: `standoff` (gap proud of the wall),
`shape` (auto/round/rect), `strapStyle`/`strapSpacing`, and `terminal`
(splash/kickout/straight).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(editor): in-world rotate & move gizmos with live-drag dimension pill
Add two new handle-descriptor capabilities to the core registry — a free
`translate` handle (ground-plane or wall-normal move cross) and
`rotationPlane` for arc-resize (yaw vs spin-flat-against-wall) — plus
`measureLabel`, which routes a resize handle's readout to a floating
dimension pill instead of its inline chip, and `overrideTarget`, a
cross-node redirect for handles that edit a sibling's value.
node-arrow-handles implements all four, merging the in-flight drag into
`useLiveNodeOverrides` so the mesh moves in real time and commits only on
release. Item gains in-world rotate + move gizmos (floor items: world-Y
rotate + floor-plane move; wall items: wall-normal spin + wall-face move).
Add the shared MeasurementPill (H · L · T) and formatMeasurement, wired
into the floating action menu (live wall/fence height drag) and the wall /
fence endpoint move tools. Side handles merge live overrides so every
affordance tracks the height mid-drag. Item duplicate now drag-to-places
(no auto-insert) and the placement coordinator rotates in 45° steps to
match the R-key step.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(gutter): multi-downspout outlets with CSG drops, routing & profiles
Build out the multi-outlet gutter against the new schema. Each outlet is
drilled through the trough floor via CSG (profile-geometry shares the
trough cross-section), and a downspout links to one outlet by id so
several no longer stack on a single drop. The downspouts panel manages the
outlet list; outlet-lookup resolves a downspout's mount from its gutter +
outlet.
Downspout gains real routing (routing.ts): offset elbows step the run back
from the eave overhang to the wall (standoff escape hatch), auto/round/rect
cross-section following the gutter profile, wall straps, and splash /
kickout / straight terminals, with inspector-editors for the new fields.
Length-snap now snaps only the dragged gutter to the geometric corner —
never moving its corner-mate — so dragging one gutter can't reset another
the user placed deliberately. Adds gutter floorplan (eave-line silhouette).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(floorplan): 2D footprints for roof nodes + rotate-handle angle wedge
Add floorplan builders for box-vent, chimney, dormer, ridge-vent, roof,
skylight, and solar-panel, and wire each into its definition, so roof-layer
nodes finally draw a 2D footprint. Roof draws the merged silhouette of its
child segments, and roof-segment now renders proper architectural roof
linework (ridge / hip / break + shed downslope arrow) per shape instead of
a bare rectangle.
Add a `pivot` to the floorplan rotate affordance so the layer can sweep a
live angle wedge + degree readout during a rotate drag — the 2D twin of the
3D rotate gizmo. Column / elevator / shelf / stair pass their pivot through.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(editor): select ceiling only via its corner handles, not the grid body
Add `viaHandle` to NodeEvent and set it on ceiling corner-bracket clicks.
The selection manager now ignores non-handle ceiling clicks without
stopping propagation, so a top-down click on the revealed ceiling grid
falls through to the item hosted beneath it instead of re-selecting the
ceiling and swallowing the click. The corner brackets draw with depthTest
off at a high render order so they stay visible and clickable through
occluding geometry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(editor): highlight wall openings on a selected wall
Add WallOpeningHighlights — an indigo accent frame + translucent pane
drawn around each door / window opening of the selected wall, so editable
children (including frameless openings with no visible geometry) are easy
to locate. The accent is deliberately distinct from the white selection
outline, and draws with depthTest off so it reads on top of the wall.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(solar-panel): derive surface frame live so panels re-seat on roof changes
Compute the panel's Y and tilt from the parent roof-segment's finished
(deck + shingle) surface every render via getRoofOuterSurfaceFrameAtPoint
— the same helper skylights use — instead of reading the stored
position[1]/surfaceNormal snapshot. Merging the segment's live overrides
means the panel re-seats and re-tilts continuously during a wall-height /
pitch drag rather than floating or burying until the value commits.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(first-person): coerce imported item geometry attrs to Float32 for collider merge
mergeGeometries requires every merged geometry to share the same typed-array
constructor per attribute. Imported item GLBs using KHR_mesh_quantization or
interleaved buffers broke the merge against Float32 wall/slab geometry, so
decode each attribute into a plain non-normalized Float32 BufferAttribute
before cloning into the collider world.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(editor): lower camera minimum zoom distance to 6m
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(mcp): bump @pascal-app/mcp to 0.3.0 in lockfile
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(nodes): registry-own move dispatch + hoist shared roof helpers
Addresses the architecture-review findings on the gutter/downspout branch:
- Move dispatch: port the bespoke roof/roof-segment/stair/stair-segment
movers (MoveRoofTool) and the building mover (MoveBuildingContent) into
@pascal-app/nodes (shared/move-roof-tool.tsx, building/move-tool.tsx) and
declare them via `affordanceTools.move`. Delete both hardcoded dispatch
lists — `LEGACY_MOVABLE_KINDS` in floating-action-menu and the roof/stair/
building arms of MoveTool. `onMove` is now purely `isRegistryMovable`.
Editor internals the movers need are exported from @pascal-app/editor
(adds clearRoofDuplicateMetadata); sfxEmitter.emit -> triggerSFX. elevator
keeps its existing capabilities.movable path (its legacy arm is the lone
remaining one, now documented).
- Cross-kind imports: hoist resolveRoofSegmentHit (roof/segment-hit.ts) and
the roof-surface normal math (getSurfaceY/getAnalyticalNormal/
surfaceQuatFromNormal, formerly in solar-panel/geometry.ts) into
packages/nodes/src/shared/, so the 8 roof accessories + skylight/box-vent
stop reaching into sibling kind folders. roof/index and solar-panel/index
re-export from shared so public surfaces are unchanged.
- Inspector: make ParametricInspector action `enabledIf` reactive by
subscribing to its boolean result (ParamActionButton), matching the
existing FieldRenderer/visibleIf pattern.
Type-checked (tsc) and linted (biome) across editor + nodes. Move behaviour
is preserved by construction but not yet runtime-verified in the editor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(nodes): add cupola + turbine-vent roof accessories; group-rotate gizmo; in-ring rotation readout
New roof-accessory node types and editor gizmo work:
- feat(nodes): cupola (roof lantern) and turbine-vent (whirlybird) node
types — schema, geometry, parametrics, renderer, panel, move/placement
tools, floorplan, and tests; registered in the node + core schema indexes
with site-panel tree nodes.
- feat(editor): group-rotate handle — a single rotation gizmo for 2+ movable
nodes that spins the selection rigidly about its shared bbox center.
- fix(editor): the live rotation readout (degree wedge + chip) now renders as
a child of the node frame, concentric and coplanar with the guide ring, so
it sits centered in the ring on pitched roofs instead of floating off to the
side. Flat-ground gizmos and the group-rotate readout are unchanged.
- refactor(nodes): rework box-vent / ridge-vent geometry + definitions and
share roof helpers; tidy roof/box-vent panels.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(nodes): eyebrow-vent kind; ridge-vent tracks roof height; vent paint + tree-node go registry-driven
New eyebrow-vent roof accessory plus two follow-on cleanups:
- feat(nodes): eyebrow-vent — a louvered roof vent in three styles (`scoop`
swept eyebrow, `half-round` D-vent, `slant-box` hooded box). Solid louver
slabs set into a framed/contained front, double-sided geometry, and
adjustable style / louver-count / dimensions / slant (`backRatio`). Full
registry wiring: core schema, node dir (definition/geometry/renderer/tool/
move-tool/panel/floorplan/preview/tests), event type, palette button.
- fix(nodes): ridge vent derives its Y from the segment's current surface
(`getSurfaceY`) instead of a stored value, so lowering a roof drops the cap
onto the new ridge automatically.
- refactor(nodes,editor): migrate the vent family (box / ridge / turbine /
cupola / eyebrow) to `capabilities.paint` via a shared single-surface
capability, removing the hardcoded `node.type` paint arms from the editor's
selection-manager + material-paint (matches chimney/dormer).
- refactor(editor): replace the five identical vent tree-node components with
one `def.presentation`-driven `RegistryTreeNode`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* 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>
Wholesale move of every remaining kind into its own subdirectory under
`packages/nodes/src/`, finishing the registry-driven migration. Each
kind now ships its definition, schema (re-exported from core), and any
of `geometry` / `renderer` / `system` / `floorplan` / `tool` /
`move-tool` / `panel` / `floorplan-move` / `floorplan-affordances` /
`parametrics` / `preview` it needs — no per-kind code remains under
`packages/editor/src/components/tools/` or
`packages/viewer/src/components/renderers/`.
Deleted (replaced by registry-driven equivalents):
- `tools/{ceiling,column,door,fence,item,slab,spawn,wall,window}/...`
(boundary editors, hole editors, placement tools, move tools,
endpoint movers, curve tools, helpers, math libs)
- `ui/helpers/{ceiling,slab,wall}-helper.tsx`
- `ui/panels/{column,door,elevator,item,roof,roof-segment,spawn,
stair,stair-segment,wall,window}-panel.tsx`
- `viewer/src/components/renderers/{building,ceiling,column,door,
elevator,fence,guide,item,level,roof,roof-segment,scan,site,slab,
spawn,stair,stair-segment,wall,window,zone}-renderer.tsx`
- `viewer/src/components/viewer/legacy-system.tsx`
Added under `packages/nodes/src/`:
- `building/`, `column/`, `elevator/`, `guide/`, `level/`, `roof/`,
`roof-segment/`, `scan/`, `shared/`, `site/`, `stair/`,
`stair-segment/` packages with definition + schema + renderer / system
/ floorplan / panel as appropriate.
- New `floorplan-move.ts` for every kind that supports 2D moves
(ceiling, door, item, shelf, slab, window) — single registry-driven
dispatch path via `def.floorplanMoveTarget`.
- New `floorplan-affordances.ts` for kinds with polygon / endpoint
drags (ceiling, fence, slab, wall) — using the shared
`polygon-vertex-affordance` factories.
- New per-kind `panel.tsx` for kinds with custom inspector content
(door, item, shelf, spawn, wall, window).
- New per-kind `tool.tsx` for placement (door, item, shelf, window).
- New per-kind `move-tool.tsx` for kinds with custom 3D move flows
(door, item, slab, window).
Coordinator + manager updates in `packages/editor/`:
- `tool-manager.tsx` resolves tools from the registry only — no
hardcoded type→component map.
- `panel-manager.tsx` resolves inspector panels the same way.
- `placement-{coordinator,strategies,types}.ts` extended with
shelf-surface placement.
- `selection-manager.tsx` adds the registry-selectable fallback.
- `floorplan-panel.tsx`, `floorplan-background-placement.ts`,
`floorplan-render-context.tsx` updated for the registry layer's new
contract (props, affordance dispatch, render context).
Viewer updates:
- `viewer/index.tsx` drops legacy renderer mounts.
- `node-renderer.tsx` resolves by registry only.
- `scene-bvh.tsx`, `use-node-events.ts`, `level-system.tsx`,
`wall-cutout.tsx`, `zone-system.tsx`, `materials.ts` adjusted for
the registry-only world.
Sidebar tree nodes for ceiling / fence / slab / shelf / tree-node
updated to read from the registered nodes instead of the deleted
legacy renderer trees.
Wiki: new `plugin-authoring.md` page, README index updated.
Tests in `packages/nodes/src/index.test.ts` validate every registered
kind has the required shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>