Shelf had the floating action menu (move/delete) showing thanks to
the previous registry-driven selection commit, but clicking move
did nothing and duplicate silently failed. Two hardcoded chains:
1) FloatingActionMenu.handleMove guarded `setMovingNode` behind a
hardcoded `node.type === 'item' || ... || node.type === 'spawn'`
chain. Added `|| isRegistrySelectable(node.type)` so any
registry kind triggers the move flow.
2) MoveTool dispatched per-kind components (MoveItemContent,
MoveColumnTool, MoveWallTool, ...). The default fallback
mounted MoveItemContent, which assumes the node is an ItemNode
with asset/scale/metadata — crashes for shelf. Added a generic
MoveRegistryNodeTool (kind-agnostic clone of MoveColumnTool):
pure position+rotation drag with grid snap, re-parses orphan
re-creates via `nodeRegistry.get(kind).schema.parse(...)`.
MoveTool dispatches to it for any `nodeRegistry.has(movingNode.type)`
before the MoveItemContent fallback.
3) FloatingActionMenu.handleDuplicate had a hardcoded
`node.type === 'door' ? DoorNode.parse(...) : ...` chain. Added
a registry-driven fallback after it:
`const def = nodeRegistry.get(node.type); duplicate = def.schema.parse(duplicateInfo)`.
Then the createNode + setMovingNode branches also augment with
`nodeRegistry.has(duplicate.type)` so the new shelf gets
created in the scene and handed off to the move tool for
placement.
After this:
- Click shelf → move icon in floating menu → cursor follows mouse,
click to place at new position.
- Click shelf → duplicate icon → new shelf appears, offset by (1,0,1),
handed to move tool so the user can position it.
Phase 4 will collapse MoveRegistryNodeTool with the per-kind movers
once they all reduce to the same position+rotation shape, and read
`capabilities.movable` to gate handleMove instead of the OR chain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>