@tscircuit/schematic-trace-solver 0.0.189 → 0.0.191

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +680 -428
  3. package/lib/solvers/InlineNetLabelSolver/getLocalTraceLabelShifts.ts +29 -0
  4. package/lib/solvers/LongDistancePairSolver/LongDistancePairSolver.ts +7 -0
  5. package/lib/solvers/LongDistancePairSolver/getParallelNetLabelPolicy.ts +98 -0
  6. package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +24 -2
  7. package/lib/solvers/MspConnectionPairSolver/getParallelRailPairs.ts +178 -0
  8. package/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts +13 -0
  9. package/lib/solvers/SameNetJunctionAlignmentSolver/SameNetJunctionAlignmentSolver.ts +4 -9
  10. package/lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts +10 -2
  11. package/lib/solvers/SameNetJunctionAlignmentSolver/collapseSameNetCycles.ts +75 -38
  12. package/package.json +1 -1
  13. package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +837 -799
  14. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +904 -866
  15. package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +533 -524
  16. package/tests/bug-reports/bug-report-20260804T171919Z/__snapshots__/bug-report-20260804T171919Z.snap.svg +2 -2
  17. package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +94 -94
  18. package/tests/bug-reports/bug-report-20260901T064117Z/bug-report-20260901T064117Z.test.ts +13 -2
  19. package/tests/bug-reports/bug-report-20260902T092425Z/__snapshots__/bug-report-20260902T092425Z.snap.svg +2 -2
  20. package/tests/bug-reports/bug-report-20260902T092425Z/bug-report-20260902T092425Z.test.ts +16 -2
  21. package/tests/bug-reports/bug-report-20260905T041712Z/__snapshots__/bug-report-20260905T041712Z.snap.svg +2 -2
  22. package/tests/bug-reports/bug-report-20260907T110144Z/__snapshots__/bug-report-20260907T110144Z.snap.svg +19 -58
  23. package/tests/bug-reports/bug-report-20260907T110144Z/bug-report-20260907T110144Z.test.ts +2 -2
  24. package/tests/bug-reports/bug-report-20260907T145640Z/__snapshots__/bug-report-20260907T145640Z.snap.svg +917 -933
  25. package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +18 -18
  26. package/tests/repros/__snapshots__/repro-repeated-power-rail-junctions.snap.svg +255 -0
  27. package/tests/repros/__snapshots__/repro-smartwatch-power-sheet.snap.svg +111 -75
  28. package/tests/repros/assets/repro-repeated-power-rail-junctions.input.json +927 -0
  29. package/tests/repros/repro-repeated-power-rail-junctions.test.ts +22 -0
  30. package/tests/repros/repro-trellis-core-decoupling-snake-traces.test.ts +66 -0
  31. package/tests/solvers/InlineNetLabelSolver/local-trace-label-shifts.test.ts +62 -0
  32. package/tests/solvers/LongDistancePairSolver/__snapshots__/decoupling-mixed-supplies.snap.svg +112 -0
  33. package/tests/solvers/LongDistancePairSolver/__snapshots__/horizontal-rails.snap.svg +79 -0
  34. package/tests/solvers/LongDistancePairSolver/__snapshots__/vertical-rails.snap.svg +86 -0
  35. package/tests/solvers/LongDistancePairSolver/parallel-decoupling-labels.test.ts +505 -0
  36. package/tests/solvers/SameNetJunctionAlignmentSolver/collapse-same-net-cycles.test.ts +30 -0
