Phase 5 Stage D: fix bend history + move-tool loop + grid-snap sfx
Three user-reported regressions, all in the Stage D move/curve ports: 1. **Fence/wall bend cancelled the fence creation on Ctrl-Z.** The action.commit's `return false` shortcut on "no offset change" bypassed the dance entirely — pastStates wasn't touched, so the next Ctrl-Z fell through to whatever preceded activation (typically the create step). Fix: always run the dance, even on no-op commits. First Ctrl-Z then absorbs a silent no-op entry, subsequent presses roll back real prior actions. Same fix applied to fence move-endpoint, fence move, slab move, ceiling move. 2. **Slab/ceiling move 'maximum update depth exceeded' loop.** The `useScene` selector in `SlabMoveTool` returned a freshly-allocated `[sx, sz]` tuple on every call. Zustand's `Object.is` equality failed each comparison → re-subscribe → re-render → loop. Fix: subscribe to the stable live-node reference and derive the center via `useMemo`. Same recipe for fence/ceiling move-tools, including memoizing the `originalCenter` fallback that was getting a new array per render. 3. **No grid-snap sfx during move drag.** Action `preview` now tracks the last snapped pointer on a mutable `lastSnapped` ctx field and emits `sfx:grid-snap` when it changes between ticks. Matches the legacy MoveFenceTool's per-tick sound. Locking test for foot-gun 1 lives in `packages/core/src/services/single-undo-dance.test.ts`. 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
d43cd569d6
commit
c2c2f66427
@@ -3,32 +3,42 @@
|
||||
import { type SlabNode, useScene } from '@pascal-app/core'
|
||||
import { CursorSphere, triggerSFX, useDragAction, useEditor } from '@pascal-app/editor'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { useMemo } from 'react'
|
||||
import { moveSlabDragAction } from './actions/move'
|
||||
|
||||
/**
|
||||
* Phase 5 Stage D — thin React wrapper around `moveSlabDragAction`.
|
||||
*
|
||||
* Replaces the legacy `MoveSlabTool` (182 LoC). All math + history
|
||||
* dance lives in the action; this wrapper just renders the cursor
|
||||
* sphere following the live polygon center.
|
||||
* dance lives in the action; this wrapper renders the cursor sphere
|
||||
* at the live polygon center.
|
||||
*
|
||||
* NOTE on selector stability: the live polygon center MUST be derived
|
||||
* via `useMemo` over the node reference rather than computed inside
|
||||
* the `useScene` selector — returning a fresh `[x, z]` tuple from the
|
||||
* selector on every call triggers "getSnapshot result not cached"
|
||||
* → infinite re-render. Same pattern in fence/ceiling move-tool.
|
||||
*/
|
||||
export const SlabMoveTool: React.FC<{ node: SlabNode }> = ({ node }) => {
|
||||
const slabId = node.id
|
||||
const initialCenter: [number, number] = useMemo(() => {
|
||||
if (node.polygon.length === 0) return [0, 0]
|
||||
let sx = 0
|
||||
let sz = 0
|
||||
for (const [x, z] of node.polygon) {
|
||||
sx += x
|
||||
sz += z
|
||||
}
|
||||
return [sx / node.polygon.length, sz / node.polygon.length]
|
||||
}, [node.polygon])
|
||||
|
||||
const initialCenter: [number, number] =
|
||||
node.polygon.length > 0
|
||||
? [
|
||||
node.polygon.reduce((s, [x]) => s + x, 0) / node.polygon.length,
|
||||
node.polygon.reduce((s, [, z]) => s + z, 0) / node.polygon.length,
|
||||
]
|
||||
: [0, 0]
|
||||
|
||||
// Live polygon center — re-derived from the scene store every tick
|
||||
// since the action writes the translated polygon onto the slab.
|
||||
const liveCenter = useScene((s) => {
|
||||
const live = s.nodes[slabId]
|
||||
if (live?.type !== 'slab') return initialCenter
|
||||
const poly = (live as SlabNode).polygon
|
||||
// Subscribe to the live node reference (stable across renders when
|
||||
// the node hasn't changed; new reference per scene update). Derive
|
||||
// the center inside `useMemo` so the selector itself stays cached.
|
||||
const liveNode = useScene((s) => s.nodes[slabId])
|
||||
const liveCenter = useMemo<[number, number]>(() => {
|
||||
if (liveNode?.type !== 'slab') return initialCenter
|
||||
const poly = (liveNode as SlabNode).polygon
|
||||
if (poly.length === 0) return initialCenter
|
||||
let sx = 0
|
||||
let sz = 0
|
||||
@@ -36,8 +46,8 @@ export const SlabMoveTool: React.FC<{ node: SlabNode }> = ({ node }) => {
|
||||
sx += x
|
||||
sz += z
|
||||
}
|
||||
return [sx / poly.length, sz / poly.length] as [number, number]
|
||||
})
|
||||
return [sx / poly.length, sz / poly.length]
|
||||
}, [liveNode, initialCenter])
|
||||
|
||||
const exitMoveMode = (committed: boolean) => {
|
||||
if (committed) triggerSFX('sfx:item-place')
|
||||
|
||||
Reference in New Issue
Block a user