127 lines
4.6 KiB
TypeScript
127 lines
4.6 KiB
TypeScript
/**
|
|
* Board-on-socket magnet — how a shield takes a board.
|
|
*
|
|
* Some components ARE sockets: the Seeed Round Display's back header takes a
|
|
* XIAO the way a breadboard takes a part. Dragging the board near the socket
|
|
* snaps it so the two pin grids coincide, exactly like the real stack; drag it
|
|
* past the tolerance and it comes free again. Same feel as the breadboard
|
|
* magnet in breadboardSnap.ts, but for BOARDS (which move through
|
|
* setBoardPosition, not the component path).
|
|
*
|
|
* The contract is rule-6a style — read from the DOM element like pinInfo, so
|
|
* private-overlay components can declare a socket without this file knowing
|
|
* they exist:
|
|
*
|
|
* get boardSocket(): { anchorPin: string; accepts: string[] }
|
|
*
|
|
* `anchorPin` names one of the component's OWN pinInfo pads. A dragged board
|
|
* whose boardKind starts with any `accepts` prefix snaps so that ITS pad of
|
|
* the same name lands on that pad. One anchor is enough: both sides lay out
|
|
* their remaining pads at the same physical pitch, which is the whole point
|
|
* of a socket.
|
|
*/
|
|
import { calculatePinPosition } from './pinPositionCalculator';
|
|
|
|
/** DynamicComponent wrapper inset: border 2 + padding 4 on every side. */
|
|
const WRAPPER_INSET = 6;
|
|
|
|
/**
|
|
* Magnet range in world px. Chunkier than the breadboard's 9: a board is a
|
|
* two-finger object and the gesture should feel like the magnet grabs it,
|
|
* not like threading a needle. Still small enough that a deliberate drag
|
|
* away releases immediately.
|
|
*/
|
|
export const SOCKET_SNAP_TOLERANCE = 18;
|
|
|
|
interface Pt {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
interface ComponentLike {
|
|
id: string;
|
|
x: number;
|
|
y: number;
|
|
properties?: Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* Snap a dragged board's tentative position onto the nearest accepting
|
|
* socket, or null when none is in range (which is also how it lets go).
|
|
*/
|
|
export function snapBoardToSocket(
|
|
boardId: string,
|
|
boardKind: string,
|
|
tentativeX: number,
|
|
tentativeY: number,
|
|
components: ComponentLike[],
|
|
): Pt | null {
|
|
const boardEl = document.getElementById(boardId) as
|
|
| (HTMLElement & { pinInfo?: Array<{ name: string; x: number; y: number }> })
|
|
| null;
|
|
const boardPins = boardEl?.pinInfo;
|
|
if (!boardPins || boardPins.length === 0) return null;
|
|
|
|
let best: { dx: number; dy: number; dist: number } | null = null;
|
|
for (const c of components) {
|
|
const el = document.getElementById(c.id) as
|
|
| (HTMLElement & { boardSocket?: { anchorPin: string; accepts: string[] } })
|
|
| null;
|
|
const sock = el?.boardSocket;
|
|
if (!sock || !Array.isArray(sock.accepts)) continue;
|
|
if (!sock.accepts.some((p) => boardKind.startsWith(p))) continue;
|
|
// Rotated sockets are not supported (same limit as the breadboard).
|
|
if (Number(c.properties?.rotation) || 0) continue;
|
|
|
|
const anchor = boardPins.find((p) => p.name === sock.anchorPin);
|
|
if (!anchor) continue;
|
|
const target = calculatePinPosition(
|
|
c.id,
|
|
sock.anchorPin,
|
|
c.x + WRAPPER_INSET,
|
|
c.y + WRAPPER_INSET,
|
|
0,
|
|
);
|
|
if (!target) continue;
|
|
|
|
const dx = target.x - (tentativeX + anchor.x);
|
|
const dy = target.y - (tentativeY + anchor.y);
|
|
const dist = Math.hypot(dx, dy);
|
|
if (dist <= SOCKET_SNAP_TOLERANCE && (!best || dist < best.dist)) {
|
|
best = { dx, dy, dist };
|
|
}
|
|
}
|
|
return best ? { x: tentativeX + best.dx, y: tentativeY + best.dy } : null;
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
boardKind: string,
|
|
x: number,
|
|
y: number,
|
|
components: ComponentLike[],
|
|
): boolean {
|
|
const seat = snapBoardToSocket(boardId, boardKind, x, y, components);
|
|
return !!seat && Math.hypot(seat.x - x, seat.y - y) <= SEATED_EPSILON;
|
|
}
|