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" />
@@ -218,12 +224,12 @@ available orientations: any" data-x="1.982519574999999" data-y="-2" x="493.97982
// Calculate real coordinates using inverse transformation
const matrix = {
- "a": 89.52604718777262,
+ "a": 92.46779165183602,
"c": 0,
- "e": 336.63604454867266,
+ "e": 337.18268983786237,
"b": 0,
- "d": -89.52604718777262,
- "f": 304.3329417421398
+ "d": -92.46779165183602,
+ "f": 294.617591191571
};
// Manually invert and apply the affine transform
// Since we only use translate and scale, we can directly compute:
diff --git a/tests/examples/__snapshots__/example21.snap.svg b/tests/examples/__snapshots__/example21.snap.svg
index 85810dff..08266c38 100644
--- a/tests/examples/__snapshots__/example21.snap.svg
+++ b/tests/examples/__snapshots__/example21.snap.svg
@@ -2,242 +2,239 @@
+x-" data-x="-1.4" data-y="-0.6" cx="59.38834391229081" cy="316.7686093479515" r="3" fill="hsl(65, 100%, 50%, 0.8)" />
+x-" data-x="-1.4" data-y="0.6" cx="59.38834391229081" cy="239.2152336987882" r="3" fill="hsl(66, 100%, 50%, 0.8)" />
+x+" data-x="1.4" data-y="0.6" cx="240.34622042700522" cy="239.2152336987882" r="3" fill="hsl(67, 100%, 50%, 0.8)" />
+x+" data-x="1.4" data-y="-0.6" cx="240.34622042700522" cy="316.7686093479515" r="3" fill="hsl(68, 100%, 50%, 0.8)" />
+x-" data-x="3.785" data-y="-0.29999999999999993" cx="394.4835545297173" cy="297.3802654356607" r="3" fill="hsl(175, 100%, 50%, 0.8)" />
+y-" data-x="4.785" data-y="-1.2999999999999998" cx="459.11136757068675" cy="362.0080784766301" r="3" fill="hsl(176, 100%, 50%, 0.8)" />
+x-" data-x="3.785" data-y="-0.4999999999999999" cx="394.4835545297173" cy="310.30582804385455" r="3" fill="hsl(177, 100%, 50%, 0.8)" />
+x+" data-x="5.785" data-y="-0.3999999999999999" cx="523.7391806116561" cy="303.8430467397576" r="3" fill="hsl(179, 100%, 50%, 0.8)" />
+y+" data-x="6.7" data-y="-0.39999999999999925" cx="582.8736295441432" cy="303.8430467397576" r="3" fill="hsl(218, 100%, 50%, 0.8)" />
+y-" data-x="6.7" data-y="-1.4999999999999993" cx="582.8736295441432" cy="374.93364108482393" r="3" fill="hsl(219, 100%, 50%, 0.8)" />
+y+" data-x="2.8699999999999997" data-y="-1.2" cx="335.3491055972303" cy="355.5452971725332" r="3" fill="hsl(337, 100%, 50%, 0.8)" />
+y-" data-x="2.8699999999999997" data-y="-2.3" cx="335.3491055972303" cy="426.63589151759953" r="3" fill="hsl(338, 100%, 50%, 0.8)" />
+y+" data-x="2.9752723250000006" data-y="0.6000000000000003" cx="342.1526257357185" cy="239.21523369878818" r="3" fill="hsl(82, 100%, 50%, 0.8)" />
+y-" data-x="2.9752723250000006" data-y="-0.4999999999999998" cx="342.1526257357185" cy="310.30582804385455" r="3" fill="hsl(83, 100%, 50%, 0.8)" />
+orientation: y-" data-x="-1.5999999999999999" data-y="-1.2" cx="46.462781304096936" cy="355.5452971725332" r="3" fill="hsl(40, 100%, 50%, 0.9)" />
+orientation: y-" data-x="2.8699999999999997" data-y="-2.5" cx="335.3491055972303" cy="439.56145412579343" r="3" fill="hsl(40, 100%, 50%, 0.9)" />
+orientation: y+" data-x="2.8699999999999997" data-y="-1.2" cx="335.3491055972303" cy="355.5452971725332" r="3" fill="hsl(40, 100%, 50%, 0.9)" />
+orientation: y+" data-x="3.6340000000000003" data-y="-0.24899999999999994" cx="384.7247547605309" cy="294.08424697057126" r="3" fill="hsl(40, 100%, 50%, 0.9)" />
+orientation: y+" data-x="-1.5999999999999999" data-y="1.2" cx="46.462781304096936" cy="200.43854587420654" r="3" fill="hsl(40, 100%, 50%, 0.9)" />
+orientation: y+" data-x="6.7" data-y="-0.19999999999999923" cx="582.8736295441432" cy="290.9174841315637" r="3" fill="hsl(40, 100%, 50%, 0.9)" />
+orientation: y+" data-x="1.551" data-y="0.651" cx="250.1050201961916" cy="235.91921523369876" r="3" fill="hsl(40, 100%, 50%, 0.9)" />
+orientation: x-" data-x="2.9752723250000006" data-y="-0.5999999999999999" cx="342.1526257357185" cy="316.7686093479515" r="3" fill="hsl(40, 100%, 50%, 0.9)" />
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
-
-
+
+available orientations: y-" data-x="-1.5999999999999999" data-y="-1.3499999999999999" x="39.999999999999986" y="355.5452971725332" width="12.9255626081939" height="19.388343912290793" fill="#00000066" stroke="#000000" stroke-width="0.015473214285714283" />
+available orientations: y-" data-x="2.8699999999999997" data-y="-2.65" x="328.88632429313327" y="439.56145412579343" width="12.9255626081939" height="19.38834391229085" fill="#00000066" stroke="#000000" stroke-width="0.015473214285714283" />
-
+
+available orientations: y+" data-x="3.6340000000000003" data-y="-0.09899999999999995" x="378.261973456434" y="274.6959030582804" width="12.9255626081939" height="19.38834391229085" fill="#ef444466" stroke="#ef4444" stroke-width="0.015473214285714283" />
+available orientations: y+" data-x="-1.5999999999999999" data-y="1.3499999999999999" x="39.999999999999986" y="181.05020196191572" width="12.9255626081939" height="19.38834391229082" fill="#ef444466" stroke="#ef4444" stroke-width="0.015473214285714283" />
+available orientations: y+" data-x="6.7" data-y="7.771561172376096e-16" x="576.4108482400462" y="265.0663589151759" width="12.9255626081939" height="25.8511252163878" fill="#ef444466" stroke="#ef4444" stroke-width="0.015473214285714283" />
+available orientations: y+" data-x="1.551" data-y="0.851" x="243.64223889209464" y="210.068090017311" width="12.925562608193871" height="25.85112521638777" fill="#ef444466" stroke="#ef4444" stroke-width="0.015473214285714283" />
+available orientations: x-, x+" data-x="2.675272325000001" data-y="-0.5999999999999999" x="303.3759379111368" y="310.30582804385455" width="38.7766878245817" height="12.9255626081939" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.015473214285714283" />
@@ -267,12 +264,12 @@ available orientations: x-, x+" data-x="2.675272325000001" data-y="-0.5999999999
// Calculate real coordinates using inverse transformation
const matrix = {
- "a": 63.88318503308236,
+ "a": 64.62781304096943,
"c": 0,
- "e": 155.05361624458132,
+ "e": 149.867282169648,
"b": 0,
- "d": -63.88318503308236,
- "f": 275.31371206935887
+ "d": -64.62781304096943,
+ "f": 277.99192152336985
};
// Manually invert and apply the affine transform
// Since we only use translate and scale, we can directly compute:
diff --git a/tests/examples/__snapshots__/example27.snap.svg b/tests/examples/__snapshots__/example27.snap.svg
index 584ffe28..a4dd1618 100644
--- a/tests/examples/__snapshots__/example27.snap.svg
+++ b/tests/examples/__snapshots__/example27.snap.svg
@@ -171,10 +171,7 @@ orientation: y-" data-x="-1.3" data-y="-0.10000000000000009" cx="240.86956521739
-
-
-
-
+
diff --git a/tests/solvers/MspConnectionPairSolver/MspConnectionPairSolver_repro79.test.ts b/tests/solvers/MspConnectionPairSolver/MspConnectionPairSolver_repro79.test.ts
new file mode 100644
index 00000000..1fa3dac4
--- /dev/null
+++ b/tests/solvers/MspConnectionPairSolver/MspConnectionPairSolver_repro79.test.ts
@@ -0,0 +1,72 @@
+import { test, expect } from "bun:test"
+import type { InputProblem } from "lib/types/InputProblem"
+import { MspConnectionPairSolver } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
+
+/**
+ * Regression test for issue #79.
+ *
+ * When a net (e.g. VCC) connects pins A.1 and B.1 via netConnections, and A.1 is
+ * also part of a direct wire connection, the shared-reference bug in
+ * getConnectivityMapsFromInputProblem caused B.1 to be merged into the direct-wire
+ * net — resulting in spurious MSP pairs (and therefore spurious wire traces) that
+ * included B.1.
+ *
+ * After the fix (cloning directConnMap.netMap before passing to netConnMap), the
+ * direct-wire net only contains A.1 and A.2. B.1 is only reachable via the global
+ * (net-label) connectivity and should NOT generate a wire trace.
+ */
+test("MspConnectionPairSolver_repro79 - no spurious pairs for net-label-only pins", () => {
+ // A.1 and A.2 are directly wired (should produce exactly one MSP pair)
+ // VCC net labels connect A.1 and B.1 (B.1 should NOT get a wire trace)
+ const inputProblem: InputProblem = {
+ chips: [
+ {
+ chipId: "A",
+ center: { x: 0, y: 0 },
+ width: 1,
+ height: 1,
+ pins: [
+ { pinId: "A.1", x: -0.5, y: 0 },
+ { pinId: "A.2", x: 0.5, y: 0 },
+ ],
+ },
+ {
+ chipId: "B",
+ center: { x: 3, y: 0 },
+ width: 1,
+ height: 1,
+ pins: [{ pinId: "B.1", x: 2.5, y: 0 }],
+ },
+ ],
+ directConnections: [
+ // Direct wire: A.1 -- A.2
+ { pinIds: ["A.1", "A.2"] },
+ ],
+ netConnections: [
+ // VCC label: connects A.1 and B.1 (no wire trace should be added for B.1)
+ { netId: "VCC", pinIds: ["A.1", "B.1"] },
+ ],
+ availableNetLabelOrientations: {
+ VCC: ["x+", "x-", "y+", "y-"],
+ },
+ }
+
+ const solver = new MspConnectionPairSolver({ inputProblem })
+ solver.solve()
+
+ expect(solver.solved).toBe(true)
+
+ // Only the direct wire pair A.1--A.2 should exist.
+ // B.1 must NOT appear in any MSP pair since it is connected only via net label.
+ const pairPinIds = solver.mspConnectionPairs.flatMap((p) =>
+ p.pins.map((pin) => pin.pinId),
+ )
+
+ expect(pairPinIds).not.toContain("B.1")
+
+ // Exactly one pair: A.1 <-> A.2
+ expect(solver.mspConnectionPairs).toHaveLength(1)
+ const pair = solver.mspConnectionPairs[0]!
+ const pairIds = pair.pins.map((p) => p.pinId).sort()
+ expect(pairIds).toEqual(["A.1", "A.2"])
+})