diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index 7d63b029..9a920220 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -50,7 +50,7 @@ import { seatOnDrop, snapPositionToBreadboard, } from '../../utils/breadboardSnap'; -import { snapBoardToSocket } from '../../utils/socketSnap'; +import { snapBoardToSocket, isBoardSeated } from '../../utils/socketSnap'; import { findWireNearPoint, findSegmentNearPoint, @@ -138,8 +138,7 @@ function carrySeatedBoards( 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) { + if (isBoardSeated(b.id, b.boardKind, b.x, b.y, [dragged])) { st.setBoardPosition({ x: b.x + dx, y: b.y + dy }, b.id); } } @@ -949,10 +948,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { const boardRunning = !!(b && (b.running || st.running)); const seatedOn = boardRunning && 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; - }) + ? st.components.find((c) => isBoardSeated(b.id, b.boardKind, b.x, b.y, [c])) : undefined; carriedSocketRef.current = { dragId: touchId, sockId: seatedOn?.id ?? '' }; } @@ -1628,10 +1624,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { const boardRunning = !!(b && (b.running || st.running)); const seatedOn = boardRunning && 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; - }) + ? st.components.find((c) => isBoardSeated(b.id, b.boardKind, b.x, b.y, [c])) : undefined; carriedSocketRef.current = { dragId: draggedComponentId, diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index b8901794..24ba269f 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -52,7 +52,7 @@ import { collectWireSegments, } from '../utils/wireAutoRoute'; import { isBreadboard } from '../utils/breadboardNets'; -import { snapBoardToSocket } from '../utils/socketSnap'; +import { isBoardSeated } from '../utils/socketSnap'; import { computeSeating } from '../utils/breadboardSnap'; import { createSerialBatcher } from './serialBatcher'; import { @@ -3169,16 +3169,9 @@ export const useSimulatorStore = create((set, get) => { // above — raising the socket alone buried its own seated board, and a // buried board cannot be grabbed to unplug it. const seatedOnIt = s.components.some((c) => c.id === id) - ? s.boards.filter((b) => { - const seat = snapBoardToSocket( - b.id, - b.boardKind, - b.x, - b.y, - s.components.filter((c) => c.id === id), - ); - return !!seat && Math.hypot(seat.x - b.x, seat.y - b.y) < 0.5; - }) + ? s.boards.filter((b) => + isBoardSeated(b.id, b.boardKind, b.x, b.y, s.components.filter((c) => c.id === id)), + ) : []; let top = s.zTop; const zOrders = { ...s.zOrders, [id]: ++top }; diff --git a/frontend/src/utils/socketSnap.ts b/frontend/src/utils/socketSnap.ts index a2ae90f9..ab7a0f62 100644 --- a/frontend/src/utils/socketSnap.ts +++ b/frontend/src/utils/socketSnap.ts @@ -95,12 +95,24 @@ export function snapBoardToSocket( } /** - * True when the board's CURRENT position IS a socket seat (within half a - * pixel). This is the z-order question, not the drag question: a seated - * board must paint above its socket component, an unseated one must stay - * below components like every other board — the blanket zIndex bump that - * preceded this check hid a resistor behind an Arduino in every ordinary - * example. + * How far off the exact seat a board may sit and still count as plugged in. + * Not zero, and deliberately far below SOCKET_SNAP_TOLERANCE: a stack the + * magnet built lands exact, but one an EXAMPLE declares (or a project saved + * before a socket's art was nudged) can be a fraction of a pixel out. At the + * old half-pixel bar such a board looked seated on screen while every seat + * test said otherwise — so it got no electrical connection and its socket + * did not travel with it. A couple of pixels is invisible to the eye and + * still nowhere near the next hole. + */ +const SEATED_EPSILON = 2; + +/** + * True when the board's CURRENT position IS a socket seat. This is the + * "is it plugged in?" question, asked by z-order (a seated board must paint + * above its socket, an unseated one stays below components like every other + * board — a blanket zIndex bump once hid a resistor behind an Arduino), + * by the electrical hop that makes seating mean connection, and by the drag + * rules that keep a plugged stack together. */ export function isBoardSeated( boardId: string, @@ -110,5 +122,5 @@ export function isBoardSeated( components: ComponentLike[], ): boolean { const seat = snapBoardToSocket(boardId, boardKind, x, y, components); - return !!seat && Math.hypot(seat.x - x, seat.y - y) < 0.5; + return !!seat && Math.hypot(seat.x - x, seat.y - y) <= SEATED_EPSILON; }