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]) }