Polish pass on the placement/interaction overhaul. Five fixes:
- Item-on-item rotation box: the cursor box held the host-local yaw instead of
world yaw, so it diverged from the item by the host's rotation when stacked on
another item (fine on the floor). The box now derives world yaw from the
mesh/host quaternion in both the R/T handler and the move-start sync.
- 2D floorplan move now respects the snapping mode (parity with 3D): grid
quantization only in grid mode, alignment guides only in lines/magnetic mode;
Shift/Alt no longer hard-bypass. Item move also plays the move "tick" SFX on
any resolved-position change, like the 3D move.
- Wall-side item footprint side: the 2D footprint depth offset extended toward the
wall (centerLocalZ -depth/2) instead of into the room, mirroring the item across
the wall; flipped to +depth/2. Aligned the undefined-side anchor to the 3D
convention (front +1 / else -1).
- 3D placement preview side: the wall-side preview bounds + base plane used a -Z
(into-wall) convention; flipped to +Z (into-room) to match the body and the
fixed 2D footprint. The 2D live preview during a 3D wall placement now publishes
the plan rotation (wall angle + item yaw) instead of the world cursor yaw, which
was pi off on a wall face and flipped the footprint to the far side.
- Cmd/Ctrl+R no longer rotates/flips the selected node (it reaches the browser
reload); guard added to the global selected-node handler and the 2D move overlay.
Verified: core/editor/nodes tsc, biome, editor 162/0 + nodes 292/0 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The footprint polygon uses `rotateVec` (R(-angle)) but the sprite was drawn via
SVG `rotate(+angle)`, so image and box counter-rotated and diverged by 2x the
item's rotation in the 2D floorplan. Negate the image rotation so the sprite
tracks its box (and the 3D orientation). Scoped to items — the only emitter of
the `image` floorplan geometry kind.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Wall-mounted items join doors/windows on roof-segment wall faces, and
the storage model moves to FACE-LOCAL coordinates so hosted children
track segment edits live.
- Children store roofFace + position [u, v, z-from-mid-plane] with
rotation 0 in-frame — the exact wall-child conventions (the wall
volume's mid-plane lands on the nominal footprint). A shared
<RoofFaceHostFrame> derives segment pose + face frame from the
live-override-merged segment: children follow resize handle drags in
real time and never jump on commit. No re-anchor cascade needed —
position is authoritative, the frame is derived. migrateNodes
converts branch-era segment-local data.
- Items: roofWallStrategy + roof:* handlers in the placement
coordinator (surface 'roof-wall'), Shift free-place normalized with
walls, ItemSystem wall-side push extended to segment hosts, correct
2D plan glyphs via face→segment→roof pose composition. The roof
hit resolver + overlap guard moved to @pascal-app/editor (the
coordinator lives there; nodes already depends on editor).
- Cuts: subtractAccessoryCuts extracted and applied in BOTH the
merged-shell and per-segment CSG paths (full edit mode / painted
segments used to lose every hole), built from the CURRENT host
geometry and live-effective children so holes follow segment and
opening drags.
- Handle rig: the grandparent portal now maps the node's world pose
into the portal frame instead of composing parent+node registry
poses — correct for any nesting (the face-frame group broke the
old assumption), identical for walls.
- Host-field hygiene: useDraftNode.commit/adopt and the window panel
duplicate forward roofSegmentId/roofFace/wallId; every roof↔wall
re-anchor clears and every revert restores them.
Codex-reviewed (design consultation, adversarial rounds on the
replaced cascade and on this refactor); frame conventions locked by
unit tests. Record: private-editor plans/editor-roof-wall-openings.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wholesale move of every remaining kind into its own subdirectory under
`packages/nodes/src/`, finishing the registry-driven migration. Each
kind now ships its definition, schema (re-exported from core), and any
of `geometry` / `renderer` / `system` / `floorplan` / `tool` /
`move-tool` / `panel` / `floorplan-move` / `floorplan-affordances` /
`parametrics` / `preview` it needs — no per-kind code remains under
`packages/editor/src/components/tools/` or
`packages/viewer/src/components/renderers/`.
Deleted (replaced by registry-driven equivalents):
- `tools/{ceiling,column,door,fence,item,slab,spawn,wall,window}/...`
(boundary editors, hole editors, placement tools, move tools,
endpoint movers, curve tools, helpers, math libs)
- `ui/helpers/{ceiling,slab,wall}-helper.tsx`
- `ui/panels/{column,door,elevator,item,roof,roof-segment,spawn,
stair,stair-segment,wall,window}-panel.tsx`
- `viewer/src/components/renderers/{building,ceiling,column,door,
elevator,fence,guide,item,level,roof,roof-segment,scan,site,slab,
spawn,stair,stair-segment,wall,window,zone}-renderer.tsx`
- `viewer/src/components/viewer/legacy-system.tsx`
Added under `packages/nodes/src/`:
- `building/`, `column/`, `elevator/`, `guide/`, `level/`, `roof/`,
`roof-segment/`, `scan/`, `shared/`, `site/`, `stair/`,
`stair-segment/` packages with definition + schema + renderer / system
/ floorplan / panel as appropriate.
- New `floorplan-move.ts` for every kind that supports 2D moves
(ceiling, door, item, shelf, slab, window) — single registry-driven
dispatch path via `def.floorplanMoveTarget`.
- New `floorplan-affordances.ts` for kinds with polygon / endpoint
drags (ceiling, fence, slab, wall) — using the shared
`polygon-vertex-affordance` factories.
- New per-kind `panel.tsx` for kinds with custom inspector content
(door, item, shelf, spawn, wall, window).
- New per-kind `tool.tsx` for placement (door, item, shelf, window).
- New per-kind `move-tool.tsx` for kinds with custom 3D move flows
(door, item, slab, window).
Coordinator + manager updates in `packages/editor/`:
- `tool-manager.tsx` resolves tools from the registry only — no
hardcoded type→component map.
- `panel-manager.tsx` resolves inspector panels the same way.
- `placement-{coordinator,strategies,types}.ts` extended with
shelf-surface placement.
- `selection-manager.tsx` adds the registry-selectable fallback.
- `floorplan-panel.tsx`, `floorplan-background-placement.ts`,
`floorplan-render-context.tsx` updated for the registry layer's new
contract (props, affordance dispatch, render context).
Viewer updates:
- `viewer/index.tsx` drops legacy renderer mounts.
- `node-renderer.tsx` resolves by registry only.
- `scene-bvh.tsx`, `use-node-events.ts`, `level-system.tsx`,
`wall-cutout.tsx`, `zone-system.tsx`, `materials.ts` adjusted for
the registry-only world.
Sidebar tree nodes for ceiling / fence / slab / shelf / tree-node
updated to read from the registered nodes instead of the deleted
legacy renderer trees.
Wiki: new `plugin-authoring.md` page, README index updated.
Tests in `packages/nodes/src/index.test.ts` validate every registered
kind has the required shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Item is the last Stage A-only kind to gain def.floorplan. Closes Stage C
across every registered kind.
Files added:
- nodes/src/item/floorplan.ts: buildItemFloorplan inlines a self-
contained parent-chain transform walker using `ctx.resolve`. Mirrors
the legacy `getItemFloorplanTransform` math from editor/lib/floorplan/
items.ts:
* Wall parent: rotate item.position by wall's angle, anchor at
wall.start, handle wall-side attachTo via wall.thickness offset.
* Item parent (nested): recurse for parent's transform.
* Level / slab / ceiling parent: item.position is level-local.
Returns a rotated width × depth rectangle. asset.floorPlanUrl image
overlay deferred for Phase 5 follow-up.
Files changed:
- nodes/src/item/definition.ts: wires `floorplan: buildItemFloorplan`.
- floorplan-panel.tsx: floorplanItemEntries useMemo short-circuits to
[] when nodeRegistry.has('item'). Phase 6 deletes the entire useMemo.
Stage C coverage (all 9 registered kinds):
shelf ✅ spawn ✅ fence ✅ slab ✅ ceiling ✅ wall ✅ door ✅ window ✅ item ✅
Next sessions: Stage B for door / window / wall (each large geometry
extraction), Stage D per kind (DragAction affordance ports), Stage E
(drop legacy panels), Phase 6 Stage F cleanup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>