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:
Wassim SAMAD
2026-05-18 09:46:23 -04:00
co-authored by Claude Opus 4.7
parent f4ea07e05b
commit 82c9b5e1df
3 changed files with 22 additions and 15 deletions
+5 -1
View File
@@ -90,8 +90,12 @@ export const MoveCeilingTool: React.FC<{ node: CeilingNode }> = ({ node }) => {
const applyPreview = (deltaX: number, deltaZ: number) => {
deltaRef.current = [deltaX, deltaZ]
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, {
position: [originalCenter[0] + deltaX, height, originalCenter[1] + deltaZ],
position: [deltaX, 0, deltaZ],
rotation: 0,
})
setCursorLocalPos([originalCenter[0] + deltaX, height, originalCenter[1] + deltaZ])
+9 -12
View File
@@ -104,17 +104,14 @@ function setMeshOffset(fenceId: FenceNode['id'], deltaX: number, deltaZ: number)
if (mesh) mesh.position.set(deltaX, 0, deltaZ)
}
function setFenceLiveTransform(
fenceId: FenceNode['id'],
start: [number, number],
end: [number, number],
deltaX: number,
deltaZ: number,
): void {
const cx = (start[0] + end[0]) / 2
const cz = (start[1] + end[1]) / 2
function setFenceLiveTransform(fenceId: FenceNode['id'], deltaX: number, deltaZ: number): void {
// 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(fenceId, {
position: [cx + deltaX, 0, cz + deltaZ],
position: [deltaX, 0, deltaZ],
rotation: 0,
})
}
@@ -168,10 +165,10 @@ export const MoveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => {
const applyPreview = (deltaX: number, deltaZ: number) => {
deltaRef.current = [deltaX, deltaZ]
setMeshOffset(fenceId, deltaX, deltaZ)
setFenceLiveTransform(fenceId, originalStart, originalEnd, deltaX, deltaZ)
setFenceLiveTransform(fenceId, deltaX, deltaZ)
for (const linked of linkedOriginals) {
setMeshOffset(linked.id, deltaX, deltaZ)
setFenceLiveTransform(linked.id, linked.start, linked.end, deltaX, deltaZ)
setFenceLiveTransform(linked.id, deltaX, deltaZ)
}
// Cursor at translated polygon center.
const centerX = (originalStart[0] + originalEnd[0]) / 2
+8 -2
View File
@@ -109,11 +109,17 @@ export const MoveSlabTool: React.FC<{ node: SlabNode }> = ({ node }) => {
// Visual: translate the slab MESH only. No scene mutation, no
// polygon rebuild, no React re-render of geometry.
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, {
position: [originalCenter[0] + deltaX, 0, originalCenter[1] + deltaZ],
position: [deltaX, 0, deltaZ],
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])
}