feat(editor): placement & interaction overhaul — FSM spine, bug tracks, perf

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>
This commit is contained in:
Wassim SAMAD
2026-06-23 09:04:58 -04:00
co-authored by Claude Opus 4.8
parent b2f1a8432e
commit f773e6b8c5
71 changed files with 2362 additions and 598 deletions
+2 -1
View File
@@ -315,7 +315,8 @@ export const itemDefinition: NodeDefinition<typeof ItemNode> = {
{ key: 'Left click', label: 'Place item' },
{ key: 'R', label: 'Rotate counterclockwise' },
{ key: 'T', label: 'Rotate clockwise' },
{ key: 'Shift', label: 'Free place' },
{ key: 'Shift', label: 'Cycle snapping mode' },
{ key: 'Alt', label: 'Free place (no snap)' },
{ key: 'Esc', label: 'Cancel' },
],
+5 -2
View File
@@ -182,6 +182,7 @@ export function buildItemFloorplan(node: ItemNode, ctx: GeometryContext): Floorp
})
const isSelected = ctx.viewState?.selected ?? false
const isMoving = ctx.viewState?.moving ?? false
const floorPlanUrl = node.asset.floorPlanUrl
const children: FloorplanGeometry[] = [
{
@@ -214,8 +215,10 @@ export function buildItemFloorplan(node: ItemNode, ctx: GeometryContext): Floorp
rotation: transform.rotation,
})
}
// Move handle — orange dot at the item center. Only when selected.
if (isSelected) {
// Move handle — orange dot at the item center. Only when selected and not
// already moving: during a move the dot sits under the cursor, so a release
// over it would re-arm the move (and re-enter edit) instead of committing.
if (isSelected && !isMoving) {
children.push({
kind: 'move-handle',
point: [cx, cy],