Commit 352954a
editor: group manipulation — Photoshop-style multi-selection move, rotate, duplicate (#481)
* 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>1 parent feeabf4 commit 352954a
33 files changed
Lines changed: 3487 additions & 568 deletions
File tree
- packages
- editor/src
- components
- editor-2d
- renderers
- editor
- tools
- select
- ui
- helpers
- sidebar/panels/settings-panel
- hooks
- lib
- floorplan
- nodes/src
- door
- item
- window
- zone
Lines changed: 87 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
0 commit comments