From d9e0a716064c8d26aae402acded9e81a244da0ab Mon Sep 17 00:00:00 2001 From: Wilson Xu Date: Mon, 4 May 2026 22:04:00 +0800 Subject: [PATCH 1/2] fix(MspConnectionPairSolver): prevent net-label-only pins from producing spurious wire traces Fixes #79 The root cause was a shared-reference bug in getConnectivityMapsFromInputProblem: new ConnectivityMap(directConnMap.netMap) passed netMap by reference, so subsequent addConnections calls on netConnMap (for netConnections like VCC/GND labels) mutated directConnMap.netMap arrays as well. This caused net-label-only pins (e.g. a capacitor pin with only a VCC label, no wire connection) to be merged into direct-wire nets, producing spurious MSP pairs and wire traces. Fix: deep-clone each array in directConnMap.netMap before constructing netConnMap, so mutations remain isolated. Additionally, _step() now uses dcConnMap (not globalConnMap) to look up pins for direct-wire nets, ensuring net-label-only pins that were merged into the global net are excluded from wire routing. Net-label-only nets (added exclusively via netConnections) continue to use globalConnMap so they still generate correct MSP pairs when multiple pins share the same net label. Updated snapshots for examples 07, 13, 15, 18, 21, 27 to reflect the corrected routing (fewer spurious cross-component wire connections via shared net labels). Co-Authored-By: Claude Sonnet 4.6 --- .../MspConnectionPairSolver.ts | 20 ++- .../getConnectivityMapFromInputProblem.ts | 7 +- .../examples/__snapshots__/example07.snap.svg | 12 +- .../examples/__snapshots__/example13.snap.svg | 21 ++- .../examples/__snapshots__/example15.snap.svg | 112 ++++++++----- .../examples/__snapshots__/example18.snap.svg | 120 +++++++------- .../examples/__snapshots__/example21.snap.svg | 147 +++++++++--------- .../examples/__snapshots__/example27.snap.svg | 5 +- .../MspConnectionPairSolver_repro79.test.ts | 71 +++++++++ 9 files changed, 329 insertions(+), 186 deletions(-) create mode 100644 tests/solvers/MspConnectionPairSolver/MspConnectionPairSolver_repro79.test.ts diff --git a/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts b/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts index 48b46c90..bda85335 100644 --- a/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +++ b/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts @@ -74,7 +74,16 @@ export class MspConnectionPairSolver extends BaseSolver { } } - this.queuedDcNetIds = Object.keys(netConnMap.netMap) + // Queue all direct-wire nets from dcConnMap plus any nets that exist only in + // globalConnMap (net-label-only nets such as GND shared via netConnections). + // Because directConnMap and netConnMap now have independent netMap arrays (deep + // clone in getConnectivityMapsFromInputProblem), net-label pins that happen to + // share a name with a direct-wire pin are NOT merged into the direct-wire net. + const directNetIds = new Set(Object.keys(directConnMap.netMap)) + const netLabelOnlyNetIds = Object.keys(netConnMap.netMap).filter( + (id) => !directNetIds.has(id), + ) + this.queuedDcNetIds = [...directNetIds, ...netLabelOnlyNetIds] } override getConstructorParams(): ConstructorParameters< @@ -93,7 +102,14 @@ export class MspConnectionPairSolver extends BaseSolver { const dcNetId = this.queuedDcNetIds.shift()! - const allIds = this.globalConnMap.getIdsConnectedToNet(dcNetId) as string[] + // For direct-wire nets use dcConnMap so that net-label-only pins merged into + // the same global net are not included in wire routing. For net-label-only + // nets (not present in dcConnMap) fall back to globalConnMap. + const dcIds = this.dcConnMap.getIdsConnectedToNet(dcNetId) as string[] + const allIds = + dcIds.length > 0 + ? dcIds + : (this.globalConnMap.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..0fe8fde1 100644 --- a/lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem.ts +++ b/lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem.ts @@ -14,7 +14,12 @@ export const getConnectivityMapsFromInputProblem = ( ]) } - const netConnMap = new ConnectivityMap(directConnMap.netMap) + // Deep-clone each array so that mutations in netConnMap (e.g. merging net-label + // pins into an existing direct-wire net) do NOT corrupt directConnMap. + const clonedNetMap = Object.fromEntries( + Object.entries(directConnMap.netMap).map(([k, v]) => [k, [...v]]), + ) + const netConnMap = new ConnectivityMap(clonedNetMap) for (const netConn of inputProblem.netConnections) { netConnMap.addConnections([[netConn.netId, ...netConn.pinIds]]) diff --git a/tests/examples/__snapshots__/example07.snap.svg b/tests/examples/__snapshots__/example07.snap.svg index 7098ddac..91ca6743 100644 --- a/tests/examples/__snapshots__/example07.snap.svg +++ b/tests/examples/__snapshots__/example07.snap.svg @@ -50,7 +50,7 @@ y-" data-x="1.757519574999999" data-y="1.2" cx="547.3539750075635" cy="270.41751 +orientation: y+" data-x="-2.3148566499999994" data-y="0.7512093000000004" cx="70.92192940766586" cy="322.9220591216218" r="3" fill="hsl(40, 100%, 50%, 0.9)" /> - - - - + @@ -125,6 +122,9 @@ orientation: x+" data-x="1.757519574999999" data-y="0.85" cx="547.3539750075635" + + + @@ -137,7 +137,7 @@ orientation: x+" data-x="1.757519574999999" data-y="0.85" cx="547.3539750075635" +available orientations: y+" data-x="-2.3148566499999994" data-y="0.9762093000000004" x="59.22281274267999" y="270.2760341291854" width="23.398233329971788" height="52.64602499243642" fill="#ef444466" stroke="#ef4444" stroke-width="0.00854765388392857" /> +orientation: y+" data-x="-3.75" data-y="1.125" cx="46.500290191526375" cy="279.37318630296" r="3" fill="hsl(40, 100%, 50%, 0.9)" /> + + + - - - - + @@ -225,6 +226,9 @@ orientation: y+" data-x="1.96375" data-y="3" cx="417.91062100986653" cy="157.492 + + + @@ -277,7 +281,12 @@ available orientations: any" data-x="-2.125" data-y="0.225" x="145.6297156123041 +available orientations: y+" data-x="-3.75" data-y="1.35" x="39.99999999999997" y="250.12188044109115" width="13.000580383052835" height="29.251305861868843" fill="#ef444466" stroke="#ef4444" stroke-width="0.015383928571428571" /> + + + +orientation: y+" data-x="-1.5675" data-y="6.705000000000002" cx="341.08322324966974" cy="62.19286657859976" r="3" fill="hsl(40, 100%, 50%, 0.9)" /> + + + + + + + + + + + + + + + +orientation: y+" data-x="-4.6850000000000005" data-y="3.9683333333333355" cx="187.3359753412594" cy="197.15837369734325" r="3" fill="hsl(40, 100%, 50%, 0.9)" /> + + + - - - - - - - - - - - - - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -888,7 +900,32 @@ available orientations: y+" data-x="-1.3099999999999998" data-y="1.4250000000000 +available orientations: y+" data-x="-1.5675" data-y="6.9300000000000015" x="336.15147512109206" y="40.00000000000006" width="9.863496257155418" height="22.192866578599705" fill="#ef444466" stroke="#ef4444" stroke-width="0.020276785714285723" /> + + + + + + + + + + + + + + + +available orientations: y+" data-x="-4.6850000000000005" data-y="4.193333333333335" x="182.4042272126817" y="174.96550711874357" width="9.863496257155418" height="22.192866578599705" fill="#ef444466" stroke="#ef4444" stroke-width="0.020276785714285723" /> + + + +x-" data-x="-1.4" data-y="0.42500000000000004" cx="207.72778152529193" cy="255.3187797395407" r="3" fill="hsl(88, 100%, 50%, 0.8)" /> +x-" data-x="-1.4" data-y="-0.42500000000000004" cx="207.72778152529193" cy="333.9164026436013" r="3" fill="hsl(84, 100%, 50%, 0.8)" /> +x+" data-x="1.4" data-y="0.5" cx="466.63759815043284" cy="248.383695365653" r="3" fill="hsl(81, 100%, 50%, 0.8)" /> +x+" data-x="1.4" data-y="0.30000000000000004" cx="466.63759815043284" cy="266.8772536960202" r="3" fill="hsl(86, 100%, 50%, 0.8)" /> +x+" data-x="1.4" data-y="0.10000000000000009" cx="466.63759815043284" cy="285.3708120263874" r="3" fill="hsl(85, 100%, 50%, 0.8)" /> +x+" data-x="1.4" data-y="-0.09999999999999998" cx="466.63759815043284" cy="303.86437035675465" r="3" fill="hsl(82, 100%, 50%, 0.8)" /> +x+" data-x="1.4" data-y="-0.3" cx="466.63759815043284" cy="322.35792868712184" r="3" fill="hsl(83, 100%, 50%, 0.8)" /> +x+" data-x="1.4" data-y="-0.5" cx="466.63759815043284" cy="340.85148701748903" r="3" fill="hsl(87, 100%, 50%, 0.8)" /> +y+" data-x="-2.3148566499999994" data-y="0.5512093000000002" cx="123.13300742179533" cy="243.64848448261662" r="3" fill="hsl(140, 100%, 50%, 0.8)" /> +y-" data-x="-2.31430995" data-y="-0.5512093000000002" cx="123.18355956349131" cy="345.58669790052545" r="3" fill="hsl(141, 100%, 50%, 0.8)" /> +y+" data-x="1.7580660749999977" data-y="2.3025814000000002" cx="499.74717737112326" cy="81.7029740349781" r="3" fill="hsl(125, 100%, 50%, 0.8)" /> +y-" data-x="1.757519574999999" data-y="1.2" cx="499.69664372298564" cy="183.6562412093678" r="3" fill="hsl(126, 100%, 50%, 0.8)" /> +y-" data-x="-1.7580660749999977" data-y="-3.3025814000000002" cx="174.61820230460145" cy="600" r="3" fill="hsl(6, 100%, 50%, 0.8)" /> +y+" data-x="-1.757519574999999" data-y="-2.2" cx="174.66873595273907" cy="498.0467328256103" r="3" fill="hsl(7, 100%, 50%, 0.8)" /> +y-" data-x="1.7580660749999977" data-y="-3.3025814000000002" cx="499.74717737112326" cy="600" r="3" fill="hsl(247, 100%, 50%, 0.8)" /> +y+" data-x="1.757519574999999" data-y="-2.2" cx="499.69664372298564" cy="498.0467328256103" r="3" fill="hsl(248, 100%, 50%, 0.8)" /> +orientation: y+" data-x="-2.3148566499999994" data-y="0.7512093000000004" cx="123.13300742179533" cy="225.1549261522494" r="3" fill="hsl(40, 100%, 50%, 0.9)" /> +orientation: y+" data-x="1.5999999999999999" data-y="-0.3" cx="485.1311564808" cy="322.35792868712184" r="3" fill="hsl(40, 100%, 50%, 0.9)" /> +orientation: y+" data-x="1.7580660749999977" data-y="2.3025814000000002" cx="499.74717737112326" cy="81.7029740349781" r="3" fill="hsl(40, 100%, 50%, 0.9)" /> +orientation: y-" data-x="-2.31430995" data-y="-0.7512093000000004" cx="123.18355956349131" cy="364.08025623089264" r="3" fill="hsl(40, 100%, 50%, 0.9)" /> +orientation: x+" data-x="1.757519574999999" data-y="0.85" cx="499.69664372298564" cy="216.0199682875104" r="3" fill="hsl(40, 100%, 50%, 0.9)" /> - + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + +available orientations: y+" data-x="1.5999999999999999" data-y="-0.07499999999999998" x="475.88437731561635" y="280.74742244379564" width="18.49355833036725" height="41.6105062433262" fill="#ef444466" stroke="#ef4444" stroke-width="0.010814576428571431" /> +available orientations: y+" data-x="1.7580660749999977" data-y="2.5285814" x="490.50039820593963" y="40.00000000000006" width="18.49355833036725" height="41.61050624332623" fill="#ef444466" stroke="#ef4444" stroke-width="0.010814576428571431" /> +available orientations: y-" data-x="-2.31430995" data-y="-0.9762093000000004" x="113.93678039830769" y="364.08025623089264" width="18.49355833036722" height="41.6105062433262" fill="#00000066" stroke="#000000" stroke-width="0.010814576428571431" /> +available orientations: any" data-x="1.982519574999999" data-y="0.85" x="499.69664372298564" y="206.7731891223268" width="41.61050624332631" height="18.493558330367193" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.010814576428571431" /> +available orientations: any" data-x="1.982519574999999" data-y="-2" x="499.69664372298564" y="470.30639533005944" width="41.61050624332631" height="18.49355833036725" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.010814576428571431" />