@@ -0,0 +1,86 @@
1
+ <svg width="1200" height="2016" viewBox="0 0 1200 2016" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Solver debug visualization on top and Circuit JSON schematic on the bottom">
2
+ <g transform="translate(0, 0) scale(1.875, 1.875) translate(0, 0)">
3
+ <rect width="100%" height="100%" fill="white"/><g><polyline data-points="0.4,0 0.4,2" data-type="line" data-label="" points="368.69565217391306,563.4782608695652 368.69565217391306,320" fill="none" stroke="hsl(222, 100%, 50%, 0.8)" stroke-width="1px" stroke-dasharray="4 2"/></g><g><polyline data-points="0.4,0 0.4,4" data-type="line" data-label="" points="368.69565217391306,563.4782608695652 368.69565217391306,76.52173913043475" fill="none" stroke="hsl(222, 100%, 50%, 0.8)" stroke-width="1px" stroke-dasharray="4 2"/></g><g><polyline data-points="0.4,2 0.4,4" data-type="line" data-label="" points="368.69565217391306,320 368.69565217391306,76.52173913043475" fill="none" stroke="hsl(222, 100%, 50%, 0.8)" stroke-width="1px" stroke-dasharray="4 2"/></g><g><polyline data-points="-0.4,0 -0.4,2" data-type="line" data-label="" points="271.30434782608694,563.4782608695652 271.30434782608694,320" fill="none" stroke="hsl(157, 100%, 50%, 0.8)" stroke-width="1px" stroke-dasharray="4 2"/></g><g><polyline data-points="-0.4,0 -0.4,4" data-type="line" data-label="" points="271.30434782608694,563.4782608695652 271.30434782608694,76.52173913043475" fill="none" stroke="hsl(157, 100%, 50%, 0.8)" stroke-width="1px" stroke-dasharray="4 2"/></g><g><polyline data-points="-0.4,2 -0.4,4" data-type="line" data-label="" points="271.30434782608694,320 271.30434782608694,76.52173913043475" fill="none" stroke="hsl(157, 100%, 50%, 0.8)" stroke-width="1px" stroke-dasharray="4 2"/></g><g><polyline data-points="0.4,0 0.6000000000000001,0 0.6000000000000001,2 0.4,2" data-type="line" data-label="" points="368.69565217391306,563.4782608695652 393.0434782608696,563.4782608695652 393.0434782608696,320 368.69565217391306,320" fill="none" stroke="purple" stroke-width="1px"/></g><g><polyline data-points="0.6000000000000001,2 0.6000000000000001,4 0.4,4" data-type="line" data-label="" points="393.0434782608696,320 393.0434782608696,76.52173913043475 368.69565217391306,76.52173913043475" fill="none" stroke="purple" stroke-width="1px"/></g><g><polyline data-points="-0.4,0 -0.6000000000000001,0 -0.6000000000000001,2 -0.4,2" data-type="line" data-label="" points="271.30434782608694,563.4782608695652 246.9565217391304,563.4782608695652 246.9565217391304,320 271.30434782608694,320" fill="none" stroke="purple" stroke-width="1px"/></g><g><polyline data-points="-0.6000000000000001,2 -0.6000000000000001,4 -0.4,4" data-type="line" data-label="" points="246.9565217391304,320 246.9565217391304,76.52173913043475 271.30434782608694,76.52173913043475" fill="none" stroke="purple" stroke-width="1px"/></g><g><rect data-type="rect" data-label="C1" data-x="0" data-y="0" x="271.30434782608694" y="526.9565217391305" width="97.39130434782612" height="73.0434782608695" fill="hsl(326, 100%, 50%, 0.8)" stroke="black" stroke-width="0.008214285714285714"/></g><g><rect data-type="rect" data-label="C2" data-x="0" data-y="2" x="271.30434782608694" y="283.47826086956525" width="97.39130434782612" height="73.0434782608695" fill="hsl(327, 100%, 50%, 0.8)" stroke="black" stroke-width="0.008214285714285714"/></g><g><rect data-type="rect" data-label="C3" data-x="0" data-y="4" x="271.30434782608694" y="40" width="97.39130434782612" height="73.0434782608695" fill="hsl(328, 100%, 50%, 0.8)" stroke="black" stroke-width="0.008214285714285714"/></g><g><rect data-type="rect" data-label="netId: VDD
4
+ globalConnNetId: connectivity_net0" data-x="0.8250000000000001" data-y="0" x="393.0434782608696" y="551.304347826087" width="54.78260869565213" height="24.347826086956502" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.008214285714285714"/></g><g><rect data-type="rect" data-label="netId: GND
5
+ globalConnNetId: connectivity_net1" data-x="-0.8250000000000001" data-y="0" x="192.17391304347825" y="551.304347826087" width="54.78260869565216" height="24.347826086956502" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.008214285714285714"/></g><g><circle data-type="point" data-label="C1.1
6
+ x+" data-x="0.4" data-y="0" cx="368.69565217391306" cy="563.4782608695652" r="3" fill="hsl(121, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="C1.2
7
+ x-" data-x="-0.4" data-y="0" cx="271.30434782608694" cy="563.4782608695652" r="3" fill="hsl(122, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="C2.2
8
+ x-" data-x="-0.4" data-y="2" cx="271.30434782608694" cy="320" r="3" fill="hsl(3, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="C2.1
9
+ x+" data-x="0.4" data-y="2" cx="368.69565217391306" cy="320" r="3" fill="hsl(2, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="C3.1
10
+ x+" data-x="0.4" data-y="4" cx="368.69565217391306" cy="76.52173913043475" r="3" fill="hsl(243, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="C3.2
11
+ x-" data-x="-0.4" data-y="4" cx="271.30434782608694" cy="76.52173913043475" r="3" fill="hsl(244, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="anchorPoint
12
+ orientation: x+" data-x="0.6000000000000001" data-y="0" cx="393.0434782608696" cy="563.4782608695652" r="3" fill="hsl(40, 100%, 50%, 0.9)"/></g><g><circle data-type="point" data-label="anchorPoint
13
+ orientation: x-" data-x="-0.6000000000000001" data-y="0" cx="246.9565217391304" cy="563.4782608695652" r="3" fill="hsl(40, 100%, 50%, 0.9)"/></g><g id="crosshair" style="display: none"><line id="crosshair-h" y1="0" y2="640" stroke="#666" stroke-width="0.5"/><line id="crosshair-v" x1="0" x2="640" stroke="#666" stroke-width="0.5"/><text id="coordinates" font-family="monospace" font-size="12" fill="#666"></text></g><script><![CDATA[
14
+ document.currentScript.parentElement.addEventListener('mousemove', (e) => {
15
+ const svg = e.currentTarget;
16
+ const rect = svg.getBoundingClientRect();
17
+ const x = e.clientX - rect.left;
18
+ const y = e.clientY - rect.top;
19
+ const crosshair = svg.getElementById('crosshair');
20
+ const h = svg.getElementById('crosshair-h');
21
+ const v = svg.getElementById('crosshair-v');
22
+ const coords = svg.getElementById('coordinates');
23
+
24
+ crosshair.style.display = 'block';
25
+ h.setAttribute('x1', '0');
26
+ h.setAttribute('x2', '640');
27
+ h.setAttribute('y1', y);
28
+ h.setAttribute('y2', y);
29
+ v.setAttribute('x1', x);
30
+ v.setAttribute('x2', x);
31
+ v.setAttribute('y1', '0');
32
+ v.setAttribute('y2', '640');
33
+
34
+ // Calculate real coordinates using inverse transformation
35
+ const matrix = {"a":121.73913043478262,"c":0,"e":320,"b":0,"d":-121.73913043478262,"f":563.4782608695652};
36
+ // Manually invert and apply the affine transform
37
+ // Since we only use translate and scale, we can directly compute:
38
+ // x' = (x - tx) / sx
39
+ // y' = (y - ty) / sy
40
+ const sx = matrix.a;
41
+ const sy = matrix.d;
42
+ const tx = matrix.e;
43
+ const ty = matrix.f;
44
+ const realPoint = {
45
+ x: (x - tx) / sx,
46
+ y: (y - ty) / sy // Flip y back since we used negative scale
47
+ }
48
+
49
+ coords.textContent = `(${realPoint.x.toFixed(2)}, ${realPoint.y.toFixed(2)})`;
50
+ coords.setAttribute('x', (x + 5).toString());
51
+ coords.setAttribute('y', (y - 5).toString());
52
+ });
53
+ document.currentScript.parentElement.addEventListener('mouseleave', () => {
54
+ document.currentScript.parentElement.getElementById('crosshair').style.display = 'none';
55
+ });
56
+ ]]></script>
57
+ </g>
58
+ <g transform="translate(0, 1216)">
59
+ <style>
60
+ .boundary { fill: rgb(245, 241, 237); }
61
+ .schematic-boundary { fill: none; stroke: #fff; }
62
+ .component { fill: none; stroke: rgb(132, 0, 0); }
63
+ .chip { fill: rgb(255, 255, 194); stroke: rgb(132, 0, 0); }
64
+ .component-pin { fill: none; stroke: rgb(132, 0, 0); }
65
+ .text { font-family: sans-serif; fill: rgb(0, 150, 0); }
66
+ .pin-number { fill: rgb(169, 0, 0); }
67
+ .port-label { fill: rgb(0, 100, 100); }
68
+ .component-name { fill: rgb(0, 100, 100); }
69
+
70
+ </style><rect class="boundary" x="0" y="0" width="1200" height="800"/><g class="trace sch-trace" data-layer="base" data-circuit-json-type="schematic_trace" data-schematic-trace-id="schematic_trace_0__stack1"><path d="M 654.54545454544 672.7272727273 L 681.81818181816 672.7272727273 L 681.81818181816 400.00000000010004 L 654.54545454544 400.00000000010004" class="trace-invisible-hover-outline sch-trace-hitbox" stroke="rgb(0, 150, 0)" fill="none" stroke-width="21.818181818176px" stroke-linecap="round" opacity="0" stroke-linejoin="round"/><path class="sch-trace-path" d="M 654.54545454544 672.7272727273 L 681.81818181816 672.7272727273 L 681.81818181816 400.00000000010004 L 654.54545454544 400.00000000010004" stroke="rgb(0, 150, 0)" fill="none" stroke-width="2.727272727272px" stroke-linecap="round" stroke-linejoin="round"/></g><g class="trace sch-trace" data-layer="base" data-circuit-json-type="schematic_trace" data-schematic-trace-id="schematic_trace_1__stack1"><path d="M 681.81818181816 400.00000000010004 L 681.81818181816 127.27272727290006 L 654.54545454544 127.27272727290006" class="trace-invisible-hover-outline sch-trace-hitbox" stroke="rgb(0, 150, 0)" fill="none" stroke-width="21.818181818176px" stroke-linecap="round" opacity="0" stroke-linejoin="round"/><path class="sch-trace-path" d="M 681.81818181816 400.00000000010004 L 681.81818181816 127.27272727290006 L 654.54545454544 127.27272727290006" stroke="rgb(0, 150, 0)" fill="none" stroke-width="2.727272727272px" stroke-linecap="round" stroke-linejoin="round"/></g><g class="trace sch-trace" data-layer="base" data-circuit-json-type="schematic_trace" data-schematic-trace-id="schematic_trace_2__stack1"><path d="M 545.45454545456 672.7272727273 L 518.18181818184 672.7272727273 L 518.18181818184 400.00000000010004 L 545.45454545456 400.00000000010004" class="trace-invisible-hover-outline sch-trace-hitbox" stroke="rgb(0, 150, 0)" fill="none" stroke-width="21.818181818176px" stroke-linecap="round" opacity="0" stroke-linejoin="round"/><path class="sch-trace-path" d="M 545.45454545456 672.7272727273 L 518.18181818184 672.7272727273 L 518.18181818184 400.00000000010004 L 545.45454545456 400.00000000010004" stroke="rgb(0, 150, 0)" fill="none" stroke-width="2.727272727272px" stroke-linecap="round" stroke-linejoin="round"/></g><g class="trace sch-trace" data-layer="base" data-circuit-json-type="schematic_trace" data-schematic-trace-id="schematic_trace_3__stack1"><path d="M 518.18181818184 400.00000000010004 L 518.18181818184 127.27272727290006 L 545.45454545456 127.27272727290006" class="trace-invisible-hover-outline sch-trace-hitbox" stroke="rgb(0, 150, 0)" fill="none" stroke-width="21.818181818176px" stroke-linecap="round" opacity="0" stroke-linejoin="round"/><path class="sch-trace-path" d="M 518.18181818184 400.00000000010004 L 518.18181818184 127.27272727290006 L 545.45454545456 127.27272727290006" stroke="rgb(0, 150, 0)" fill="none" stroke-width="2.727272727272px" stroke-linecap="round" stroke-linejoin="round"/></g><g class="trace-overlays sch-trace-overlays" data-layer="overlay" data-circuit-json-type="schematic_trace" data-schematic-trace-id="schematic_trace_0__stack1"><circle cx="681.81818181816" cy="400.00000000010004" r="4.090909090907999" class="trace-junction sch-trace-junction" fill="rgb(0, 150, 0)"/></g><g class="trace-overlays sch-trace-overlays" data-layer="overlay" data-circuit-json-type="schematic_trace" data-schematic-trace-id="schematic_trace_1__stack1"/><g class="trace-overlays sch-trace-overlays" data-layer="overlay" data-circuit-json-type="schematic_trace" data-schematic-trace-id="schematic_trace_2__stack1"><circle cx="518.18181818184" cy="400.00000000010004" r="4.090909090907999" class="trace-junction sch-trace-junction" fill="rgb(0, 150, 0)"/></g><g class="trace-overlays sch-trace-overlays" data-layer="overlay" data-circuit-json-type="schematic_trace" data-schematic-trace-id="schematic_trace_3__stack1"/><g class="sch-component" data-circuit-json-type="schematic_component" data-schematic-component-id="schematic_component_0__stack1"><rect class="component-overlay sch-component-overlay" x="545.45454545456" y="643.6327272727623" width="109.0909090908799" height="58.1890909090755" fill="transparent"/><path class="sch-component-symbol-path" d="M 654.54545454544 672.7272727273 L 610.909090909088 672.7272727273" stroke="rgb(132, 0, 0)" fill="none" stroke-width="2.727272727272px" stroke-linecap="round"/><path class="sch-component-symbol-path" d="M 589.090909090912 672.7272727273 L 545.45454545456 672.7272727273" stroke="rgb(132, 0, 0)" fill="none" stroke-width="2.727272727272px" stroke-linecap="round"/><path class="sch-component-symbol-path" d="M 610.909090909088 643.6327272727623 L 610.909090909088 701.8218181818378" stroke="rgb(132, 0, 0)" fill="none" stroke-width="2.727272727272px" stroke-linecap="round"/><path class="sch-component-symbol-path" d="M 589.090909090912 643.6327272727623 L 589.090909090912 701.8218181818378" stroke="rgb(132, 0, 0)" fill="none" stroke-width="2.727272727272px" stroke-linecap="round"/><text class="sch-component-name sch-component-text" x="600" y="629.090909090948" stroke="rgb(245, 241, 237)" stroke-width="2.727272727272px" paint-order="stroke" fill="rgb(15, 15, 15)" font-family="sans-serif" text-anchor="middle" dominant-baseline="ideographic" font-size="24.545454545448px">C1</text><text class="sch-component-text" x="600" y="716.3636363636521" fill="rgb(15, 15, 15)" font-family="sans-serif" text-anchor="middle" dominant-baseline="hanging" font-size="24.545454545448px"></text></g><g class="sch-component" data-circuit-json-type="schematic_component" data-schematic-component-id="schematic_component_1__stack1"><rect class="component-overlay sch-component-overlay" x="545.45454545456" y="370.90545454556235" width="109.0909090908799" height="58.18909090907539" fill="transparent"/><path class="sch-component-symbol-path" d="M 654.54545454544 400.00000000010004 L 610.909090909088 400.00000000010004" stroke="rgb(132, 0, 0)" fill="none" stroke-width="2.727272727272px" stroke-linecap="round"/><path class="sch-component-symbol-path" d="M 589.090909090912 400.00000000010004 L 545.45454545456 400.00000000010004" stroke="rgb(132, 0, 0)" fill="none" stroke-width="2.727272727272px" stroke-linecap="round"/><path class="sch-component-symbol-path" d="M 610.909090909088 370.90545454556235 L 610.909090909088 429.09454545463774" stroke="rgb(132, 0, 0)" fill="none" stroke-width="2.727272727272px" stroke-linecap="round"/><path class="sch-component-symbol-path" d="M 589.090909090912 370.90545454556235 L 589.090909090912 429.09454545463774" stroke="rgb(132, 0, 0)" fill="none" stroke-width="2.727272727272px" stroke-linecap="round"/><text class="sch-component-name sch-component-text" x="600" y="356.363636363748" stroke="rgb(245, 241, 237)" stroke-width="2.727272727272px" paint-order="stroke" fill="rgb(15, 15, 15)" font-family="sans-serif" text-anchor="middle" dominant-baseline="ideographic" font-size="24.545454545448px">C2</text><text class="sch-component-text" x="600" y="443.6363636364521" fill="rgb(15, 15, 15)" font-family="sans-serif" text-anchor="middle" dominant-baseline="hanging" font-size="24.545454545448px"></text></g><g class="sch-component" data-circuit-json-type="schematic_component" data-schematic-component-id="schematic_component_2__stack1"><rect class="component-overlay sch-component-overlay" x="545.45454545456" y="98.17818181836236" width="109.0909090908799" height="58.18909090907539" fill="transparent"/><path class="sch-component-symbol-path" d="M 654.54545454544 127.27272727290006 L 610.909090909088 127.27272727290006" stroke="rgb(132, 0, 0)" fill="none" stroke-width="2.727272727272px" stroke-linecap="round"/><path class="sch-component-symbol-path" d="M 589.090909090912 127.27272727290006 L 545.45454545456 127.27272727290006" stroke="rgb(132, 0, 0)" fill="none" stroke-width="2.727272727272px" stroke-linecap="round"/><path class="sch-component-symbol-path" d="M 610.909090909088 98.17818181836236 L 610.909090909088 156.36727272743775" stroke="rgb(132, 0, 0)" fill="none" stroke-width="2.727272727272px" stroke-linecap="round"/><path class="sch-component-symbol-path" d="M 589.090909090912 98.17818181836236 L 589.090909090912 156.36727272743775" stroke="rgb(132, 0, 0)" fill="none" stroke-width="2.727272727272px" stroke-linecap="round"/><text class="sch-component-name sch-component-text" x="600" y="83.63636363654805" stroke="rgb(245, 241, 237)" stroke-width="2.727272727272px" paint-order="stroke" fill="rgb(15, 15, 15)" font-family="sans-serif" text-anchor="middle" dominant-baseline="ideographic" font-size="24.545454545448px">C3</text><text class="sch-component-text" x="600" y="170.90909090925206" fill="rgb(15, 15, 15)" font-family="sans-serif" text-anchor="middle" dominant-baseline="hanging" font-size="24.545454545448px"></text></g><g class="schematic-port-hover sch-port-hover" data-schematic-port-id="source_port_0_0__stack1"><circle cx="668.1818181818" cy="672.7272727273" r="5.454545454544" fill="red" opacity="0"/></g><g class="schematic-port-hover sch-port-hover" data-schematic-port-id="source_port_0_1__stack1"><circle cx="559.09090909092" cy="672.7272727273" r="5.454545454544" fill="red" opacity="0"/></g><g class="schematic-port-hover sch-port-hover" data-schematic-port-id="source_port_1_0__stack1"><circle cx="559.09090909092" cy="400.00000000010004" r="5.454545454544" fill="red" opacity="0"/></g><g class="schematic-port-hover sch-port-hover" data-schematic-port-id="source_port_1_1__stack1"><circle cx="668.1818181818" cy="400.00000000010004" r="5.454545454544" fill="red" opacity="0"/></g><g class="schematic-port-hover sch-port-hover" data-schematic-port-id="source_port_2_0__stack1"><circle cx="668.1818181818" cy="127.27272727290006" r="5.454545454544" fill="red" opacity="0"/></g><g class="schematic-port-hover sch-port-hover" data-schematic-port-id="source_port_2_1__stack1"><circle cx="559.09090909092" cy="127.27272727290006" r="5.454545454544" fill="red" opacity="0"/></g><path class="net-label sch-net-label" data-circuit-json-type="schematic_net_label" data-schematic-net-label-id="schematic_net_label_0__stack1" d="
71
+ M 681.81818181816,672.7272727273
72
+ L 689.1818181817945,659.09090909094
73
+ L 735.9454545454183,659.09090909094
74
+ L 735.9454545454183,686.36363636366
75
+ L 689.1818181817945,686.36363636366
76
+ Z
77
+ " fill="rgba(255, 255, 255, 0.6)" stroke="rgb(132, 0, 0)" stroke-width="2.727272727272px"/><text class="net-label-text sch-net-label-text" data-circuit-json-type="schematic_net_label" data-schematic-net-label-id="schematic_net_label_0__stack1" x="694.090909090884" y="672.7272727273" fill="rgb(132, 0, 0)" text-anchor="start" dominant-baseline="central" font-family="sans-serif" font-variant-numeric="tabular-nums" font-size="24.545454545448px" transform="">XX</text><path class="net-label sch-net-label" data-circuit-json-type="schematic_net_label" data-schematic-net-label-id="schematic_net_label_1__stack1" d="
78
+ M 518.18181818184,672.7272727273
79
+ L 510.8181818182056,686.36363636366
80
+ L 464.05454545458167,686.36363636366
81
+ L 464.05454545458167,659.09090909094
82
+ L 510.8181818182056,659.09090909094
83
+ Z
84
+ " fill="rgba(255, 255, 255, 0.6)" stroke="rgb(132, 0, 0)" stroke-width="2.727272727272px"/><text class="net-label-text sch-net-label-text" data-circuit-json-type="schematic_net_label" data-schematic-net-label-id="schematic_net_label_1__stack1" x="505.90909090911595" y="672.7272727273" fill="rgb(132, 0, 0)" text-anchor="end" dominant-baseline="central" font-family="sans-serif" font-variant-numeric="tabular-nums" font-size="24.545454545448px" transform="">XX</text>
85
+ </g>
86
+ </svg>
@@ -0,0 +1,505 @@
1
+ import { expect, test } from "bun:test"
2
+ import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
3
+ import { getTraceConnectedPinComponents } from "lib/solvers/SchematicTraceLinesSolver/getTraceConnectedPinComponents"
4
+ import type { InputProblem } from "lib/types/InputProblem"
5
+ import "tests/fixtures/matcher"
6
+
7
+ const createBank = (): InputProblem => ({
8
+ chips: [0, 2, 4].map((x, index) => ({
9
+ chipId: `C${index + 1}`,
10
+ center: { x, y: 0 },
11
+ width: 0.6,
12
+ height: 0.8,
13
+ pins: [
14
+ { pinId: `C${index + 1}.1`, x, y: 0.4 },
15
+ { pinId: `C${index + 1}.2`, x, y: -0.4 },
16
+ ],
17
+ })),
18
+ directConnections: [],
19
+ netConnections: [
20
+ { netId: "VDD", pinIds: ["C1.1", "C2.1", "C3.1"] },
21
+ { netId: "GND", isGround: true, pinIds: ["C1.2", "C2.2", "C3.2"] },
22
+ ],
23
+ availableNetLabelOrientations: { VDD: ["y+"], GND: ["y-"] },
24
+ maxMspPairDistance: 1,
25
+ })
26
+
27
+ const solve = (input: InputProblem) => {
28
+ const solver = new SchematicTracePipelineSolver(input)
29
+ solver.solve()
30
+ expect(solver.solved).toBe(true)
31
+ expect(solver.failed).toBe(false)
32
+ return solver
33
+ }
34
+
35
+ for (const rotated of [false, true]) {
36
+ test(`aligned parallel capacitors share rails (rotated=${rotated})`, async () => {
37
+ const input = createBank()
38
+ if (rotated) {
39
+ for (const chip of input.chips) {
40
+ chip.center = { x: chip.center.y, y: chip.center.x }
41
+ ;[chip.width, chip.height] = [chip.height, chip.width]
42
+ for (const pin of chip.pins) [pin.x, pin.y] = [pin.y, pin.x]
43
+ }
44
+ input.availableNetLabelOrientations = { VDD: ["x+"], GND: ["x-"] }
45
+ }
46
+ // Rail detection should not depend on pin order or component metadata.
47
+ input.chips[1]!.pins.reverse()
48
+ const solver = solve(input)
49
+ expect(solver.longDistancePairSolver!.getOutput().newTraces).toEqual([])
50
+ const { traces, netLabelPlacements } =
51
+ solver.netLabelToTraceSolver!.getOutput()
52
+ expect(netLabelPlacements).toHaveLength(2)
53
+ expect(traces).toHaveLength(4)
54
+ for (const net of input.netConnections) {
55
+ const components = getTraceConnectedPinComponents({
56
+ pinIds: net.pinIds,
57
+ traces,
58
+ })
59
+ expect(components).toHaveLength(1)
60
+ // Every rail stays outside the component bodies on its own side.
61
+ const sign = net.netId === "GND" ? -1 : 1
62
+ for (const trace of components[0]!.traces) {
63
+ for (const point of trace.tracePath) {
64
+ expect(sign * point[rotated ? "x" : "y"]).toBeGreaterThanOrEqual(
65
+ 0.4 - 1e-6,
66
+ )
67
+ }
68
+ }
69
+ }
70
+ await expect(solver).toMatchSolverSnapshot(
71
+ import.meta.path,
72
+ rotated ? "vertical-rails" : "horizontal-rails",
73
+ )
74
+ })
75
+ }
76
+
77
+ test("keeps shared rails within the configured routing distance", () => {
78
+ const input = createBank()
79
+ input.maxMspPairDistance = 2
80
+ const solver = solve(input)
81
+ expect(solver.mspConnectionPairSolver!.mspConnectionPairs).toHaveLength(4)
82
+ expect(
83
+ solver.netLabelToTraceSolver!.getOutput().netLabelPlacements,
84
+ ).toHaveLength(2)
85
+ })
86
+
87
+ test("uniformly spaced capacitor rows retain shared rails at a wider pitch", () => {
88
+ const input = createBank()
89
+ for (const chip of input.chips) {
90
+ chip.center.x *= 3
91
+ for (const pin of chip.pins) pin.x *= 3
92
+ }
93
+ const solver = solve(input)
94
+ expect(solver.mspConnectionPairSolver!.mspConnectionPairs).toHaveLength(4)
95
+ expect(solver.longDistancePairSolver!.getOutput().newTraces).toEqual([])
96
+ const { traces, netLabelPlacements } =
97
+ solver.netLabelToTraceSolver!.getOutput()
98
+ expect(traces).toHaveLength(4)
99
+ expect(netLabelPlacements).toHaveLength(2)
100
+ })
101
+
102
+ test("preserves explicitly wired rails while labeling the independent ground pins", () => {
103
+ const input = createBank()
104
+ input.directConnections = [
105
+ { pinIds: ["C1.1", "C2.1"] },
106
+ { pinIds: ["C2.1", "C3.1"] },
107
+ ]
108
+ const solver = solve(input)
109
+ const recovered = solver.longDistancePairSolver!.getOutput().newTraces
110
+ expect(recovered.length).toBeGreaterThan(0)
111
+ expect(
112
+ recovered.every((trace) => trace.pinIds.every((pin) => pin.endsWith(".1"))),
113
+ ).toBe(true)
114
+ })
115
+
116
+ test("fallback labels allow shared rails without long-distance recovery", () => {
117
+ const input = createBank()
118
+ input.directConnections = [
119
+ { pinIds: ["C1.1", "C2.1"], netLabelWidth: 0.5 },
120
+ { pinIds: ["C2.1", "C3.1"], netLabelWidth: 0.5 },
121
+ ]
122
+ const solver = solve(input)
123
+ expect(solver.longDistancePairSolver!.getOutput().newTraces).toEqual([])
124
+ expect(
125
+ solver.netLabelToTraceSolver!.getOutput().netLabelPlacements,
126
+ ).toHaveLength(2)
127
+ })
128
+
129
+ test("recognizes a ground alias by its metadata and global connectivity", () => {
130
+ const input = createBank()
131
+ input.netConnections[1]!.netId = "return-net"
132
+ input.netConnections[1]!.isGround = true
133
+ input.netConnections.push({ netId: "alias", pinIds: ["C1.2"] })
134
+ input.availableNetLabelOrientations = { VDD: ["y+"], "return-net": ["y-"] }
135
+ const solver = solve(input)
136
+ expect(solver.longDistancePairSolver!.getOutput().newTraces).toEqual([])
137
+ expect(solver.mspConnectionPairSolver!.mspConnectionPairs).toHaveLength(4)
138
+ })
139
+
140
+ test("preserves long-distance recovery for unrelated signal connections", () => {
141
+ const input = createBank()
142
+ input.netConnections[1]!.netId = "SIGNAL"
143
+ input.netConnections[1]!.isGround = false
144
+ expect(
145
+ solve(input).longDistancePairSolver!.getOutput().newTraces.length,
146
+ ).toBeGreaterThan(0)
147
+ })
148
+
149
+ test.each([undefined, false])(
150
+ "does not infer ground from the net name when isGround=%s",
151
+ (isGround) => {
152
+ const input = createBank()
153
+ input.netConnections[1]!.isGround = isGround
154
+ const solver = solve(input)
155
+ expect(solver.mspConnectionPairSolver!.mspConnectionPairs).toHaveLength(0)
156
+ expect(
157
+ solver.longDistancePairSolver!.getOutput().newTraces.length,
158
+ ).toBeGreaterThan(0)
159
+ },
160
+ )
161
+
162
+ for (const { rotated, pitch } of [
163
+ { rotated: false, pitch: 2 },
164
+ { rotated: true, pitch: 2 },
165
+ { rotated: false, pitch: 6 },
166
+ { rotated: true, pitch: 6 },
167
+ ]) {
168
+ test(`large gaps separate aligned capacitor banks (rotated=${rotated}, pitch=${pitch})`, () => {
169
+ const input = createBank()
170
+ input.chips = [0, 1, 4.5, 5.5].map((offset, index) => ({
171
+ ...structuredClone(input.chips[0]!),
172
+ chipId: `C${index + 1}`,
173
+ center: rotated
174
+ ? { x: 0, y: offset * pitch }
175
+ : { x: offset * pitch, y: 0 },
176
+ width: rotated ? 0.8 : 0.6,
177
+ height: rotated ? 0.6 : 0.8,
178
+ pins: [0.4, -0.4].map((pinOffset, pinIndex) => ({
179
+ pinId: `C${index + 1}.${pinIndex + 1}`,
180
+ x: rotated ? pinOffset : offset * pitch,
181
+ y: rotated ? offset * pitch : pinOffset,
182
+ })),
183
+ }))
184
+ input.netConnections[0]!.isGround = false
185
+ for (const [index, net] of input.netConnections.entries()) {
186
+ net.pinIds = input.chips.map((chip) => chip.pins[index]!.pinId)
187
+ }
188
+ if (rotated) {
189
+ input.availableNetLabelOrientations = { VDD: ["x+"], GND: ["x-"] }
190
+ }
191
+ const solver = solve(input)
192
+ expect(solver.mspConnectionPairSolver!.mspConnectionPairs).toHaveLength(4)
193
+ expect(solver.longDistancePairSolver!.getOutput().newTraces).toEqual([])
194
+ const { traces, netLabelPlacements } =
195
+ solver.netLabelToTraceSolver!.getOutput()
196
+ expect(traces).toHaveLength(4)
197
+ expect(netLabelPlacements).toHaveLength(4)
198
+ for (const [index, net] of input.netConnections.entries()) {
199
+ const components = getTraceConnectedPinComponents({
200
+ pinIds: net.pinIds,
201
+ traces,
202
+ })
203
+ expect(components.map((component) => component.pinIds.sort())).toEqual([
204
+ [`C1.${index + 1}`, `C2.${index + 1}`],
205
+ [`C3.${index + 1}`, `C4.${index + 1}`],
206
+ ])
207
+ for (const component of components) {
208
+ expect(
209
+ netLabelPlacements.filter((label) =>
210
+ label.pinIds.some((pinId) => component.pinIds.includes(pinId)),
211
+ ),
212
+ ).toHaveLength(1)
213
+ }
214
+ }
215
+ })
216
+ }
217
+
218
+ test("two isolated capacitors do not form a rail across a large gap", () => {
219
+ const input = createBank()
220
+ input.chips.splice(1, 1)
221
+ for (const net of input.netConnections) net.pinIds.splice(1, 1)
222
+ const solver = solve(input)
223
+ expect(solver.mspConnectionPairSolver!.mspConnectionPairs).toHaveLength(0)
224
+ expect(solver.longDistancePairSolver!.getOutput().newTraces).toEqual([])
225
+ const { traces, netLabelPlacements } =
226
+ solver.netLabelToTraceSolver!.getOutput()
227
+ expect(traces).toHaveLength(0)
228
+ expect(netLabelPlacements).toHaveLength(4)
229
+ })
230
+
231
+ test.each([4, 7])(
232
+ "rail separation survives ground recovery across a gap of %d",
233
+ (gap) => {
234
+ const input = createBank()
235
+ input.chips.splice(1, 1)
236
+ input.chips[1]!.center.x = gap
237
+ for (const pin of input.chips[1]!.pins) pin.x = gap
238
+ input.chips.push({
239
+ chipId: "U1",
240
+ center: { x: gap / 2, y: -4 },
241
+ width: 1,
242
+ height: 1,
243
+ pins: [{ pinId: "U1.GND", x: gap / 2, y: -4.5 }],
244
+ })
245
+ input.netConnections = [
246
+ { netId: "VDD1", isGround: false, pinIds: ["C1.1"] },
247
+ { netId: "VDD2", isGround: false, pinIds: ["C3.1"] },
248
+ { netId: "GND", isGround: true, pinIds: ["C1.2", "C3.2", "U1.GND"] },
249
+ ]
250
+ input.availableNetLabelOrientations = {
251
+ VDD1: ["y+"],
252
+ VDD2: ["y+"],
253
+ GND: ["y-"],
254
+ }
255
+
256
+ const solver = solve(input)
257
+ expect(solver.mspConnectionPairSolver!.mspConnectionPairs).toHaveLength(0)
258
+ expect(solver.longDistancePairSolver!.getOutput().newTraces).toEqual([])
259
+ const { traces, netLabelPlacements } =
260
+ solver.netLabelToTraceSolver!.getOutput()
261
+ expect(traces).toHaveLength(0)
262
+ expect(netLabelPlacements).toHaveLength(5)
263
+ },
264
+ )
265
+
266
+ test("keeps distant net-only loads local even when they are staggered", () => {
267
+ const input = createBank()
268
+ for (const [index, chip] of input.chips.entries()) {
269
+ chip.center.y += index * 2
270
+ for (const pin of chip.pins) pin.y += index * 2
271
+ }
272
+ expect(solve(input).longDistancePairSolver!.getOutput().newTraces).toEqual([])
273
+ })
274
+
275
+ test("different-size components do not bypass the net connection distance", () => {
276
+ const input = createBank()
277
+ for (const [index, chip] of input.chips.entries()) {
278
+ chip.height += index * 0.2
279
+ chip.pins[0]!.y += index * 0.1
280
+ chip.pins[1]!.y -= index * 0.1
281
+ }
282
+ const solver = solve(input)
283
+ expect(solver.longDistancePairSolver!.getOutput().newTraces).toEqual([])
284
+ expect(
285
+ solver.netLabelToTraceSolver!.getOutput().netLabelPlacements,
286
+ ).toHaveLength(6)
287
+ })
288
+
289
+ test("parallel branch labeling does not depend on optional symbol metadata", () => {
290
+ const input = createBank()
291
+ for (const chip of input.chips) chip.symbolName = "resistor"
292
+ const solver = solve(input)
293
+ expect(solver.longDistancePairSolver!.getOutput().newTraces).toEqual([])
294
+ expect(
295
+ solver.netLabelToTraceSolver!.getOutput().netLabelPlacements,
296
+ ).toHaveLength(6)
297
+ })
298
+
299
+ test("recognizes parallel banks across named-net aliases", () => {
300
+ const input = createBank()
301
+ input.netConnections = [
302
+ { netId: "VDD", pinIds: ["C1.1", "C2.1"] },
303
+ { netId: "VDD_ALIAS", pinIds: ["C2.1", "C3.1"] },
304
+ input.netConnections[1]!,
305
+ ]
306
+ expect(solve(input).longDistancePairSolver!.getOutput().newTraces).toEqual([])
307
+ })
308
+
309
+ test("final label-to-trace recovery preserves shared bank rails", () => {
310
+ const input = createBank()
311
+ input.netConnections[0]!.isGround = false
312
+ input.netConnections[1]!.isGround = true
313
+ const output = solve(input).netLabelToTraceSolver!.getOutput()
314
+ expect(output.traces.filter((trace) => trace.pinIds.length > 1)).toHaveLength(
315
+ 4,
316
+ )
317
+ expect(output.netLabelPlacements).toHaveLength(2)
318
+ })
319
+
320
+ test("an explicit bank wire does not extend to other branches", () => {
321
+ const input = createBank()
322
+ input.directConnections = [{ pinIds: ["C1.1", "C2.1"] }]
323
+ const traces = solve(input).longDistancePairSolver!.getOutput().newTraces
324
+ expect(traces.map((trace) => [...trace.pinIds].sort())).toEqual([
325
+ ["C1.1", "C2.1"],
326
+ ])
327
+ })
328
+
329
+ test("different supplies share only the ground rail", () => {
330
+ const input = createBank()
331
+ input.netConnections = [
332
+ { netId: "VDD1", pinIds: ["C1.1"] },
333
+ { netId: "VDD2", pinIds: ["C2.1"] },
334
+ { netId: "VDD3", pinIds: ["C3.1"] },
335
+ input.netConnections[1]!,
336
+ ]
337
+ const output = solve(input).netLabelToTraceSolver!.getOutput()
338
+ expect(output.traces).toHaveLength(2)
339
+ expect(
340
+ output.traces.every((trace) =>
341
+ trace.pinIds.every((pinId) => pinId.endsWith(".2")),
342
+ ),
343
+ ).toBe(true)
344
+ expect(output.netLabelPlacements).toHaveLength(4)
345
+ })
346
+
347
+ test("loads connected through source wires retain their existing recovery", () => {
348
+ const input = createBank()
349
+ input.netConnections[0]!.pinIds = ["C1.1"]
350
+ input.directConnections = [
351
+ { pinIds: ["C1.1", "C2.1"] },
352
+ { pinIds: ["C2.1", "C3.1"] },
353
+ ]
354
+ const traces = solve(input).longDistancePairSolver!.getOutput().newTraces
355
+ expect(
356
+ traces.some((trace) => trace.pinIds.every((pinId) => pinId.endsWith(".2"))),
357
+ ).toBe(true)
358
+ })
359
+
360
+ test("two aligned capacitors can share both rails beyond the local distance", () => {
361
+ const input = createBank()
362
+ input.chips.pop()
363
+ for (const net of input.netConnections) net.pinIds.pop()
364
+ const solver = solve(input)
365
+ expect(solver.mspConnectionPairSolver!.mspConnectionPairs).toHaveLength(2)
366
+ expect(
367
+ solver.netLabelToTraceSolver!.getOutput().netLabelPlacements,
368
+ ).toHaveLength(2)
369
+ })
370
+
371
+ test("an intervening component breaks the shared rail", () => {
372
+ const input = createBank()
373
+ input.chips.push({
374
+ chipId: "barrier",
375
+ center: { x: 1, y: 0.6 },
376
+ width: 0.2,
377
+ height: 0.8,
378
+ pins: [],
379
+ })
380
+ const output = solve(input).netLabelToTraceSolver!.getOutput()
381
+ expect(output.traces).toHaveLength(2)
382
+ expect(
383
+ output.traces.every(
384
+ (trace) => !trace.pinIds.some((pinId) => pinId.startsWith("C1.")),
385
+ ),
386
+ ).toBe(true)
387
+ expect(
388
+ output.netLabelPlacements.filter((label) =>
389
+ label.pinIds.some((pinId) => pinId.startsWith("C1.")),
390
+ ),
391
+ ).toHaveLength(2)
392
+ })
393
+
394
+ test("shared rails stay within their schematic section", () => {
395
+ const input = createBank()
396
+ input.chips[2]!.sectionId = "separate"
397
+ const output = solve(input).netLabelToTraceSolver!.getOutput()
398
+ expect(output.traces).toHaveLength(2)
399
+ expect(
400
+ output.traces.every(
401
+ (trace) => !trace.pinIds.some((pinId) => pinId.startsWith("C3.")),
402
+ ),
403
+ ).toBe(true)
404
+ expect(output.netLabelPlacements).toHaveLength(4)
405
+ })
406
+
407
+ test("a common ground rail does not recover wires across interleaved supplies", () => {
408
+ const input = createBank()
409
+ input.chips = [0, 2, 4, 6, 8, 10].map((x, index) => ({
410
+ chipId: `C${index + 1}`,
411
+ center: { x, y: 0 },
412
+ width: 0.6,
413
+ height: 0.8,
414
+ pins: [
415
+ { pinId: `C${index + 1}.1`, x, y: 0.4 },
416
+ { pinId: `C${index + 1}.2`, x, y: -0.4 },
417
+ ],
418
+ }))
419
+ input.netConnections = [
420
+ { netId: "VDD", isGround: false, pinIds: ["C1.1", "C3.1", "C5.1"] },
421
+ { netId: "VEE", isGround: false, pinIds: ["C2.1", "C4.1", "C6.1"] },
422
+ {
423
+ netId: "GND",
424
+ isGround: true,
425
+ pinIds: input.chips.map((chip) => chip.pins[1]!.pinId),
426
+ },
427
+ ]
428
+ const solver = solve(input)
429
+ const output = solver.netLabelToTraceSolver!.getOutput()
430
+ expect(solver.longDistancePairSolver!.getOutput().newTraces).toHaveLength(0)
431
+ expect(output.traces).toHaveLength(5)
432
+ expect(
433
+ output.traces.every((trace) =>
434
+ trace.pinIds.every((pinId) => pinId.endsWith(".2")),
435
+ ),
436
+ ).toBe(true)
437
+ expect(output.netLabelPlacements).toHaveLength(7)
438
+ })
439
+
440
+ test("decoupling row shares GND while keeping DEC supplies separate", async () => {
441
+ const input = createBank()
442
+ input.chips = Array.from({ length: 6 }, (_, index) => ({
443
+ chipId: [
444
+ "C_MCU_VDD1",
445
+ "C_MCU_VDD3",
446
+ "C_MCU_VDD2",
447
+ "C_MCU_DEC1",
448
+ "C_MCU_DEC3",
449
+ "C_MCU_DEC4",
450
+ ][index]!,
451
+ symbolName: "capacitor_down",
452
+ center: { x: index * 3, y: 0 },
453
+ width: 0.6,
454
+ height: 0.76,
455
+ pins: [
456
+ { pinId: `C${index + 1}.1`, x: index * 3, y: 0.38 },
457
+ { pinId: `C${index + 1}.2`, x: index * 3, y: -0.38 },
458
+ ],
459
+ }))
460
+ input.netConnections = [
461
+ { netId: "V3V3", netLabelText: "V3V3", pinIds: ["C1.1", "C2.1", "C3.1"] },
462
+ ...["DEC1", "DEC3", "DEC4"].map((netId, index) => ({
463
+ netId,
464
+ netLabelText: netId,
465
+ pinIds: [`C${index + 4}.1`],
466
+ })),
467
+ {
468
+ netId: "GND",
469
+ isGround: true,
470
+ netLabelText: "GND",
471
+ pinIds: input.chips.map((chip) => chip.pins[1]!.pinId),
472
+ },
473
+ ]
474
+ input.availableNetLabelOrientations = {
475
+ V3V3: ["y+"],
476
+ DEC1: ["y+"],
477
+ DEC3: ["y+"],
478
+ DEC4: ["y+"],
479
+ GND: ["y-"],
480
+ }
481
+ const solver = solve(input)
482
+ const { traces, netLabelPlacements } =
483
+ solver.netLabelToTraceSolver!.getOutput()
484
+ expect(traces).toHaveLength(7)
485
+ expect(netLabelPlacements).toHaveLength(5)
486
+ for (const net of input.netConnections) {
487
+ expect(
488
+ getTraceConnectedPinComponents({ pinIds: net.pinIds, traces }),
489
+ ).toHaveLength(1)
490
+ expect(
491
+ netLabelPlacements.filter((label) => label.netId === net.netId),
492
+ ).toHaveLength(1)
493
+ }
494
+ for (const trace of traces) {
495
+ const pinNets = trace.pinIds.map(
496
+ (pinId) =>
497
+ input.netConnections.find((net) => net.pinIds.includes(pinId))!.netId,
498
+ )
499
+ expect(new Set(pinNets).size).toBe(1)
500
+ }
501
+ await expect(solver).toMatchSolverSnapshot(
502
+ import.meta.path,
503
+ "decoupling-mixed-supplies",
504
+ )
505
+ })
@@ -87,6 +87,36 @@ test("collapses a same-net cycle between a shared pin and crossing", () => {
87
87
  expect(result.netLabelPlacements[0]?.anchorPoint).toEqual({ x: 0, y: 0 })
88
88
  })
89
89
 
90
+ test("collapses a cycle within one same-net trace", () => {
91
+ const traces = [
92
+ createTraceFixture({
93
+ mspPairId: "self-crossing-trace",
94
+ pinIds: ["start", "end"],
95
+ tracePath: [
96
+ { x: 0, y: 4 },
97
+ { x: 0, y: 0 },
98
+ { x: 4, y: 0 },
99
+ { x: 4, y: 2 },
100
+ { x: 1, y: 2 },
101
+ { x: 1, y: -1 },
102
+ ],
103
+ }),
104
+ ]
105
+
106
+ const result = collapseSameNetCycles({
107
+ traces,
108
+ netLabelPlacements: [],
109
+ })
110
+
111
+ expect(result.collapsedCycleCount).toBe(1)
112
+ expect(result.traces[0]?.tracePath).toEqual([
113
+ { x: 0, y: 4 },
114
+ { x: 0, y: 0 },
115
+ { x: 1, y: 0 },
116
+ { x: 1, y: -1 },
117
+ ])
118
+ })
119
+
90
120
  test("keeps a valid shared same-net branch", () => {
91
121
  const traces = [
92
122
  createTraceFixture({