Phase 5 Stage D moves: live-drag for slab/ceiling + cursor follows pointer
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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
c2c2f66427
commit
d1231b4901
@@ -209,6 +209,33 @@ describe('Single-undo dance', () => {
|
||||
expect((after as { curveOffset: number }).curveOffset).toBe(0)
|
||||
})
|
||||
|
||||
test('REAL bend (draft != original): one Ctrl-Z undoes only the bend', () => {
|
||||
useScene.getState().createNode(makeFence(0))
|
||||
const stateAfterCreate = useScene.getState().nodes[FENCE_ID] as { curveOffset: number }
|
||||
expect(stateAfterCreate.curveOffset).toBe(0)
|
||||
|
||||
const scene = createSceneApi(useScene)
|
||||
scene.pauseHistory()
|
||||
// Simulate a real drag: capture original, mutate to non-zero.
|
||||
scene.update(FENCE_ID, { curveOffset: 0.5 } as Partial<AnyNode>)
|
||||
expect((useScene.getState().nodes[FENCE_ID] as { curveOffset: number }).curveOffset).toBe(0.5)
|
||||
|
||||
// Dance.
|
||||
scene.restoreAll()
|
||||
expect((useScene.getState().nodes[FENCE_ID] as { curveOffset: number }).curveOffset).toBe(0)
|
||||
scene.resumeHistory()
|
||||
scene.update(FENCE_ID, { curveOffset: 0.5 } as Partial<AnyNode>)
|
||||
expect((useScene.getState().nodes[FENCE_ID] as { curveOffset: number }).curveOffset).toBe(0.5)
|
||||
|
||||
// First Ctrl-Z should undo the bend.
|
||||
useScene.temporal.getState().undo()
|
||||
const afterFirstUndo = useScene.getState().nodes[FENCE_ID] as
|
||||
| { curveOffset: number }
|
||||
| undefined
|
||||
expect(afterFirstUndo).toBeDefined()
|
||||
expect(afterFirstUndo?.curveOffset).toBe(0)
|
||||
})
|
||||
|
||||
test('a SECOND undo rolls the create step back', () => {
|
||||
useScene.getState().createNode(makeFence(0))
|
||||
const scene = createSceneApi(useScene)
|
||||
|
||||
Reference in New Issue
Block a user