velxio/frontend/src/utils/breadboardOccupancy.ts

91 lines
3.1 KiB
TypeScript

/**
* Breadboard hole-occupancy rules ("vocabulario"):
*
* - each HOLE (pin) holds at most ONE wire end — a seated component leg
* counts as occupying its hole;
* - a terminal-strip ROW (5 holes) is one net, so two wires landing in
* different holes of the same row are connected — that's the legal way
* to fan out;
* - clicking a hole occupied by a (visible) wire selects that wire
* instead of starting a new one — otherwise wires whose whole length
* lies over hole overlays are impossible to select.
*
* These helpers are pure over the wires list + pin names so they're
* unit-testable without a DOM.
*/
import type { Wire } from '../types/wire';
import { breadboardGroupKey } from './breadboardNets';
/**
* The topmost VISIBLE wire with an endpoint exactly on (componentId, pinName),
* or null. Invisible `bb` seating wires are never returned — they're not
* user-interactive — but see `holeIsOccupied` for occupancy checks.
*/
export function findWireAtHole(
wires: Wire[],
componentId: string,
pinName: string,
): Wire | null {
for (let i = wires.length - 1; i >= 0; i--) {
const w = wires[i];
if (w.bb) continue;
if (
(w.start.componentId === componentId && w.start.pinName === pinName) ||
(w.end.componentId === componentId && w.end.pinName === pinName)
) {
return w;
}
}
return null;
}
/**
* True when any wire end — including an invisible seating wire (a seated
* component leg) — already lives in this hole.
*/
export function holeIsOccupied(wires: Wire[], componentId: string, pinName: string): boolean {
return wires.some(
(w) =>
(w.start.componentId === componentId && w.start.pinName === pinName) ||
(w.end.componentId === componentId && w.end.pinName === pinName),
);
}
/**
* Resolve the hole a NEW wire end should land in, honouring one-wire-per-hole:
* returns `pinName` itself when free, else the nearest FREE hole in the same
* internal group (5-hole strip / power rail) — electrically identical, so the
* connection intent is preserved. Falls back to the original hole when the
* whole group is occupied (stacking is still electrically traced).
*
* `allPinNames` is the breadboard element's full pin list (from pinInfo);
* "nearest" is by index distance within the group, which matches physical
* adjacency for both strips and rails.
*/
export function resolveFreeHole(
metadataId: string,
componentId: string,
pinName: string,
wires: Wire[],
allPinNames: string[],
): string {
if (!holeIsOccupied(wires, componentId, pinName)) return pinName;
const group = breadboardGroupKey(metadataId, pinName);
if (!group) return pinName;
const clickedIdx = allPinNames.indexOf(pinName);
const candidates = allPinNames
.filter(
(name) =>
name !== pinName &&
breadboardGroupKey(metadataId, name) === group &&
!holeIsOccupied(wires, componentId, name),
)
.sort(
(a, b) =>
Math.abs(allPinNames.indexOf(a) - clickedIdx) -
Math.abs(allPinNames.indexOf(b) - clickedIdx),
);
return candidates[0] ?? pinName;
}