Commit Graph
8 Commits
Author SHA1 Message Date
a590747748 Adjustments pass: three 0.185, undo/cancel semantics, guide & panel fixes (#496)
* fix(editor): only start handle drags on primary button

Right-click over a rotation/move/resize handle started the gesture and
stopPropagation()'d, fighting the camera orbit. Guard every gesture
starter (shared useHandleDrag, group rotate gizmo, wall endpoint/height/
move, fence move, roof trim) with event.button !== 0 before it swallows
the event.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(viewer): default units from timezone/locale until user picks

Derive the metric/imperial default from the IANA timezone (US, Liberia,
Myanmar zones -> imperial; anything else -> metric), falling back to an
explicit locale region subtag only when no timezone resolves. Timezone
tracks actual location, unlike navigator.language where en-US is a common
default far outside the US. The unit is only persisted once the user
explicitly sets it, so an untouched preference keeps tracking location;
existing persisted values are treated as explicit and left alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(viewer): toggle shadows via renderer.shadowMap.enabled, not castShadow

Flipping a light's castShadow at runtime crashes three r184's WebGPU
renderer: toggling off disposes the shadow map's GPU texture, but the
node builder cache evicts with the post-toggle key, so the shadows-on
entry survives still referencing the destroyed texture. Re-enabling
reuses that stale state and every frame submit fails with
GPUValidationError ("Invalid CommandBuffer from CommandEncoder").

Keep castShadow static and drive the user-facing toggle through the
Canvas shadows prop (renderer.shadowMap.enabled), which rebuilds
materials without disposing shadow resources.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(core,editor): floor undo history at scene load

Undo could step back past the scene load into the pre-load (empty)
state, wiping the whole project — which autosave would then persist.
Two defects: clearSceneHistory() had zero call sites, so every load left
the empty pre-load state in zundo's pastStates; and setScene wrote the
store twice, recording a half-normalized intermediate as a second undo
target.

- applySceneGraphToEditor, JSON import, and reset-to-default now clear
  history so the loaded scene is the undo floor
- setScene collapses to a single tracked write (final state identical)
- clearSceneHistory also resumes tracking so a load landing inside a
  pause window can't strand undo recording off

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* build(three): upgrade runtime to 0.185.1, pin @types/three at 0.184

r185 renames directionToColor/colorToDirection to packNormalToRGB/
unpackRGBToNormal and splits SSGI's packed rgba output into separate AO
(getAONode, single channel) and GI (getGINode) textures; wind-node's
positionLocal reads become positionGeometry.

@types/three stays at 0.184.1: the 0.185 typings send tsgo's inference
into unbounded allocation (microsoft/typescript-go#2125 class — it ate
~70GB/90s and OOM-killed the machine). viewer/lib/tsl-compat.ts bridges
the two renamed TSL exports with 0.184-typed signatures; drop it and the
pin together once tsgo copes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(viewer): gate .ktx2 preset-texture loads on detectSupport

KTX2Loader.load throws before detectSupport has run, and materials
created while a standalone capture canvas's renderer was still
initializing cached themselves permanently texture-less — fabric slots
rendered white in item thumbnails. .ktx2 loads now await whenKtx2Ready()
(resolved by the first successful ensureKtx2Support), which is exported
so hosts with standalone canvases can arm it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(viewer): accept host-supplied country for the unit default

applyCountryUnitDefault lets the host app feed an authoritative
IP-derived country (e.g. Vercel's x-vercel-ip-country) into the unit
default. Stronger signal than the timezone heuristic applied at store
creation, still never overrides an explicit user choice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(editor): box select starts over locked guide images

A locked guide's hit-rect swallowed pointer-down via stopPropagation, so
marquee selection couldn't start on top of it. Locked guides now let the
event bubble to the svg root; click-to-select and the unlock affordance
still work because a non-drag release fires onClick as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(editor): live rotation readout while rotating a guide image

Rotating a guide with the 2D handles gave no angle feedback. Reuse the
registry layer's RotationAngleOverlay (wedge + degree chip) for guide
rotate drags: sweeps from the grabbed corner's bearing at grab to its
current snapped bearing, suppressed under ~0.5deg so a fresh grab doesn't
flash a sliver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(editor): lingo free-text input in the guide set-scale dialog

The real-length field accepts natural measurement text via
@pascal-app/lingo — 5'11", 180cm, 1m80, 12ft — parsed in the dropdown's
unit (a bare number still means that unit, a typed unit wins). A faint
'= 1.80 m' hint previews non-trivial input, unparseable text gets a
clear error, and the odd onBlur force-reset to 0.0001 is gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(editor): cmd+z mid-interaction cancels the gesture instead of undoing

Undo pressed while the mouse is mid-action (moving, drawing, dragging a
handle) used to history-jump under the live pointer — stale carry, half
gestures committing against a rewound scene. Now it reads as 'abort this
action', exactly like Escape:

- the global undo/redo arms first route through the tool:cancel path
  (covers build drafts, placement ghosts, move tools) and skip the
  history jump when anything was in flight (consumed, scope-active, or
  inputDragging);
- pointer drags that only knew pointercancel (generic handle drags,
  group rotate, wall side/height handles, roof trim) gain the same
  capture-phase Escape/cmd+z keydown the group-move drags already had —
  fixing Escape for them too;
- the existing capture-phase handlers (3D/2D group move, 2D registry
  move overlay) additionally accept cmd+z as cancel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(editor): box select arms over any guide image that won't drag

Follow-up to cd830279, which only let LOCKED guides bubble pointer-down.
An unlocked, unselected guide also swallowed the event for nothing (no
translate drag starts), so marquee selection could never start on top of
it. Now only the one case that uses the event consumes it — selected +
unlocked → translate drag — and everything else bubbles to the svg root.
Click-to-select still works: a non-drag release never crosses the
box-select threshold, so the trailing click fires the guide's onClick.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(editor): cmd+z during draft placement cancels instead of undoing

The preset/item placement flow (useDraftNode + placement coordinator)
registers no interaction scope and holds no pointer, so the cmd+z cancel
guard from c699d74e saw it as idle and history-jumped mid-placement.
Paused scene history is the universal tell — the draft cycle (and every
adopted-move session) keeps temporal paused for the whole gesture, and
an undo against a paused store lands on a stale baseline anyway. Treat
!isTracking as in-flight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(editor): cmd+z mid-placement completes the cancel, not just skips undo

4ffda723 stopped the history jump during preset/item placement but left
the draft alive: the item tool passes no coordinator onCancel — it is
Escape's fall-through (switch to select, unmount the tool) that actually
destroys the draft. Extract that fall-through and run it from the cmd+z
path too whenever a gesture is live and nothing consumed tool:cancel,
so cmd+z now behaves exactly like Escape end to end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(editor): node selection clears a lingering reference selection

Selecting a guide clears the node selection (handleGuideSelect), but the
reverse was never wired: clicking a wall with a floorplan reference
selected left selectedReferenceId set, and the panel manager's
reference-first priority kept showing the floorplan panel until it was
closed by hand. PanelManager now drops the stale reference the moment a
scene selection (nodes or zone) appears.

Also: the inspector's expanded state is shared across panel swaps by
design, but it survived close/reopen too — deselecting everything now
resets it, so a fresh selection opens the panel collapsed again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(plugin-trees): wind displacement reads positionLocal, not positionGeometry

The three-0.185 migration renamed positionLocal to positionGeometry in
the wind nodes, but positionLocal was never removed in r185 — and the
two are not interchangeable here. NodeMaterial.setupPosition applies the
instance transform by mutating positionLocal, then overwrites it with
positionNode's output; reading raw positionGeometry therefore discarded
every instance matrix — leaf cards rendered unscaled at tree-local
coordinates (a giant canopy filling the sky) and grass/flower instances
collapsed invisibly. Reading positionLocal (instance transform included)
restores r184 behavior exactly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 13:26:35 -04:00
bf25af6add viewer: Fix Dutch roof trim artifacts (#452)
* 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>
2026-06-30 16:41:13 -04:00
555cf06c12 editor: building rotation, alt-click single wall, world-grid alignment, tap-to-engage move (#388)
* Add roof surface placement support for items

Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.

- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fixed conflict

* Snapshot: ridge vent locks to segment ridge during placement, free sliders after

Placement tool clamps the cursor to the segment's ridge line for all roof
types (gable, hip, shed, gambrel, dutch, mansard; flat rejected). Renderer
re-derives Y from the live surface plus a small lift, and treats stored
position[1] / position[2] as user offsets so the inspector sliders move
the vent after placement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(roof): contribute outer silhouette to alignment-guide candidates

Roof has no centred-box footprint (it's the union of its `roof-segment`
children) so the capability bridge needs the resolved AABB directly. We
build it from the children's corners in roof-local space, then transform
to world coords. Roofs only contribute as static candidates — the
move-roof tool drives them by origin, so the relocatable-box path never
applies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(floorplan): upright text geometry that stays horizontal under scene rotation

Floor-plan text labels were rotating with the 90° default scene rotation,
making zone names read sideways. Add an `upright` flag to text geometry:
when set, the registry layer counter-rotates the label by sceneRotationDeg
around its anchor so it reads horizontally on screen. Zone labels opt in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(editor): redesign material-paint cursor badge and gate floor-plan paint mode

Paint cursor swaps the inline icon+label chip for a stacked badge — a
glowing accent stem pointing down to the hit point with the paint icon
above — so the cue reads as "pointer" rather than "tooltip". Switches
the accent to indigo. The 2D floor-plan now also shows the paint-icon
overlay and routes pointer interactions to the painter when in
material-paint mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(editor): 90° rotate action on the floating building menu

Adds a Rotate button to the building action menu (and a generic `onRotate`
slot to NodeActionMenu). Each click rotates the building 90° CW around
its world-bbox center, mirroring the offset compensation
`MoveBuildingContent` uses during R/T-rotate drags — so the building
spins in place instead of orbiting its (often off-centre) origin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(column,shelf): show cursor sphere during placement preview

Matches the placement affordance other floor-placed tools already have
so users see the snap point as a discrete dot alongside the ghost mesh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ifc-converter): update next-env types path to .next/dev/types

Next.js moved generated route types under `.next/dev/types/` — regenerate
the reference so the type-check resolves them again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ridge-vent): track parent segment's live overrides for real-time follow

While dragging a roof-segment resize handle (width / depth / pitch /
wallHeight / rotation), the new dimensions stream through
useLiveNodeOverrides and only flush to the store on release — so the
ridge vent was snapping to the new ridge only after the drag ended.
Merge segment overrides into the renderer's segment view so the vent
rides the resize in real time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(editor): step building rotation in 15° increments

Cuts the floating building menu's rotate-step from 90° to 15° so each
click nudges the building rather than slamming it to the next cardinal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(editor): world-frame alignment guides and grid snap under building rotation

Brings the 2D floor-plan alignment + grid behaviour in line with 3D so
guides and snap follow the world XZ grid regardless of how the active
building is rotated.

Core: adds resolveAlignmentInBuildingWorld + snapWorldXZToBuildingLocal,
plus a shared BuildingPose type, so callers can resolve in world space
while staying in building-local coords downstream.

Editor: introduces packages/editor/src/lib/world-grid-snap.ts as the
single entry point for tools that need world-frame alignment / grid
snap (resolveAlignmentForActiveBuilding, getActiveBuildingPose,
snapWorldXZForActiveBuilding, snapBuildingLocalToWorldGrid). Honours
useLiveTransforms and resolves the active building via level.parentId
first so the helper stays in sync with the floor-plan panel.

Alignment store unification: applyFloorplanAlignment and the registry
move overlay now publish guides in world XZ — the same frame the 3D
tools already use — so the shared useAlignmentGuides store carries one
consistent coordinate system. The 2D layer renders world XZ via the
fixed world → SVG transform (a 90° rotation around the building's
world position; independent of building rotation), so guides come out
parallel to the world-axis-aligned floor-plan grid in every case.

Drafting + move sessions across walls, fences, slabs, ceilings,
columns, items, shelves, roofs, stairs, zones and elevators are
re-routed through the new helpers so cursor placement, grid snap,
draft endpoints, and move commits all land on the world XZ grid
even when the building has been rotated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* revert(editor): drop building drag-rotate and grid building-XZ chase

Two behaviours added recently were causing the world grid to slide
around during building edits. Reverts both, keeping the rest of the
world-frame alignment work intact.

- floating-building-action-menu / node-action-menu: roll the rotate
  button back to a single click that increments rotation by 15°
  around the bbox center (state at 3eb02fde). The drag-rotate from
  b50c0d5a streamed live position updates per frame, which the
  floor plan and grid both followed.
- grid: stop chasing the active building's world XZ each frame
  (pre-c00a2469 behaviour). The grid stays anchored at world (0, 0)
  so it doesn't visibly sweep when the building origin moves —
  during a drag, an R/T rotation in the move tool, or a click
  rotate that re-anchors the origin to the bbox-pivot. Y still
  lerps to the active level's floor height.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(editor): consistent building rotation pivot + 2D position sync

Several connected fixes to building rotate/move so the 3D mesh, the
2D floor plan, and alignment guides all stay visually consistent
across rotations and drags.

Rotation-invariant pivot
- New `getBuildingLocalBboxCenter` helper walks descendant mesh
  geometry bounds in the building's LOCAL frame, returning a
  centroid that doesn't depend on the building's current world
  rotation. The previous approach derived the pivot from the world
  AABB at the current rotation, which made the local pivot subtly
  drift each time the building was rotated.
- Floating menu click-rotate and move-tool R/T rotation now seed
  their pivot from this helper, so successive rotations stay
  pinned at the same world point.

Move-tool stability
- R/T rotation always pivots around the current world bbox center,
  even before the cursor has moved onto the grid (previously it
  fell back to rotating around the origin in that case).
- Commit (`onGridClick`) now writes the mesh's actual on-screen
  pose instead of recomputing it from the click event's grid
  coords, removing a one-cell snap on release.

Alignment resolver
- Tie-break is now perp-first, primary-second. The previous order
  was reliable when wall faces shared an exact axis-aligned coord
  (pre-rotation), but post-rotation float deltas meant primary
  never tied and the resolver could lock onto the far corner of a
  candidate. Perp-first picks the visually closest point.

2D ↔ 3D sync
- The floor-plan SVG scene group now applies a live `translate(...)`
  derived from `(committedBuildingPosition → live buildingPosition)`,
  in addition to the existing live rotation. Building-local content
  slides in lockstep with the 3D mesh during a drag.
- The alignment-guide layer projects from world XZ using the
  COMMITTED building position, so guides stay locked to their world
  coordinates while the building slides past them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Merge remote-tracking branch 'origin/main' into fix/fir-5-june

* feat(wall): alt-click commits a single wall instead of chaining

Holding Alt on the click that completes a segment now stops drafting
instead of starting a new segment from the just-placed endpoint. Same
behaviour wired in both the 3D wall tool and the 2D floor-plan
placement hook (which already passed `singleWall: event.altKey` —
the 2D handler was ignoring the flag).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* revert(editor): drop 15° rotate from floating building action menu

Removes the rotate button on the floating building menu and the
`getBuildingLocalBboxCenter` pivot helper that only existed to support
it. The selected-group rotate handle remains the way to rotate a
building.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ifc-converter): point next-env at .next/types instead of .next/dev/types

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(editor): unify 3D move grip with floating-menu Move via tap-to-engage

The translate gizmo on the 3D bounding box now hands the node to the
registry move tool (same path as the floating action menu's Move
button) instead of running its own plane-drag. One move flow — green
bounding box, cursor follow, alignment guides, R/T rotation,
click-to-commit — drives both entry points.

Adds a `dragBounds` capability on the node registry so kinds whose
rendered mesh contains extras (per-level landing assemblies on an
elevator, etc.) can declare the box the user thinks of as "the thing
being dragged." Elevator and stair opt in; everything else still
auto-measures. `MoveRoofTool` now shows the green box for whole-stair
and whole-roof moves, and aligns roofs by their footprint corners
(previously only stairs did). Slab floor-plan moves clear
`autoFromWalls` on commit so space-detection doesn't snap the slab
back, via a new `extraCommitFields` hook on the polygon-centroid
move target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(editor): 2D alignment guides in building-local frame + world-grid wall snap

The 2D floor-plan alignment layer now mounts inside the rotated scene
<g> and reads from an editor-local `useAlignmentGuides` store —
guides are stored in building-local meters and follow the SVG
transform that already carries the rest of the floor-plan geometry.
Pill labels are counter-rotated by the scene rotation so they stay
upright under building rotation. The 3D pipeline keeps its own
world-frame store; the two surfaces no longer share frames.

Wall drafting picks up an optional `gridSnap` override so the 2D
floor-plan can snap drafts onto the world-XZ grid even when the
active building is rotated — without it, a rotated building's
drafted wall would chase the local grid and miss the visible lines.
`snapPointTo45Degrees` and `snapWallDraftPointDetailed` route through
the override when supplied; otherwise the prior local-axis grid snap
is unchanged. The floor-plan panel wires `snapBuildingLocalToWorldGrid`
through the placement hook and exports `WALL_GRID_STEP` to support it.

Also fixes the minor/major grid stroke palette swap so minor lines
render with the minor palette and majors with the major palette.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(editor): drop dead onRotate slot, memoize dragBounds snapshot

Removes the unreachable `onRotate` prop, `RotateCw` import, and
rotate button from `NodeActionMenu` — its only caller was the
floating building menu's 15° rotate, which was removed in d65fd5fe.

Memoizes `MoveRegistryNodeTool`'s `dragBounds` lookup so it captures
the scene snapshot once per node instead of re-reading
`useScene.getState().nodes` on every render. Bounds depend only on
`node` (locked for the tool's lifetime) and start-time sibling state
(elevator shaft height from the level set), so a one-shot snapshot is
the right semantics — and it avoids implying a live subscription that
isn't there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: biome check --write (import order, formatting, unused import)

Auto-fixes from `bun check:fix`:
- import-order normalization in editor/nodes/core
- unused `snapToGrid` / `useLayoutEffect` imports dropped
- minor whitespace/wrapping tweaks

No behavioural change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 11:05:27 -04:00
ce6f999310 arch: enforce layer boundaries — ceiling dispatch, store relocation, shared helper (#382)
* Add roof surface placement support for items

Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.

- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fixed conflict

* editor: per-door-type floor-plan symbols

Render a distinct, static plan symbol for each door type in the
registry floor-plan builder (`packages/nodes/src/door/floorplan.ts`),
independent of the door's live open/close animation:

- single / hinged: fixed 90° swing with a dashed quarter-circle arc
- double / french: two mirrored half-width leaves + dashed arcs
- folding / bifold: static zigzag accordion (~80% span) on the wall face
- sliding: bypass — two overlapping panels on parallel tracks + arrow
- pocket: thin white leaf, ~60% closed, sliding into the solid wall
- barn: surface-mounted panel parked over the wall, dashed closed-ghost
  + slide arrow

The swing arc is dashed in screen-pixel units (the renderer uses
non-scaling-stroke). Symbols are oriented by hingesSide / swingDirection
/ slideDirection as appropriate.

Also includes pre-existing working-tree changes unrelated to the door
symbols: group move/rotate transform and box-select tweaks, and a
regenerated ifc-converter next-env.d.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix recessed ceiling fixtures and draw safety

* feat(editor): magnetic wall-snap with per-kind beacon (2D + 3D)

Snap the wall draft / endpoint-move point onto existing wall geometry —
corners, midpoints, wall–wall intersections, and along-wall edges — and
show a beacon at the snap point whose glyph encodes what it caught
(square = corner, triangle = midpoint, ✕ = intersection, circle = edge).

- Pure snap geometry extracted to wall-snap-geometry.ts (unit-tested).
- Ephemeral useWallSnapIndicator store drives a 3D pillar+glyph beacon
  and a 2D SVG glyph beacon, both indigo to match the alignment guides.
- Gated by a new persisted "Magnetic snap" toggle in the Display menu
  (useEditor); honored by draw + commit + endpoint-move in both views.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* editor: garage and open-doorway floor-plan symbols

Extend the per-door-type plan symbols in the registry floor-plan
builder (packages/nodes/src/door/floorplan.ts):

- open doorway (openingKind === 'opening'): bare gap, no leaf/arc/panel
  (mirrors the 3D system, which renders only the cutout for openings)
- garage sectional: closed leaf + side tracks into the garage + dashed
  parked ghost at the inner end
- garage roll-up: closed leaf + coil barrel (capsule) with a coil hint
- garage tilt-up: closed leaf + dashed parked panel + dashed curved
  up-and-over swing path
- gate the swing arc to actual swing doors (hinged/double/french) so
  other types fall back to the plain footprint

Garage mechanisms sit on the interior (door-local -z) side to match the
3D garage builders, independent of swingDirection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* arch: enforce layer boundaries — registry dispatch, store relocation, shared helper

Three architectural fixes to bring the branch into full compliance:

1. **ceiling-system kind check → CeilingCutCapability**
   Replace `child.type === 'item'` branch in `ceiling-system` with registry
   dispatch. Add `CeilingCutCapability` type to `packages/core` registry types,
   implement `buildCeilingHole` on `itemDefinition`, and rewrite
   `collectRecessedItemHoles` → `collectCeilingHoles` to dispatch through
   `nodeRegistry` — viewer never again inspects a node's kind directly.

2. **useAlignmentGuides + useWallSnapIndicator → packages/editor**
   These stores are editor-only UI (snap beacons, alignment guides). Move them
   from `packages/core/src/store/` to `packages/editor/src/store/`, re-export
   from `packages/editor`, and update all 34 consumer files across
   `packages/editor` and `packages/nodes` to import from `@pascal-app/editor`.

3. **findLevelAncestorId extracted to core**
   `item-light-system` had a private `resolveNodeLevelId` that duplicated
   level-ancestor traversal logic. Extract it as `findLevelAncestorId` in
   `packages/core` (spatial-grid-sync), export it, and replace the local copy.

All four packages typecheck cleanly (zero errors).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: fix lint, untrack .claude/launch.json

Run bun check --write to clear 8 Biome errors (formatting + import order
+ one unused import). Untrack .claude/launch.json and add it plus
.claude/settings.local.json to .gitignore so local IDE/agent configs
stop landing in commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 13:14:39 -04:00
d1b40aa98d editor: level-scoped alignment, reference-floor symbols & registry slab tool (#373)
* feat(editor): level-scoped alignment, reference-floor registry symbols, registry slab tool

- Scope alignment candidates to the active level so a node directly below
  on another floor no longer snaps (alignment is XZ-only); building-scoped
  nodes like elevator shafts stay in the pool across floors.
- Derive the elevator alignment footprint from its outer shaft (not the
  inset cab), so its guide actually surfaces within the snap threshold.
- Extract shared stair footprint geometry (rotateXZ, segment transforms,
  stairFootprintAABB) so opening-sync and alignment anchors derive the
  chain identically; stairs now contribute plan bbox anchors.
- Render reference-floor stairs/roofs/elevators/shelves/spawns through
  their registry floorplan builders for pixel-identical symbols.
- Move slab creation to the registry-driven slab tool (parity with
  ceiling); 2D handlers only maintain draft state.
- Lift the 3D alignment guide ribbon to the active level's Y each frame.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(core): registry-drive elevator/stair alignment footprints

Address architecture review of the level-scoped alignment work: the core
anchor bridge (`alignment-anchors.ts`) hardcoded `if (node.type === 'elevator')`
and `if (node.type === 'stair')` branches to derive their plan footprints.
Move that onto the kinds themselves via a new `alignmentFootprint` capability
so the bridge dispatches generically — matching the registry composition model.

- Add `Capabilities.alignmentFootprint`: returns a `box` (rotatable rect
  centred on position, relocatable for movable kinds) or an `aabb` (already
  resolved, for non-rectangular plan shapes). Elevator uses `box` (its outer
  shaft, and it's movable); stair uses `aabb` (segment chain / annular sector,
  moves by origin).
- Drop both hardcoded branches; the bridge now consults the capability via
  `floorFootprint` (box) and a unified `alignmentAABB` (box ∪ aabb).
- Export `stairFootprintAABB` from core so the stair definition consumes it.
- Tests register synthetic defs carrying the capability (the bridge no longer
  knows elevator/stair by name), reproducing the production glue from the same
  core helpers.
- Document why `REFERENCE_REGISTRY_KINDS` is a deliberate editor-local
  curation, not an auto-derived set (most floorplan-builder kinds shouldn't
  appear as standalone reference symbols, and "reference floor" is an editor
  concept core must not know).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(editor): hide node arrow handles during thumbnail capture

The arrow-handle rig lives on SCENE_LAYER (so its chevrons read as proper
3D plates), but the thumbnail camera only filters EDITOR_LAYER + GRID_LAYER —
so a node selected at capture time would leak its arrows into the snapshot.
Hide the rig on `thumbnail:before-capture` and restore it on
`thumbnail:after-capture`, the same emitter handshake SelectionManager uses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:04:14 -04:00
c00a2469fd editor: in-world selection UI across wall / door / window / stair (#334)
* Add roof surface placement support for items

Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.

- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fixed conflict

* wall: 2D floor-plan move, side arrows, drag-to-move endpoints

Bundles the in-progress wall editing work on this branch:

- Wall corner endpoint drag in 3D (`floating-action-menu.tsx`,
  `wall/move-endpoint-tool.tsx`): press-and-drag on the floating
  endpoint button or the new 3D corner sphere, release to commit.
  Replaces the prior click-to-arm / click-to-place flow.
- New 2D move side arrows on selected walls via a new
  `move-arrow` floor-plan geometry kind (core type + registry-layer
  renderer + wall floor-plan builder emission), mirroring the 3D
  `WallMoveSideHandles`.
- 2D wall body move: new `wallFloorplanMoveTarget` translates the
  moving wall and cascades shared endpoints onto linked walls so
  L-corners stay connected through the drag.
- `MoveWallTool` cleanup gains an external-commit guard so a 2D
  commit doesn't get clobbered by the 3D mover's cleanup restore.
- HMR-safe `bootstrap.ts` no longer re-registers builtin kinds
  whose registry entry survived the closure reset.
- Misc 2D polish: floor-plan auto-fit measures the painted scene
  via `getBBox`, wall dimension offset bumped, swallow-click guard
  in `handleSelect` so registry-driven selection holds through the
  post-pointerdown re-render.

Floor-plan move-target / move-arrow code still carries diagnostic
console logs for the cascade flow; keeping for debug on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* wall: 2D floor-plan move adopts 3D junction planner

2D wall drag now produces the same scene topology as 3D — linked corners
cascade per `planWallMoveJunctions`, off-axis branches stay rectilinear
with a bridge wall inserted between the original and new corner, and
same-direction consumed walls collapse and delete. Previously the 2D
handler did a naive endpoint-stretch cascade with no bridges or
collapses, so dragging an L-corner in 2D vs 3D yielded different scenes.

`FloorplanMoveTargetSession` gains an optional `commit` hook. The
default overlay path snapshots affected nodes and writes a diff back on
release — fine for kinds whose commit is a pure position update, but
insufficient when commit needs to also create or delete nodes. When
`commit` is present, the overlay reverts to baseline, resumes history,
and delegates the atomic write; one Ctrl-Z rolls back the entire
operation including bridge creates and collapsed deletes.

Shared helpers (`planWallMoveJunctions` plan → updates, linked-wall
snapshots, bridge synthesis) lifted to a new `packages/nodes/src/wall/
move-shared.ts` so both the 3D `MoveWallTool` and the 2D
`wallFloorplanMoveTarget` import them. Net -163 LoC after dedup.

Auto-slab live preview and ghost bridge previews mid-drag — visible in
3D today — remain 3D-only; 2D surfaces them at commit time through the
normal scene reactions. Tracked as follow-up.

Also drops three `// temp diagnostic` console.log blocks left over from
the prior wall-move branch (2D setup, 2D canCommit, 3D cleanup).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* door/window: R flips side, E toggles open/close

R previously toggled the open/closed state of operable doors and
operable windows. It now flips the opening's side (front ↔ back,
rotation += π) for both — same gesture as flipping a furniture item
that knows about handedness.

The open/close toggle moved to E, which was unbound for doors and
windows before. T is now a no-op on doors and windows so it doesn't
free-rotate a wall-bound node by π/4 (which made no architectural
sense).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* door/window: keep 2D plan in sync during placement drag

While drafting a door or window across the same host wall, the tool
was bypassing the scene store and mutating the Three.js mesh
directly. That kept 3D snappy but left the 2D floor plan reading the
last committed position — drafts froze in place on the 2D side during
a same-wall drag.

Route same-wall moves back through \`updateNode\` so 2D and 3D both
re-render from a single source. The reparent path (cross-wall drag)
still uses \`updateNode\` with \`parentId\` and \`wallId\` — we only
avoid forwarding those fields when the wall hasn't changed so the
host wall's \`children\` array doesn't churn each tick and trigger a
WebGPU "Vertex buffer slot 0 ... was not set" warning from the
briefly re-rendered placeholder geometry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* floor-plan: live wall-draft measurement + opening placement event order

Two changes to the floor-plan panel:

  1. Length + angle labels render alongside the wall draft in 2D,
     matching the 3D \`WallTool\` feedback. Length sits at the segment
     midpoint with a plate that flips when its on-screen orientation
     would read upside-down; angle arcs anchor at each endpoint that
     meets an existing wall and label the deviation from that wall's
     direction.

  2. The pointer-move handler ran the registry catch-all
     (\`isFloorplanGridInteractionActive\`) before the opening-placement
     branch. Door and window are registered kinds, so during their
     build mode the catch-all emitted \`grid:move\` and returned —
     starving the \`wall:enter\` / \`wall:move\` events the placement
     tools listen for. Reorder so opening placement runs first; the
     wall-build skip in the catch-all is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* wall: pass geometry as prop on move arrow handles

R3F's \`<primitive attach="geometry">\` path emits a \`Draw(0, 1, 0, 0)\`
on the first frame because the host \`<mesh>\` briefly renders with the
default empty \`BufferGeometry\` before the primitive child attaches.
Combined with \`frustumCulled={false}\`, WebGPU flagged "Vertex buffer
slot 0 ... was not set" every time a wall or fence was selected and
the move arrows mounted.

Pass \`arrowGeometry\` as a prop on the \`<mesh>\` so it's never
mounted with the default placeholder. Same fix applied to both the
wall and fence move-arrow handles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ifc-converter: regenerate next-env.d.ts after Next route path move

Next.js moved the generated routes typings from
\`./.next/dev/types/routes.d.ts\` to \`./.next/types/routes.d.ts\` in
the current version pinned by the workspace. Regenerated via
\`next typegen\` so the project compiles against the right path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* move: track which view finalised so split-view cleanups don't race

In split view, both the 2D move overlay and the 3D move tool mount
for the same \`movingNode\` and each captures its own pre-drag
snapshot. When one side finalises (commit or Esc), the other side
unmounts because \`setMovingNode(null)\` propagates — and its effect
cleanup had to *guess* whether the live scene was already-committed
state (skip restore) or its own drag's uncommitted state (revert).

Both cleanups did this via the same heuristic: diff snapshot fields
against current scene state. Cheap, but it conflates "the other side
committed" with "the user's apply() actually changed something" —
and fails outright if a commit happens to land on the same numeric
values as the snapshot.

Replace the heuristic with an explicit \`movingNodeOrigin\` state
field: '2d' | '3d' | null. The finalising side sets its origin
before \`setMovingNode(null)\` runs; the other side's cleanup reads
it. \`movingNodeOrigin\` is preserved across \`setMovingNode(null)\`
(so it's still observable when the cleanup fires) and reset the
next time a non-null \`setMovingNode\` starts a fresh drag.

Wired on the wall move-tool (3D) and \`FloorplanRegistryMoveOverlay\`
(2D) — the two real call sites today. Other 3D move tools can adopt
the same flag incrementally as their own split-view races surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* wall: 2D drag publishes to useLiveNodeOverrides, zustand only on commit

Side-arrow / corner-dot / curve-handle drags in the 2D floor plan now
publish `{ start, end, curveOffset }` to `useLiveNodeOverrides` each
tick instead of writing to `useScene`. WallSystem, the 2D registry
layer, and the wall sidebar all merge the overrides in when reading
endpoints, so the visual + slider preview tracks the cursor while
zustand stays at the pre-drag values until pointer-up. Commit writes
one tracked `applyNodeChanges` (junction-aware) and clears the
overrides; Esc / pointercancel / mid-drag unmount also clear them.

Also bundles the in-progress branch work this depends on:
 - FloorplanAffordanceSession gains optional `commit?()` mirror of the
   move-target hook; the dispatcher reverts → resumes → calls it
   when present (vs. its default snapshot-diff dance).
 - Selected wall body is now pointer-events-inert (polygon
   `pointerEvents: 'none'` + hit-line skipped) so only the arrows /
   endpoint dots / curve dot start a drag.
 - Move button removed from the 2D floating action menu and the wall
   sidebar inspector for walls — redundant with the side-arrows.
 - `useWallMoveGhosts` store + `FloorplanWallMoveGhostLayer` for the
   dashed bridge previews painted mid-drag.
 - WebGPU "Vertex buffer slot 0 ... was not set" fixes on grid +
   guide renderer + wall draft preview by passing geometry as a
   prop (same pattern as wall-move-side-handles).
 - Floor-plan wall-tool fallback: when the 3D wall tool's
   `grid:click` already committed the wall, treat
   `createWallOnCurrentLevel` returning null as "the 3D side handled
   it" and chain the next draft segment instead of clearing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* wall: new in-world selection UI — side arrows, height handle, corner pickers, ground menu

3D affordances for a selected wall, replacing the HTML floating pill:

  - side move arrows: thinner chevron+shaft silhouette (extruded, beveled);
    press-hold-drag-release commits on pointerup (MoveWallTool no longer
    uses grid:click)
  - height arrow above the wall midpoint, drags vertically against a
    camera-facing plane and updates wall.height live; new resizingWallHeight
    state gates camera orbit; commit plays sfx:item-place
  - corner picker per endpoint: billboarded hex disc at floor + dashed
    vertical leader cylinder; pointerdown routes to the existing
    movingWallEndpoint flow (works for 2D and 3D)
  - ground action menu (curve / duplicate / delete): three Lucide SVGs
    rendered as canvas-textured planes lying flat on the floor, anchored
    one wall thickness + clearance outside the camera-facing face; one
    rigid container moves them as a unit (auto-flips sides + rotates with
    the wall, on curved walls uses the t=0.5 curve frame)
  - floating action menu hidden for walls (replaced by the above)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* wall: smooth ground-menu side-flip with hysteresis + lerp

The three floor icons appeared to "move one at a time" when orbiting:
binary side decision flickered on grazing orbits, and the 180° rotation
flip swapped curve/delete across each other while duplicate (offset 0)
stayed put. Now lerps position+rotation toward target with a hysteresis
dead-zone, so the menu swings around the wall as one unit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* wall: raise ground-action menu to 10 cm so icons clear floor textures

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* wall: anchor 2D action menu and 3D height arrow at curve apex

2D menu centres on getWallMidpointHandlePoint and stays horizontal 32 px
above the wall; 3D height arrow uses getWallCurveFrameAt(0.5) so the
apex+tangent match the side handles on curved walls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* door: in-world selection UI — side width arrows, height arrow, ground menu

Side arrows resize width anchored at the opposite edge; top arrow drags
height anchored at the floor. Ground menu mirrors the wall pattern with
move + duplicate + delete icons that flip to the camera side. Handles
portal into the level (not the wall mesh) and wrap in a per-frame
transform mirror so wall hover outline doesn't pick them up.

New viewer flag handleDragging gates node pointer events during in-world
drags; pointerup also swallows the follow-up synthetic click so the
PointerMissedHandler doesn't deselect the active item on commit. Wall
height arrow, wall move arrow, and fence move arrow all opt in.

Scale chevron arrows down to 65 % across wall + door so the family reads
as one. Panel type grids (door, window, column, skylight) get matched
breathing room (px-3 py-2.5, gap-2) so labels stop hugging the borders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* door: 2D width arrows + world-relative move dot, deterministic commits

Side-arrow width drag in the 2D floor plan: doors now emit two width
arrows at the wall-tangent edges when selected, routed through a new
`resize-width` affordance that anchors at the opposite edge, clamps to
wall bounds, and previews per-tick via scene writes so both the floor
plan and the 3D viewer track the drag in real time.

`move-arrow` kind gains optional `affordance` + `payload` so the same
chevron primitive can route to either the move flow (walls) or an
arbitrary affordance (door width-resize) without forking the renderer.

Move-dot for the door is now world-anchored — it scales with zoom in
place of the previous screen-constant size, matching the rest of the
door's chrome.

Both `doorWidthAffordance.commit()` and `doorFloorplanMoveTarget.commit()`
own their atomic final write so the dispatchers take the deterministic
revert → resume → commit path. The diff path was silently reverting
when the post-apply state happened to match the snapshot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* window: in-world selection UI, 2D width arrows, deterministic commits

Bring window 3D + 2D selection chrome to parity with door. Selecting a
window in 3D now emits two side width arrows, top + bottom height arrows
(top anchors at the sill, bottom anchors at the lintel and clamps to the
wall floor), and an in-world action menu that rides just below the bottom
arrow's tip so the column moves with the sill.

2D plan adds two `resize-width` arrows at the start / end edges, routed
through the new `windowWidthAffordance` — same anchored-edge + wall-bounds
clamp + per-tick scene-write preview the door uses.

`windowFloorplanMoveTarget.commit()` is now self-owned: `apply()` snapshots
the last valid placement and `commit()` re-applies it, so the dispatcher
takes the deterministic revert → resume → commit path instead of the diff
path that silently reverts when the post-apply state happens to match
the snapshot. Mirrors the door fix.

The HTML floating-action-menu skips windows now that the in-world ground
menu owns those actions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* stair: in-world selection UI — side/length/height arrows, ground menu

Bring stair-segment selection chrome to parity with wall / door / window.
Selecting a stair segment in 3D now emits two side width arrows (each
slides the opposite edge anchor under the user), a length arrow at the
back face that extends the run, and — for stair-type segments — a height
arrow on top. A ground action menu (duplicate / delete) sits beside the
segment and flips sides as the camera orbits, with hysteresis + lerp so
it doesn't dither.

The handles portal into the stair's PARENT object (level / building / scene
root) rather than the stair group itself: StairRenderer attaches
`useNodeEvents` to the stair group, so any descendant pointer-over would
bubble up and set `hoveredId = stairId`, which then makes the post-processing
outline traverse the entire stair group and stroke our icons. Mirrors the
door fix. A two-layer transform mirror (`stairPoseRef` + `segmentPoseRef`)
keeps the handles aligned with the chained per-segment pose that
StairSystem writes imperatively each frame.

Duplicate forces `attachmentSide: 'front'` on the clone so it continues the
chain end cleanly instead of inheriting the original's side and U-turning.

New `resizingStairSegment{Width,Length,Height}` editor state lets
`CustomCameraControls` suppress orbit/zoom while an arrow is dragging,
matching the wall/door/window handle pattern.

The HTML floating-action-menu skips stair-segments now that the in-world
ground menu owns those actions.

Stair-segment panel swaps its bespoke fill-to-floor toggle for the shared
`ToggleControl` so it looks like the other panels and groups with the
thickness slider under one `space-y-3` block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* stair: 2D move-revert fix, parent ground menu, curved/spiral in-world arrows

- Route stair 2D moves through `floorplanMoveTarget` and honor
  `movingNodeOrigin === '2d'` in `MoveRoofTool` cleanup so the 3D
  tool's restore-from-snapshot no longer stomps the 2D commit.
- Parent stair selection shows an in-world ground action menu
  (move / duplicate / delete) anchored beside the stair; the
  screen-space floating menu is suppressed for `type === 'stair'`
  to match door / window / segment.
- Curved & spiral stairs gain in-world resize arrows: rise (centered
  on the pillar for spirals), width, inner radius, and two sweep
  handles (one per arc end) clustered beside the width arrow.
- Camera controls pause during curved-stair drags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* editor: reuse measurement-bar geometry; fix two post-merge dangling refs

MeasurementBar was building a fresh BoxGeometry per render for every wall
measurement bar, which the WebGPU backend flagged ("Vertex buffer slot N
... was not set") when walls moved. Hoist a unit cube and scale it instead.

Two refs left dangling after the main-branch merge resolved its conflicts
on GitHub:

- floorplan-panel.tsx referenced a `theme` variable that no longer
  exists; the file already derives `isDark` from `getSceneTheme(state.
  sceneTheme).appearance === 'dark'` higher up. Use that.
- grid.tsx applied `EDITOR_LAYER` but only imports `GRID_LAYER` (the new
  dedicated grid layer). Use the imported one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* editor: pin building bbox center to cursor during move

The floating drag button anchors at the building's bbox center, but the
move tool was teleporting the building's origin to the cursor — so the
moment a drag started the building jumped by `bbox_center - origin`.

Capture the local-space offset from origin to bbox center at mount and
apply it on every grid move, grid click, and R/T rotation, so the bbox
center stays pinned to the cursor through the whole drag. Also seed the
cursor sphere at the bbox center instead of the origin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* stair: 2D resize affordances + dispose curved/spiral geometry on swap

Floorplan view: emit `move-arrow` children alongside the existing chrome,
mirroring the in-world arrows on selected stairs:
  - straight: per-segment side (left/right width) and front (length)
  - curved & spiral: width, inner-radius, and two sweep-end arrows
Hidden during placement so they don't fight the cursor follow.

Stroke widths on curved/spiral chrome converted to screen pixels (paired
with `non-scaling-stroke`); the old world-metre values rendered as
sub-pixel at every zoom. First step line is now also emphasised on
curved stairs to match legacy chrome. Skip the straight-only
direction-arrow polyline for curved/spiral — the arc-aligned arrow above
already conveys "up" and `buildFloorplanStairArrow` produces a malformed
polyline once the chain is wrapped around an arc.

Renderer: extract `SpiralColumnMesh` and `SpiralStepSupportMesh` and add
the same prop-+-dispose pattern used by `CurvedStepMesh` /
guide/renderer.tsx. Without disposing the prior BufferGeometry on each
resize tick, WebGPU keeps a stale pipeline reference and flags
"Vertex buffer slot 0 ... was not set" mid-drag on Lambert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* editor: revert in-world ground menus to HTML floating; align grid to building

- Wall/door/window/stair/stair-segment selection menus return to the
  shared HTML floating menu; remove the in-world ground icons, SVG
  textures, hysteresis/lerp constants, and unused imports across
  wall/door/window/stair-segment handle files.
- Drop Move from the floating menu (the in-world side arrows cover it);
  delete the now-unused handleMove.
- Floating menu scales with camera zoom (ortho.zoom or 1/distance),
  clamped at MIN 0.5 / MAX 1 so zoom-in keeps the default pixel size
  and zoom-out shrinks to a readable floor.
- Per-type y-offsets tuned: wall 0.5, opening 0.6, landing 0.5,
  flight 0.75, parent stair 0.2, structural 0.4, default 0.05.
- Align wall/fence arrow materials with the door/window pattern
  (depthTest/depthWrite false, transparent: true) so they render on
  top of geometry consistently.
- Grid cellSize now follows `gridSnapStep` via a small `SnapAwareGrid`
  wrapper, and the grid mesh anchors its world XZ to the active
  building's mesh — snapped wall endpoints (in building-local coords)
  now fall on visible grid lines instead of mid-cell.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* handles: registry-driven in-world resize arrows

Adds a `handles?: HandleDescriptor[] | (node) => HandleDescriptor[]`
field to `NodeDefinition` so each kind declares its in-world resize
affordances as pure data instead of shipping a bespoke React component.

- New `packages/core/src/registry/handles.ts` exposes a discriminated
  union: `linear-resize` (axis + center/min/max anchor), `radial-resize`
  (1:1 outward growth), plus stubs for `arc-resize` and `endpoint-move`
  for follow-up migrations.
- New `packages/editor/src/components/editor/node-arrow-handles.tsx`
  reads `def.handles`, mounts arrows with shared drag plumbing (raycast
  plane, NDC, pointer listeners, SFX, history pause, handle-dragging
  guard). Portal modes: `'parent'` (column-like, single wrapper rides
  self pose) and `'grandparent'` (door/window-like, outer wrapper rides
  parent pose + inner group rides self pose so handles escape the
  parent's selection-outline traversal). `apply` receives the
  node-at-drag-start so edge-anchored resizes (door width re-centers
  position) compute their fixed anchor from pre-drag state.
- Migrate column, door, window, stair-segment. Old per-kind handle
  files (`column-side-handles.tsx`, `door-side-handles.tsx`,
  `window-side-handles.tsx`) removed; `stair-segment-handles.tsx`
  retains `StairHandles` (parent stair curved/spiral arrows) pending
  the `arc-resize` migration.
- Column: height + crossSection-aware footprint (radius / uniform
  width=depth / independent width+depth / brace width+depth for
  non-vertical supports).
- Door / window: edge-anchored width (left + right) with wall-length
  max bound; bottom-anchored height (door) / top + bottom edges
  (window).
- Stair-segment: width (chain auto-centers), length anchored at chain
  start, height for step flights only (landings skip it).

Wall and parent-stair curved/spiral arrows stay on legacy components
for now — they need `endpoint-move` + `arc-resize` descriptor variants
and rotated-axis projection, which are their own focused sessions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* handles: migrate wall + parent stair to registry; add arc-resize

Closes the wall + parent-stair gap on the registry-driven handle
migration. Net −1177 lines (the per-kind handle files were 1500+
lines of duplicated drag plumbing; their replacements are ~50-line
config blocks on each NodeDefinition).

- `arc-resize` reworked to take a raw `delta` (radians) instead of
  `newValue` so two-field writes like curved-stair sweep (which
  updates `sweepAngle` AND `rotation` together to keep the
  non-dragged edge world-fixed) stay in the descriptor without
  awkward inverse-currentValue gymnastics. `currentValue` removed
  from arc-resize for the same reason — applies own their math.
- New `ArcArrow` renderer in `node-arrow-handles.tsx`: raycasts a
  horizontal drag plane at the arrow's Y, measures the signed angle
  delta around the node's local origin (atan2 in world XZ,
  normalised to [-π, π] so wraparound doesn't flip mid-gesture),
  hands the delta to `descriptor.apply` along with the initial node.
- Wall: height arrow migrated (linear-resize axis='y' anchor='min',
  placement uses curve apex for curved walls, chord midpoint for
  straight). Side-move arrows + corner pickers stay on the legacy
  `wall-move-side-handles.tsx` because they're tap-to-engage-mode
  affordances (move whole wall / move endpoint), not drag-resize —
  modelling them in the registry needs an editor-action descriptor
  variant which is a follow-up.
- Parent stair: curved + spiral stairs declare 5 handles
  — rise (linear-resize axis='y' anchor='min'),
  width (linear-resize axis='x' anchor='min'),
  inner-radius (linear-resize that also writes width to keep outer
  rim fixed), and sweep start / end (arc-resize variants writing
  sweepAngle + rotation). Straight stairs declare nothing — their
  segment children own resize.
- Old `stair-segment-handles.tsx` (1405 lines) deleted; all its
  arrows now flow through the registry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* handles: tap-action descriptor + EditorApi; finish wall + fence migration

Closes the final gap in the registry-driven handle migration. Wall side-
move + corner pickers + fence side-move were the last legacy handles
because they're click-to-engage-mode affordances (hand the node to its
move tool / start an endpoint drag), not drag-resize — `apply(node,
value, sceneApi)` had no path to editor state.

- New `EditorApi` interface in core (alongside `SceneApi`) exposes
  `engageMove(node)` + `engageEndpointMove(node, endpoint)`. Concrete
  implementation in `packages/editor/src/lib/editor-api.ts` casts
  through `useEditor`'s setters so the descriptor layer never imports
  editor internals.
- New `TapActionHandle` descriptor variant: `placement` + `onActivate
  (node, sceneApi, editorApi)`. `shape` field picks the visual —
  defaults to the chevron arrow; `'corner-picker'` renders the dashed
  vertical leader + billboarded hex disc + ring (sized to
  `nodeHeight(node)`).
- `TapActionArrow` renderer in `node-arrow-handles.tsx` wires up
  pointer-down → descriptor.onActivate. Pulled the chevron and corner
  visuals into `ArrowShape` / `CornerPickerShape` building blocks so
  future shapes can be added without touching the descriptor union.
- Wall: front/back side-move (engageMove) + start/end corner pickers
  (engageEndpointMove). Joined by the existing height arrow on the
  same `def.handles` list. Old `wall-move-side-handles.tsx` (600
  lines) deleted — wall now has zero per-kind handle component.
- Fence: front/back side-move. The bespoke endpoint move buttons in
  the floating menu stay until they migrate to a tap-action too.

Net for this commit: -620 +513. Combined with the prior two
migration commits: -2287 +912 across the full registry migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* wall: revert side-move arrows + corner pickers to legacy component

The registry-driven tap-action path didn't render the four non-height
wall handles, even with descriptors resolved and the wall mesh in
sceneRegistry. Fence uses the same descriptor shape and renders fine,
so the bug is wall-specific and not in the descriptor layer itself —
left for a real diagnosis later.

Restored the pre-5756f241 wall-move-side-handles.tsx (height arrow +
front/back side-move + start/end corner leaders, 753 lines) and
mounted it next to NodeArrowHandles in editor/index.tsx. Dropped the
def.handles field on wallDefinition so the two paths don't race.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* editor: level naming helper + ambient floorplan render during building moves

Two unrelated WIP fixes bundled:

- Level names: extract \`getDefaultLevelName(n)\` /
  \`getLevelDisplayName(level)\` into \`packages/editor/src/lib/level-name.ts\`
  and swap in across rename inputs, command palette, floating selector,
  site panel, level-tree node, level-duplicate dialog, view toggles, and
  viewer-overlay breadcrumb. Default labels now read "Ground Floor" /
  "Floor N" / "Basement N" instead of the bare "Level N" string each
  caller was concatenating itself.

- Building-move ambient floorplan: when a building is selected (or
  mid-move) without an explicit level, FloorplanRegistryLayer falls
  back to that building's level 0 (or lowest level) and renders it
  dimmed + non-interactive so the floor stays visible as context
  instead of disappearing. FloorplanPanel allows the SVG to mount in
  that case. MoveBuildingTool publishes per-frame pose to
  useLiveTransforms so the floor-plan follows the drag without
  reading from the Three.js mesh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* handles: 3D resize arrows publish to useLiveNodeOverrides, commit on release

Door / window / wall height-arrow drags now stage the patch in
\`useLiveNodeOverrides\` each frame and write to zustand exactly once on
pointerup. The kind's system reads via \`getEffectiveNode\` and rebuilds
the mesh imperatively, so the React tree never re-renders mid-drag and
undo isn't polluted by per-frame writes.

- \`packages/core\`: shared \`getEffectiveNode<T>(node)\` helper exported
  from \`@pascal-app/core\`; spreads any override fields onto the input,
  returns it unchanged when none. Replaces the inline merge wall-system
  had as \`getEffectiveWall\`.

- \`DoorSystem\` / \`WindowSystem\`: subscribe to
  \`useLiveNodeOverrides.overrides\` (so override-only ticks re-run the
  component and pick up the latest dirtyNodes), merge via
  \`getEffectiveNode\` before \`updateXMesh\`. Parent-wall dirty cascade
  uses the effective node's parentId.

- \`WallSystem.updateWallGeometry\`: door / window children are merged
  through \`getEffectiveNode\` before being passed to
  \`generateExtrudedWall\`, so cutouts track the in-flight resize.

- \`LinearArrow\` (registry handle): onMove → override + markDirty;
  onUp → one tracked \`sceneApi.update(lastPatch)\` + clear; onCancel →
  clear + markDirty to revert geometry.

- Legacy \`WallHeightArrowHandle\` in wall-move-side-handles.tsx
  switched to the same pattern (was the only inline-drag handle in
  that file — side-move + corner pickers hand off to other tools).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* floating menu: enable Move icon for wall / door / window

Widens the \`onMove\` gate on \`NodeActionMenu\` so wall, door, and
window join column in showing the Move chevron. \`handleMove\` calls
\`setMovingNode(node)\` which dispatches through the existing
\`affordanceTools.move\` path on each kind's definition (already
present for all three).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* handles: live drag, guide rings, dimension chips; floating-menu Move on stairs

- Window bottom height arrow: flip Z rotation so chevron points down
  when placement Y < 0. Door / column height arrows unaffected (still
  above the node).
- Floating menu: raise stair-segment offsets (segmentType is 'stair' |
  'landing', so the legacy 'stair-flight' key was dead); enable the
  Move icon for parent stair and stair-segment.
- HandleDecoration on LinearResizeHandle + RadialResizeHandle. Generic
  GuideRing renders at node-local (0, y, 0) in the XZ plane when the
  arrow is hovered or dragging. Curved/spiral stair width arrow gets
  an outer rim ring, inner-radius arrow gets an inner pillar ring,
  and column radius arrow gets a footprint ring on round / octagonal /
  sixteen-sided shafts.
- ArcArrow migrated to the live-override pattern (sweepAngle +
  rotation). NodeArrowHandles subscribes to useLiveNodeOverrides for
  the selected node and merges into the effective node, so arrow
  positions, decorations, and dimension chips all track the in-flight
  drag instead of freezing at pre-drag values.
- StairRenderer and ColumnRenderer subscribe narrowly to their own
  override entry and render against the merged effective node, so the
  curved/spiral mesh and the column body update per pointer move
  without zustand churn.
- DimensionLabel chip (<Html>) rendered next to every linear-resize /
  radial-resize arrow on hover or drag. Format follows the wall /
  fence label recipe (metric / imperial via useViewer.unit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fence + column: registry handles, brace spread arrows, per-style defaults

- Fence: side-move arrows already on the registry path; add the height
  arrow (axis 'y' linear-resize, anchor min) + start/end corner pickers
  (tap-action, shape 'corner-picker' with dashed leader + billboarded
  hex). Move icon enabled on the floating menu; the menu floats above
  the height arrow via a fence-specific MENU_Y_OFFSET. Endpoint move
  buttons + Alt-detach plumbing removed from the floating menu — corner
  pickers cover that flow. Legacy wall-move-side-handles.tsx no longer
  branches into fence (dedupes the side-move arrows that were stacked).
- Column: bottom + top spread arrows for non-vertical supports — anchor
  'center' so dragging the right leg outward grows the full leg-to-leg
  span symmetrically. Conditionally added per supportStyle:
    - a-frame:  both bottom and top spreads
    - y-frame / v-frame: top spread only
  Per-style preset map applied on supportStyle switch (panel.tsx) so
  every style snaps to its renderer's natural proportions (defaults
  lifted from each support's fall-through expressions); a customised
  A-frame switched to Y-frame no longer carries its 1.4 m bottom into
  state, and an X-brace gets equal parallel legs rather than inheriting
  A-frame's pinched 0.12 m top.
- GeometrySystem: merge `getEffectiveNode(node)` before calling
  `def.geometry`. Smooths drags for every kind on the parametric path
  (fence, shelf, item, anything that ships `def.geometry`): live
  override mutates the mesh per pointer move, zustand only hears the
  commit. Mirrors WallSystem / DoorSystem / WindowSystem / StairRenderer
  / ColumnRenderer hookups.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* elevator + column: registry handles, rotation gizmo with curved arrow

- Elevator: width / depth / cab-height arrows on the registry path
  (anchor='center' for width/depth so dragging outward grows the full
  span symmetrically; anchor='min' for cab-height with shaftTopY
  resolved through `resolveElevatorLevels` so the arrow lands above
  the full shaft on multi-level elevators, not just the cab top).
  Floating-menu Move icon enabled + a fence-style MENU_Y_OFFSET so
  the menu floats above the height arrow.
- Whole-node rotation gizmo for both elevator and column. Uses
  arc-resize with `shape: 'rotate'` + a new `decoration` ring on
  ArcResizeHandle. Curved-arrow geometry is a two-headed icon (arc
  ribbon with chevron wings + tangential tip at each end), rendered
  in node-local XZ plane at mid-height. Guide ring traces the rotation
  circle (footprint-diagonal + small offset) on hover or drag.
  Position offsets along +Z only — sticks out the front of the node
  instead of diagonally at the corner. apply() negates the cursor
  angular delta (atan2(z,x) is opposite-handed from three.js Ry) so
  dragging CCW around the node rotates the node CCW.
- ArcArrow renderer extended: tracks `isDragging` like LinearArrow,
  renders the optional ring decoration, and swaps geometry between
  the chevron (default, used by stair-sweep handles) and the new
  curved-arrow shape when `shape: 'rotate'` is set.
- HandlePlacement.position / .rotationY now optionally take a
  `sceneApi` so descriptors that depend on cross-node state (the
  elevator's level-chain resolution) can compute placement against
  the live scene. SceneApi gains a `nodes()` accessor returning the
  full record. Test stubs in core (relations-resolver, drag-session,
  hosting) updated to satisfy the new shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* slab + ceiling + shelf: registry handles, live polygon preview, cursor polish

- slab: per-edge resize chevrons in PolygonEditor (gated on
  `allowEdgeMove`, so site / zone editors are unaffected), height arrow
  via `def.handles`, and a floating-menu Move icon. Polygon drags now
  publish the in-flight polygon to `useLiveNodeOverrides` through a new
  `onPolygonPreview` prop; GeometrySystem rebuilds the slab mesh at
  pointer rate while the store stays untouched until the single commit
  on release. Hole editor wired the same way. Handle materials switched
  meshStandard → meshBasic so the blue corner / green midpoint cylinders
  read true colour instead of dimming in scene lighting.
- ceiling: same Move icon, per-edge arrows, height arrow, and live
  preview through the boundary + hole editors. CeilingSystem now merges
  via `getEffectiveNode`, so polygon and height overrides flow through
  on every dirty tick. Height arrow placement is mesh-local (not
  `height + offset`) because CeilingSystem parks `mesh.position.y`
  on the height value.
- shelf: width / depth / height arrows + a curved rotation gizmo with
  ring decoration. Move icon on the floating menu. Shelf stores
  rotation as a tuple, so the rotate `apply` reads back `[x, y, z]` and
  only mutates `y`.
- LinearArrow: snapshot `rideObject.matrixWorld.invert()` at drag-start
  and reuse it in `onMove`. Kinds that park `mesh.position` on the
  field being dragged (ceiling `height`) used to chase a moving ride
  frame, so the local-Y delta collapsed and the value stalled / jittered.
- ArcArrow: cursor is `'grab'` on hover and `'grabbing'` during the
  drag (was the misleading `'ew-resize'`); the `Cursor` type gains
  those two members.
- ParametricNodeRenderer: merge `useLiveNodeOverrides` for position +
  rotation so the rotation gizmo shows live motion through the outer
  group — GeometrySystem already covered geometry-affecting fields.
- floating menu offsets: slab 0.4 → 0.7, ceiling 0.4 → 1.0, shelf 0.6,
  so the menu floats above each kind's new height arrow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* roof-segment: registry handles, live override path, analytical-normal fix

- roof-segment: width / depth / wall-height / pitch / rotation arrows; pitch
  drag back-solves the angle from peak-height via the slope frame
- roof-system: getEffectiveNode + useLiveNodeOverrides so drags rebuild
  the segment + merged shell live, commit-on-release stays a single write
- floating menu: Move icon for roof-segment; uniform EXTRA_MENU_LIFT
- skylight / solar-panel / box-vent ghost: fix analytical normal — shed
  sign flip, mansard / dutch +X face direction, gambrel + mansard tier
  awareness; one (dx·tan, 1, dz·tan) formula across all roof types

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* floorplan: resize/rotate arrows for column, shelf, elevator, fence; rotation handedness fix

- Column / shelf / elevator: per-cross-section resize arrows in the 2D
  floor plan, matching the 3D handle set (width / depth / uniform /
  radius / brace dims), plus a corner rotate-arrow. Body move stays on
  the move-handle dot via the registry overlay's generic translate.
- Fence: floor-plan curve sagitta handle + side move-arrows + a
  body-move target (`fenceFloorplanMoveTarget`) with linked-fence
  endpoint cascade and ALT-detach. Commit strips `isNew` metadata and
  re-selects so the chrome stays visible at the new position.
- Roof-segment: floor-plan resize + rotate arrows wired through new
  affordances; `resolveSegmentFrame` aligns with the builder's
  transform so handles stay glued to the rendered footprint.
- Stair: in-world rotate gizmo bow orientation derived from the
  gizmo's position (was a stray `-π/4` that read as "pointing outward"
  on the spiral). StairSystem now merges the live override before the
  slab-elevation spatial query, so dragging the rotate gizmo no longer
  drops the group's Y when a segment swings off its pre-drag footprint.
- Rotation handedness: floor-plan now plots column / shelf /
  roof-segment at `-rotation` so SVG's CW-with-y-down `rotate`
  visually matches Three.js Y-rotation (CCW from top-down). Same
  `rotation` value rotates the same direction in both views, and the
  same cursor gesture writes the same sign — `- delta` in every rotate
  affordance, lined up with the 3D handles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* registry: floorplanScope + movable capability; cursor focus under mouse

- Registry: duplicate-kind throws in production, warns in dev (HMR).
  New `kindsWithFloorplanScope('building')` and `isRegistryMovable`
  helpers; `resolveBuildingForLevel` extracted into spatial-grid-sync.
- Floorplan registry layer: building-scoped kinds (elevator today) now
  dispatched via `def.floorplanScope === 'building'` instead of a
  hardcoded `node.type === 'elevator'` arm.
- FloatingActionMenu: Move button gated by `isRegistryMovable(kind)`,
  replacing the 13-arm `node?.type === '…'` chain so adding a movable
  kind no longer touches this file.
- 2D cursor indicator: render at the raw mouse position in all modes
  (drop the snapped `cursorAnchorPosition` machinery) so the badge
  always sits under the cursor.
- 3D grid reveal ring: the shader's `positionLocal.xy` is in
  grid-mesh-local space, but the cursor uniform was in world coords —
  so the ring drifted by the building's world XZ. Store the last world
  cursor and re-derive the local uniform every frame after the mesh's
  XZ lerp, so the ring stays locked under the mouse including during
  the catch-up frames after a building rotation commits.
- Roof system: tighten the merged-shell filter's type predicate so TS
  narrows `n` before `hasSegmentMaterialOverride(n)`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* wall: corner billboard, perpendicular grid snap, drop 45° from move/draft

- Corner picker discs (3D move + wall corner leader) now solve
  `parentWorld⁻¹ · cameraWorld` so they face the camera even when an
  ancestor building/level has a rotation; the old `camera.quaternion`
  copy silently broke under any parent rotation.
- Side-handle wall move snaps the wall centre's *absolute* perpendicular
  projection to grid lines, so axis-aligned walls land on real grid
  positions regardless of where they started.
- Wall draft + endpoint move (3D and 2D) drop the 45°-from-start angle
  snap. It was useful for picking a direction during the very first
  draft, but during a perpendicular endpoint drag it pulls the cursor
  onto a 45° ray from the fixed corner instead of tracking the grid.
- Shift now selects the fine grid step (`WALL_FINE_GRID_STEP = 0.05`)
  for precision placement in every wall snap call site, replacing the
  former "Shift = bypass angle snap" semantics with a consistent
  "Shift = finer snap" convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fence: drop 45° angle snap from draft + endpoint move, Shift = fine step

Mirrors the wall convention shipped in e89a8228:

- `snapFenceDraftPoint` gains an optional `step` override.
- Fence draft (3D `tool.tsx` and 2D `floorplan-panel.tsx`,
  `use-floorplan-background-placement.ts`) snaps to the active grid
  step only — no 45°-from-start snap. Shift switches to
  `WALL_FINE_GRID_STEP` for precision placement.
- Fence endpoint move (3D `actions/move-endpoint.ts` and 2D
  `floorplan-affordances.ts`) drops `start`/`angleSnap` so a
  perpendicular drag tracks the grid instead of pulling onto a 45°
  ray from the fixed endpoint. Shift switches to the fine step.

Also fixes the matching wall click path in
`use-floorplan-background-placement.ts:215` that was missed in
e89a8228, plus its locally-injected `snapWallDraftPoint` signature.

Side-handle perpendicular slide (`fence/move-tool.tsx`) was already
grid-snap-only without 45°, so it's untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* floorplan menu: show Move button for selected walls

Removes the `node.type !== 'wall'` exclusion that hid the Move icon
on the 2D floor-plan floating menu for walls. The dispatcher already
has a working path for walls — `def.affordanceTools.move` routes to
`MoveWallTool` (perpendicular slide + linked-wall cascade) — so the
menu just needs to expose the button.

The original opt-out called the menu entry "redundant" because walls
also have side-arrow handles, but the user wants the same icon walls
get the same affordance as every other selected element in the
floating menu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* floorplan wall move: axis-lock to wall normal, match 3D MoveWallTool

The 2D `wallFloorplanMoveTarget` was applying the raw cursor delta in
XZ, so dragging a selected wall in the floor plan let it free-float
sideways and lengthwise. The 3D `MoveWallTool` constrains the same
drag to the wall's perpendicular axis (sideways slide only) — this
brings the 2D path into parity.

- Captures the wall's centre and the `getPerpendicularWallMoveAxis`
  normal at session start.
- Each tick, projects `originalCentre + rawDelta` onto the axis,
  snaps that absolute scalar to the active grid step, and translates
  the wall by `axis * perpDelta`. Same math as the 3D tool.
- Shift bypasses snap (raw projection), matching the 3D convention.
- Degenerate zero-length walls fall back to free XZ motion (rare;
  they're already destined for deletion via the junction planner).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* floorplan move overlay: commit at last pointermove, not pointer-up

The overlay used to re-run \`session.apply\` with the pointer-up
coordinates before committing, on the assumption that pointer-up
might fire without a preceding pointermove. Side effect: when the
pointer-up coord crossed a grid-snap boundary relative to the last
pointermove, the snap flipped to a different cell and the moved node
visibly jumped at release from where the drag had painted it.

Trust the last pointermove instead — modern browsers reliably emit a
final pointermove right before pointerup, and "what you saw is what
gets committed" is the UX users expect. The previous sub-pixel
drift fix loses to the visible boundary-jump it caused.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* floorplan: door wall-hit placement, fence/stair move fixes, wall auto-ceiling sync

- Door / window placement: registry layer entries no longer swallow
  pointer events while a door / window tool is active, so clicking
  ON a wall now triggers placement (previously only clicks NEAR a
  wall worked — the wall's registry-entry `<g>` was stopping the
  pointer event before it reached the SVG background handler that
  emits `wall:click`).
- Fence floor-plan move: 3D `MoveFenceTool` now respects
  `movingNodeOrigin === '2d'` on unmount. Without the guard, the 2D
  overlay's commit would call `setMovingNode(null)`, unmounting the
  3D tool, whose cleanup then ran `restoreOriginal()` and reverted
  the just-committed positions — the "fence reverts on commit"
  symptom. Mirrors the wall move-tool's existing guard.
- Stair floor-plan move: anchored, delta-based motion (was
  position-jumps-to-snapped-cursor), and reads `getWallGridStep()`
  instead of hard-coded 0.5 so the stair snaps to the editor's
  current grid step in real time. Matches the 3D `MoveRegistryNode`
  commit position.
- Stair segment length arrow: drop the placement `rotationY` —
  `axis: 'z'` already auto-rotates the chevron by `-π/2`, stacking
  another `-π/2` spun the tip to `-X` (sideways) instead of `+Z`
  (forward off the run). Matches shelf / roof-segment.
- Stair segment system: merge `useLiveNodeOverrides` when rebuilding
  geometry, chain transforms, merged mesh, and slab elevation, so
  width / length / height drags show the live value on the mesh and
  the store only gets the final tracked write on commit.
- Stair length arrow position: offset 0.06 m past the front edge so
  the head clears the stair fill and reads as pointing forward off
  the run rather than lying across the edge.
- Stair default railing mode: `'both'` for new placements (was
  `'right'`).
- Wall floor-plan move: anchored at first cursor sample (was raw
  centre) so the floating-menu drag-icon offset doesn't jump the
  wall to a different snap cell on grab.
- Wall move: live auto-slab + auto-ceiling preview via
  `useLiveNodeOverrides`. The store stays at pre-drag values during
  the drag; commit writes the final plan in one atomic
  `applyNodeChanges` (creates / updates / deletes deferred from
  per-tick to commit so undo rolls the whole topology change back
  in one step). Adds `planAutoCeilingsForLevel` + `AutoCeilingSyncPlan`
  exports mirroring the existing auto-slab planner.
- Wall draft: expose `WALL_ENDPOINT_SNAP_RADIUS` (0.7 m) for
  endpoint snap intent — strongest user intent (closing polygons,
  attaching to corners) wants a more generous radius than the
  generic join snap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* open-pr skill: refresh existing PR description instead of bailing

Adds a 3b branch to the open-pr skill: when gh pr view finds an
existing PR, regenerate the body from current branch commits/diff
while preserving Screenshots verbatim and the user's checklist
tick state, then apply via gh pr edit. Previously the skill would
print the URL and exit, leaving stale descriptions on long-lived
feature branches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* floorplan: capability/hook dispatch, generic resize + endpoint state

Removes per-kind `node.type ===` arms from the floorplan layer
(door/window opening placement, wall live-override merge, building
ambient context) in favour of new NodeDefinition capabilities and
hooks. Collapses 12 `resizing*` editor-store fields into one
`activeHandleDrag`, the wall/fence endpoint-move dispatch into a
kind-keyed table, and renames now-shared wall utilities to
segment-generic names.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 3d wall measurement: hide label for selected walls

Item measurements still render; the wall branch is left in place so
re-enabling is a one-line gate flip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* editor: set EDITOR_LAYER on arrow handles to exclude from thumbnails

Arrow meshes portaled into the 3D scene were missing EDITOR_LAYER, so
ThumbnailGenerator's camera (which calls cam.layers.disable(EDITOR_LAYER))
would render selection handles into captures.

Add a useEffect in NodeArrowHandlesForNode that traverses the portal root
group and sets EDITOR_LAYER on every child. The effect re-runs whenever
descriptors change so newly created meshes (e.g. when handle count changes
for the same selected node) get the layer tag immediately.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: open-pascal <open@pascal.app>
2026-05-27 14:06:06 -04:00
f5ff111708 fix(ifc-converter): broken Contributions GitHub URL (#335)
apps/ifc-converter/app/page.tsx pointed at
github.com/pascalorg/editor/apps/ifc-converter which 404s. The
canonical path includes /tree/main/. Same URL is reused by the
forthcoming community /ifc page (in pascalorg/private-editor) so
keeping them consistent matters.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:58:01 -04:00
Wassim SAMADandClaude Opus 4.7 0df51219d0 feat: IFC → Pascal converter (package + app)
Brings the IFC-to-Pascal converter into the monorepo as a pure-logic
package plus a Next.js app, replacing the standalone repo that consumed
published @pascal-app/* packages (and drifted from their schemas).

packages/ifc-converter — pure conversion. Parses IFC via web-ifc and
maps elements onto @pascal-app/core node schemas (workspace-linked, so
no more version drift). Builds doors/windows, walls, slabs, columns,
roofs, stairs, sites/buildings/levels. Validates each node via the real
Zod schemas at build time (tryParse) and strips undefined metadata.

apps/ifc-converter — the UI: drop zone, example picker, element search,
JSON download, and a 3D preview rendered through the real
@pascal-app/viewer (registry bootstrap + read-only scene) with a custom
toolbar (camera/level/wall/grid/theme), level selector with
camera-focus, auto-fit, and selection bridged to an inspector.

Conversion specifics worth noting:
- Door/window vertical centering (height/2; windows + sill).
- Nearest-wall hosting fallback for files lacking IFCRELFILLSELEMENT,
  preferring walls long enough to contain the opening and clamping the
  along-wall position so cutouts can't overflow and break wall CSG.
- Plain IFCWALL (Brep/mapped geometry) falls back to default
  height/thickness instead of collapsing to zero-height slivers.
- Columns convert as plain structural shafts (no decorative
  base/capital) sized from the IFC profile.
- Beams + items are skipped for now (no Pascal beam type; items need a
  catalog asset) — counted in the conversion summary.

Large example IFCs are fetched from a public bucket at runtime; the four
small ones are committed. web-ifc.wasm is copied into public/ on
install/dev/build. README flags early-alpha + invites contributions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:45:12 -04:00