From faed282135c09a5ef3ff3a9bb14a64c1a66bb403 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Sat, 1 Aug 2026 05:32:46 +0200 Subject: [PATCH] fix(canvas): latch the socket at drag start so the stack cannot come apart --- .../components/simulator/SimulatorCanvas.tsx | 53 +++++++++++++------ 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index e779351a..5a3a9685 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -371,6 +371,13 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { // Which drag session already raised its item (drag-to-front fires once per // drag, on the first mousemove). const raisedThisDragRef = useRef(null); + /** + * The socket a dragged board was plugged into when the drag STARTED. + * Latched once per drag: "is it seated?" is only true at the seat, so + * re-asking after the first pixel of movement answers no and the stack + * would come apart one frame in. + */ + const carriedSocketRef = useRef<{ dragId: string; sockId: string } | null>(null); // Captures (x, y) of the dragged component at mousedown so a drag-end // can record the diff as a single undoable Move. Boards are intentionally // skipped — board moves don't go through component history. @@ -932,14 +939,20 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { const st = useSimulatorStore.getState(); const b = st.boards.find((bb) => bb.id === boardId); // Same stack rule as the mouse path: while simulating, a seated - // board drags its socket along instead of unplugging. - const sock = - st.running && b - ? st.components.find((c) => { - const seat = snapBoardToSocket(b.id, b.boardKind, b.x, b.y, [c]); - return !!seat && Math.hypot(seat.x - b.x, seat.y - b.y) < 0.5; - }) - : undefined; + // board drags its socket along instead of unplugging. Latched at + // drag start for the same reason (see carriedSocketRef). + if (carriedSocketRef.current?.dragId !== touchId) { + const seatedOn = + st.running && b + ? st.components.find((c) => { + const seat = snapBoardToSocket(b.id, b.boardKind, b.x, b.y, [c]); + return !!seat && Math.hypot(seat.x - b.x, seat.y - b.y) < 0.5; + }) + : undefined; + carriedSocketRef.current = { dragId: touchId, sockId: seatedOn?.id ?? '' }; + } + const sockId = carriedSocketRef.current.sockId; + const sock = sockId ? st.components.find((c) => c.id === sockId) : undefined; if (b && sock) { updateComponent(sock.id, { x: sock.x + (nb.x - b.x), @@ -1602,13 +1615,21 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { // While SIMULATING, a seated stack moves as one piece: dragging the // board drags its socket along instead of unplugging it. Unplugging // is an edit-mode gesture (carrySeatedBoards is the other half). - const sock = - st.running && b - ? st.components.find((c) => { - const seat = snapBoardToSocket(b.id, b.boardKind, b.x, b.y, [c]); - return !!seat && Math.hypot(seat.x - b.x, seat.y - b.y) < 0.5; - }) - : undefined; + if (carriedSocketRef.current?.dragId !== draggedComponentId) { + const seatedOn = + st.running && b + ? st.components.find((c) => { + const seat = snapBoardToSocket(b.id, b.boardKind, b.x, b.y, [c]); + return !!seat && Math.hypot(seat.x - b.x, seat.y - b.y) < 0.5; + }) + : undefined; + carriedSocketRef.current = { + dragId: draggedComponentId, + sockId: seatedOn?.id ?? '', + }; + } + const sockId = carriedSocketRef.current.sockId; + const sock = sockId ? st.components.find((c) => c.id === sockId) : undefined; if (b && sock) { updateComponent(sock.id, { x: sock.x + (nb.x - b.x), @@ -1940,6 +1961,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { recalculateAllWirePositions(); setDraggedComponentId(null); raisedThisDragRef.current = null; + carriedSocketRef.current = null; } }; @@ -2911,6 +2933,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { setPan({ ...panRef.current }); setDraggedComponentId(null); raisedThisDragRef.current = null; + carriedSocketRef.current = null; }} onContextMenu={(e) => { e.preventDefault();