91 lines
3.1 KiB
TypeScript
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;
|
||
|
|
}
|