* feat(editor): group R/T — keyboard rotate for multi-selections
Extract the rigid rotate/translate math from the group gizmos into
group-transform-shared (rotateGroupPatches / translateGroupPatches) and
add a keyboard group-rotate path: R/T on a multi-selection steps the
whole group ±45° around its bbox center, welded junctions and
connected-component expansion included, committed as one updateNodes
batch (one undo step). Single-selection R/T arms (reference, door/window
flip, registry keyboardActions) are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(editor): polygon participant kind — slabs/ceilings/zones join group transforms
classifyParticipant learns a 'polygon' kind ([x,z] vertex arrays + optional
hole rings): slab/ceiling/zone now ride along in group move and rotate —
the two 3D gizmos and the keyboard group R/T all flow through the shared
rotateGroupPatches/translateGroupPatches, so the wiring is the classifier
plus the rotate gizmo's spread sampling. Zone's 3D renderer merges its live
polygon override so it previews during the drag (slab/ceiling already
rebuild via getEffectiveNode in their systems). Classifier + patch unit
tests included.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(editor): 2D floorplan group drag — Photoshop-style multi-selection move
Plain pointer-down on a transformable member of a multi-selection now
slides the whole selection rigidly in the floor plan (the 2D parity fill
for the 3D GroupMoveHandle), instead of collapsing the selection to the
pressed node. A plain click (no drag) still collapses on release; modified
clicks, Cmd-drag direct move, and reshape affordances keep their paths.
The move-handle dot routes through the same group session when its node
is part of the multi-selection.
The session shares group-transform-shared verbatim: welded LinkedNeighbor
endpoints, connected-component expansion, live previews via
useLiveNodeOverrides.setMany, grid snapping on the delta plus Figma
alignment through applyFloorplanAlignment (guides in every mode except
off, magnetic pull in lines mode), interaction scope handle-drag with the
shared group-move label, and a single batched updateNodes (one undo).
A dashed group bbox overlay shows what rides along and tracks the live
delta. clientToPlan moves to lib/floorplan/plan-coords for reuse.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(editor): group manipulation polish — helper text + shortcuts dialog
Multi-selection select-mode HUD now advertises the group gestures (drag
any member to move the whole selection, R/T to step-rotate), and the
keyboard-shortcuts dialog documents both under Selection.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(editor): restore zone renderer useLiveNodeOverrides import
Formatter stripped it between the import and usage edits; nodes'
tsc --build catches it where the cached turbo typecheck did not.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(editor): tag the 2D group selection box for drivability
data-group-selection-box on the overlay <g> so smoke drivers and devtools
can assert the multi-selection chrome.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(editor): multi-selection shows highlight only — hide per-node edit chrome
While 2+ nodes are selected the group is manipulated as one rigid piece,
so per-member edit affordances hide in both views: the 2D overlay pass
strips handle/dimension chrome centrally (stripHandleChrome — body
highlight, hit targets, and zone name text stay), and in 3D the slab /
ceiling boundary editors and slab hole click-to-edit outlines mount only
for a sole selection (the arrow-handle rig, wall side arrows, selection
affordances, and floating menu were already single-gated). Transformable
members show a move cursor in the floor plan.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(editor): 3D body-drag group move replaces the move gizmo cross
Pressing any transformable member of a multi-selection in 3D and dragging
past the threshold now slides the whole selection on the ground plane
(group-move-3d.ts, armed from the selection manager's pointer-down choke
point) — same participant snapshot, welded junctions, grid + alignment
snapping, live overrides, and single-undo commit as the 2D session; the
2D dashed bbox rides the same delta in split view. A plain click still
collapses the selection to the pressed node. The purple GroupMoveHandle
cross is deleted, and the hover cursor advertises the gesture: 'move'
over any transformable member in both views (3D selection-manager cursor
+ 2D entry cursor).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(editor): group action menu — move / duplicate / delete the whole selection
Multi-selections get a floating Move / Duplicate / Delete pill in BOTH
views (GroupFloatingActionMenu anchored above the 3D group bbox,
FloorplanGroupActionMenu anchored above the 2D dashed group box); the
single-node menus are now sole-selection only. All three actions target
the whole selection via shared group-actions:
- Move starts a group pick-up: the selection rides the cursor
(delta-relative, grid + alignment snapped) across BOTH surfaces —
floorplan via the scene CTM, 3D via a ground-plane raycast through a
three-context bridge published by the selection manager — until a
click places it as one undo step; Escape / right-click cancels.
- Duplicate clones the selection through the clipboard pipeline
(subtrees + id remap, without touching the user's clipboard — new
duplicateNodesToLevel), selects the clones, and picks them up;
cancelling deletes the clones again. The pick-up scopes to the
selection (no component expansion / welded links) because the clones
sit exactly on the originals, and falls back to participant-data
bounds because the clones' meshes haven't mounted yet.
- Delete removes everything selected with the keyboard arm's
bulk-confirm semantics.
Verified end-to-end with the scripted smoke (16/16): chrome hiding,
2D/3D/split group drags, R/T round-trip, one-step undo, menu move /
duplicate / delete, 3D move cursor. (Headless runs of the suite can
deadlock in SwiftShader's GL readback on the split-view resize —
environment-only; run headed.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(editor): group transforms no longer re-create auto rooms
Every group commit (2D/3D body drags, pick-up, rotate gizmo, keyboard
R/T) now wraps its single updateNodes in pauseSpaceDetection /
resumeSpaceDetection: the wall-driven room auto-detection rolls its
baseline forward instead of treating rigidly-moved walls as a new room
and duplicating its floors/ceilings. Room creation stays where it
belongs — building and editing walls.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(editor): polygon hosts carry their attached items in group transforms
Ceiling-mounted items are children of the ceiling, but polygon hosts
(slab/ceiling) move by rewriting vertices — no group transform — so,
unlike wall children which ride the wall mesh, their children were left
behind by group moves/rotates. collectParticipants now snapshots a
polygon participant's positioned children (level-frame coords, since the
host group sits at the origin) so they ride every group path. Unit test
included.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(editor): click picks up the group; the 2D dashed box is the drag handle
Clicking a selected member of a multi-selection (no drag) now enters the
group pick-up — the same click-to-move the sole-selection has — instead
of collapsing the selection, in 2D and 3D alike; clicking outside still
deselects. The 2D dashed selection box is itself the group's handle:
move cursor across its whole area, press-drag anywhere inside slides the
group, a plain click picks it up, and holding Cmd/Ctrl/Shift lets clicks
pass through to the entries so membership toggling keeps working. HUD
hint + shortcuts dialog updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(editor): 3D dashed group selection box doubles as the drag handle
The multi-selection now shows a dashed wireframe box in 3D (sibling of
the 2D dashed rect): move cursor across its whole volume, press-drag
anywhere on it slides the group, a plain click picks it up, and holding
a selection modifier passes the press through so members inside can
still be toggled. Rides the shared live-drag delta so it tracks the
group mid-gesture in both panes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(editor): mid-move R/T, 2D corner rotate, realtime opening symbols, box cursor + Esc hint
- R/T while carrying a group (body drag or pick-up) now rotates the
carried snapshots around the rest pivot — identical to the idle
keyboard rotate — instead of leaking into the store mid-session; the
session keydown owns the keys in capture phase and the group-move HUD
advertises them.
- The 2D dashed box grows corner rotate handles: drag a corner to spin
the group in 15° steps, Shift for free rotation — the floor-plan
sibling of the 3D rotate gizmo, sharing its scope, HUD hints, and
single-undo + space-detection-paused commit.
- Door / window floor-plan symbols now track wall drags in realtime:
their defs merge the walls' live overrides (wallFloorplanSiblingOverrides)
so ctx.parent is the effective wall instead of the stale store one.
- The 3D selection box asserts the move cursor across its whole volume
on pointer move (the old ''-guard lost to app default cursors).
- Multi-select HUD + shortcuts dialog mention Esc / click-outside to
clear, and the mid-move rotate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(editor): rotate cursor + spinning box on 2D corner rotate; Delete ends a carry
- The 2D corner handles show a proper curved-arrow rotate cursor (inline
SVG data URI, black glyph with white halo, grab fallback).
- The dashed selection box spins live with the group during a corner
rotate: the rotate session publishes pivot+angle through the shared
drag store and the box applies the SVG rotate transform.
- Delete / Backspace during any group session (drag, rotate, pick-up)
reverts the session first and then lets the global Delete arm remove
the selection — no more dangling carry after deleting mid-move; the
duplicate flow's cancel still discards the clones instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(editor): per-node direct manipulation stands down for multi-selections
Cmd is both the selection-toggle key and the single-node direct-move /
direct-rotate trigger, so a Cmd+click that wobbled past the drag
threshold over a selected member (slab or any kind) armed the legacy
single-node move and yanked that one member out of the group. All four
per-node direct-manipulation entry points — the 3D Cmd-drag move and
Cmd+right-drag rotate in the selection manager, and their 2D siblings in
the registry layer — now require the pressed node to be the SOLE
selection; with a multi-selection the gesture is simply not registered
(the group sessions own plain drags, Cmd+click keeps toggling). The
shortcuts dialog notes the single-selection scope.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(editor): group gestures own their ending pointerup — no synthesized click leak
use-node-events synthesizes a selection click on EVERY canvas pointerup
(deliberately more forgiving than R3F's onClick). Body drags were safe
only because inputDragging suppresses it, but the pick-up flows leaked
it as a race: the release that started a pick-up could collapse the
multi-selection to the pressed node first (then only that node was
carried), and the placement press re-selected whatever sat under the
cursor after commit.
The group sessions now claim their gesture-ending pointer events in the
CAPTURE phase and stop propagation, so the canvas never sees them and no
click is synthesized: the pick-up claims the placement press on
pointerdown and commits on its pointerup; the 3D and 2D drag/rotate
sessions stop their ending pointerup (split view: a 2D-started release
over the 3D canvas had the same hole).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(editor): screen-rect marquee tests projected oriented bounds, not double-inflated AABBs
The 3D screen-rect box select intersected the marquee with each node's
world AABB re-boxed in screen space — two nested axis-aligned inflations.
Any oblique camera padded every node's claim, and rotated geometry (now
common: group rotation) made it flagrant — a diagonal wall's world AABB
spans a full square, so marquees selected objects visually far from the
cursor.
Membership now tests the marquee against the convex hull of the node's
ORIENTED (local-frame) bounding box projected to screen — tight under any
rotation and camera, uniform for every kind. Pure helpers
(convexHull2D / rectIntersectsHull) live in marquee-geometry.ts with unit
tests covering the diagonal-wall regression, containment both ways, and
edge-only crossings. The plane marquee variant was already precise and is
untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(editor): keep box select alive after group gestures
The capture-phase stopPropagation the sessions used to silence the
canvas's synthesized selection click also killed the window bubble
listeners that clear the box-select pointer suppression — and the mouse
pointerId is constant, so one group gesture left the marquee dead until
blur. The sessions now use the designed suppression instead: they hold
inputDragging raised through the release event's dispatch (lowered on a
0ms timer) so use-node-events suppresses the click itself, and every
other pointerup listener runs normally.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(editor): marquee membership by plan footprint; 2D item highlight; site line steps back
- The 3D screen-rect marquee now projects itself onto the active level's
floor plane and tests the DATA kinds exactly there — walls/fences by
their segment, slabs/ceilings/zones by their polygon — matching the
plane-marquee tool's semantics. This kills the two remaining
imprecision sources the hull pass couldn't: vertex-baked rotation
(polygon/fence geometry lives in level coords, so after a group
rotation even the local bbox is an inflated axis-aligned square) and
ceiling parallax (an elevated plane projects to a screen quad offset
from its room, selecting 'from far'). Mesh-transform kinds (items,
columns) keep the projected oriented-bbox hull test; plan-space
segment/polygon intersection helpers join marquee-geometry.ts with
unit tests.
- Selected items finally read as selected in the floor plan: palette
stroke + heavier weight on the footprint, plus a selection ring drawn
above the thumbnail (the move dot used to be the only cue, and it
hides in multi-selections).
- The site's dashed property line drops to 20% opacity while a multi or
in-flight marquee selection exists so it stops fighting the dashed
group selection box.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(editor): 2D marquee plan-footprint membership; item marquee preview; overlays re-measure after undo
- The 2D screen marquee intersected each entry's getBoundingClientRect —
an axis-aligned DOM box, inflated for rotated polygons and diagonal
walls (the same double-inflation class as the 3D bug). It now maps the
marquee through the scene CTM into a plan quad (rotated views included)
and tests the data kinds exactly — walls/fences by segment,
slab/ceiling/zone by polygon — reusing the shared marquee-geometry
helpers; other kinds keep the DOM-rect fallback.
- Items now show the marquee preview tint in 2D (highlighted viewState =
the same palette stroke + thumbnail ring the selection shows, slightly
lighter), instead of giving zero feedback until the drop.
- Everything that measures the selection's meshes (2D/3D dashed boxes,
the 3D group menu anchor, the rotate gizmo pivot) re-measures once the
meshes settle after a scene change via useMeshSettleEpoch — an undo
after a move no longer leaves the box at the pre-undo position
(computeGroupBox reads mesh world bounds, which lag the store commit by
a frame or two).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(editor): dim the actual site boundary line during multi-selections
The earlier dimming targeted registry-layer site entries, but the site
lives at the scene root — outside the level DFS and the building-scoped
sweep — so no entry ever matched and the dashed property line kept full
opacity next to the dashed group selection box. Move the dimming to
FloorplanSiteLayer (the component that actually draws the line) and
drop the dead registry plumbing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(snapping): show alignment lines in all modes, snap only in lines; north export; R/T hint; grid default
- Alignment guides display in every snapping mode (grid/lines/angles/off);
magnetic pull applies only in 'lines'. New isAlignmentGuideActive() predicate
decouples guide display from the snap delta across all placement/move/draw
producers (item, wall, fence, slab/ceiling/roof, column/shelf/spawn, door/
window, MEP, 2D floorplan drafting, surface snap).
- Floorplan PDF export now rotates to north-up (FLOORPLAN_VIEW_ROTATION_DEG -
building rotation), matching the on-screen aligned-to-north view.
- Item placement rotate hint collapsed to a single 'R / T Rotate' row.
- Default item snapping mode changed lines -> grid.
* feat(walls): detect rooms across shared walls + tight connect-snap in all modes
Room detection: planarize the wall graph before face-finding — split straight
walls at T-junctions where another wall ends mid-span, so a room closed against
the middle of an existing wall is detected, not just isolated 4-wall rooms.
Auto-close: wire the wall builder's "Room (auto-close)" to the same room graph
via wallClosesRoom(), so drafting stops when a segment seals a room against the
existing structure — not only when the chain returns to its own start.
Connectivity snap: add a tight wall-connect snap (WALL_CONNECT_SNAP_RADIUS 0.05)
that also runs in grid/off/angles — within range of a wall (body or corner,
uniform radius) the endpoint sticks onto it and the beacon shows, so rooms close
in every mode. Lines keeps its wider magnetic radii. Gate alignment guides in
non-magnetic modes to the same connect distance so a corner dot no longer
magnetises the cursor from far.
Tests: cover T-junction detection + wallClosesRoom; update the item-default and
wall-split tests for the mode-driven behaviour.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(surfaces): gate alignment guides to connect distance in non-magnetic modes
Port the wall drafting/endpoint gating to the shared slab/ceiling/roof snap
(resolveSurfacePlanPointSnap): in grid/off/angles, only anchors within the
connect distance are fed to the alignment resolver, so guides form to nearby
points and far corner dots stop lighting up from across the plan. Filtering the
candidates (local-frame, like the cursor) rather than the resolved guides avoids
the floor-plan view rotation baked into the guide coords. Lines mode keeps the
full-range guides; the surface wall connect-snap is unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(slabs): deterministic wall-slab overlap via clipped overlap length
Wall elevation used ray-cast point samples that landed exactly on the
slab boundary for perpendicular walls, so whether a butting wall
followed the slab's elevation depended on which side of the slab it
touched. Clip the wall centerline and face lines against the polygon
and require >=5cm of on/inside length instead: walls along the slab
edge follow it on every side, point contact never does.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(grid): lift lattice 1mm off slab tops; wall-align on wall-item move start
The horizontal snap lattice sat exactly at the followed surface Y, so it
z-fought elevated slab tops while moving items. The visual mesh now rides
1mm above; the grid event plane keeps the true height.
Moving a wall-hosted item showed a horizontal grid until the first
pointer move published a wall surface — the mesh fallback assumed UP.
Derive the host wall normal from the item mesh's world orientation
(local +Z faces out of the wall) so the lattice is wall-aligned from
the first frame.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(editor): unify selection shortcuts + truthful select-mode hints
Shift+click now toggles selection membership in 3D like Cmd/Ctrl (and
appends on box-select release), matching the 2D floorplan. The HUD rows
describe what actually works in both views: move is plain left-drag (the
grip/dot), the two vague selection rows collapse into one Cmd/Shift
or-group, and the modifier-held variants (freely / with guides / bypass
snaps) are gone — guides follow the snapping mode now. Key pills join
with + for combos and / for alternatives; Shift renders as the shift
icon.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(editor): group gizmos — reliable rotate grabs, mutual hide, snapping-mode integration
The group-rotate gizmo never received pointer events through the shared
invisible hit-area path (EDITOR_LAYER + custom raycast) in its portalled
context; its handlers now live on the visible arrow plus a plain
default-layer invisible torus, so hover and drag work with a fat target.
Group drags begin a handle-drag scope: each gizmo hides while its
sibling drags (the frozen corner goes stale), idle hints leave the HUD,
and the drag gets contextual hints — Shift free-rotation for rotate, the
snapping chips for move.
Group move joins the snapping-mode system via the 'item' context: Shift
cycles the mode, Ctrl the grid step (both read live mid-drag), and
'lines' runs the same Figma-style alignment as single-node moves against
the group's bbox anchors. Chip clicks now tick like the keyboard cycles.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(editor): group-move ticks in every snapping mode
Mirror the single-node move's sfx: emit per delta change rather than
only on grid crossings, so lines/off get the same rate-limited texture.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(items): keep host transfers glued to the cursor
Moving between hosts used to diverge from the pointer and mangle state:
- Grab offsets: every surface anchor (wall / ceiling / shelf / item
surface / floor) preserved the grab offset by re-seeding from the
item's carried-over position on each new host, landing it far from
the cursor. The grab offset now survives only on the original host
and only until the item anchors anywhere else — after that every
host (the original included) centers the item under the cursor.
Applied uniformly to the placement coordinator and the window/door
move tools.
- Rotation: detaching from a rotated shelf/table back to the floor kept
the HOST-local yaw as the level yaw, visibly spinning the item. The
detach now re-expresses the item's world yaw in the level frame.
- Elevation: the same detach wrote the level parentId to the store but
not the draft ref, so the floor-elevation resolver bailed on its
parent-must-be-a-level guard — the snap grid and the item stopped
following slab elevations for the rest of the drag.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(items): latch grab-offset forgetting across every host kind
Review follow-up (PR #458): unify the per-surface grab-forget rule into
one grabForgotten latch. A wall/ceiling item could not actually reach a
shelf or the floor (item-surface enter rejects attachTo assets), but a
wall item CAN anchor on a roof face — and returning to its original
wall then restored the stale grab offset. Roof-wall transitions (and
floor landings after a host visit) now trip the latch in the
coordinator and the window/door move tools alike.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every placement and move path now publishes its ghost pose to a single
`useFacingPose` store, drawn by one editor-side `<FacingPoseIndicator>` overlay,
instead of each path drawing its own triangle (which left the nodes-package and
PlacementBox paths invisible):
- column/shelf presets + all moves (PlacementBox via move-registry, and
DragBoundingBox) now publish the pose, so the triangle finally shows
- stair create + move use a declarative `facingIndicator: { reversed: true }`
(new registry resolver) so the triangle sits before the entry pointing out —
resolved in one place, so create and move match automatically
- stair placement defaults to single and respects the shared `point`
continuation (C) toggle, like the other placement tools
Checkpoint on the placement-interaction epic: also carries the in-flight
continuation-profile extraction (lib/continuation), grid surface (item #8), and
HUD work. Door/window still render their own legacy inline triangle and are
migrated to the overlay next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate the remaining floor-placed kinds onto the unified snapping/modifier
model and generalize floor collision so any solid floor kind blocks any other.
- shelf/column/spawn declare `snapProfile: 'item'` → contextual snapping chip,
Shift=cycle, Ctrl=grid step during placement; their tools read the active
mode (grid/lines/off) instead of legacy Shift/Alt bypass; spawn fresh
placement now respects alignment ("lines") like its move.
- Resize/radial handles claim the handle-drag scope (new RESIZE_HANDLE_DRAG_LABEL)
so the HUD shows no select-mode shortcuts mid-resize.
- Column move migrated to the generic MoveRegistryNodeTool (declare `movable`,
drop the bespoke move-tool) — gains mode-driven snapping, alignment, R/T,
slab lift, grid SFX, and the collision box for free. 2D move still routes
through `floorplanMoveTarget`.
- Cross-kind floor collision: new declarative `FloorPlacedConfig.collides`
(item/shelf/column opt in; spawn/MEP/stair stay off). `canPlaceOnFloor` now
treats every colliding floor kind as an obstacle (was item-only), reading the
declarative footprint; the generic move tool's red/green placement box gates
on `collides`. Column footprint uses the visible `columnFootprintHalf` extent
so the box/slab-lift/collision track the real (round/square) column size.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements plans/editor-placement-interaction-overhaul.md: an authoritative
interaction-scope state machine plus the catalogued placement/interaction
fixes, and split-view floor-plan performance.
- Interaction-scope spine (lib/interaction/* + store/use-interaction-scope),
driven from central useEditor setters; overlay scoping (zone labels,
context badges, floating action menu) reads resolveOverlayPolicy.
- Bug tracks A/B/D/E/F/G/H: handle/cutout raycast, footprint validity,
auto-slab loop, ceiling hosting, B-key tool desync, 2D drop offset,
per-frame jank.
- Snapping modes (grid/lines/angles/off) + contextual HUD chips; modifier
model (Shift=cycle, Alt=free place, Ctrl=grid step).
- Item move now tracks the cursor 1:1 (was a laggy per-frame lerp); handle
rig hides during a whole-node move; rotate gizmo advertises Shift=free
rotation in the HUD and hides the move cross while rotating.
- Floor-plan perf: pause live reactivity while in 3D-only view; per-node
geometry cache so only changed nodes rebuild on a drag; hoist wall miters
to a once-per-pass ctx.levelData (O(N^2) -> O(N) on wall/opening drags).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add roof surface placement support for items
Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.
- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fixed conflict
* feat(duct): ceiling-snap drawing + connected-joint endpoint move
Duct draw tool's ceiling mode now hangs each path point just below the
ceiling actually covering it (per-room heights tracked), with a
translucent surface highlight and a plumb line to the floor so the
in-flight point reads clearly from any angle.
Dragging a duct corner that sits on a fitting now carries the fitting's
other ducts along (port-connectivity second hop), so the joint moves
together instead of tearing apart.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(mep): detach + vertical modifiers for duct/pipe joint editing
Alt detaches a dragged duct/pipe endpoint or fitting from its connected
joint (no elbow re-aim, no connectivity follow); Ctrl/Cmd drives vertical
riser movement on the fitting move. Behavioral parity across 2D and 3D.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(mep): full DWV pipe parity for joint editing
Bring pipe-segment endpoint drags and pipe-fitting moves to parity with
duct: free-drag endpoints, Alt-detach, Ctrl/Cmd-vertical riser, elbow
re-aim, and connectivity follow. Generalizes the shared elbow-reaim and
auto-fitting helpers to dispatch by run kind so 2D and 3D share one path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(mep): wall-style arrow handles for duct fittings + segments
Add violet directional arrow affordances to duct-fitting selection (height,
move cross, rotate arc) mirroring the duct-segment rig: portaled into the
parent frame to stay out of the selection outline, rendered via the shared
HandleArrow, and carrying mated-run connectivity through the single-undo
dance. The move cross engages press-drag-release (placementDragMode) the same
way the floating drag does, so the markup hit-areas go inert and the fitting
move tool commits on pointer-up.
Also re-export the HandleArrow primitives from @pascal-app/editor and extend
the duct-segment side-move/floorplan affordances.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(mep): click-to-latch cube handles for duct + fitting editing
Replace the hover-reveal / multi-handle selection rigs with a single
click-to-latch cube that opens a directional cluster, shared between
duct segments and fittings via a new selection-handles module
(HandleCube / MoveChevron / RotateArc, all sized to the roof pitch cube).
- Duct segment: per-vertex + run-center cubes reveal axis-locked move
chevrons (down arrow always shown), plus a roll arc at the run center.
- Duct fitting: center cube reveals six ±XYZ move arrows and three
per-axis rotation arcs (oriented in place), replacing the old
height/move/rotate trio with axis-cycling.
- Rotation (fitting arcs + duct roll) snaps to 45° steps; Shift = smooth.
- thin chevron profile + press-drag-release commit retained.
* fix(mep): orient duct roll arc consistently + drop Ctrl-vertical drag
Build a fully-determined basis for the duct roll gizmo so the curved
arrow always seats at the top-outer 45° corner regardless of run
direction, instead of an arbitrary apex from a single setFromUnitVectors.
The selection-rig ±Y arrows now own vertical movement, so the redundant
Ctrl-modifier riser drag is removed from the fitting move tool.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(mep): run-aligned duct handles, swing snapping, elbow flatten
Align the duct run-center cube + horizontal arrows to the run axis
(matching the per-vertex handles) while keeping whole-run translate.
Endpoint side / up-down swing arrows now follow grid snap points and
port-snap onto nearby collars (Shift sweeps smoothly). Relax elbow
realign + fitting schemas to flatten to a straight 0° coupling. Surface
HVAC-specific hints in the select-mode helper panel for duct / fitting.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(mep): per-segment linesets/liquid-lines with joint-follow editing
Linesets and liquid lines now commit one independent two-point node per
drawn segment instead of folding into a single mitered polyline, so each
line selects and deletes on its own. Endpoint caps fill shared-coordinate
joints so connected segments still read as continuous pipe.
Dragging a shared endpoint carries mated segments along via port
connectivity (Alt detaches), so a run still edits as one welded piece.
Liquid-line follow mode traces the whole connected lineset run, laying a
per-segment parallel line down its full assembled length.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(mep): vertical-offset auto-routing on duct center-cube ±Y drag
Lifting/lowering a connected run with the run-center cube now keeps each
connected end welded to its stationary partner instead of dragging the
whole network. Run-to-run ends get the classic S/Z offset (two elbows +
plumb riser, partner trimmed back one leg); elbow-connected ends form a
clean L — the existing elbow stays put and re-aims its collar vertical,
with one new top elbow + riser reconnecting to the lifted endpoint. The
offset is ghosted live and minted as a single undo step on release.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Add roof accessory placement guides
Measure roof accessory placement against the active roof face using visible surface bounds and preview geometry footprints. Add dormer-local guides and special linear handling for ridge vents and gutters.
* Improve duct and placement routing
* Fix duct vertical movement routing
* Fix duct vertical offsets and roof accessory movement
* Add DWV movement parity and line endpoint controls
* Fix MEP handle review issues
* Fix chimney placement and duct offset cleanup
* Use snapped targets for roof accessory commits
* fix(nodes): repair MEP movement review issues
- auto-fitting: tee branch now follows the drawn lateral angle; update the
stale square-tee test + doc comment that contradicted the rewrite
- duct-segment: re-enable the vertical auto-offset rewind (the disabled stub
left mintedIds empty, so re-dragging a tagged duct stranded old elbows/risers
and stacked duplicates); remove the dead stub
- duct-segment: strip the stale auto-offset tag on manual corner/roll commits
so the horizontal-move path no longer trusts an out-of-date base
- chimney: resume history before mutating segment children arrays so a
cross-segment move reparents in one tracked transaction (undo stays consistent)
- dormer: align schema test with the new windowSill=false default
* fix: address mep movement review issues
* fix: address follow-up mep review comments
* fix: address additional mep review comments
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: pascal-open-bot <open@pascal.app>