MoveTool: dispatch by capabilities.movable, not nodeRegistry.has

User report: after wall registered, the move tool's smart sims-style
arrow UX (endpoint handles + linked-wall corner cascade + ALT-detach)
was replaced by a generic whole-wall-translate. The dispatch was
unconditionally routing every registered kind through
MoveRegistryNodeTool — but MoveRegistryNodeTool is for kinds whose
move semantics are "translate position on X/Z plane" (shelf, spawn,
single-position items). Wall / fence / slab / stair endpoint drags
are bespoke and need their legacy movers until each gets a proper
DragAction-based affordance port.

Fix: gate the registry-mover dispatch on `def.capabilities.movable`.
When a kind opts in (`movable: { axes, gridSnap }`), use the generic
mover; when a kind omits the capability deliberately (wall and fence
do), fall through to the legacy per-kind branch below.

This is the registry-aware analogue of "the registry doesn't limit
custom behavior — it lets kinds opt in to generic dispatch". Adding
`movable` is an opt-in; omitting it is an opt-out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-05-15 14:02:29 -04:00
co-authored by Claude Opus 4.7
parent 71b211de97
commit fc9a5d02a0
@@ -103,13 +103,22 @@ export const MoveTool: React.FC<{
if (!movingNode) return null
// Registry-first dispatch. Any kind registered via @pascal-app/nodes
// gets the imperative MoveRegistryNodeTool (smooth, framerate-locked
// motion via sceneRegistry — see plan: "Validated patterns from the
// spike"). The legacy per-kind movers below are short-circuited for
// any kind in the registry — that's how Phase 5 will progressively
// delete them as kinds migrate.
if (nodeRegistry.has(movingNode.type)) {
// Capability-driven dispatch. A registered kind opts INTO the generic
// mover by declaring `capabilities.movable` — that's the "I'm a simple
// translate-on-the-X/Z-plane node" signal (shelf, spawn, future
// single-position items). Kinds with bespoke move semantics (wall
// endpoint drag + linked-wall corner cascade + ALT-detach, fence
// endpoint drag + curve sagitta, slab polygon vertex edit, stair
// endpoint drag, etc.) deliberately OMIT `capabilities.movable` so
// this branch falls through to their legacy per-kind movers below.
//
// Without this guard, every registered kind would be force-routed
// through MoveRegistryNodeTool's "translate position" pattern,
// breaking wall / fence / slab / stair endpoint UX (the smart
// sims-style arrows that move the dragged endpoint while cascading
// to linked walls / re-anchoring hosted children / etc.).
const def = nodeRegistry.get(movingNode.type)
if (def?.capabilities?.movable) {
return <MoveRegistryNodeTool node={movingNode} />
}