Phase 5 Stage D fence: port MoveFenceTool to DragAction (whole-fence move)
Third Stage D affordance port (302 LoC legacy → action + thin wrapper). The action (`actions/move.ts`) uses the live-drag exception documented in editor/wiki/architecture/tools.md: visual-only updates via `sceneRegistry.nodes.get(id).position` + `useLiveTransforms` during the drag, no scene mutations. Avoids re-rebuilding the fence geometry (many posts + infill panels) every pointer tick. Commit performs the single-undo dance — writes final start/end to scene, geometry rebuilds once, Ctrl-Z reverses the whole drag. Linked-fence cascade follows the same shape as MoveFenceEndpoint — any fence in the same parent that shared an endpoint at activation moves with the drag. `getRegistryAffordanceTool` extracted to `tools/shared/affordance-dispatch.ts` so move-tool.tsx and tool-manager.tsx share the lazy-load helper (no duplicate caches). MoveTool dispatch gains a generic `affordanceTools.move` check after the capability-driven `movable` shortcut — fence routes through here; wall / slab / etc. fall through to their legacy per-kind chain until their D ports land. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
36c48b7fc9
commit
c16878c876
@@ -0,0 +1,61 @@
|
||||
'use client'
|
||||
|
||||
import { type FenceNode, useLiveTransforms } from '@pascal-app/core'
|
||||
import { CursorSphere, triggerSFX, useDragAction, useEditor } from '@pascal-app/editor'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { moveFenceDragAction } from './actions/move'
|
||||
|
||||
/**
|
||||
* Phase 5 Stage D — thin React wrapper around `moveFenceDragAction`.
|
||||
*
|
||||
* Replaces the legacy `MoveFenceTool` (302 LoC). The action owns all
|
||||
* the math (snap + linked cascade + live-drag mesh offsets +
|
||||
* single-undo dance on commit). The wrapper just renders the cursor
|
||||
* sphere and tracks its position from the live-transform store.
|
||||
*
|
||||
* Mounted by ToolManager when `useEditor.movingNode` is a fence
|
||||
* (capability-driven dispatch — fence has no `movable` capability, so
|
||||
* the legacy MoveTool's per-kind branch chain falls through to here
|
||||
* via the new affordance dispatch).
|
||||
*/
|
||||
export const FenceMoveTool: React.FC<{ node: FenceNode }> = ({ node }) => {
|
||||
const fenceId = node.id
|
||||
const originalCenter: [number, number, number] = [
|
||||
(node.start[0] + node.end[0]) / 2,
|
||||
0,
|
||||
(node.start[1] + node.end[1]) / 2,
|
||||
]
|
||||
|
||||
// Live position from the live-transforms store — the action writes
|
||||
// here every preview tick (live-drag exception). Falls back to the
|
||||
// original center until the first move.
|
||||
const liveCenter = useLiveTransforms((s) => {
|
||||
const t = s.get(fenceId)
|
||||
return t?.position ?? originalCenter
|
||||
})
|
||||
|
||||
const exitMoveMode = (committed: boolean) => {
|
||||
if (committed) triggerSFX('sfx:item-place')
|
||||
useViewer.getState().setSelection({ selectedIds: [fenceId] })
|
||||
useEditor.getState().setMovingNode(null)
|
||||
}
|
||||
|
||||
useDragAction({
|
||||
active: true,
|
||||
action: moveFenceDragAction,
|
||||
initial: {
|
||||
node,
|
||||
point: [originalCenter[0], originalCenter[2]],
|
||||
},
|
||||
onCommit: () => exitMoveMode(true),
|
||||
onCancel: () => exitMoveMode(false),
|
||||
})
|
||||
|
||||
return (
|
||||
<group>
|
||||
<CursorSphere position={liveCenter as [number, number, number]} showTooltip={false} />
|
||||
</group>
|
||||
)
|
||||
}
|
||||
|
||||
export default FenceMoveTool
|
||||
Reference in New Issue
Block a user