* 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 spiral stair openings and fence handle arrows
* Implement roof trim planes and ridge vent clipping
* Fix mansard roof and ridge vent placement
* Fix mansard merged roof cutouts
* Fix Dutch roof gable overhang
* Refactor roof segment, ridge vent, and surface geometry
Remove Dutch ridge axis abstraction and rework roof edit system,
ridge vent clipping geometry, and roof surface placement.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Simplify Dutch roof shape
* Add Dutch roof gable top geometry controls
* Fix Dutch roof slope material slots
* Render dutch roof tops as double-sided faces
* Add auto ridge vent toggle to roof segments
Track ridge vent auto-generation via an `autoRidgeVent` metadata flag so
geometry changes only regenerate default vents when enabled, treating
legacy segments with generated vents as auto-enabled for back-compat.
Expose a panel toggle to opt in/out per segment.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Snap new walls to the floor below
Feed the walls of the level directly beneath the active one into the
draft snap pipeline as extra references, so a new wall can align with
the floor below. They share the same local XZ origin, and the list is
kept separate from the current-level walls so the measurement HUD and
wall splitting only act on the active level.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Set Dutch roof shape defaults on type switch
Seed the Dutch shape parameters (waist width/height/length, top rake
thickness/length) with sensible defaults whenever a segment is created
as or switched to Dutch, so the gablet is well-formed regardless of
leftover values from the previous roof type.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Use green accent for corner and endpoint snap markers
Color the corner/endpoint snap markers and the vertical cursor pillar
green across the 2D floorplan beacon, the 3D alignment guide dots, and
the wall snap beacon so snap targets read as a consistent accent.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Add magnetic wall snapping to the roof tool
Snap roof draft corners onto wall corners, midpoints, crossings, and
bodies on the active level and the floor below, reusing the wall tool's
snap pipeline so the beacon and coloring match. The cursor's ground
dot/ring is hidden while a wall snap is active to avoid overlapping the
beacon glyph.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Update auto-generated Next.js route types path
Regenerated next-env.d.ts now references ./.next/dev/types/routes.d.ts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Show cutaway outline while dragging roof trim
Slice an untrimmed segment volume generated from the live node instead of
the registry mesh, whose CSG rebuild lags a few frames behind the drag and
may still hold placeholder geometry — so the section outline now renders
deterministically. Use LineBasicNodeMaterial so the outline draws under the
WebGPU pipeline, and export generateRoofSegmentGeometry for the slice source.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Fill and clip roof trim cutaway, gate it to active drag
Add a violet silhouette fill behind the cutaway outline, extend the
section slicing to angled diagonal/corner trims via a generic vertical
cut plane, and clip each slice to its footprint span so the infinite
plane no longer sprouts stray lines across the rest of the roof. The
cutaway now renders only while a trim handle is being dragged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Separate and extend Dutch roof end slopes
Pull the Dutch hip end slopes out of the watertight shingle shell into
their own slab wedge so they can be reshaped independently, and extend
each end slope inward up its own hip plane until the top edge meets the
gablet's inner triangle. Refactor roof-segment shape geometry into a
shared roof-segment-shape module.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Render roof trim cutaway as a material-only section cut
Replace the triangle-mesh slicer with a CSG intersection of a thin slab
against the untrimmed roof shell, so the cutaway shows red only on real
material (wall + deck bands) and leaves the hollow attic empty. Add an
analytic surface-edge outline, style both solid red like a SketchUp
section, and make the cutaway persist whenever a segment is trimmed.
Keep the merged roof shell visible during trim editing (re-trimmed live
from each segment's drag override) instead of swapping in the per-segment
meshes, whose abutting end-cap faces showed as stray white planes the
commit never had. Extend each slab past free cut-line ends only — trimmed
ends clamp to the cut line — so the red section stays inside the trim box.
Re-export INTERSECTION from the viewer CSG surface for the editor.
* Outline roof cutaway by fill silhouette, restyle to destructive red
Derive the section-cut outline from the fill geometry's edges
(EdgesGeometry) so it traces the real cut shape — wall/deck band
boundaries and the hollow-attic edge — instead of just the top surface
line. Drop the fill to 85% opacity and recolor both fill and outline to
the app's destructive red, matching the delete/destructive UI.
* Include roof accessories in trim clipping and red cutaway
Roof accessories (chimney, vents, skylight, dormer, gutter, downspout,
solar-panel, cupola) now slice at the trim plane like the roof shell and
appear in the red section-cut while dragging a trim handle:
- Export clipGeometryBySegmentTrim from the viewer as a reusable
segment-local trim-clip primitive.
- Add a shared useSegmentTrimClippedGeometry hook + TrimClippedMesh
wrapper (nodes) that slice accessory geometry by the host segment's
live trim override, so the cut tracks the drag.
- Wire the clip into all 11 accessory renderers, including skylight
glass panes and dormer window glass/frame/sill.
- Feed every hosted accessory mesh into the editor's red cutaway, welding
triangle-soup geometry (e.g. ridge vent) so CSG INTERSECTION yields a
cross-section.
- Register skylight in the scene-graph tree-node map so it shows in the
outliner when placed on a roof.
Co-Authored-By: Claude <noreply@anthropic.com>
* Add smooth spline fences with editable curve handles
Fences can now be drawn as one continuous Catmull-Rom/Bezier curve via an
optional `path` (+ per-point `tangents`), selectable in a Straight/Curved
mode toggle. Selected spline fences expose draggable control-point dots
(hexagon) and symmetric tangent handles (circle) joined by a violet line,
editable in both 2D plan and 3D. Side-move arrows are dropped for splines.
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix dutch roof ridge vent handling
* Fix Dutch ridge vent placement and support
* Fix Dutch roof trim artifacts
* Fix Dutch roof trim preview geometry
* Tag roof trim overlay meshes with EDITOR_LAYER
Child meshes relied on a parent group's layer, which three.js does not
propagate, so the trim section/rail/plane overlays rendered on the scene
layer — getting inked/SSGI-darkened and leaking into thumbnail exports.
Co-Authored-By: Claude <noreply@anthropic.com>
* Apply Biome cleanup
* fix(core): address Dutch roof review feedback
* chore: apply biome check cleanup
* fix(core): relax Dutch roof surface helper input
* fix
* Fix biome checks and dev verification
* fixes
* Remove unsupported Biome noShadow override
* Improve roof interactions and fence editing
* Fix fence drag and ridge vent default handling
* editor: drop wall-snap debug log, gate curved-fence finish hint on draft start
Remove the leftover TEMP DIAGNOSTIC console.log in the wall tool's onMove
hot path.
Curved fences commit on a closing gesture (double-click / Enter) rather than
per-click, so surface a 'Finish curve' hint in the fence HUD — but only once a
point has been placed and a curve is actually in flight. The draft point count
is published from SplineFenceDraft into a small ephemeral editor store
(useFenceCurveDraft) that the contextual helper reads, mirroring the existing
useSegmentDraftChain pattern.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Wassim SAMAD <wass08@gmail.com>
Walk 4→2 m/s, run 5.5→5 m/s, jump 6→5 (~1.27 m peak). Applied to both
the GLB walkthrough controller (viewer) and the editor first-person
controls so the two surfaces stay consistent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Polish pass on the placement/interaction overhaul. Five fixes:
- Item-on-item rotation box: the cursor box held the host-local yaw instead of
world yaw, so it diverged from the item by the host's rotation when stacked on
another item (fine on the floor). The box now derives world yaw from the
mesh/host quaternion in both the R/T handler and the move-start sync.
- 2D floorplan move now respects the snapping mode (parity with 3D): grid
quantization only in grid mode, alignment guides only in lines/magnetic mode;
Shift/Alt no longer hard-bypass. Item move also plays the move "tick" SFX on
any resolved-position change, like the 3D move.
- Wall-side item footprint side: the 2D footprint depth offset extended toward the
wall (centerLocalZ -depth/2) instead of into the room, mirroring the item across
the wall; flipped to +depth/2. Aligned the undefined-side anchor to the 3D
convention (front +1 / else -1).
- 3D placement preview side: the wall-side preview bounds + base plane used a -Z
(into-wall) convention; flipped to +Z (into-room) to match the body and the
fixed 2D footprint. The 2D live preview during a 3D wall placement now publishes
the plan rotation (wall angle + item yaw) instead of the world cursor yaw, which
was pi off on a wall face and flipped the footprint to the far side.
- Cmd/Ctrl+R no longer rotates/flips the selected node (it reaches the browser
reload); guard added to the global selected-node handler and the 2D move overlay.
Verified: core/editor/nodes tsc, biome, editor 162/0 + nodes 292/0 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a Floorplan group to the settings Export panel alongside the 3D
model exports, with "Full floorplan" and "Structure only" buttons.
Export re-runs the live registry-driven floorplan pipeline
(def.floorplan -> FloorplanGeometryRenderer) headlessly with a neutral
viewState, fits each level to its own page, and titles each page with
the level label. Every level of the active building becomes a page in
one landscape A4 PDF. "Structure only" keeps category === 'structure'
nodes; "Full" keeps every visible node with a floorplan builder.
- new lib/floorplan/floorplan-export.tsx; jsPDF + svg2pdf dynamically
imported so they only load on export
- export five pure helpers from floorplan-registry-layer for reuse
(buildContext, getFloorplanLevelData, floorplanLayerRank,
splitFloorplanOverlay, isFloorplanNodeVisible) — no behaviour change
- bake vector-effect:non-scaling-stroke widths into real units before
svg2pdf (which ignores the hint and would otherwise draw door/window/
stair linework as metre-wide strokes)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes the lone biome organizeImports error so the quality gate passes on the
post-#448 base. Type-check (9/9), biome, and all suites green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two zone fixes.
helper-manager: the contextual HUD only rendered for tools with `def.toolHints`,
so zone (none) showed no HUD and no snapping chip even though it resolves a snap
context. Render the generic RegisteredToolHelper whenever the tool has hints OR a
snap/continuation context; hoist the legacy `roof` RoofHelper above it so the new
fallback doesn't capture it. Any snappable hint-less draft tool now advertises
Shift = cycle.
zone-tool: a not-yet-migrated legacy tool — it used Shift as a snap bypass and
applied `gridSnapStep` unconditionally, so Off mode still snapped to grid.
Migrated to the mode-driven exclusive-modes convention (zone resolves to the
'wall' context): grid quantize gated on isGridSnapActive(), 15° ray gated on
isAngleSnapActive(), Off/Lines leave the raw cursor. Dropped the Shift-bypass and
its key listeners — Shift now cycles the mode globally. Recorded as migrated in
the review skill's known-legacy list.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Capture the durable patterns from the placement-interaction overhaul so reviews
and new devs don't regress them:
- tools.md: new "Data-driven live drag" section — kinds whose geometry is
recomputed from fields (wall/opening/endpoint) preview via useLiveNodeOverrides
(merged by getEffectiveWall/getEffectiveNode), store written once on commit;
per-tick useScene.updateNodes is the documented anti-pattern (churns the nodes
ref → app-wide re-render flood). Plus "Floorplan registry: per-node
subscriptions" — each entry subscribes to its own live slice, memo'd with
stable props, sibling-epoch invalidation; widening to the whole Map / dropping
memo is a regression. Plus a note that the HUD snapping chip renders for any
snap-context tool, not only those with def.toolHints.
- review SKILL.md: matching blockers in §C (data-driven drag / no per-tick store
write) and §D (per-node list subscriptions).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The grid only appeared while a ghost was in flight (moving node / placing scope /
armed GLB item), so a merely-armed draft tool (wall / slab / fence / ceiling /
zone / column / MEP) showed no lattice. Gate visibility on isGridSnapActive()
alone — it already derives the snap context from the interaction scope OR the
armed build tool and is true only when that context resolves to grid (false for
select / idle / paint / lines / off), so it is exactly the right condition.
Draft tools publish no surface; the grid's horizontal branch already falls back
to the active level floor, with the reveal patch following the cursor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The footprint polygon uses `rotateVec` (R(-angle)) but the sprite was drawn via
SVG `rotate(+angle)`, so image and box counter-rotated and diverged by 2x the
item's rotation in the 2D floorplan. Negate the image rotation so the sprite
tracks its box (and the 3D orientation). Scoped to items — the only emitter of
the `image` floorplan geometry kind.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In split view, dragging any node re-rendered ALL ~150 per-node
InteractiveGeometry instances every tick (profiler: InteractiveGeometry ×3192),
because FloorplanRegistryLayer subscribed to the whole useLiveTransforms /
useLiveNodeOverrides maps and the per-node component wasn't memoized.
Extract a memoized FloorplanRegistryEntry that subscribes to ONLY its own slice
(useLiveTransforms(s => s.transforms.get(id)) / overrides.get(id)). The live
stores write a fresh value object for the changed node only, so unchanged nodes
keep identity and don't re-render. The parent now watches just the stable
node-id list; sibling invalidation (wall miters / opening cuts) moves to a
store-subscribe that bumps only affected siblings' epoch. InteractiveGeometry is
also memoized. Geometry cache, affected-sibling epochs, the base/overlay
two-pass, hit-testing, and selection/hover are all preserved verbatim.
An item move now re-renders only its own entry; a wall drag only the moving wall
plus its linked-corner siblings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two fixes to the wall-endpoint reshape interaction.
selection-manager: a node:click is synthesized on R3F pointer-up, so an endpoint
handle that sits on the wall body lets the wall mesh (raycast-hit behind it from
a 3D angle) emit its own click on the same release — selecting the wall and
arming its move tool on top of the endpoint move. Ignore the body click while an
`endpoint` reshape owns the pointer. Scoped to `endpoint` so hole-edit (which
relies on node clicks to exit) is unaffected.
move-endpoint-tool: a press-drag committed on release but a tap dismissed, and
whether the tap's release ran at all raced the window pointer-up listener
mounting a tick after the handle's pointerdown ("works once, then needs a long
press"). Unify on one rule: commit only when the endpoint actually moved,
otherwise stay armed. A tap now grabs the endpoint (it follows the cursor; the
next click after a move commits) exactly like a press-drag — both engage
identically. Drops the now-dead hasDraggedRef.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two changes from the split-view / wall-endpoint perf + UX pass.
floorplan-panel: render the heavy 2D <svg> scene only when the panel is visible
(`isFloorplanOpen`, i.e. viewMode !== '3d'). The panel stays mounted in 3D
(display:none) to keep the portalled compass + viewport state warm, but the
registry layer / per-node InteractiveGeometry / handle layers no longer
reconcile on every scene change while invisible. Renders fully in 2D and split;
viewport pan/zoom is preserved across the toggle.
wall move-endpoint-tool: preview via `useLiveNodeOverrides` instead of writing
`useScene.updateNodes` every grid:move tick. The per-tick store write handed a
fresh `nodes` ref to every `useScene(s => s.nodes)` subscriber (WallPanel, the
contextual HUD, tooltips, floor plan), rebuilding them all each frame. Overrides
are merged by the wall system, wall panel, and 2D floor plan, so the preview
still tracks live with no store churn; the store is written once on commit, and
one Ctrl-Z reverts to the original endpoint. Also swallow the click that follows
every endpoint-tool release so it can't fall through to the wall body and arm
the wall move tool (no-drag tap or post-commit).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Camera-zoom hitch: the FloorplanPanel stays mounted (display:none in 3D mode),
so its navigation-pose subscriber fired every camera onUpdate. During zoom the
view-width changes continuously, so the epsilon guard never short-circuited and
`syncFloorplanViewportToNavigationPose` ran each frame → setViewport/
setFloorplanUserRotationDeg → a full re-render of the ~10k-line floorplan SVG,
even though nothing is visible (React reconciles display:none subtrees).
Gate the viewport sync on `isFloorplanOpen` via a ref the per-frame subscriber
reads, and re-run the mount catch-up effect when the panel reopens so the
viewport snaps to the current camera. The compass is unaffected — it's portaled
to the always-visible viewer area and still receives the pose; only the panel's
own viewport sync is skipped while hidden.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BUG 1 — wall-attached furniture facing. The coordinator drew its own inline
facing triangle and published the wall grid normal from the cursor ghost's own
yaw, which is the symmetric-wireframe yaw — π off the item's true facing for a
wall (and a different frame for a roof-segment face). The triangle pointed into
the wall and the grid normal was sign-flipped while the box still looked right.
Fold the coordinator into the unified `useFacingPose` overlay (drop the inline
triangle, geometry/material/constants). The per-frame surface publisher now
derives the true outward facing from the draft mesh's world orientation (its
local +Z faces out of the host surface) for wall/roof-wall and feeds that single
yaw to BOTH the grid normal and the facing triangle. Floor/ceiling/item-surface/
shelf paths are unchanged behaviourally — same cursor yaw and Y, just routed
through the overlay.
Right-click cancel vs camera orbit. The right button also orbits the camera, so
the old unconditional `contextmenu → onCancel` cancelled placement on every
right-drag. Gate the cancel on pointerup: only when the right press moved ≤4px
within ≤200ms (a quick stationary click); a longer/further press is an orbit and
is left alone. contextmenu now only suppresses the OS menu.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Validated live by Wassim. The click-to-move branch set `clickHandledRef = true`
then returned early, skipping the 50ms reset the normal select path runs at the
end. The flag stayed true, so `onGridClick`'s guard silently blocked every
empty-click deselect until the next normal select cleared it. Schedule the reset
right after the flag is set so EVERY branch (incl. the early return) clears it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Validated live by Wassim.
- Wall grid is anchored to the wall PLANE (its foot), not the moving ghost, so
sliding a door/window only moves the reveal patch — the lattice stays a fixed
snap reference instead of "following" the opening.
- DoubleSide so the lattice renders when an opening is handled from the far side.
- depthTest is conditional: ON for the floor (the ground occludes a sub-floor
lattice) and OFF on a wall (visible through the wall from the opposite side).
- Resolution change is a uniform write only — `cellSize` no longer rebuilds the
uniform + material (which recompiled the shader and stalled on every step).
- Y follow snaps instantly (was a lerp); `gridY` state only updates on change.
- Reveal radius 5 → 12.
- Door/window publish the wall surface on mount (+ claim the pointer for the
wall) so the grid is vertical from the FIRST frame — no horizontal flash.
- Export the active-placement-surface module so the opening tools can publish.
- Drop the editor-side "Show Grid" setting: the 3D grid is now purely a
placement aid (shown only while placing/moving in grid-snap mode).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Door and window are placed via preset and moved with a bespoke wall-bound
`move-tool` (affordanceTools.move), not the draw tool — so the previous inline
triangle never showed for the paths actually used. Migrate both:
- move-tool (move + preset, the community path): publish the on-wall ghost pose
to `useFacingPose` in the same building-local frame the ghost renders in,
dropped to the floor under the wall (the ghost Y is the opening centre); clear
on every off-wall / hide / reveal / unmount path
- draw tool (standalone from-scratch path): publish the on-host pose, clear on
fallback/hide; frame depth read via a ref to keep the setup effect deps clean
Removes the now-dead `FacingIndicator` public export (the editor-side overlay is
its only consumer, via relative import). The unified overlay now covers every
placement/move path: items, column/shelf, stair, and door/window.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every placement and move path now publishes its ghost pose to a single
`useFacingPose` store, drawn by one editor-side `<FacingPoseIndicator>` overlay,
instead of each path drawing its own triangle (which left the nodes-package and
PlacementBox paths invisible):
- column/shelf presets + all moves (PlacementBox via move-registry, and
DragBoundingBox) now publish the pose, so the triangle finally shows
- stair create + move use a declarative `facingIndicator: { reversed: true }`
(new registry resolver) so the triangle sits before the entry pointing out —
resolved in one place, so create and move match automatically
- stair placement defaults to single and respects the shared `point`
continuation (C) toggle, like the other placement tools
Checkpoint on the placement-interaction epic: also carries the in-flight
continuation-profile extraction (lib/continuation), grid surface (item #8), and
HUD work. Door/window still render their own legacy inline triangle and are
migrated to the overlay next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The walkthrough rode the default 50° orbit camera, which feels cramped on
foot. Both walkthrough controllers (baked GlbWalkthroughController and the
parametric WalkthroughControls fallback) now set a shared WALKTHROUGH_FOV = 60
on enter and restore the prior FOV on exit, leaving orbit framing untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bundled ceiling-fan model.glb was stale (no animation clip, two slots).
Replace it with the variant matching production storage: an `On` animation
clip and a third `slot_base` paint slot. Same dimensions/offset, so no catalog
metadata change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`poseDoorMovingParts` assigned a single euler axis (`group.rotation.y` /
`.x`). The live system was fine because the group's euler stays a clean
(0, y, 0). But the GLB exporter clones the door and decomposes its matrix,
which re-derives a gimbal-flipped euler (x=z=π) for any rotation beyond
±90° — folding panels reach ~158°. The reset to t=0 then only zeroed `.y`,
leaving the π residue on x/z and baking a 180°-flipped rest pose (panels
folded out toward a wrong position even when closed).
Set the full euler triple via `.set()` in every pose branch so the other
two axes are always zeroed, clearing any decomposed residue. Add a
regression test that exports an open folding door and asserts an identity
rest pose for all panels.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
interaction-scope.md + review-architecture skill: the sanctioned Alt-as-toggle
(wall/fence chain mode, the one place Alt-as-force is meaningless), and a second
known-legacy pattern — `event.altKey` alignment-bypass in the roof/polygon/slab
previews + ceiling/slab snap paths (migrate-on-touch; wall+fence already done).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the legacy held-Alt mechanism on wall and fence drafting with a
mode toggle, mirroring the snapping-mode chip:
- Wall: `wallChainMode` room (auto-close on loop) / single. Room mode
finishes automatically when the new endpoint lands within the join-snap
radius of the chain's first vertex; single commits one wall per click.
- Fence: `fenceChainMode` continuous (chain until double-click/Esc) /
single. Fences are linear barriers, so continuous has no auto-close.
- Both: Alt-tap cycles the active drafting tool's chain mode (clean-tap,
scoped to wall/fence drafting); a clickable HUD chip shows the mode.
Persisted + migrated in `useEditor`.
Migrate wall and fence off held-Alt-bypass-alignment to the unified
convention: alignment now follows the magnetic snap mode, which frees Alt
for the toggle. 2D floorplan parity kept in sync with the 3D tools.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two issues surfaced testing the baked viewer:
- Folding door folded toward +z (into the room) — the joint rotation sign
was inverted, so the accordion opened the wrong way ("weird position").
Flip to `(prevDirection - direction) * foldAngle` so leaves fold toward
−z, matching the original inline rig. Verified panel-for-panel against the
original formula at every operationState.
- Openable clips were named by display name (`<name>: open`), but the baked
viewer drives playback by clip name (`useAnimations` maps name → action).
Several windows share the name "Window 1", so their clips collapsed to one
action and triggering any one opened the first. Key the clip name by node
id (`<id>: open`) — unique, matching the item-loop convention; the
human-readable name still lives in `extras.label`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Only swing doors (hinged/double/french) baked an open clip into the GLB —
they carry a `pascalSwingLeaf` marker the exporter reads. Every operation
door type (sliding, pocket, barn, folding, garage-sectional/rollup/tiltup)
baked its `operationState` straight into mesh vertex positions at build
time, so the exporter had no re-poseable node to sample and the artifact
never flagged them `openable`.
Give operation doors the same build-once + pose-at-t split windows already
use. Each builder now emits its moving parts in a named group at the CLOSED
pose, and `poseDoorMovingParts` (the single source of truth, shared by the
live system and the GLB exporter) drives the open motion:
- sliding/pocket/barn: rigid leaf translation
- garage-tiltup: rigid hinge about the lintel
- folding: hinged accordion chain (nested groups, per-joint fold)
- garage-sectional: per-panel groups posed along the overhead curve
- garage-rollup: the one type whose live geometry changes (slats roll onto
a drum, which a glTF clip can't express) keeps its full-detail live
rebuild; the curtain is wrapped in a top-pivoted group the exporter
scales up into the lintel as the baked approximation.
The exporter samples each operation door's motion into keyframe tracks
(16 segments) so the non-linear rigs (curve, accordion) stay faithful, and
stamps `extras.openable` + `extras.clips` so any glTF consumer can play it.
Tests: per-type kinematics (groups build, rest closed, open) +
sliding/roll-up clip baking (sampled position/scale tracks).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The floor grab-offset comment claimed floorStrategy.move reads localPosition
"under Alt (free place)"; it reads event.position with mode-governed snapToGrid
and has no Alt branch (Alt is force-place-only). Describe the real reason both
frames carry the offset: it's computed local-space but the strategy consumes the
world point.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The snapping model (Shift = cycle mode, Alt = force/free, mode-driven reads via
isGridSnapActive/isMagneticSnapActive/isAngleSnapActive, snapProfile-declared
context) lived only in code and the plan; tools.md still preached the legacy
"Shift = bypass snapping". Close the drift so the architecture review refuses
tool changes that revert to the old pattern:
- tools.md: replace the held-Shift-bypass manipulation policy with the unified
mode-driven model + the single snap read path.
- interaction-scope.md: new "Snapping mode & modifiers" section (contexts, read
path, modifiers, the chip-needs-a-scope rule) + a Rules bullet + the
known-legacy MEP movers (migrate-on-touch) incl. the dual-path constraint
(a bespoke mover must not open a `moving` scope — it re-mounts the generic
mover via useMovingNode).
- review-architecture skill: add interaction-scope.md to the reads and a new
"F. Interaction scope, snapping & modifiers" checklist — new shiftKey-bypass,
ungated grid step, missing snapProfile, a new useEditor interaction flag, or a
bespoke mover opening a moving scope are blockers; touching the legacy MEP
movers forces migration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MEP run/fitting/terminal tools anchored their cursor readout pill at
~+0.35m above the placement point, so with the tall CursorSphere line
(badge at +2.7m) the pill sat right on the cursor and overlapped it,
especially when zoomed in. Editor main (#438) already raised the duct
pill to +1.45m; this long-lived branch predates that merge. Bring every
MEP tool that uses the tall cursor onto the same +1.45m anchor:
duct/pipe/liquid/lineset runs, duct/pipe fittings, and duct-terminal.
hvac-equipment (height-aware anchor) and pipe-trap (no cursor line) are
left as-is — their HUDs already clear the ghost.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Baking some projects failed with "Cannot read properties of undefined (reading
'isShaderMaterial')". GLTFExporter reads material.isShaderMaterial unconditionally,
so a renderable (Mesh / Line / Points) with no material crashes the export — and a
non-Mesh renderable slips past both the isMesh prune check and material conversion.
Guard it in pruneNonRenderableMeshes: a material-less renderable is dropped if it's
a leaf, or neutralised (empty geometry + a hidden placeholder material) if it has
children so its subtree survives. Post-FX disable now lets these scenes reach the
exporter, which is why it surfaced.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The last two hot per-pointer-move 2D states still lived in FloorplanPanel's
useState, re-rendering the ~10k-line panel on every move:
- marquee (box-select): the whole drag struct moves to a dedicated
use-floorplan-marquee store; down/move/up/cancel read+write it via
getState() (panel holds nothing), and a FloorplanMarqueeOverlay leaf
subscribes to the moving corner and renders the rect alone. Drops the 3
bounds memos + the useState.
- reference-scale: the rubber-band's moving end was always equal to the
shared cursorPoint (written every move anyway), so drop the `cursor` field
from the draft and read it from useFloorplanDraftPreview in a new
FloorplanReferenceScaleDraftLine leaf. The draft now carries only the
per-click guide + start anchor, so it no longer re-renders the panel.
Closes out the 2D edition perf pass — every build/edit/select hot path now
writes a store, not panel state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Roof-segment edit/move ignored the active snap mode and showed no chip:
- add snapProfile:'structural' so a body-move resolves the no-angle polygon
context (grid/lines/off) like every other structural move;
- resize uses getSegmentGridStep() (0 outside grid mode = the "smooth" resize
that used to need a held Shift), dropping the captured gridSnapStep + Shift;
- move drops its Shift bypass;
- the affordance dispatcher opens a boundary reshape scope for the resize so
the snapping chip shows and the context resolves.
2D rotation handles now match the 3D gizmo across all six rotate affordances
(column / elevator / roof-segment / shelf / spawn / stair): a shared
rotateAffordanceDelta snaps to the 15° step unless Shift (free), the
dispatcher opens the same ROTATE_HANDLE_DRAG_LABEL handle-drag scope the 3D
gizmo uses so the contextual HUD shows the "Shift = rotate freely" hint, and
the live degree readout snaps to match the committed rotation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wall and fence endpoint-move affordances hard-snapped to the grid via a
hardcoded WALL_GRID_STEP and always ran Figma line-alignment, ignoring the
active snapping mode. Now:
- grid step follows getSegmentGridStep() (0 outside grid mode), so lines /
angles / off no longer force a grid snap the mode chip says is inactive;
- Figma alignment is gated on isMagneticSnapActive() (the lines mode);
- angles mode angle-locks the endpoint off the fixed corner (free length),
mirroring the draft tool;
- fence drops its legacy Shift-bypass to match the wall's unified model
(Alt stays as linked-segment detach).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 2D affordance dispatcher (`startAffordanceDrag`) now begins the matching
reshaping interaction scope (boundary / hole / curve / endpoint) on pointer-down
and tears it down on release/cancel, matched by node id. This makes the
contextual snapping HUD show the right chip during polygon vertex/edge and wall
endpoint/curve edits, and lets `getActiveSnapContext()` resolve the correct
per-context snapping mode the affordance snap math already reads.
Wall creation is owned by the 3D `WallTool`, which is dead in 2D-only view
(canvas `display:none`). Mirror the slab/ceiling 2D-only committers: commit
locally via `createWallOnCurrentLevel`, gated on `viewMode === '2d'`, chaining
the next segment from the committed wall's resolved end. Split/3D keep their
single-owner tool commit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same fix as slab: ceiling is committed by its 3D registry tool, dead in 2D-only
view. Commit it from the panel on both close paths, gated to viewMode==='2d'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Slab is committed by its 3D registry tool, which accumulates the grid:click
vertices the 2D panel emits and commits on close. That path is dead in 2D-only
view — the 3D canvas is display:none, so the tool never commits and the slab is
never created (split/3D work because the 3D side is live). Mirror the zone
pattern: the panel commits the slab itself on both close paths (double-click +
click-first-vertex), gated to viewMode==='2d' so split/3D keep their single-owner
tool commit (no double-create).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The shared polygon-vertex affordance snapped via snapPointToGrid(rawPoint), whose
default step is the hardcoded WALL_GRID_STEP (0.5m) — so slab/zone/ceiling vertex,
edge, and add-vertex edits always quantized to half-meters regardless of the
active mode OR the user's grid-step setting (plan open bugs #1-2). Use the
mode-aware getSegmentGridStep() (0 in non-grid modes) so grid quantizes to the
live step, lines/off pass through to the wall-snap/alignment resolver. Drop the
legacy shiftKey bypass from the slab/ceiling magnetic resolvers (they already
gate on isMagneticSnapActive).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Perf: move the per-move wall/fence/roof draft END points into
useFloorplanDraftPreview; a new FloorplanLinearDraftLayer leaf owns the live
draft polygon + fence segment + wall measurement, subscribing to the store. The
shared FloorplanDraftLayer keeps only the per-click anchors. Wall/fence/roof
drafts now have zero per-move panel setState — buttery smooth like slab/zone.
Parity: migrate the remaining legacy Shift=bypass paths to the unified
mode-driven model. roof (move + click) honored only always-grid + bypassSnap —
now grid/lines/off (footprint → no angle). wall + fence click-commit still used
the legacy bypass while their move-preview didn't — now consistent. Wall Alt
stays 'commit single wall' (open product decision, untouched).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The polygon-draft snap path used the legacy model: bypassSnap = shiftPressed and
angleSnap = pointCount > 0 && !bypassSnap, so the 15deg angle lock engaged after
the first vertex regardless of mode — hijacking grid/lines/off into angle-snap
even though the HUD chip showed the right mode.
Migrate all three placement paths (move preview, single-click vertex, double-
click close) to the unified model: angleSnap = isAngleSnapActive(); grid flows
through snapToHalf (step 0 in non-grid modes); wall-snap/alignment already gates
on isMagneticSnapActive(). Behavior now matches the chip — grid quantizes,
angles locks 15deg rays, lines snaps to walls/alignment, off is free. Drop the
now-dead bypassSnap param from snapPolygonDraftPoint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the editor packages compatible with, and benefit from, the native Go
compiler (TypeScript 7 / tsgo) while keeping the npm publish path on stable tsc.
- Add @typescript/native-preview (pinned 7.0.0-dev.20260624.1). tsgo coexists
with typescript@6, which Next typegen and the IDE plugin still need until the
TS 7.1 programmatic API ships.
- Fix two react-three-fiber JSX augmentations the native checker rejects but
tsc tolerated:
- viewer: map only LineBasicNodeMaterial (the one webgpu node material used as
a JSX tag) instead of the whole three/webgpu namespace (TS2320/TS2590).
- plane-box-select-tool: drop the redundant `IntrinsicElements extends
ThreeElements` block — it duplicated R3F's global augmentation and, by
referencing @react-three/fiber's ThreeElements directly, hit a bun peer-dep
variant-directory duplicate under tsgo (TS2320). r3f.d.ts already covers
those JSX intrinsics. Types-only — no runtime change.
- check-types -> tsgo --noEmit (editor, @repo/ui, editor app, ifc-converter app)
- emit-package dev watch -> tsgo --build --watch
- build/publish stay tsc --build (prepublishOnly + release.yml unchanged;
emitted .d.ts is byte-identical to tsc output)
- bump next 16.2.6 -> 16.2.9
tsc remains the source of truth and fallback. Published artifacts unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 2D build/edit tools republish the snapped cursor point AND the screen-space
coordinate-badge position on every pointer move. Both lived in FloorplanPanel
useState, so each move re-rendered the whole ~310ms panel (the badge fires on
every pointermove while any build tool is active — the dominant culprit).
Move both into a useFloorplanDraftPreview store written via getState() (no panel
re-render); render the crosshair + live polygon-draft edge from a
FloorplanDraftCursorLayer leaf and the coordinate badge from a
FloorplanCursorIndicator leaf, each subscribing to the store. Same store+leaf
pattern as the stair build preview. Slab/zone/ceiling drafts now have zero
per-move panel setState. Wall/fence/roof draftEnd are follow-up slices.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>