User-reported regressions after the previous Stage D move ports: 1. Slab/ceiling moves were sluggish because the actions wrote scene.update(polygon) every grid:move tick → React re-render + CSG-with-holes geometry rebuild per frame. Apply the live-drag exception (same recipe fence move already used): visual translation via `sceneRegistry.nodes.get(id).position` + `useLiveTransforms`; scene.polygon is only written on commit. Polygon center precomputed in ctx, mesh-offset clears on commit/cancel. 2. Cursor sphere sat at the polygon center (offset from the user's actual cursor by `originalCenter - first_cursor`). Move wrappers now subscribe to `grid:move` and set `cursorRef.current.position` directly — no React state, no per-tick reconcile. Cursor lands on the user's pointer. 3. Fence move had the same React-reconcile-per-tick cost via its `useLiveTransforms` subscription. Switched to the same direct ref-mutation pattern. Adds a "REAL bend" test pinning that one Ctrl-Z after a real curve drag undoes only the bend, not the create. The previously reported "first undo does nothing" outcome reproduces only for no-op bends (drags within `normalizeWallCurveOffset`'s straight-snap threshold, ≈1.5cm on a 3m fence). For visible bends the dance pushes a real pastState entry and one undo step rolls back the bend. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
66 lines
2.0 KiB
TypeScript
66 lines
2.0 KiB
TypeScript
'use client'
|
|
|
|
import { emitter, type FenceNode, type GridEvent } from '@pascal-app/core'
|
|
import { CursorSphere, triggerSFX, useDragAction, useEditor } from '@pascal-app/editor'
|
|
import { useViewer } from '@pascal-app/viewer'
|
|
import { useEffect, useRef } from 'react'
|
|
import type { Group } from 'three'
|
|
import { moveFenceDragAction } from './actions/move'
|
|
|
|
/**
|
|
* Phase 5 Stage D — thin React wrapper around `moveFenceDragAction`.
|
|
*
|
|
* Cursor sphere follows the raw grid pointer via direct ref mutation —
|
|
* no React state, no per-tick re-render. The fence mesh translates
|
|
* visually through the action's `mesh.position` + `useLiveTransforms`
|
|
* writes (live-drag exception); scene start/end are written on commit
|
|
* via the single-undo dance.
|
|
*/
|
|
export const FenceMoveTool: React.FC<{ node: FenceNode }> = ({ node }) => {
|
|
const fenceId = node.id
|
|
const cursorRef = useRef<Group>(null)
|
|
|
|
useEffect(() => {
|
|
const onMove = (event: GridEvent) => {
|
|
if (!cursorRef.current) return
|
|
cursorRef.current.position.set(
|
|
event.localPosition[0],
|
|
event.localPosition[1],
|
|
event.localPosition[2],
|
|
)
|
|
}
|
|
emitter.on('grid:move', onMove)
|
|
return () => {
|
|
emitter.off('grid:move', onMove)
|
|
}
|
|
}, [])
|
|
|
|
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,
|
|
// Initial point — useDragAction requires a Vec2. The action's
|
|
// begin captures everything else from input.node; this is just
|
|
// a placeholder until the first grid:move latches the anchor.
|
|
point: [(node.start[0] + node.end[0]) / 2, (node.start[1] + node.end[1]) / 2],
|
|
},
|
|
onCommit: () => exitMoveMode(true),
|
|
onCancel: () => exitMoveMode(false),
|
|
})
|
|
|
|
return (
|
|
<group>
|
|
<CursorSphere ref={cursorRef} showTooltip={false} />
|
|
</group>
|
|
)
|
|
}
|
|
|
|
export default FenceMoveTool
|