@tscircuit/schematic-trace-solver 0.0.158 → 0.0.160

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 (27) hide show
  1. package/dist/index.d.ts +41 -9
  2. package/dist/index.js +762 -110
  3. package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +536 -94
  4. package/lib/solvers/InlineNetLabelSolver/pushAnchoredNetLabelsAwayFromInlineLabels.ts +9 -7
  5. package/lib/solvers/InlineNetLabelSolver/pushInlineTerminalLabelsAwayFromAnchoredLabels.ts +295 -0
  6. package/lib/solvers/LongDistancePairSolver/LongDistancePairSolver.ts +15 -8
  7. package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +5 -0
  8. package/lib/solvers/MspConnectionPairSolver/getGroundConnectionPolicy.ts +83 -0
  9. package/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts +7 -35
  10. package/lib/solvers/SameNetJunctionAlignmentSolver/SameNetJunctionAlignmentSolver.ts +6 -1
  11. package/lib/solvers/SameNetJunctionAlignmentSolver/placeGroundRailLabelsAtOuterEnd.ts +122 -0
  12. package/lib/solvers/SchematicTraceLinesSolver/getTraceConnectedPinComponents.ts +60 -0
  13. package/lib/types/InputProblem.ts +5 -4
  14. package/package.json +1 -1
  15. package/tests/bug-reports/bug-report-20260826T072956Z/__snapshots__/bug-report-20260826T072956Z.snap.svg +600 -783
  16. package/tests/bug-reports/bug-report-20260826T072956Z/bug-report-20260826T072956Z.test.ts +66 -1
  17. package/tests/fixtures/parallel-ground-rail.ts +53 -0
  18. package/tests/repros/__snapshots__/repro-mspm0l1306-capacitor-symmetry.snap.svg +34 -16
  19. package/tests/repros/repro-mspm0l1306-capacitor-symmetry.test.ts +40 -7
  20. package/tests/repros/repro-usb-power-vbus-label-detour.test.ts +14 -0
  21. package/tests/solvers/InlineNetLabelSolver/multi-pin-connected-components.test.ts +252 -0
  22. package/tests/solvers/InlineNetLabelSolver/opposite-side-placement.test.ts +71 -0
  23. package/tests/solvers/InlineNetLabelSolver/push-anchored-net-labels-away.test.ts +8 -2
  24. package/tests/solvers/InlineNetLabelSolver/push-inline-terminal-labels-away.test.ts +91 -0
  25. package/tests/solvers/InlineNetLabelSolver/two-pin-terminal-stubs-atomic.test.ts +7 -1
  26. package/tests/solvers/MspConnectionPairSolver/local-ground-branches.test.ts +105 -0
  27. package/tests/solvers/SameNetJunctionAlignmentSolver/ground-rail-label.test.ts +188 -0
