diff --git a/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts b/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts index 552e36f59..b25944758 100644 --- a/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts +++ b/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts @@ -43,6 +43,7 @@ interface TraceCrossing { otherTrace: SolvedTracePath otherSegmentIndex: number isInitialBundleCrossing: boolean + isComponentWrapCrossing: boolean } /** @@ -197,6 +198,69 @@ export class UntangleTraceSubsolver extends BaseSolver { ) } + private _getWrappedFourPinChipId(trace: SolvedTracePath) { + const chipId = trace.pins[0]?.chipId + if (chipId === undefined || trace.pins.length < 2) return undefined + if (!trace.pins.every((pin) => pin.chipId === chipId)) return undefined + + const chip = this.input.inputProblem.chips.find( + (candidate) => candidate.chipId === chipId, + ) + if (chip?.pins.length !== 4) return undefined + return chipId + } + + private _wrapDirectionsArePerpendicularToSharedPin( + wrapTrace: SolvedTracePath, + otherTrace: SolvedTracePath, + chipId: string, + ) { + const wrapDirections = new Set( + wrapTrace.pins.map((pin) => pin._facingDirection), + ) + const sharedPinDirection = otherTrace.pins.find( + (pin) => pin.chipId === chipId, + )?._facingDirection + const wrapsVertically = wrapDirections.has("y+") && wrapDirections.has("y-") + if (wrapsVertically) { + return sharedPinDirection === "x+" || sharedPinDirection === "x-" + } + + const wrapsHorizontally = + wrapDirections.has("x+") && wrapDirections.has("x-") + return ( + wrapsHorizontally && + (sharedPinDirection === "y+" || sharedPinDirection === "y-") + ) + } + + private _isComponentWrapPair( + first: SolvedTracePath, + second: SolvedTracePath, + ) { + const firstChipId = this._getWrappedFourPinChipId(first) + if ( + firstChipId !== undefined && + this._wrapDirectionsArePerpendicularToSharedPin( + first, + second, + firstChipId, + ) + ) { + return true + } + + const secondChipId = this._getWrappedFourPinChipId(second) + return ( + secondChipId !== undefined && + this._wrapDirectionsArePerpendicularToSharedPin( + second, + first, + secondChipId, + ) + ) + } + private _findCrossing(): TraceCrossing | null { const traces = this.input.allTraces for (let firstIndex = 0; firstIndex < traces.length; firstIndex++) { @@ -209,11 +273,16 @@ export class UntangleTraceSubsolver extends BaseSolver { const otherTrace = traces[secondIndex]! if (trace.globalConnNetId === otherTrace.globalConnNetId) continue const isInitialBundleCrossing = this._isTraceBundle(trace, otherTrace) + const isComponentWrapCrossing = this._isComponentWrapPair( + trace, + otherTrace, + ) const isEligibleInitialCrossing = this.input.eligibleTraceIds?.has(trace.mspPairId) === true || this.input.eligibleTraceIds?.has(otherTrace.mspPairId) === true if ( !isInitialBundleCrossing && + !isComponentWrapCrossing && !isEligibleInitialCrossing && !this.reroutedTraceIds.has(trace.mspPairId) && !this.reroutedTraceIds.has(otherTrace.mspPairId) @@ -224,6 +293,7 @@ export class UntangleTraceSubsolver extends BaseSolver { const crossings = findPerpendicularPathCrossings( trace.tracePath, otherTrace.tracePath, + { includeTerminalSegments: isComponentWrapCrossing }, ) for (const { pathSegmentIndex, otherPathSegmentIndex } of crossings) { const crossing = { @@ -232,6 +302,7 @@ export class UntangleTraceSubsolver extends BaseSolver { otherTrace, otherSegmentIndex: otherPathSegmentIndex, isInitialBundleCrossing, + isComponentWrapCrossing, } if (!this.ignoredCrossings.has(this._crossingKey(crossing))) { return crossing @@ -299,6 +370,7 @@ export class UntangleTraceSubsolver extends BaseSolver { candidate.traceId, ).isColliding && (crossing.isInitialBundleCrossing || + crossing.isComponentWrapCrossing || !candidate.collision.isColliding), ) .sort( diff --git a/tests/bug-reports/bug-report-20260901T055358Z/__snapshots__/bug-report-20260901T055358Z.snap.svg b/tests/bug-reports/bug-report-20260901T055358Z/__snapshots__/bug-report-20260901T055358Z.snap.svg index 3cb6787e9..0edf660f1 100644 --- a/tests/bug-reports/bug-report-20260901T055358Z/__snapshots__/bug-report-20260901T055358Z.snap.svg +++ b/tests/bug-reports/bug-report-20260901T055358Z/__snapshots__/bug-report-20260901T055358Z.snap.svg @@ -1,6 +1,6 @@ - cathodeanodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeEH1EH2pin13_alt1pin14_alt1A1B12A4B9B8A5B7A6A7B6A8B5B4A9B1A12IOVDD6GPIO0GPIO1GPIO2GPIO3GPIO4GPIO5GPIO6GPIO7IOVDD5GPIO8GPIO9GPIO10GPIO11GPIO12GPIO13GPIO14GPIO15TESTENXINXOUTIOVDD4DVDD2SWCLKSWDRUNGPIO16GPIO17GPIO18GPIO19GPIO20GPIO21IOVDD3GPIO22GPIO23GPIO24GPIO25GPIO26_ADC0GPIO27_ADC1GPIO28_ADC2GPIO29_ADC3IOVDD2ADC_AVDDVREG_INVREG_VOUTUSB_DMUSB_DPUSB_VDDIOVDD1DVDD1QSPI_SD3QSPI_SCLKQSPI_SD0QSPI_SD2QSPI_SD1QSPI_SSGND/thermalpadCS23GND5CLK7VCCEPVINGNDENNCVOUTgnd1gnd2X1X212341234cathodeanodecathodeanodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathode1111cathodeanodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeEH1EH2pin13_alt1pin14_alt1A1B12A4B9B8A5B7A6A7B6A8B5B4A9B1A12IOVDD6GPIO0GPIO1GPIO2GPIO3GPIO4GPIO5GPIO6GPIO7IOVDD5GPIO8GPIO9GPIO10GPIO11GPIO12GPIO13GPIO14GPIO15TESTENXINXOUTIOVDD4DVDD2SWCLKSWDRUNGPIO16GPIO17GPIO18GPIO19GPIO20GPIO21IOVDD3GPIO22GPIO23GPIO24GPIO25GPIO26_ADC0GPIO27_ADC1GPIO28_ADC2GPIO29_ADC3IOVDD2ADC_AVDDVREG_INVREG_VOUTUSB_DMUSB_DPUSB_VDDIOVDD1DVDD1QSPI_SD3QSPI_SCLKQSPI_SD0QSPI_SD2QSPI_SD1QSPI_SSGND/thermalpadCS23GND5CLK7VCCEPVINGNDENNCVOUTgnd1gnd2X1X212341234cathodeanodecathodeanodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathodeanodecathode1111XXXXXXXXXINXIN + + A1A0 + + + Y14213C3C4XXXXXX + + \ No newline at end of file diff --git a/tests/repros/repro-four-pin-crystal-crossing.test.ts b/tests/repros/repro-four-pin-crystal-crossing.test.ts new file mode 100644 index 000000000..cb95906fe --- /dev/null +++ b/tests/repros/repro-four-pin-crystal-crossing.test.ts @@ -0,0 +1,111 @@ +import { expect, test } from "bun:test" +import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver" +import { findPerpendicularPathCrossings } from "lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import type { InputProblem } from "lib/types/InputProblem" +import "tests/fixtures/matcher" + +const inputProblem: InputProblem = { + chips: [ + { + chipId: "Y1", + center: { x: 0, y: 0 }, + width: 1.08, + height: 1.42, + pins: [ + { pinId: "Y1.4", x: 0, y: 0.71, _facingDirection: "y+" }, + { pinId: "Y1.2", x: -0.02, y: -0.71, _facingDirection: "y-" }, + { pinId: "Y1.1", x: -0.54, y: -0.01, _facingDirection: "x-" }, + { pinId: "Y1.3", x: 0.54, y: -0.01, _facingDirection: "x+" }, + ], + }, + { + chipId: "C3", + center: { x: -0.885, y: -1.61 }, + width: 0.92, + height: 0.76, + pins: [ + { pinId: "C3.1", x: -1.02, y: -1.23, _facingDirection: "y+" }, + { pinId: "C3.2", x: -1.02, y: -1.99, _facingDirection: "y-" }, + ], + }, + { + chipId: "C4", + center: { x: 1.155, y: -1.61 }, + width: 0.92, + height: 0.76, + pins: [ + { pinId: "C4.1", x: 1.02, y: -1.23, _facingDirection: "y+" }, + { pinId: "C4.2", x: 1.02, y: -1.99, _facingDirection: "y-" }, + ], + }, + ], + directConnections: [], + netConnections: [ + { + netId: "GND", + isGround: true, + netLabelWidth: 0.42, + netLabelHeight: 0.48, + pinIds: ["Y1.4", "Y1.2", "C3.2", "C4.2"], + }, + { + netId: "A1", + allowInlineNetLabel: true, + netLabelWidth: 0.36, + anchoredNetLabelWidth: 0.36, + inlineNetLabelHeight: 0.12, + inlineNetLabelWidth: 0.24, + pinIds: ["Y1.1", "C3.1"], + }, + { + netId: "A0", + allowInlineNetLabel: true, + netLabelWidth: 0.36, + anchoredNetLabelWidth: 0.36, + inlineNetLabelHeight: 0.12, + inlineNetLabelWidth: 0.24, + pinIds: ["Y1.3", "C4.1"], + }, + ], + textBoxes: [ + { chipId: "Y1", center: { x: 0.52, y: 0.51 }, width: 0.24, height: 0.18 }, + { chipId: "Y1", center: { x: 1.12, y: 0.25 }, width: 1.44, height: 0.18 }, + ], + availableNetLabelOrientations: { + A1: ["x-", "x+"], + GND: ["y-"], + A0: ["x-", "x+"], + }, + maxMspPairDistance: 2.4, +} + +const countCrossings = (traces: SolvedTracePath[]) => { + let count = 0 + for (let firstIndex = 0; firstIndex < traces.length; firstIndex++) { + for ( + let secondIndex = firstIndex + 1; + secondIndex < traces.length; + secondIndex++ + ) { + const firstTrace = traces[firstIndex]! + const secondTrace = traces[secondIndex]! + if (firstTrace.globalConnNetId === secondTrace.globalConnNetId) continue + count += findPerpendicularPathCrossings( + firstTrace.tracePath, + secondTrace.tracePath, + { includeTerminalSegments: true }, + ).length + } + } + return count +} + +test("routes a four-pin crystal without crossing its signal traces", () => { + const solver = new SchematicTracePipelineSolver(inputProblem) + solver.solve() + + const traces = solver.netLabelToTraceSolver!.getOutput().traces + expect(countCrossings(traces)).toBe(0) + expect(solver).toMatchSolverSnapshot(import.meta.path) +})