From 15ddd78b795519774532247cb94cd36188d97506 Mon Sep 17 00:00:00 2001 From: Joseph Steele Date: Sat, 16 May 2026 17:39:00 +0100 Subject: [PATCH] fix: don't route traces between net-label-only pins (#79) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When pins share a net only via net labels (no directConnections), the solver was still creating routed traces between them. This is the "trace jumping" bug shown in repro61 from tscircuit/core#1503: two capacitors with GND/VCC net labels on each pin end up with horizontal traces drawn between same-net pins, on top of the four expected labels. Root causes: 1. `getConnectivityMapFromInputProblem` passed `directConnMap.netMap` into the new `netConnMap` by reference. Subsequent `netConnMap.addConnections(...)` calls then mutated the "direct only" map too, so `dcConnMap` ended up containing net-only pins. Clone the netMap to keep the two views independent. 2. `MspConnectionPairSolver._step` looked up pins via `globalConnMap`, which by design unions direct + net connections. Switching to `dcConnMap` means net-only nets produce zero MSP pairs (and so zero routed traces). 3. `LongDistancePairSolver` iterated every net in `netConnMap` to fill in unconnected pins, including nets that exist only as labels — re-creating the bug at the pipeline level. It now skips any net that has no primary MSP pair: long-distance routing should only complete partially-routed nets, never invent traces for label-only nets. Tests: - New `tests/repros/repro61-net-label-only-no-trace.test.ts` locks in the fix at both the MSP and final-pipeline levels. - `MspConnectionPairSolver_repro1.test.ts` updated: GND (net-only, 3 pins) no longer produces 2 MSP pairs, so the expected count for the 2 directConnections + 1 net-only-GND input is 2, not 4. - 22 example snapshots regenerated to drop the spurious net-only traces. The visual change is consistent across them: net labels stand alone where there is no direct connection. Closes #79 --- .../LongDistancePairSolver.ts | 8 + .../MspConnectionPairSolver.ts | 6 +- .../getConnectivityMapFromInputProblem.ts | 6 +- .../examples/__snapshots__/example01.snap.svg | 75 +- .../examples/__snapshots__/example03.snap.svg | 149 ++-- .../examples/__snapshots__/example07.snap.svg | 38 +- .../examples/__snapshots__/example10.snap.svg | 60 +- .../examples/__snapshots__/example12.snap.svg | 88 ++- .../examples/__snapshots__/example13.snap.svg | 243 ++++--- .../examples/__snapshots__/example14.snap.svg | 166 +++-- .../examples/__snapshots__/example15.snap.svg | 680 ++++++++++-------- .../examples/__snapshots__/example16.snap.svg | 80 ++- .../examples/__snapshots__/example18.snap.svg | 128 ++-- .../examples/__snapshots__/example20.snap.svg | 66 +- .../examples/__snapshots__/example21.snap.svg | 173 +++-- .../examples/__snapshots__/example22.snap.svg | 72 +- .../examples/__snapshots__/example24.snap.svg | 70 +- .../examples/__snapshots__/example25.snap.svg | 172 ++--- .../examples/__snapshots__/example26.snap.svg | 70 +- .../examples/__snapshots__/example27.snap.svg | 5 +- .../examples/__snapshots__/example28.snap.svg | 43 +- .../examples/__snapshots__/example30.snap.svg | 168 +++-- .../examples/__snapshots__/example31.snap.svg | 36 +- .../examples/__snapshots__/example32.snap.svg | 250 ++++--- .../examples/__snapshots__/example33.snap.svg | 80 ++- .../repro61-net-label-only-no-trace.test.ts | 60 ++ .../MspConnectionPairSolver_repro1.test.ts | 6 +- 27 files changed, 1638 insertions(+), 1360 deletions(-) create mode 100644 tests/repros/repro61-net-label-only-no-trace.test.ts diff --git a/lib/solvers/LongDistancePairSolver/LongDistancePairSolver.ts b/lib/solvers/LongDistancePairSolver/LongDistancePairSolver.ts index 9911d394..2e7a5c5c 100644 --- a/lib/solvers/LongDistancePairSolver/LongDistancePairSolver.ts +++ b/lib/solvers/LongDistancePairSolver/LongDistancePairSolver.ts @@ -52,9 +52,11 @@ export class LongDistancePairSolver extends BaseSolver { // 1. Create initial maps and sets for efficient lookup const primaryConnectedPinIds = new Set() + const netsWithPrimaryPairs = new Set() for (const pair of primaryMspConnectionPairs) { primaryConnectedPinIds.add(pair.pins[0].pinId) primaryConnectedPinIds.add(pair.pins[1].pinId) + netsWithPrimaryPairs.add(pair.globalConnNetId) } const { netConnMap } = getConnectivityMapsFromInputProblem(inputProblem) @@ -74,6 +76,12 @@ export class LongDistancePairSolver extends BaseSolver { const addedPairKeys = new Set() for (const netId of Object.keys(netConnMap.netMap)) { + // Long-distance routing only "fills in" pins on a net that is already + // partially routed via a direct connection. Nets that exist only as + // net-label connections should remain label-only — drawing extra traces + // here is the bug seen in repro61 (issue #79). + if (!netsWithPrimaryPairs.has(netId)) continue + const allPinIdsInNet = netConnMap.getIdsConnectedToNet(netId) if (allPinIdsInNet.length < 2) continue diff --git a/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts b/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts index 5aae3806..d0742c3a 100644 --- a/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +++ b/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts @@ -94,7 +94,11 @@ export class MspConnectionPairSolver extends BaseSolver { const dcNetId = this.queuedDcNetIds.shift()! - const allIds = this.globalConnMap.getIdsConnectedToNet(dcNetId) as string[] + // Only pins that share a *direct* connection should be paired into a + // routed trace. Pins linked only via a net connection (net label) are + // expressed visually by the label itself, not by an MSP-routed trace. + // See repro61 (tscircuit/schematic-trace-solver#79). + const allIds = this.dcConnMap.getIdsConnectedToNet(dcNetId) as string[] const directlyConnectedPins = allIds.filter((id) => !!this.pinMap[id]) if (directlyConnectedPins.length <= 1) { diff --git a/lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem.ts b/lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem.ts index d3a098c3..fb9835e0 100644 --- a/lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem.ts +++ b/lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem.ts @@ -14,7 +14,11 @@ export const getConnectivityMapsFromInputProblem = ( ]) } - const netConnMap = new ConnectivityMap(directConnMap.netMap) + // ConnectivityMap stores the netMap by reference; if we passed + // `directConnMap.netMap` directly, subsequent `netConnMap.addConnections` + // calls would mutate `directConnMap` too, polluting the "direct only" view. + // Clone so the two maps stay independent. (See repro61, tscircuit/schematic-trace-solver#79.) + const netConnMap = new ConnectivityMap(structuredClone(directConnMap.netMap)) for (const netConn of inputProblem.netConnections) { netConnMap.addConnections([[netConn.netId, ...netConn.pinIds]]) diff --git a/tests/examples/__snapshots__/example01.snap.svg b/tests/examples/__snapshots__/example01.snap.svg index 2614ba80..4829176e 100644 --- a/tests/examples/__snapshots__/example01.snap.svg +++ b/tests/examples/__snapshots__/example01.snap.svg @@ -2,106 +2,111 @@ +x-" data-x="-0.8" data-y="0.2" cx="422.5742574257426" cy="289.44950495049505" r="3" fill="hsl(319, 100%, 50%, 0.8)" /> +x-" data-x="-0.8" data-y="0" cx="422.5742574257426" cy="311.62772277227725" r="3" fill="hsl(320, 100%, 50%, 0.8)" /> +x-" data-x="-0.8" data-y="-0.2" cx="422.5742574257426" cy="333.80594059405945" r="3" fill="hsl(321, 100%, 50%, 0.8)" /> +x+" data-x="0.8" data-y="-0.2" cx="600" cy="333.80594059405945" r="3" fill="hsl(322, 100%, 50%, 0.8)" /> +x+" data-x="0.8" data-y="0" cx="600" cy="311.62772277227725" r="3" fill="hsl(323, 100%, 50%, 0.8)" /> +x+" data-x="0.8" data-y="0.2" cx="600" cy="289.44950495049505" r="3" fill="hsl(324, 100%, 50%, 0.8)" /> +y+" data-x="-2" data-y="0.5" cx="289.50495049504957" cy="256.1821782178218" r="3" fill="hsl(121, 100%, 50%, 0.8)" /> +y-" data-x="-2" data-y="-0.5" cx="289.50495049504957" cy="367.0732673267327" r="3" fill="hsl(122, 100%, 50%, 0.8)" /> +y+" data-x="-4" data-y="0.5" cx="67.72277227722776" cy="256.1821782178218" r="3" fill="hsl(2, 100%, 50%, 0.8)" /> +y-" data-x="-4" data-y="-0.5" cx="67.72277227722776" cy="367.0732673267327" r="3" fill="hsl(3, 100%, 50%, 0.8)" /> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + +globalConnNetId: connectivity_net0" data-x="-1.1" data-y="0.42500000000000016" x="378.21782178217825" y="239.54851485148515" width="22.178217821782198" height="49.9009900990099" fill="#ef444466" stroke="#ef4444" stroke-width="0.009017857142857143" /> +globalConnNetId: connectivity_net1" data-x="-1.5" data-y="0" x="320" y="300.5386138613862" width="49.90099009900996" height="22.17821782178214" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.009017857142857143" /> + + + + + + +globalConnNetId: connectivity_net2" data-x="-2" data-y="-0.726" x="278.41584158415844" y="367.1841584158416" width="22.178217821782198" height="49.90099009900996" fill="#00000066" stroke="#000000" stroke-width="0.009017857142857143" />