@@ -0,0 +1,252 @@
1
+ import { expect, test } from "bun:test"
2
+ import { InlineNetLabelSolver } from "lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver"
3
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
4
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
5
+ import type { InputProblem } from "lib/types/InputProblem"
6
+
7
+ test("a multi-pin net labels routed and disconnected components inline", () => {
8
+ const inputProblem: InputProblem = {
9
+ chips: [
10
+ {
11
+ chipId: "J1",
12
+ center: { x: 0, y: 0 },
13
+ width: 1,
14
+ height: 1,
15
+ pins: [
16
+ {
17
+ pinId: "J1.1",
18
+ x: -0.5,
19
+ y: 0.2,
20
+ _facingDirection: "x-",
21
+ },
22
+ {
23
+ pinId: "J1.2",
24
+ x: -0.5,
25
+ y: -0.2,
26
+ _facingDirection: "x-",
27
+ },
28
+ ],
29
+ },
30
+ {
31
+ chipId: "U1",
32
+ center: { x: 2, y: 0 },
33
+ width: 1,
34
+ height: 1,
35
+ pins: [
36
+ {
37
+ pinId: "U1.1",
38
+ x: 2.5,
39
+ y: 0,
40
+ _facingDirection: "x+",
41
+ },
42
+ ],
43
+ },
44
+ ],
45
+ directConnections: [],
46
+ netConnections: [
47
+ {
48
+ netId: "MOTOR",
49
+ pinIds: ["J1.1", "J1.2", "U1.1"],
50
+ allowInlineNetLabel: true,
51
+ inlineNetLabelWidth: 0.6,
52
+ inlineNetLabelHeight: 0.12,
53
+ },
54
+ ],
55
+ availableNetLabelOrientations: { MOTOR: ["x-", "x+"] },
56
+ }
57
+ const shortConnectorTrace: SolvedTracePath = {
58
+ mspPairId: "J1.1-J1.2",
59
+ mspConnectionPairIds: ["J1.1-J1.2"],
60
+ dcConnNetId: "MOTOR",
61
+ globalConnNetId: "MOTOR",
62
+ userNetId: "MOTOR",
63
+ pins: [
64
+ { pinId: "J1.1", chipId: "J1", x: -0.5, y: 0.2 },
65
+ { pinId: "J1.2", chipId: "J1", x: -0.5, y: -0.2 },
66
+ ],
67
+ pinIds: ["J1.1", "J1.2"],
68
+ tracePath: [
69
+ { x: -0.5, y: 0.2 },
70
+ { x: -0.8, y: 0.2 },
71
+ { x: -0.8, y: -0.2 },
72
+ { x: -0.5, y: -0.2 },
73
+ ],
74
+ }
75
+
76
+ const solver = new InlineNetLabelSolver({
77
+ inputProblem,
78
+ traces: [shortConnectorTrace],
79
+ netLabelPlacements: [],
80
+ })
81
+ solver.solve()
82
+
83
+ const output = solver.getOutput()
84
+ expect(output.inlineNetLabelPlacements).toHaveLength(3)
85
+ expect(
86
+ output.inlineNetLabelPlacements.every(
87
+ (placement) => placement.stubTracePath !== undefined,
88
+ ),
89
+ ).toBe(true)
90
+ expect(output.traces).toHaveLength(0)
91
+ expect(output.netLabelPlacements).toHaveLength(0)
92
+ })
93
+
94
+ test("a short routed component is retained when replacing it would strand an intermediate pin", () => {
95
+ const inputProblem: InputProblem = {
96
+ chips: [
97
+ {
98
+ chipId: "J1",
99
+ center: { x: 0, y: 0 },
100
+ width: 1,
101
+ height: 1,
102
+ pins: [
103
+ { pinId: "J1.1", x: -0.5, y: 0.2, _facingDirection: "x-" },
104
+ { pinId: "J1.2", x: -0.5, y: -0.2, _facingDirection: "x-" },
105
+ ],
106
+ },
107
+ {
108
+ chipId: "U1",
109
+ center: { x: 2, y: 0 },
110
+ width: 1,
111
+ height: 1,
112
+ pins: [{ pinId: "U1.1", x: 2.5, y: 0, _facingDirection: "x+" }],
113
+ },
114
+ ],
115
+ directConnections: [{ pinIds: ["J1.1", "J1.2"] }],
116
+ netConnections: [
117
+ {
118
+ netId: "MOTOR",
119
+ pinIds: ["J1.1", "U1.1"],
120
+ allowInlineNetLabel: true,
121
+ inlineNetLabelWidth: 0.6,
122
+ inlineNetLabelHeight: 0.12,
123
+ },
124
+ ],
125
+ availableNetLabelOrientations: { MOTOR: ["x-", "x+"] },
126
+ }
127
+ const shortTrace: SolvedTracePath = {
128
+ mspPairId: "J1.1-J1.2",
129
+ mspConnectionPairIds: ["J1.1-J1.2"],
130
+ dcConnNetId: "MOTOR",
131
+ globalConnNetId: "MOTOR",
132
+ userNetId: "MOTOR",
133
+ pins: [
134
+ { pinId: "J1.1", chipId: "J1", x: -0.5, y: 0.2 },
135
+ { pinId: "J1.2", chipId: "J1", x: -0.5, y: -0.2 },
136
+ ],
137
+ pinIds: ["J1.1", "J1.2"],
138
+ tracePath: [
139
+ { x: -0.5, y: 0.2 },
140
+ { x: -0.8, y: 0.2 },
141
+ { x: -0.8, y: -0.2 },
142
+ { x: -0.5, y: -0.2 },
143
+ ],
144
+ }
145
+ const anchoredLabels: NetLabelPlacement[] = [
146
+ {
147
+ globalConnNetId: "MOTOR",
148
+ netId: "MOTOR",
149
+ mspConnectionPairIds: ["J1.1-J1.2"],
150
+ pinIds: ["J1.1", "J1.2"],
151
+ orientation: "x-",
152
+ anchorPoint: { x: -0.8, y: 0 },
153
+ center: { x: -1.2, y: 0 },
154
+ width: 0.6,
155
+ height: 0.12,
156
+ },
157
+ {
158
+ globalConnNetId: "MOTOR",
159
+ netId: "MOTOR",
160
+ mspConnectionPairIds: [],
161
+ pinIds: ["U1.1"],
162
+ orientation: "x+",
163
+ anchorPoint: { x: 2.5, y: 0 },
164
+ center: { x: 2.9, y: 0 },
165
+ width: 0.6,
166
+ height: 0.12,
167
+ },
168
+ ]
169
+
170
+ const solver = new InlineNetLabelSolver({
171
+ inputProblem,
172
+ traces: [shortTrace],
173
+ netLabelPlacements: anchoredLabels,
174
+ })
175
+ solver.solve()
176
+
177
+ const output = solver.getOutput()
178
+ expect(output.inlineNetLabelPlacements).toHaveLength(0)
179
+ expect(output.traces).toEqual([shortTrace])
180
+ expect(output.netLabelPlacements).toEqual(anchoredLabels)
181
+ })
182
+
183
+ test("a generated anchored-label connector becomes an inline terminal stub", () => {
184
+ const inputProblem: InputProblem = {
185
+ chips: [
186
+ {
187
+ chipId: "U1",
188
+ center: { x: 0, y: 0 },
189
+ width: 1,
190
+ height: 1,
191
+ pins: [{ pinId: "U1.1", x: -0.5, y: 0, _facingDirection: "x-" }],
192
+ },
193
+ {
194
+ chipId: "U2",
195
+ center: { x: 3, y: 0 },
196
+ width: 1,
197
+ height: 1,
198
+ pins: [{ pinId: "U2.1", x: 2.5, y: 0, _facingDirection: "x-" }],
199
+ },
200
+ {
201
+ chipId: "U3",
202
+ center: { x: 6, y: 0 },
203
+ width: 1,
204
+ height: 1,
205
+ pins: [{ pinId: "U3.1", x: 5.5, y: 0, _facingDirection: "x-" }],
206
+ },
207
+ ],
208
+ directConnections: [],
209
+ netConnections: [
210
+ {
211
+ netId: "SIGNAL",
212
+ pinIds: ["U1.1", "U2.1", "U3.1"],
213
+ allowInlineNetLabel: true,
214
+ inlineNetLabelWidth: 0.6,
215
+ inlineNetLabelHeight: 0.12,
216
+ },
217
+ ],
218
+ availableNetLabelOrientations: { SIGNAL: ["x-"] },
219
+ }
220
+ const generatedLabelConnector: SolvedTracePath = {
221
+ mspPairId: "available-net-orientation-0-SIGNAL",
222
+ mspConnectionPairIds: ["available-net-orientation-0-SIGNAL"],
223
+ dcConnNetId: "SIGNAL",
224
+ globalConnNetId: "SIGNAL",
225
+ userNetId: "SIGNAL",
226
+ pins: [
227
+ { pinId: "U1.1", chipId: "U1", x: -0.5, y: 0 },
228
+ { pinId: "U1.1", chipId: "U1", x: -0.5, y: 0 },
229
+ ],
230
+ pinIds: ["U1.1"],
231
+ tracePath: [
232
+ { x: -0.5, y: 0 },
233
+ { x: -1.5, y: 0 },
234
+ ],
235
+ }
236
+
237
+ const solver = new InlineNetLabelSolver({
238
+ inputProblem,
239
+ traces: [generatedLabelConnector],
240
+ netLabelPlacements: [],
241
+ })
242
+ solver.solve()
243
+
244
+ const output = solver.getOutput()
245
+ expect(output.inlineNetLabelPlacements).toHaveLength(3)
246
+ expect(
247
+ output.inlineNetLabelPlacements.every(
248
+ (placement) => placement.stubTracePath !== undefined,
249
+ ),
250
+ ).toBe(true)
251
+ expect(output.traces).toHaveLength(0)
252
+ })
@@ -0,0 +1,71 @@
1
+ import { expect, test } from "bun:test"
2
+ import { InlineNetLabelSolver } from "lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver"
3
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
4
+ import type { InputProblem } from "lib/types/InputProblem"
5
+
6
+ test("an inline label tries the opposite side when the preferred side is blocked", () => {
7
+ const inputProblem: InputProblem = {
8
+ chips: [
9
+ {
10
+ chipId: "U1",
11
+ center: { x: -2, y: 0 },
12
+ width: 1,
13
+ height: 1,
14
+ pins: [{ pinId: "U1.1", x: -1.5, y: 0 }],
15
+ },
16
+ {
17
+ chipId: "U2",
18
+ center: { x: 2, y: 0 },
19
+ width: 1,
20
+ height: 1,
21
+ pins: [{ pinId: "U2.1", x: 1.5, y: 0 }],
22
+ },
23
+ ],
24
+ directConnections: [
25
+ {
26
+ netId: "SIGNAL",
27
+ pinIds: ["U1.1", "U2.1"],
28
+ allowInlineNetLabel: true,
29
+ inlineNetLabelWidth: 0.8,
30
+ inlineNetLabelHeight: 0.12,
31
+ },
32
+ ],
33
+ netConnections: [],
34
+ textBoxes: [
35
+ {
36
+ center: { x: 0, y: 0.11 },
37
+ width: 3,
38
+ height: 0.12,
39
+ text: "preferred-side obstacle",
40
+ },
41
+ ],
42
+ availableNetLabelOrientations: { SIGNAL: ["x-", "x+"] },
43
+ }
44
+ const trace: SolvedTracePath = {
45
+ mspPairId: "U1.1-U2.1",
46
+ mspConnectionPairIds: ["U1.1-U2.1"],
47
+ dcConnNetId: "SIGNAL",
48
+ globalConnNetId: "SIGNAL",
49
+ userNetId: "SIGNAL",
50
+ pins: [
51
+ { pinId: "U1.1", chipId: "U1", x: -1.5, y: 0 },
52
+ { pinId: "U2.1", chipId: "U2", x: 1.5, y: 0 },
53
+ ],
54
+ pinIds: ["U1.1", "U2.1"],
55
+ tracePath: [
56
+ { x: -1.5, y: 0 },
57
+ { x: 1.5, y: 0 },
58
+ ],
59
+ }
60
+
61
+ const solver = new InlineNetLabelSolver({
62
+ inputProblem,
63
+ traces: [trace],
64
+ netLabelPlacements: [],
65
+ })
66
+ solver.solve()
67
+
68
+ expect(solver.inlineNetLabelPlacements).toHaveLength(1)
69
+ expect(solver.inlineNetLabelPlacements[0]!.side).toBe("y-")
70
+ expect(solver.inlineNetLabelPlacements[0]!.center.y).toBeLessThan(0)
71
+ })
@@ -11,9 +11,15 @@ test("pushes a regular endpoint label and its wick past nearby inline text", ()
11
11
  {
12
12
  chipId: "U1",
13
13
  center: { x: 0, y: 0 },
14
- width: 1,
14
+ // The routing box includes component text, so the real pins can sit
15
+ // inside it. A generated connector may legitimately cross that
16
+ // expanded owner box on its way out from the pin.
17
+ width: 2,
15
18
  height: 1,
16
- pins: [],
19
+ pins: [
20
+ { pinId: "U1.1", x: -0.5, y: 0.2, _facingDirection: "x-" },
21
+ { pinId: "U1.2", x: -0.5, y: 0, _facingDirection: "x-" },
22
+ ],
17
23
  },
18
24
  ],
