Phase 5 Stage D moves: align useLiveTransforms with the direct mesh.position delta
User reported the slab/fence mesh jittered/teleported continuously
while dragging (not on commit — during the drag). Root cause: the
move tool wrote TWO conflicting values per grid:move tick:
1. `mesh.position.set(deltaX, 0, deltaZ)` — relative offset, direct
Three.js mutation.
2. `useLiveTransforms.set(id, { position: [originalCenter + deltaX,
0, originalCenter + deltaZ], rotation: 0 })` — absolute world
position of the translated polygon center.
`ParametricNodeRenderer` reads `useLiveTransforms` and binds it via
React: `<group position={liveTransform.position}>`. So every Zustand
notification re-rendered the renderer and reconciled the group's
position back to "originalCenter + delta" (the absolute), overriding
the "delta" the direct mutation had just written. The two systems
fought every frame → visible jitter.
Fix: `useLiveTransforms.position` now holds the SAME delta the direct
mutation uses (`[deltaX, 0, deltaZ]`). React reconciles to the same
value the direct mutation already set — no conflict.
The cursor sphere position stays as the translated polygon center
(it's tracked separately via React state, not `useLiveTransforms`).
Ceiling aligned for consistency, though CeilingRenderer doesn't read
`useLiveTransforms` so the value there has no rendering effect.
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
f4ea07e05b
commit
82c9b5e1df
@@ -90,8 +90,12 @@ export const MoveCeilingTool: React.FC<{ node: CeilingNode }> = ({ node }) => {
|
|||||||
const applyPreview = (deltaX: number, deltaZ: number) => {
|
const applyPreview = (deltaX: number, deltaZ: number) => {
|
||||||
deltaRef.current = [deltaX, deltaZ]
|
deltaRef.current = [deltaX, deltaZ]
|
||||||
setMeshOffset(ceilingId as AnyNodeId, deltaX, deltaZ, height)
|
setMeshOffset(ceilingId as AnyNodeId, deltaX, deltaZ, height)
|
||||||
|
// Aligned with slab/fence: the delta matches the direct mesh
|
||||||
|
// mutation. CeilingRenderer doesn't bind position via React, so
|
||||||
|
// this entry isn't consumed for rendering, but kept consistent
|
||||||
|
// in case other systems read it.
|
||||||
useLiveTransforms.getState().set(ceilingId, {
|
useLiveTransforms.getState().set(ceilingId, {
|
||||||
position: [originalCenter[0] + deltaX, height, originalCenter[1] + deltaZ],
|
position: [deltaX, 0, deltaZ],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
})
|
})
|
||||||
setCursorLocalPos([originalCenter[0] + deltaX, height, originalCenter[1] + deltaZ])
|
setCursorLocalPos([originalCenter[0] + deltaX, height, originalCenter[1] + deltaZ])
|
||||||
|
|||||||
@@ -104,17 +104,14 @@ function setMeshOffset(fenceId: FenceNode['id'], deltaX: number, deltaZ: number)
|
|||||||
if (mesh) mesh.position.set(deltaX, 0, deltaZ)
|
if (mesh) mesh.position.set(deltaX, 0, deltaZ)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFenceLiveTransform(
|
function setFenceLiveTransform(fenceId: FenceNode['id'], deltaX: number, deltaZ: number): void {
|
||||||
fenceId: FenceNode['id'],
|
// useLiveTransforms holds the SAME delta the direct mesh.position
|
||||||
start: [number, number],
|
// mutation uses — ParametricNodeRenderer reads it and reconciles
|
||||||
end: [number, number],
|
// `<group position={liveTransform.position}>` via React. Mismatched
|
||||||
deltaX: number,
|
// values here cause the two systems to fight per frame (jitter
|
||||||
deltaZ: number,
|
// during drag).
|
||||||
): void {
|
|
||||||
const cx = (start[0] + end[0]) / 2
|
|
||||||
const cz = (start[1] + end[1]) / 2
|
|
||||||
useLiveTransforms.getState().set(fenceId, {
|
useLiveTransforms.getState().set(fenceId, {
|
||||||
position: [cx + deltaX, 0, cz + deltaZ],
|
position: [deltaX, 0, deltaZ],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -168,10 +165,10 @@ export const MoveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => {
|
|||||||
const applyPreview = (deltaX: number, deltaZ: number) => {
|
const applyPreview = (deltaX: number, deltaZ: number) => {
|
||||||
deltaRef.current = [deltaX, deltaZ]
|
deltaRef.current = [deltaX, deltaZ]
|
||||||
setMeshOffset(fenceId, deltaX, deltaZ)
|
setMeshOffset(fenceId, deltaX, deltaZ)
|
||||||
setFenceLiveTransform(fenceId, originalStart, originalEnd, deltaX, deltaZ)
|
setFenceLiveTransform(fenceId, deltaX, deltaZ)
|
||||||
for (const linked of linkedOriginals) {
|
for (const linked of linkedOriginals) {
|
||||||
setMeshOffset(linked.id, deltaX, deltaZ)
|
setMeshOffset(linked.id, deltaX, deltaZ)
|
||||||
setFenceLiveTransform(linked.id, linked.start, linked.end, deltaX, deltaZ)
|
setFenceLiveTransform(linked.id, deltaX, deltaZ)
|
||||||
}
|
}
|
||||||
// Cursor at translated polygon center.
|
// Cursor at translated polygon center.
|
||||||
const centerX = (originalStart[0] + originalEnd[0]) / 2
|
const centerX = (originalStart[0] + originalEnd[0]) / 2
|
||||||
|
|||||||
@@ -109,11 +109,17 @@ export const MoveSlabTool: React.FC<{ node: SlabNode }> = ({ node }) => {
|
|||||||
// Visual: translate the slab MESH only. No scene mutation, no
|
// Visual: translate the slab MESH only. No scene mutation, no
|
||||||
// polygon rebuild, no React re-render of geometry.
|
// polygon rebuild, no React re-render of geometry.
|
||||||
setMeshOffset(slabId as AnyNodeId, deltaX, deltaZ)
|
setMeshOffset(slabId as AnyNodeId, deltaX, deltaZ)
|
||||||
|
// useLiveTransforms holds the same delta the direct mesh.position
|
||||||
|
// mutation uses — ParametricNodeRenderer reads it and reconciles
|
||||||
|
// `<group position={liveTransform.position}>` via React. Mismatched
|
||||||
|
// values here cause the two systems to fight per frame (jitter
|
||||||
|
// during drag).
|
||||||
useLiveTransforms.getState().set(slabId, {
|
useLiveTransforms.getState().set(slabId, {
|
||||||
position: [originalCenter[0] + deltaX, 0, originalCenter[1] + deltaZ],
|
position: [deltaX, 0, deltaZ],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
})
|
})
|
||||||
// Cursor sphere follows the new polygon center.
|
// Cursor sphere follows the new polygon center (independent of
|
||||||
|
// group position).
|
||||||
setCursorLocalPos([originalCenter[0] + deltaX, 0, originalCenter[1] + deltaZ])
|
setCursorLocalPos([originalCenter[0] + deltaX, 0, originalCenter[1] + deltaZ])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user