fix(sim): a seated stack moves as one piece; unplugging is edit-only

This commit is contained in:
David Montero Crespo 2026-08-01 04:16:21 +02:00
parent 5f8a3e171c
commit 84080a0ff8
1 changed files with 77 additions and 8 deletions

View File

@ -117,6 +117,34 @@ const LONG_PRESS_MOVE_TOLERANCE = 8;
*/
const DRAG_PROMOTE_THRESHOLD_PX = 8;
/**
* A socket carries its seated board while it is dragged: the pair is plugged
* together like the physical stack, so moving the shield must not strand
* and silently disconnect the board, least of all mid-simulation (the
* stranded board's sketch suddenly loses its I2C peer and error-spams).
* Unplugging is the opposite gesture: dragging the BOARD off the socket.
*/
function carrySeatedBoards(
dragged: { id: string; x: number; y: number; properties?: Record<string, unknown> },
nx: number,
ny: number,
): void {
const dx = nx - dragged.x;
const dy = ny - dragged.y;
if (!dx && !dy) return;
const el = document.getElementById(dragged.id) as (HTMLElement & { boardSocket?: unknown }) | null;
if (!el?.boardSocket) return;
const st = useSimulatorStore.getState();
for (const b of st.boards) {
// Restricting the candidate list to the dragged component asks the
// narrow question: is this board seated on THIS socket?
const seat = snapBoardToSocket(b.id, b.boardKind, b.x, b.y, [dragged]);
if (seat && Math.hypot(seat.x - b.x, seat.y - b.y) < 0.5) {
st.setBoardPosition({ x: b.x + dx, y: b.y + dy }, b.id);
}
}
}
/** Check if a board kind is an ESP32-family board. */
function isEsp32Kind(kind: BoardKind): boolean {
return (
@ -901,9 +929,28 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
x: world.x - touchDragOffsetRef.current.x,
y: world.y - touchDragOffsetRef.current.y,
};
const bk = useSimulatorStore.getState().boards.find((b) => b.id === boardId)?.boardKind;
const seated = bk && snapBoardToSocket(boardId, bk, nb.x, nb.y, componentsRef.current ?? []);
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;
if (b && sock) {
updateComponent(sock.id, {
x: sock.x + (nb.x - b.x),
y: sock.y + (nb.y - b.y),
} as any);
setBoardPosition(nb, boardId);
} else {
const seated =
b && snapBoardToSocket(boardId, b.boardKind, nb.x, nb.y, componentsRef.current ?? []);
setBoardPosition(seated || nb, boardId);
}
} else if (touchId === '__board__') {
setBoardPosition({
x: world.x - touchDragOffsetRef.current.x,
@ -934,6 +981,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
nx = snapped.x;
ny = snapped.y;
}
carrySeatedBoards(dragged, nx, ny);
}
updateComponent(touchDraggedComponentIdRef.current!, {
x: nx,
@ -1548,13 +1596,33 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
if (draggedComponentId.startsWith('__board__:')) {
const boardId = draggedComponentId.slice('__board__:'.length);
{
const nb = { x: world.x - dragOffset.x, y: world.y - dragOffset.y };
const st = useSimulatorStore.getState();
const b = st.boards.find((bb) => bb.id === boardId);
// 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 (b && sock) {
updateComponent(sock.id, {
x: sock.x + (nb.x - b.x),
y: sock.y + (nb.y - b.y),
} as any);
setBoardPosition(nb, boardId);
} else {
// Socket magnet: a shield that declares boardSocket (the Round
// Display's XIAO header) grabs a matching board dragged onto it.
const nb = { x: world.x - dragOffset.x, y: world.y - dragOffset.y };
const bk = useSimulatorStore.getState().boards.find((b) => b.id === boardId)?.boardKind;
const seated = bk && snapBoardToSocket(boardId, bk, nb.x, nb.y, componentsRef.current ?? []);
const seated =
b && snapBoardToSocket(boardId, b.boardKind, nb.x, nb.y, componentsRef.current ?? []);
setBoardPosition(seated || nb, boardId);
}
}
} else if (draggedComponentId === '__board__') {
// legacy fallback
setBoardPosition({ x: world.x - dragOffset.x, y: world.y - dragOffset.y });
@ -1583,6 +1651,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
nx = snapped.x;
ny = snapped.y;
}
carrySeatedBoards(dragged, nx, ny);
}
updateComponent(draggedComponentId, { x: nx, y: ny } as any);
}