19
25
  directConnections: [],
@@ -0,0 +1,91 @@
1
+ import { expect, test } from "bun:test"
2
+ import type { InlineNetLabelPlacement } from "lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver"
3
+ import { pushInlineTerminalLabelsAwayFromAnchoredLabels } from "lib/solvers/InlineNetLabelSolver/pushInlineTerminalLabelsAwayFromAnchoredLabels"
4
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
5
+ import type { InputProblem } from "lib/types/InputProblem"
6
+
7
+ test("moves an inline terminal row outward past a retained anchored label", () => {
8
+ const inputProblem: InputProblem = {
9
+ chips: [
10
+ {
11
+ chipId: "J1",
12
+ center: { x: 0, y: 0 },
13
+ width: 1,
14
+ height: 1,
15
+ pins: [
16
+ { pinId: "J1.A0", x: -0.5, y: 0.2, _facingDirection: "x-" },
17
+ { pinId: "J1.SCLK", x: -0.5, y: 0, _facingDirection: "x-" },
18
+ { pinId: "J1.MISO", x: -0.5, y: -0.2, _facingDirection: "x-" },
19
+ ],
20
+ },
21
+ ],
22
+ directConnections: [],
23
+ netConnections: [],
24
+ availableNetLabelOrientations: {},
25
+ }
26
+ const anchoredNetLabelPlacements: NetLabelPlacement[] = [
27
+ {
28
+ globalConnNetId: "A0",
29
+ netId: "A0",
30
+ mspConnectionPairIds: [],
31
+ pinIds: ["J1.A0"],
32
+ orientation: "x-",
33
+ anchorPoint: { x: -0.5, y: 0.2 },
34
+ center: { x: -0.75, y: 0.2 },
35
+ width: 0.5,
36
+ height: 0.2,
37
+ },
38
+ ]
39
+ const inlineNetLabelPlacements: InlineNetLabelPlacement[] = [
40
+ {
41
+ globalConnNetId: "SCLK",
42
+ netId: "SCLK",
43
+ pinIds: ["J1.SCLK"],
44
+ stubTracePath: [
45
+ { x: -0.5, y: 0 },
46
+ { x: -1.1, y: 0 },
47
+ ],
48
+ axis: "x",
49
+ anchorPoint: { x: -0.8, y: 0 },
50
+ center: { x: -0.8, y: 0.11 },
51
+ width: 0.4,
52
+ height: 0.12,
53
+ side: "y+",
54
+ },
55
+ {
56
+ globalConnNetId: "MISO",
57
+ netId: "MISO",
58
+ pinIds: ["J1.MISO"],
59
+ stubTracePath: [
60
+ { x: -0.5, y: -0.2 },
61
+ { x: -1.1, y: -0.2 },
62
+ ],
63
+ axis: "x",
64
+ anchorPoint: { x: -0.8, y: -0.2 },
65
+ center: { x: -0.8, y: -0.09 },
66
+ width: 0.4,
67
+ height: 0.12,
68
+ side: "y+",
69
+ },
70
+ ]
71
+
72
+ const output = pushInlineTerminalLabelsAwayFromAnchoredLabels({
73
+ inputProblem,
74
+ traces: [],
75
+ anchoredNetLabelPlacements,
76
+ inlineNetLabelPlacements,
77
+ })
78
+
79
+ expect(output.movedGroupCount).toBe(1)
80
+ expect(output.inlineNetLabelPlacements[0]!.center.x).toBeLessThan(-0.8)
81
+ expect(output.inlineNetLabelPlacements[1]!.center.x).toBe(
82
+ output.inlineNetLabelPlacements[0]!.center.x,
83
+ )
84
+ expect(output.inlineNetLabelPlacements[0]!.stubTracePath![0]).toEqual({
85
+ x: -0.5,
86
+ y: 0,
87
+ })
88
+ expect(output.inlineNetLabelPlacements[0]!.stubTracePath![1]!.x).toBeLessThan(
89
+ -1.1,
90
+ )
91
+ })
@@ -36,7 +36,13 @@ test("two-pin terminal stubs fall back atomically when one endpoint is obstructe
36
36
  center: { x: -0.85, y: 0.11 },
37
37
  width: 1.1,
38
38
  height: 0.12,
39
- text: "obstacle",
39
+ text: "upper obstacle",
40
+ },
41
+ {
42
+ center: { x: -0.85, y: -0.11 },
43
+ width: 1.1,
44
+ height: 0.12,
45
+ text: "lower obstacle",
40
46
  },
