@tscircuit/schematic-trace-solver 0.0.190 → 0.0.192

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 (58) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +437 -211
  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/collapseRedundantSharedEndpointStubs.ts +12 -6
  10. package/package.json +1 -1
  11. package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +837 -799
  12. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +904 -866
  13. package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +533 -524
  14. package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +2 -2
  15. package/tests/bug-reports/bug-report-20260724T175257Z/__snapshots__/bug-report-20260724T175257Z.snap.svg +2 -2
  16. package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +2 -2
  17. package/tests/bug-reports/bug-report-20260804T171919Z/__snapshots__/bug-report-20260804T171919Z.snap.svg +2 -2
  18. package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +94 -94
  19. package/tests/bug-reports/bug-report-20260815T073240Z/__snapshots__/bug-report-20260815T073240Z.snap.svg +2 -2
  20. package/tests/bug-reports/bug-report-20260901T055358Z/__snapshots__/bug-report-20260901T055358Z.snap.svg +2 -2
  21. package/tests/bug-reports/bug-report-20260901T064117Z/bug-report-20260901T064117Z.test.ts +13 -2
  22. package/tests/bug-reports/bug-report-20260902T092425Z/__snapshots__/bug-report-20260902T092425Z.snap.svg +2 -2
  23. package/tests/bug-reports/bug-report-20260902T092425Z/bug-report-20260902T092425Z.test.ts +16 -2
  24. package/tests/bug-reports/bug-report-20260905T041712Z/__snapshots__/bug-report-20260905T041712Z.snap.svg +2 -2
  25. package/tests/bug-reports/bug-report-20260907T083336Z/__snapshots__/bug-report-20260907T083336Z.snap.svg +2 -2
  26. package/tests/bug-reports/bug-report-20260907T110144Z/__snapshots__/bug-report-20260907T110144Z.snap.svg +19 -58
  27. package/tests/bug-reports/bug-report-20260907T110144Z/bug-report-20260907T110144Z.test.ts +2 -2
  28. package/tests/bug-reports/bug-report-20260907T145640Z/__snapshots__/bug-report-20260907T145640Z.snap.svg +917 -933
  29. package/tests/examples/__snapshots__/example13.snap.svg +2 -2
  30. package/tests/examples/__snapshots__/example15.snap.svg +2 -2
  31. package/tests/examples/__snapshots__/example51.snap.svg +2 -2
  32. package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +2 -2
  33. package/tests/repros/__snapshots__/board-15984-same-net-junction.snap.svg +2 -2
  34. package/tests/repros/__snapshots__/repro-bq25895-cross-net-trace-overlap.snap.svg +2 -2
  35. package/tests/repros/__snapshots__/repro-cc2340r5.snap.svg +2 -2
  36. package/tests/repros/__snapshots__/repro-esp12f-section.snap.svg +2 -2
  37. package/tests/repros/__snapshots__/repro-example35-minimize-trace-crossing.snap.svg +2 -2
  38. package/tests/repros/__snapshots__/repro-hub-usb-sheet.snap.svg +2 -2
  39. package/tests/repros/__snapshots__/repro-isolated-rs485-isow7841.snap.svg +2 -2
  40. package/tests/repros/__snapshots__/repro-pga300-redundant-ground-traces.snap.svg +2 -2
  41. package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +2 -2
  42. package/tests/repros/__snapshots__/repro-powerbank-3v-system-power.snap.svg +2 -2
  43. package/tests/repros/__snapshots__/repro-repeated-power-rail-junctions.snap.svg +2 -2
  44. package/tests/repros/__snapshots__/repro-rp2040-gamepad-trace-alignment.snap.svg +2 -2
  45. package/tests/repros/__snapshots__/repro-smartwatch-power-sheet.snap.svg +111 -75
  46. package/tests/repros/__snapshots__/repro-tida00553-j4-cell-divider.snap.svg +2 -2
  47. package/tests/repros/__snapshots__/repro-tida010076-page02.snap.svg +2 -2
  48. package/tests/repros/__snapshots__/repro-type-c-vbus-led-ground-label.snap.svg +2 -2
  49. package/tests/repros/__snapshots__/repro-usb-power-vbus-label-detour.snap.svg +2 -2
  50. package/tests/repros/__snapshots__/repro130-bq27441-fuel-gauge-trace-through-c1.snap.svg +2 -2
  51. package/tests/repros/__snapshots__/repro161-power-section-autolayout.snap.svg +2 -2
  52. package/tests/repros/repro-trellis-core-decoupling-snake-traces.test.ts +66 -0
  53. package/tests/solvers/InlineNetLabelSolver/local-trace-label-shifts.test.ts +62 -0
  54. package/tests/solvers/LongDistancePairSolver/__snapshots__/decoupling-mixed-supplies.snap.svg +112 -0
  55. package/tests/solvers/LongDistancePairSolver/__snapshots__/horizontal-rails.snap.svg +79 -0
  56. package/tests/solvers/LongDistancePairSolver/__snapshots__/vertical-rails.snap.svg +86 -0
  57. package/tests/solvers/LongDistancePairSolver/parallel-decoupling-labels.test.ts +505 -0
  58. package/tests/solvers/SameNetJunctionAlignmentSolver/collapse-redundant-shared-endpoint-stubs.test.ts +45 -0
@@ -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
+ })
@@ -99,3 +99,48 @@ test("collapses a shared stub and partial rail overlap", () => {
99
99
  { x: -1, y: -1 },
100
100
  ])
101
101
  })
102
+
103
+ test("collapses every duplicate stub at a shared same-net endpoint", () => {
104
+ const traces = [
105
+ createTrace({
106
+ mspPairId: "outer-shared",
107
+ pinIds: ["outer", "shared"],
108
+ tracePath: [
109
+ { x: 2, y: 3 },
110
+ { x: 0, y: 3 },
111
+ { x: 0, y: 0 },
112
+ ],
113
+ }),
114
+ createTrace({
115
+ mspPairId: "inner-shared",
116
+ pinIds: ["inner", "shared"],
117
+ tracePath: [
118
+ { x: 2, y: 1 },
119
+ { x: 0, y: 1 },
120
+ { x: 0, y: 0 },
121
+ ],
122
+ }),
123
+ createTrace({
124
+ mspPairId: "middle-shared",
125
+ pinIds: ["middle", "shared"],
126
+ tracePath: [
127
+ { x: 2, y: 2 },
128
+ { x: 0, y: 2 },
129
+ { x: 0, y: 0 },
130
+ ],
131
+ }),
132
+ ]
133
+
134
+ const result = collapseRedundantSharedEndpointStubs({
135
+ traces,
136
+ netLabelPlacements: [],
137
+ netLabelConnectorTraceIds: new Set<MspConnectionPairId>(),
138
+ multiPinNetPinIds: new Set(["shared"]),
139
+ })
140
+
141
+ expect(
142
+ result.filter((trace) =>
143
+ trace.tracePath.some((point) => point.x === 0 && point.y === 0),
144
+ ),
145
+ ).toHaveLength(1)
146
+ })