* 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>