41
47
  ],
42
48
  availableNetLabelOrientations: { NET_SIGNAL: ["x-", "x+"] },
@@ -0,0 +1,105 @@
1
+ import { expect, test } from "bun:test"
2
+ import { LongDistancePairSolver } from "lib/solvers/LongDistancePairSolver/LongDistancePairSolver"
3
+ import { getGroundConnectionPolicy } from "lib/solvers/MspConnectionPairSolver/getGroundConnectionPolicy"
4
+ import { MspConnectionPairSolver } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
5
+ import { createParallelGroundRailProblem } from "tests/fixtures/parallel-ground-rail"
6
+
7
+ test("keeps a wired parallel ground rail but labels independent staggered branches", () => {
8
+ const inputProblem = createParallelGroundRailProblem()
9
+ const before = structuredClone(inputProblem)
10
+ const solver = new MspConnectionPairSolver({ inputProblem })
11
+ solver.solve()
12
+ expect(solver.mspConnectionPairs).toHaveLength(2)
13
+ const routed = new Set(
14
+ solver.mspConnectionPairs.flatMap((pair) =>
15
+ pair.pins.map((pin) => pin.pinId),
16
+ ),
17
+ )
18
+ expect([...routed].sort()).toEqual(["A.2", "B.2", "U.GND"])
19
+ expect(inputProblem).toEqual(before)
20
+ })
21
+
22
+ test("long-distance recovery does not reconnect the separated ground branches", () => {
23
+ const inputProblem = createParallelGroundRailProblem()
24
+ const primary = new MspConnectionPairSolver({ inputProblem })
25
+ primary.solve()
26
+ const solver = new LongDistancePairSolver({
27
+ inputProblem,
28
+ primaryMspConnectionPairs: primary.mspConnectionPairs,
29
+ alreadySolvedTraces: [],
30
+ failedConnectionPairs: [],
31
+ })
32
+ solver.solve()
33
+ expect(solver.getOutput().newTraces).toEqual([])
34
+ })
35
+
36
+ test("the branch policy is symmetric in its endpoints", () => {
37
+ const canRoute = getGroundConnectionPolicy(createParallelGroundRailProblem())
38
+ expect(canRoute("B.2", "C.2")).toBe(false)
39
+ expect(canRoute("C.2", "B.2")).toBe(false)
40
+ expect(canRoute("C.2", "D.2")).toBe(false)
41
+ expect(canRoute("D.2", "C.2")).toBe(false)
42
+ })
43
+
44
+ test("explicit direct wiring overrides the local-ground preference", () => {
45
+ const inputProblem = createParallelGroundRailProblem()
46
+ inputProblem.directConnections.push({ pinIds: ["B.2", "C.2"] })
47
+ const canRoute = getGroundConnectionPolicy(inputProblem)
48
+ expect(canRoute("B.2", "C.2")).toBe(true)
49
+ expect(canRoute("A.2", "C.2")).toBe(true)
50
+ expect(canRoute("C.2", "D.2")).toBe(false)
51
+ })
52
+
53
+ for (const y of [1, 0.5, 0]) {
54
+ test(`keeps level or nearby ground pins at y=${y} on the rail`, () => {
55
+ const inputProblem = createParallelGroundRailProblem()
56
+ inputProblem.chips[2]!.pins[1]!.y = y
57
+ const canRoute = getGroundConnectionPolicy(inputProblem)
58
+ expect(canRoute("B.2", "C.2")).toBe(true)
59
+ })
60
+ }
61
+
62
+ test("preserves existing behavior without an explicitly wired parallel rail", () => {
63
+ const inputProblem = createParallelGroundRailProblem()
64
+ inputProblem.directConnections = []
65
+ expect(getGroundConnectionPolicy(inputProblem)("B.2", "C.2")).toBe(true)
66
+ })
67
+
68
+ test("a repeated direct-connection net ID does not invent a physical rail", () => {
69
+ const inputProblem = createParallelGroundRailProblem()
70
+ inputProblem.directConnections = [
71
+ { netId: "GND", pinIds: ["A.2", "U.GND"] },
72
+ { netId: "GND", pinIds: ["B.2", "D.2"] },
73
+ ]
74
+ expect(getGroundConnectionPolicy(inputProblem)("B.2", "C.2")).toBe(true)
75
+ })
76
+
77
+ test("resolves a ground alias through global connectivity", () => {
78
+ const inputProblem = createParallelGroundRailProblem()
79
+ inputProblem.netConnections[0]!.netId = "RETURN"
80
+ inputProblem.netConnections.push({ netId: "GND", pinIds: ["U.GND"] })
81
+ expect(getGroundConnectionPolicy(inputProblem)("B.2", "C.2")).toBe(false)
82
+ })
83
+
84
+ test("does not apply the ground rule to power or signal nets", () => {
85
+ const inputProblem = createParallelGroundRailProblem()
86
+ inputProblem.netConnections[0]!.netId = "VDD"
87
+ expect(getGroundConnectionPolicy(inputProblem)("B.2", "C.2")).toBe(true)
88
+ })
89
+
90
+ test("infers ground-facing pins when explicit directions are absent", () => {
91
+ const inputProblem = createParallelGroundRailProblem()
92
+ for (const chip of inputProblem.chips) {
93
+ for (const pin of chip.pins) delete pin._facingDirection
94
+ }
95
+ expect(getGroundConnectionPolicy(inputProblem)("B.2", "C.2")).toBe(false)
96
+ })
97
+
98
+ test("does not isolate side-facing or multi-pin components", () => {
99
+ const inputProblem = createParallelGroundRailProblem()
100
+ inputProblem.chips[2]!.pins[1]!._facingDirection = "x-"
101
+ expect(getGroundConnectionPolicy(inputProblem)("B.2", "C.2")).toBe(true)
102
+ inputProblem.chips[2]!.pins[1]!._facingDirection = "y-"
103
+ inputProblem.chips[2]!.pins.push({ pinId: "C.3", x: 2, y: -1.6 })
104
+ expect(getGroundConnectionPolicy(inputProblem)("B.2", "C.2")).toBe(true)
105
+ })