From 82c9b5e1df7c87cc639d94948a561fecd7cfde91 Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Mon, 18 May 2026 09:46:23 -0400 Subject: [PATCH] Phase 5 Stage D moves: align useLiveTransforms with the direct mesh.position delta MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: ``. 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) --- packages/nodes/src/ceiling/move-tool.tsx | 6 +++++- packages/nodes/src/fence/move-tool.tsx | 21 +++++++++------------ packages/nodes/src/slab/move-tool.tsx | 10 ++++++++-- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/nodes/src/ceiling/move-tool.tsx b/packages/nodes/src/ceiling/move-tool.tsx index fb991d4b..b1a42538 100644 --- a/packages/nodes/src/ceiling/move-tool.tsx +++ b/packages/nodes/src/ceiling/move-tool.tsx @@ -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]) diff --git a/packages/nodes/src/fence/move-tool.tsx b/packages/nodes/src/fence/move-tool.tsx index 7602845a..52e89f9b 100644 --- a/packages/nodes/src/fence/move-tool.tsx +++ b/packages/nodes/src/fence/move-tool.tsx @@ -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 + // `` 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 diff --git a/packages/nodes/src/slab/move-tool.tsx b/packages/nodes/src/slab/move-tool.tsx index e319a3d4..112ecbbb 100644 --- a/packages/nodes/src/slab/move-tool.tsx +++ b/packages/nodes/src/slab/move-tool.tsx @@ -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 + // `` 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]) }