diff --git a/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts b/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts index d5f9c514c..bdfe98d05 100644 --- a/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +++ b/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts @@ -15,6 +15,7 @@ import { doesPairCrossRestrictedCenterLines } from "./doesPairCrossRestrictedCen import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInputProblem" import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins" import { isLabeledPeripheralConnection } from "./isLabeledPeripheralConnection" +import { replaceIntraComponentMspEdges } from "./replaceIntraComponentMspEdges" export type MspConnectionPairId = string export const DEFAULT_MAX_MSP_PAIR_DISTANCE = 1 @@ -173,8 +174,11 @@ export class MspConnectionPairSolver extends BaseSolver { PinId, InputPin & { chipId: string } > - const msp = getOrthogonalMinimumSpanningTree( - directlyConnectedPins.map((p) => this.pinMap[p]!).filter(Boolean), + const connectedPins = directlyConnectedPins + .map((pinId) => this.pinMap[pinId]!) + .filter(Boolean) + const minimumSpanningTree = getOrthogonalMinimumSpanningTree( + connectedPins, { maxDistance: this.maxMspPairDistance, forbidEdge: (a, b) => @@ -192,6 +196,14 @@ export class MspConnectionPairSolver extends BaseSolver { }), }, ) + const msp = replaceIntraComponentMspEdges({ + edges: minimumSpanningTree, + pins: connectedPins, + inputProblem: this.inputProblem, + chipMap: this.chipMap, + pinIdMap, + maxDistance: this.maxMspPairDistance, + }) for (const [pin1, pin2] of msp) { const p1Obj = this.pinMap[pin1!]! diff --git a/lib/solvers/MspConnectionPairSolver/replaceIntraComponentMspEdges.ts b/lib/solvers/MspConnectionPairSolver/replaceIntraComponentMspEdges.ts new file mode 100644 index 000000000..a07f85954 --- /dev/null +++ b/lib/solvers/MspConnectionPairSolver/replaceIntraComponentMspEdges.ts @@ -0,0 +1,211 @@ +import type { + InputChip, + InputPin, + InputProblem, + PinId, +} from "lib/types/InputProblem" +import { arePinsInDifferentSchematicSections } from "lib/utils/arePinsInDifferentSchematicSections" +import { doesPairCrossRestrictedCenterLines } from "./doesPairCrossRestrictedCenterLines" + +type PinWithChip = InputPin & { chipId: string } +type MspEdge = [PinId, PinId] +const MIN_HUB_DEGREE_WITH_LOCAL_EDGE = 3 +const CHIP_EDGE_TOLERANCE = 1e-6 + +interface ReplacementCandidate { + edge: MspEdge + distance: number + lowerPinId: PinId + upperPinId: PinId +} + +const getConnectedPinIds = ({ + startPinId, + edges, +}: { + startPinId: PinId + edges: MspEdge[] +}) => { + const adjacencyMap = new Map() + for (const [firstPinId, secondPinId] of edges) { + adjacencyMap.set(firstPinId, [ + ...(adjacencyMap.get(firstPinId) ?? []), + secondPinId, + ]) + adjacencyMap.set(secondPinId, [ + ...(adjacencyMap.get(secondPinId) ?? []), + firstPinId, + ]) + } + + const connectedPinIds = new Set([startPinId]) + const queue = [startPinId] + for (let queueIndex = 0; queueIndex < queue.length; queueIndex++) { + const pinId = queue[queueIndex]! + for (const adjacentPinId of adjacencyMap.get(pinId) ?? []) { + if (connectedPinIds.has(adjacentPinId)) continue + connectedPinIds.add(adjacentPinId) + queue.push(adjacentPinId) + } + } + + return connectedPinIds +} + +const getManhattanDistance = ({ + firstPin, + secondPin, +}: { + firstPin: InputPin + secondPin: InputPin +}) => Math.abs(firstPin.x - secondPin.x) + Math.abs(firstPin.y - secondPin.y) + +const hasPinsOnSingleAxisOfChipEdges = (chip: InputChip) => { + const leftEdge = chip.center.x - chip.width / 2 + const rightEdge = chip.center.x + chip.width / 2 + const topEdge = chip.center.y + chip.height / 2 + const bottomEdge = chip.center.y - chip.height / 2 + const allPinsOnVerticalEdges = chip.pins.every( + (pin) => + Math.abs(pin.x - leftEdge) < CHIP_EDGE_TOLERANCE || + Math.abs(pin.x - rightEdge) < CHIP_EDGE_TOLERANCE, + ) + const allPinsOnHorizontalEdges = chip.pins.every( + (pin) => + Math.abs(pin.y - topEdge) < CHIP_EDGE_TOLERANCE || + Math.abs(pin.y - bottomEdge) < CHIP_EDGE_TOLERANCE, + ) + + return allPinsOnVerticalEdges || allPinsOnHorizontalEdges +} + +const findReplacementEdge = ({ + connectedPinIds, + pins, + inputProblem, + chipMap, + pinIdMap, + maxDistance, +}: { + connectedPinIds: ReadonlySet + pins: PinWithChip[] + inputProblem: InputProblem + chipMap: Record + pinIdMap: Map + maxDistance: number +}): MspEdge | null => { + let bestCandidate: ReplacementCandidate | null = null + + for (const firstPin of pins) { + for (const secondPin of pins) { + if (!connectedPinIds.has(firstPin.pinId)) continue + if (connectedPinIds.has(secondPin.pinId)) continue + if (firstPin.chipId === secondPin.chipId) continue + + const distance = getManhattanDistance({ firstPin, secondPin }) + if (distance > maxDistance) continue + if ( + arePinsInDifferentSchematicSections(inputProblem, firstPin, secondPin) + ) { + continue + } + if ( + doesPairCrossRestrictedCenterLines({ + inputProblem, + chipMap, + pinIdMap, + p1: firstPin, + p2: secondPin, + }) + ) { + continue + } + + const edge: MspEdge = [firstPin.pinId, secondPin.pinId] + const candidate: ReplacementCandidate = { + edge, + distance, + lowerPinId: firstPin.pinId, + upperPinId: secondPin.pinId, + } + if (candidate.lowerPinId > candidate.upperPinId) { + candidate.lowerPinId = secondPin.pinId + candidate.upperPinId = firstPin.pinId + } + if (!bestCandidate || candidate.distance < bestCandidate.distance) { + bestCandidate = candidate + continue + } + if ( + candidate.distance === bestCandidate.distance && + (candidate.lowerPinId < bestCandidate.lowerPinId || + (candidate.lowerPinId === bestCandidate.lowerPinId && + candidate.upperPinId < bestCandidate.upperPinId)) + ) { + bestCandidate = candidate + } + } + } + + return bestCandidate?.edge ?? null +} + +/** Reconnects avoidable same-component tree edges through nearby components. */ +export const replaceIntraComponentMspEdges = ({ + edges, + pins, + inputProblem, + chipMap, + pinIdMap, + maxDistance, +}: { + edges: MspEdge[] + pins: PinWithChip[] + inputProblem: InputProblem + chipMap: Record + pinIdMap: Map + maxDistance: number +}) => { + const outputEdges = [...edges] + + for (let edgeIndex = 0; edgeIndex < outputEdges.length; edgeIndex++) { + const [firstPinId, secondPinId] = outputEdges[edgeIndex]! + const firstPin = pinIdMap.get(firstPinId)! + const secondPin = pinIdMap.get(secondPinId)! + if (firstPin.chipId !== secondPin.chipId) continue + if (!hasPinsOnSingleAxisOfChipEdges(chipMap[firstPin.chipId]!)) continue + + const firstPinDegree = outputEdges.filter((edge) => + edge.includes(firstPinId), + ).length + const secondPinDegree = outputEdges.filter((edge) => + edge.includes(secondPinId), + ).length + const connectsLeafToHub = + (firstPinDegree === 1 && + secondPinDegree >= MIN_HUB_DEGREE_WITH_LOCAL_EDGE) || + (secondPinDegree === 1 && + firstPinDegree >= MIN_HUB_DEGREE_WITH_LOCAL_EDGE) + if (!connectsLeafToHub) continue + + const remainingEdges = outputEdges.filter( + (_edge, candidateIndex) => candidateIndex !== edgeIndex, + ) + const connectedPinIds = getConnectedPinIds({ + startPinId: firstPinId, + edges: remainingEdges, + }) + const replacementEdge = findReplacementEdge({ + connectedPinIds, + pins, + inputProblem, + chipMap, + pinIdMap, + maxDistance, + }) + if (!replacementEdge) continue + outputEdges[edgeIndex] = replacementEdge + } + + return outputEdges +} diff --git a/tests/functions/replaceIntraComponentMspEdges.test.ts b/tests/functions/replaceIntraComponentMspEdges.test.ts new file mode 100644 index 000000000..9be9c26b1 --- /dev/null +++ b/tests/functions/replaceIntraComponentMspEdges.test.ts @@ -0,0 +1,63 @@ +import { expect, test } from "bun:test" +import { replaceIntraComponentMspEdges } from "lib/solvers/MspConnectionPairSolver/replaceIntraComponentMspEdges" +import type { InputProblem, PinId } from "lib/types/InputProblem" + +test("replaces an avoidable intra-component MSP edge", () => { + const pins = [ + { pinId: "U1.1", chipId: "U1", x: 0, y: 1 }, + { pinId: "U1.2", chipId: "U1", x: 0, y: 0 }, + { pinId: "C1.1", chipId: "C1", x: -2, y: 1 }, + { pinId: "R1.1", chipId: "R1", x: 2, y: 1 }, + ] + const inputProblem: InputProblem = { + chips: [ + { + chipId: "U1", + center: { x: 1, y: 0.5 }, + width: 2, + height: 2, + pins: pins.filter((pin) => pin.chipId === "U1"), + }, + { + chipId: "C1", + center: { x: -2.5, y: 1 }, + width: 1, + height: 1, + pins: pins.filter((pin) => pin.chipId === "C1"), + }, + { + chipId: "R1", + center: { x: 2.5, y: 1 }, + width: 1, + height: 1, + pins: pins.filter((pin) => pin.chipId === "R1"), + }, + ], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: {}, + } + const chipMap = Object.fromEntries( + inputProblem.chips.map((chip) => [chip.chipId, chip]), + ) + const pinIdMap = new Map(pins.map((pin) => [pin.pinId as PinId, pin])) + + const edges = replaceIntraComponentMspEdges({ + edges: [ + ["U1.2", "U1.1"], + ["C1.1", "U1.1"], + ["R1.1", "U1.1"], + ], + pins, + inputProblem, + chipMap, + pinIdMap, + maxDistance: 5, + }) + + expect(edges).toEqual([ + ["U1.2", "C1.1"], + ["C1.1", "U1.1"], + ["R1.1", "U1.1"], + ]) +}) diff --git a/tests/repros/__snapshots__/component-5-v5.snap.svg b/tests/repros/__snapshots__/component-5-v5.snap.svg new file mode 100644 index 000000000..95e22c8cf --- /dev/null +++ b/tests/repros/__snapshots__/component-5-v5.snap.svg @@ -0,0 +1,44 @@ +Component 5V5: red = replacement, green = hub branchport 10port 12 diff --git a/tests/repros/__snapshots__/component-9-gnd.snap.svg b/tests/repros/__snapshots__/component-9-gnd.snap.svg new file mode 100644 index 000000000..02fae8d3f --- /dev/null +++ b/tests/repros/__snapshots__/component-9-gnd.snap.svg @@ -0,0 +1,44 @@ +Component 9GND: red = replacement, green = hub branchport 21port 22 diff --git a/tests/repros/assets/repro-focusbeam-v5-junctions.input.json b/tests/repros/assets/repro-focusbeam-v5-junctions.input.json new file mode 100644 index 000000000..869419c8c --- /dev/null +++ b/tests/repros/assets/repro-focusbeam-v5-junctions.input.json @@ -0,0 +1,538 @@ +{ + "chips": [ + { + "chipId": "schematic_component_0", + "center": { + "x": -2.5299999999999994, + "y": 3.43 + }, + "width": 1.0400000000000005, + "height": 0.7600000000000007, + "pins": [ + { + "pinId": "schematic_port_0", + "x": -2.7249999999999996, + "y": 3.8100000000000005, + "_facingDirection": "y+" + }, + { + "pinId": "schematic_port_1", + "x": -2.7249999999999996, + "y": 3.05, + "_facingDirection": "y-" + } + ], + "sectionId": "PowerInput" + }, + { + "chipId": "schematic_component_1", + "center": { + "x": -3.3099999999999996, + "y": 5.535 + }, + "width": 2, + "height": 0.40000000000000036, + "pins": [ + { + "pinId": "schematic_port_2", + "x": -4.31, + "y": 5.535 + }, + { + "pinId": "schematic_port_3", + "x": -2.3099999999999996, + "y": 5.535 + } + ], + "sectionId": "PowerInput" + }, + { + "chipId": "schematic_component_2", + "center": { + "x": -2.1, + "y": 1.125 + }, + "width": 0.6000000000000005, + "height": 0.6799999999999999, + "pins": [ + { + "pinId": "schematic_port_4", + "x": -1.7999999999999998, + "y": 1.1250000000000002 + }, + { + "pinId": "schematic_port_5", + "x": -2.4000000000000004, + "y": 1.1250000000000002 + } + ], + "sectionId": "PowerInput" + }, + { + "chipId": "schematic_component_3", + "center": { + "x": 0.9074999999999999, + "y": -1.625 + }, + "width": 0.845, + "height": 0.6000000000000001, + "pins": [ + { + "pinId": "schematic_port_6", + "x": 0.8099999999999998, + "y": -1.325, + "_facingDirection": "y+" + }, + { + "pinId": "schematic_port_7", + "x": 0.8099999999999998, + "y": -1.925, + "_facingDirection": "y-" + } + ], + "sectionId": "PowerInput" + }, + { + "chipId": "schematic_component_4", + "center": { + "x": -1.9925, + "y": -0.575 + }, + "width": 0.8450000000000002, + "height": 0.6000000000000001, + "pins": [ + { + "pinId": "schematic_port_8", + "x": -2.09, + "y": -0.2749999999999999, + "_facingDirection": "y+" + }, + { + "pinId": "schematic_port_9", + "x": -2.09, + "y": -0.875, + "_facingDirection": "y-" + } + ], + "sectionId": "PowerInput" + }, + { + "chipId": "schematic_component_5", + "center": { + "x": -0.09999999999999964, + "y": 3.825 + }, + "width": 2, + "height": 0.5999999999999996, + "pins": [ + { + "pinId": "schematic_port_10", + "x": -1.0999999999999996, + "y": 4.025 + }, + { + "pinId": "schematic_port_11", + "x": -1.0999999999999996, + "y": 3.825 + }, + { + "pinId": "schematic_port_12", + "x": -1.0999999999999996, + "y": 3.625 + }, + { + "pinId": "schematic_port_13", + "x": 0.9000000000000004, + "y": 3.725 + }, + { + "pinId": "schematic_port_14", + "x": 0.9000000000000004, + "y": 3.9250000000000003 + } + ], + "sectionId": "PowerInput" + }, + { + "chipId": "schematic_component_6", + "center": { + "x": -0.5499999999999996, + "y": 6.029999999999999 + }, + "width": 0.9199999999999999, + "height": 0.7599999999999998, + "pins": [ + { + "pinId": "schematic_port_15", + "x": -0.6849999999999996, + "y": 6.409999999999999, + "_facingDirection": "y+" + }, + { + "pinId": "schematic_port_16", + "x": -0.6849999999999996, + "y": 5.6499999999999995, + "_facingDirection": "y-" + } + ], + "sectionId": "PowerInput" + }, + { + "chipId": "schematic_component_7", + "center": { + "x": 2.1, + "y": -0.6650000000000003 + }, + "width": 0.6000000000000005, + "height": 0.6800000000000002, + "pins": [ + { + "pinId": "schematic_port_17", + "x": 2.4000000000000004, + "y": -0.6650000000000003 + }, + { + "pinId": "schematic_port_18", + "x": 1.7999999999999998, + "y": -0.6650000000000003 + } + ], + "sectionId": "PowerInput" + }, + { + "chipId": "schematic_component_8", + "center": { + "x": -4.63, + "y": 3.4300000000000006 + }, + "width": 0.9199999999999999, + "height": 0.7600000000000007, + "pins": [ + { + "pinId": "schematic_port_19", + "x": -4.765, + "y": 3.810000000000001, + "_facingDirection": "y+" + }, + { + "pinId": "schematic_port_20", + "x": -4.765, + "y": 3.0500000000000003, + "_facingDirection": "y-" + } + ], + "sectionId": "PowerInput" + }, + { + "chipId": "schematic_component_9", + "center": { + "x": 0, + "y": 0.625 + }, + "width": 2.2, + "height": 1.7999999999999998, + "pins": [ + { + "pinId": "schematic_port_21", + "x": -1.1, + "y": 1.325 + }, + { + "pinId": "schematic_port_22", + "x": -1.1, + "y": 1.125 + }, + { + "pinId": "schematic_port_23", + "x": -1.1, + "y": 0.9249999999999999 + }, + { + "pinId": "schematic_port_24", + "x": -1.1, + "y": 0.7249999999999999 + }, + { + "pinId": "schematic_port_25", + "x": -1.1, + "y": 0.5249999999999999 + }, + { + "pinId": "schematic_port_26", + "x": -1.1, + "y": 0.32499999999999996 + }, + { + "pinId": "schematic_port_27", + "x": -1.1, + "y": 0.125 + }, + { + "pinId": "schematic_port_28", + "x": -1.1, + "y": -0.07499999999999996 + }, + { + "pinId": "schematic_port_29", + "x": 1.1, + "y": -0.07499999999999996 + }, + { + "pinId": "schematic_port_30", + "x": 1.1, + "y": 0.12500000000000006 + }, + { + "pinId": "schematic_port_31", + "x": 1.1, + "y": 0.32500000000000007 + }, + { + "pinId": "schematic_port_32", + "x": 1.1, + "y": 0.5250000000000001 + }, + { + "pinId": "schematic_port_33", + "x": 1.1, + "y": 0.7250000000000001 + }, + { + "pinId": "schematic_port_34", + "x": 1.1, + "y": 0.925 + }, + { + "pinId": "schematic_port_35", + "x": 1.1, + "y": 1.125 + }, + { + "pinId": "schematic_port_36", + "x": 1.1, + "y": 1.325 + } + ], + "sectionId": "PowerInput" + }, + { + "chipId": "schematic_component_10", + "center": { + "x": 2.1875, + "y": 0.625 + }, + "width": 0.845, + "height": 0.6000000000000001, + "pins": [ + { + "pinId": "schematic_port_37", + "x": 2.09, + "y": 0.925, + "_facingDirection": "y+" + }, + { + "pinId": "schematic_port_38", + "x": 2.09, + "y": 0.32499999999999996, + "_facingDirection": "y-" + } + ], + "sectionId": "PowerInput" + } + ], + "directConnections": [ + { + "netId": ".R16 > .pin1 to .P1 > .S1", + "pinIds": [ + "schematic_port_4", + "schematic_port_21" + ] + }, + { + "netId": ".R22 > .pin1 to .P1 > .A7", + "pinIds": [ + "schematic_port_6", + "schematic_port_31" + ] + }, + { + "netId": ".R21 > .pin2 to .P1 > .A6", + "pinIds": [ + "schematic_port_18", + "schematic_port_30" + ] + }, + { + "netId": ".P1 > .A5 to .R17 > .pin1", + "pinIds": [ + "schematic_port_28", + "schematic_port_8" + ] + }, + { + "netId": ".P1 > .B5 to .R18 > .pin1", + "pinIds": [ + "schematic_port_34", + "schematic_port_37" + ] + }, + { + "netId": ".P1 > .B6 to .P1 > .A6", + "pinIds": [ + "schematic_port_32", + "schematic_port_30" + ] + }, + { + "netId": ".P1 > .B7 to .P1 > .A7", + "pinIds": [ + "schematic_port_29", + "schematic_port_31" + ] + } + ], + "netConnections": [ + { + "netId": "V5", + "netLabelWidth": 0.42, + "netLabelHeight": 0.36, + "pinIds": [ + "schematic_port_0", + "schematic_port_2", + "schematic_port_10", + "schematic_port_12", + "schematic_port_19", + "schematic_port_26", + "schematic_port_35" + ] + }, + { + "netId": "GND", + "netLabelWidth": 0.42, + "netLabelHeight": 0.48, + "pinIds": [ + "schematic_port_1", + "schematic_port_3", + "schematic_port_4", + "schematic_port_5", + "schematic_port_9", + "schematic_port_11", + "schematic_port_16", + "schematic_port_20", + "schematic_port_21", + "schematic_port_22", + "schematic_port_23", + "schematic_port_24", + "schematic_port_25", + "schematic_port_36", + "schematic_port_38" + ] + }, + { + "netId": "FB_N20", + "netLabelWidth": 0.84, + "pinIds": [ + "schematic_port_7" + ] + }, + { + "netId": "V3V3", + "netLabelWidth": 0.42, + "netLabelHeight": 0.6, + "pinIds": [ + "schematic_port_14", + "schematic_port_15" + ] + }, + { + "netId": "FB_N44", + "netLabelWidth": 0.84, + "pinIds": [ + "schematic_port_17" + ] + } + ], + "textBoxes": [ + { + "chipId": "schematic_component_1", + "center": { + "x": -3.0699999999999994, + "y": 5.205 + }, + "width": 1.6800000000000002, + "height": 0.17999999999999972, + "text": "TPD1E10B06DPYR" + }, + { + "chipId": "schematic_component_1", + "center": { + "x": -3.79, + "y": 5.85 + }, + "width": 0.3599999999999999, + "height": 0.2499999999999991, + "text": "D5" + }, + { + "chipId": "schematic_component_5", + "center": { + "x": -0.039999999999999813, + "y": 3.395 + }, + "width": 1.3199999999999998, + "height": 0.17999999999999972, + "text": "AP2205-W5-7" + }, + { + "chipId": "schematic_component_5", + "center": { + "x": -0.5799999999999997, + "y": 4.24 + }, + "width": 0.36000000000000004, + "height": 0.2499999999999991, + "text": "U6" + }, + { + "chipId": "schematic_component_9", + "center": { + "x": 0.25999999999999984, + "y": -0.4049999999999998 + }, + "width": 1.92, + "height": 0.17999999999999994, + "text": "USB4105-GF-A-060" + }, + { + "chipId": "schematic_component_9", + "center": { + "x": -0.5800000000000001, + "y": 1.6400000000000001 + }, + "width": 0.35999999999999993, + "height": 0.2500000000000002, + "text": "P1" + } + ], + "availableNetLabelOrientations": { + "GND": [ + "y-" + ], + "V5": [ + "y+" + ], + "V3V3": [ + "y+" + ], + "FB_N20": [ + "x-", + "x+" + ], + "FB_N44": [ + "x-", + "x+" + ] + }, + "maxMspPairDistance": 5, + "_hideRatsNet": false +} diff --git a/tests/repros/repro-focusbeam-v5-junctions.test.ts b/tests/repros/repro-focusbeam-v5-junctions.test.ts new file mode 100644 index 000000000..a20534dfa --- /dev/null +++ b/tests/repros/repro-focusbeam-v5-junctions.test.ts @@ -0,0 +1,120 @@ +import { expect, test } from "bun:test" +import { getSvgFromGraphicsObject, type GraphicsObject } from "graphics-debug" +import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import type { InputChip, InputProblem, PinId } from "lib/types/InputProblem" +import inputProblemJson from "./assets/repro-focusbeam-v5-junctions.input.json" +import "tests/fixtures/matcher" + +const LOCAL_PAIR_IDS = new Set([ + "schematic_port_10-schematic_port_12", + "schematic_port_21-schematic_port_22", +]) +const REPLACEMENT_PAIR_IDS = new Set([ + "schematic_port_10-schematic_port_0", + "schematic_port_21-schematic_port_4", +]) +const getComponentNetSvg = ({ + chip, + netId, + focusPinIds, + traces, +}: { + chip: InputChip + netId: string + focusPinIds: Set + traces: SolvedTracePath[] +}) => { + const focusPins = chip.pins.filter((pin) => focusPinIds.has(pin.pinId)) + const focusTraces = traces.filter((trace) => + trace.pins.some((pin) => focusPinIds.has(pin.pinId)), + ) + const graphics: GraphicsObject = { + rects: [ + { + center: chip.center, + width: chip.width, + height: chip.height, + fill: "#fff7ed", + }, + ], + lines: focusTraces.map((trace) => { + let strokeColor = "#15803d" + if (REPLACEMENT_PAIR_IDS.has(trace.mspPairId)) strokeColor = "#dc2626" + return { + points: trace.tracePath, + strokeColor, + strokeWidth: 0.025, + label: trace.mspPairId, + } + }), + circles: focusPins.map((pin) => ({ + center: pin, + radius: 0.035, + fill: "#111827", + })), + texts: [ + { + x: chip.center.x, + y: chip.center.y, + text: chip.chipId.replace("schematic_component_", "Component "), + fontSize: 0.14, + color: "#7c2d12", + }, + { + x: chip.center.x, + y: chip.center.y + chip.height / 2 + 0.22, + text: `${netId}: red = replacement, green = hub branch`, + fontSize: 0.12, + color: "#111827", + }, + ...focusPins.map((pin) => ({ + x: pin.x - 0.08, + y: pin.y + 0.06, + text: pin.pinId.replace("schematic_port_", "port "), + fontSize: 0.1, + color: "#111827", + })), + ], + } + + return getSvgFromGraphicsObject(graphics, { backgroundColor: "white" }) +} + +test("replaces FocusBeam local leaf-to-junction loops", () => { + const inputProblem: InputProblem = JSON.parse( + JSON.stringify(inputProblemJson), + ) + const solver = new SchematicTracePipelineSolver(inputProblem) + + solver.solve() + + const outputTraces = solver.sameNetJunctionAlignmentSolver!.outputTraces + const localTraces = outputTraces.filter((trace) => + LOCAL_PAIR_IDS.has(trace.mspPairId), + ) + const replacementTraces = outputTraces.filter((trace) => + REPLACEMENT_PAIR_IDS.has(trace.mspPairId), + ) + + expect(localTraces).toHaveLength(0) + expect(replacementTraces).toHaveLength(2) + const component5Svg = getComponentNetSvg({ + chip: inputProblem.chips.find( + (chip) => chip.chipId === "schematic_component_5", + )!, + netId: "V5", + focusPinIds: new Set(["schematic_port_10", "schematic_port_12"]), + traces: outputTraces, + }) + const component9Svg = getComponentNetSvg({ + chip: inputProblem.chips.find( + (chip) => chip.chipId === "schematic_component_9", + )!, + netId: "GND", + focusPinIds: new Set(["schematic_port_21", "schematic_port_22"]), + traces: outputTraces, + }) + expect(component5Svg).toMatchSvgSnapshot(import.meta.path, "component-5-v5") + expect(component9Svg).toMatchSvgSnapshot(import.meta.path, "component-9-gnd") +})