Phase 5 Stage D wall: port CurveWallTool to DragAction (first wall D)

Direct copy of the fence curve recipe — pure
`curveWallDragAction` (chord-perpendicular projection + clamp +
normalize + single-undo dance) plus a thin wrapper feeding
`useDragAction`. Mounted via `def.affordanceTools.curve`.

Slight precision difference vs legacy: the legacy CurveWallTool snapped
the pointer position to `getWallGridStep()` before projecting onto the
chord normal; the ported action skips that pre-snap and relies on
`normalizeWallCurveOffset` to settle the final value. The user-visible
result is the same magnitude of step, just with the snap applied at
the offset level instead of the position level.

Remaining wall D affordances (endpoint move, whole-wall move,
placement) are larger and queued for future sessions — wall's move
tool alone is 804 LoC with the linked-wall corner-cascade logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-05-15 18:34:40 -04:00
co-authored by Claude Opus 4.7
parent de3efa1b53
commit 7f2f9c685b
4 changed files with 162 additions and 1 deletions
+62
View File
@@ -0,0 +1,62 @@
'use client'
import { getWallMidpointHandlePoint, useScene, type WallNode } from '@pascal-app/core'
import { CursorSphere, triggerSFX, useDragAction, useEditor } from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { useEffect, useState } from 'react'
import { curveWallDragAction } from './actions/curve'
/**
* Phase 5 Stage D — thin React wrapper around `curveWallDragAction`.
*
* Replaces the legacy `CurveWallTool` (178 LoC). Same UX as the fence
* curve port — cursor sphere follows the chord-perpendicular projection
* of the pointer, dragging the wall's `curveOffset` live; grid:click
* commits with the single-undo dance, Esc cancels.
*
* Mounted by ToolManager via `def.affordanceTools.curve` when
* `useEditor.curvingWall` activates.
*/
export const WallCurveTool: React.FC<{ node: WallNode }> = ({ node }) => {
const initialHandle = getWallMidpointHandlePoint(node)
const [cursorPos, setCursorPos] = useState<[number, number, number]>([
initialHandle.x,
0,
initialHandle.y,
])
const exitCurveMode = (committed: boolean) => {
if (committed) triggerSFX('sfx:item-place')
useViewer.getState().setSelection({ selectedIds: [node.id] })
useEditor.getState().setCurvingWall(null)
}
useDragAction({
active: true,
action: curveWallDragAction,
initial: {
node,
point: [initialHandle.x, initialHandle.y],
},
onCommit: () => exitCurveMode(true),
onCancel: () => exitCurveMode(false),
})
const liveCurveOffset = useScene((s) => {
const live = s.nodes[node.id]
return live?.type === 'wall' ? ((live as WallNode).curveOffset ?? 0) : 0
})
useEffect(() => {
const handlePoint = getWallMidpointHandlePoint({ ...node, curveOffset: liveCurveOffset })
setCursorPos([handlePoint.x, 0, handlePoint.y])
}, [liveCurveOffset, node])
return (
<group>
<CursorSphere position={cursorPos} showTooltip={false} />
</group>
)
}
export default WallCurveTool