@tscircuit/schematic-trace-solver 0.0.187 → 0.0.189

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 (60) hide show
  1. package/dist/index.d.ts +42 -23
  2. package/dist/index.js +1053 -316
  3. package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +612 -230
  4. package/lib/solvers/InlineNetLabelSolver/alignPortOnlyInlineNetLabelStubs.ts +2 -6
  5. package/lib/solvers/InlineNetLabelSolver/getAnchoredNetLabelRenderedBounds.ts +13 -0
  6. package/lib/solvers/InlineNetLabelSolver/getInlineLabelObstacles.ts +64 -0
  7. package/lib/solvers/InlineNetLabelSolver/getLocalTraceLabelShifts.ts +407 -0
  8. package/lib/solvers/InlineNetLabelSolver/getOutputLabelCollisions.ts +145 -0
  9. package/lib/solvers/InlineNetLabelSolver/isLabelRepresentedInline.ts +15 -0
  10. package/lib/solvers/InlineNetLabelSolver/pushAnchoredNetLabelsAwayFromInlineLabels.ts +61 -31
  11. package/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts +94 -16
  12. package/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts +4 -3
  13. package/lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts +7 -7
  14. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +3 -0
  15. package/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts +5 -1
  16. package/lib/types/InputProblem.ts +2 -2
  17. package/package.json +1 -1
  18. package/site/SchematicTracePipelineSolver/repro-robot-controller-imu-tof.page.tsx +7 -0
  19. package/site/bug-reports/bug-report-20260907T110144Z.page.tsx +4 -0
  20. package/site/bug-reports/bug-report-20260907T145640Z.page.tsx +4 -0
  21. package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +2 -2
  22. package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +2 -2
  23. package/tests/bug-reports/bug-report-20260811T075142Z/__snapshots__/bug-report-20260811T075142Z.snap.svg +18 -18
  24. package/tests/bug-reports/bug-report-20260819T091818Z/__snapshots__/bug-report-20260819T091818Z.snap.svg +2 -2
  25. package/tests/bug-reports/bug-report-20260819T091818Z/bug-report-20260819T091818Z.test.ts +21 -0
  26. package/tests/bug-reports/bug-report-20260825T103621Z/__snapshots__/bug-report-20260825T103621Z.snap.svg +5 -38
  27. package/tests/bug-reports/bug-report-20260826T072956Z/__snapshots__/bug-report-20260826T072956Z.snap.svg +3 -9
  28. package/tests/bug-reports/bug-report-20260901T055358Z/__snapshots__/bug-report-20260901T055358Z.snap.svg +18 -30
  29. package/tests/bug-reports/bug-report-20260901T055358Z/bug-report-20260901T055358Z.test.ts +33 -0
  30. package/tests/bug-reports/bug-report-20260907T110144Z/__snapshots__/bug-report-20260907T110144Z.snap.svg +282 -0
  31. package/tests/bug-reports/bug-report-20260907T110144Z/bug-report-20260907T110144Z.json +1073 -0
  32. package/tests/bug-reports/bug-report-20260907T110144Z/bug-report-20260907T110144Z.test.ts +12 -0
  33. package/tests/bug-reports/bug-report-20260907T145640Z/__snapshots__/bug-report-20260907T145640Z.snap.svg +1107 -0
  34. package/tests/bug-reports/bug-report-20260907T145640Z/bug-report-20260907T145640Z.json +3563 -0
  35. package/tests/bug-reports/bug-report-20260907T145640Z/bug-report-20260907T145640Z.test.ts +117 -0
  36. package/tests/repros/__snapshots__/repro-battery-management-bq24073.snap.svg +4 -4
  37. package/tests/repros/__snapshots__/repro-board-648-esp12f-section.snap.svg +2 -2
  38. package/tests/repros/__snapshots__/repro-core-tb67s579ftg-inline-label-routing.snap.svg +20 -20
  39. package/tests/repros/__snapshots__/repro-esp12f-section.snap.svg +37 -55
  40. package/tests/repros/__snapshots__/repro-hub-usb-sheet.snap.svg +44 -68
  41. package/tests/repros/__snapshots__/repro-mspm0l1306-capacitor-symmetry.snap.svg +3 -9
  42. package/tests/repros/__snapshots__/repro-robot-controller-imu-tof.snap.svg +199 -0
  43. package/tests/repros/__snapshots__/repro-robot-controller-imu-tof.source.svg +33 -0
  44. package/tests/repros/__snapshots__/repro-usb-power-vbus-label-detour.snap.svg +50 -74
  45. package/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +2 -2
  46. package/tests/repros/__snapshots__/rp2040-robot-controller-disconnected-gnd.snap.svg +103 -0
  47. package/tests/repros/assets/repro-robot-controller-imu-tof.input.json +540 -0
  48. package/tests/repros/assets/rp2040-robot-controller.input.json +349 -0
  49. package/tests/repros/repro-battery-management-bq24073.test.ts +13 -1
  50. package/tests/repros/repro-hub-usb-sheet.test.ts +25 -0
  51. package/tests/repros/repro-robot-controller-imu-tof.test.ts +83 -0
  52. package/tests/repros/repro-usb-power-vbus-label-detour.test.ts +59 -13
  53. package/tests/repros/rp2040-robot-controller-disconnected-gnd.test.ts +59 -0
  54. package/tests/solvers/InlineNetLabelSolver/get-anchored-net-label-rendered-bounds.test.ts +24 -0
  55. package/tests/solvers/InlineNetLabelSolver/local-trace-label-shifts.test.ts +296 -0
  56. package/tests/solvers/InlineNetLabelSolver/multi-pin-connected-components.test.ts +7 -4
  57. package/tests/solvers/InlineNetLabelSolver/push-anchored-net-labels-away.test.ts +2 -1
  58. package/tests/solvers/InlineNetLabelSolver/shared-direct-and-named-connection.test.ts +62 -0
  59. package/tests/solvers/InlineNetLabelSolver/two-pin-terminal-stubs-atomic.test.ts +9 -3
  60. package/tests/solvers/TraceCleanupSolver/perpendicular-detours-preserve-endpoints.test.ts +60 -0
@@ -0,0 +1,349 @@
1
+ {
2
+ "chips": [
3
+ {
4
+ "chipId": "schematic_component_39",
5
+ "center": {
6
+ "x": -30,
7
+ "y": 0
8
+ },
9
+ "width": 2.1000000000000014,
10
+ "height": 1,
11
+ "pins": [
12
+ {
13
+ "pinId": "schematic_port_159",
14
+ "x": -31.05,
15
+ "y": 0.4
16
+ },
17
+ {
18
+ "pinId": "schematic_port_160",
19
+ "x": -31.05,
20
+ "y": 0.2
21
+ },
22
+ {
23
+ "pinId": "schematic_port_161",
24
+ "x": -31.05,
25
+ "y": 0
26
+ },
27
+ {
28
+ "pinId": "schematic_port_162",
29
+ "x": -31.05,
30
+ "y": -0.20000000000000007
31
+ },
32
+ {
33
+ "pinId": "schematic_port_163",
34
+ "x": -31.05,
35
+ "y": -0.4
36
+ },
37
+ {
38
+ "pinId": "schematic_port_164",
39
+ "x": -28.95,
40
+ "y": -0.30000000000000004
41
+ },
42
+ {
43
+ "pinId": "schematic_port_165",
44
+ "x": -28.95,
45
+ "y": -0.10000000000000003
46
+ },
47
+ {
48
+ "pinId": "schematic_port_166",
49
+ "x": -28.95,
50
+ "y": 0.09999999999999998
51
+ },
52
+ {
53
+ "pinId": "schematic_port_167",
54
+ "x": -28.95,
55
+ "y": 0.30000000000000004
56
+ }
57
+ ],
58
+ "sectionId": "Motors"
59
+ },
60
+ {
61
+ "chipId": "schematic_component_40",
62
+ "center": {
63
+ "x": -30,
64
+ "y": 4
65
+ },
66
+ "width": 1.2000000000000028,
67
+ "height": 0.5999999999999996,
68
+ "pins": [
69
+ {
70
+ "pinId": "schematic_port_168",
71
+ "x": -29.4,
72
+ "y": 4.1
73
+ },
74
+ {
75
+ "pinId": "schematic_port_169",
76
+ "x": -29.4,
77
+ "y": 3.9
78
+ }
79
+ ],
80
+ "sectionId": "Motors"
81
+ },
82
+ {
83
+ "chipId": "schematic_component_41",
84
+ "center": {
85
+ "x": -32.8675,
86
+ "y": -4
87
+ },
88
+ "width": 1.1650000000000063,
89
+ "height": 0.7600000000000011,
90
+ "pins": [
91
+ {
92
+ "pinId": "schematic_port_170",
93
+ "x": -33,
94
+ "y": -3.619999999999999,
95
+ "_facingDirection": "y+"
96
+ },
97
+ {
98
+ "pinId": "schematic_port_171",
99
+ "x": -33,
100
+ "y": -4.380000000000001,
101
+ "_facingDirection": "y-"
102
+ }
103
+ ],
104
+ "sectionId": "Motors"
105
+ },
106
+ {
107
+ "chipId": "schematic_component_42",
108
+ "center": {
109
+ "x": -29.8075,
110
+ "y": -4
111
+ },
112
+ "width": 1.2849999999999966,
113
+ "height": 0.7600000000000011,
114
+ "pins": [
115
+ {
116
+ "pinId": "schematic_port_172",
117
+ "x": -30,
118
+ "y": -3.619999999999999,
119
+ "_facingDirection": "y+"
120
+ },
121
+ {
122
+ "pinId": "schematic_port_173",
123
+ "x": -30,
124
+ "y": -4.380000000000001,
125
+ "_facingDirection": "y-"
126
+ }
127
+ ],
128
+ "sectionId": "Motors"
129
+ }
130
+ ],
131
+ "directConnections": [],
132
+ "netConnections": [
133
+ {
134
+ "netId": "V3V3",
135
+ "netLabelWidth": 0.42,
136
+ "netLabelHeight": 0.6,
137
+ "pinIds": [
138
+ "schematic_port_166",
139
+ "schematic_port_172"
140
+ ]
141
+ },
142
+ {
143
+ "netId": "GND",
144
+ "netLabelWidth": 0.42,
145
+ "netLabelHeight": 0.48,
146
+ "pinIds": [
147
+ "schematic_port_162",
148
+ "schematic_port_167",
149
+ "schematic_port_171",
150
+ "schematic_port_173"
151
+ ]
152
+ },
153
+ {
154
+ "netId": "MA1",
155
+ "netLabelWidth": 0.48,
156
+ "pinIds": [
157
+ "schematic_port_164"
158
+ ]
159
+ },
160
+ {
161
+ "netId": "MA2",
162
+ "netLabelWidth": 0.48,
163
+ "pinIds": [
164
+ "schematic_port_163"
165
+ ]
166
+ },
167
+ {
168
+ "netId": "MOTOR_EN",
169
+ "netLabelWidth": 1.08,
170
+ "pinIds": [
171
+ "schematic_port_165"
172
+ ]
173
+ },
174
+ {
175
+ "netId": "VM",
176
+ "netLabelWidth": 0.42,
177
+ "netLabelHeight": 0.36,
178
+ "pinIds": [
179
+ "schematic_port_159",
180
+ "schematic_port_170"
181
+ ]
182
+ },
183
+ {
184
+ "netId": "OUT0A",
185
+ "netLabelWidth": 0.72,
186
+ "pinIds": [
187
+ "schematic_port_160",
188
+ "schematic_port_168"
189
+ ]
190
+ },
191
+ {
192
+ "netId": "OUT0B",
193
+ "netLabelWidth": 0.72,
194
+ "pinIds": [
195
+ "schematic_port_161",
196
+ "schematic_port_169"
197
+ ]
198
+ }
199
+ ],
200
+ "textBoxes": [
201
+ {
202
+ "chipId": "schematic_component_39",
203
+ "center": {
204
+ "x": -29.99,
205
+ "y": -0.63
206
+ },
207
+ "width": 1.3200000000000003,
208
+ "height": 0.17999999999999994,
209
+ "text": "DRV8837DSGR"
210
+ },
211
+ {
212
+ "chipId": "schematic_component_39",
213
+ "center": {
214
+ "x": -30.409999999999997,
215
+ "y": 0.615
216
+ },
217
+ "width": 0.5999999999999979,
218
+ "height": 0.25,
219
+ "text": "U_M0"
220
+ },
221
+ {
222
+ "chipId": "schematic_component_40",
223
+ "center": {
224
+ "x": -29.96,
225
+ "y": 4.414999999999999
226
+ },
227
+ "width": 0.5999999999999979,
228
+ "height": 0.2499999999999991,
229
+ "text": "J_M0"
230
+ }
231
+ ],
232
+ "availableNetLabelOrientations": {
233
+ "VM": [
234
+ "y+"
235
+ ],
236
+ "VSERVO": [
237
+ "y+"
238
+ ],
239
+ "V3V3": [
240
+ "y+"
241
+ ],
242
+ "VBUS": [
243
+ "y+"
244
+ ],
245
+ "GND": [
246
+ "y-"
247
+ ],
248
+ "MA1": [
249
+ "x-",
250
+ "x+"
251
+ ],
252
+ "MA2": [
253
+ "x-",
254
+ "x+"
255
+ ],
256
+ "MB1": [
257
+ "x-",
258
+ "x+"
259
+ ],
260
+ "MB2": [
261
+ "x-",
262
+ "x+"
263
+ ],
264
+ "EA": [
265
+ "x-",
266
+ "x+"
267
+ ],
268
+ "EB": [
269
+ "x-",
270
+ "x+"
271
+ ],
272
+ "EC": [
273
+ "x-",
274
+ "x+"
275
+ ],
276
+ "ED": [
277
+ "x-",
278
+ "x+"
279
+ ],
280
+ "SDA": [
281
+ "x-",
282
+ "x+"
283
+ ],
284
+ "SCL": [
285
+ "x-",
286
+ "x+"
287
+ ],
288
+ "SERVO1": [
289
+ "x-",
290
+ "x+"
291
+ ],
292
+ "SERVO2": [
293
+ "x-",
294
+ "x+"
295
+ ],
296
+ "SERVO3": [
297
+ "x-",
298
+ "x+"
299
+ ],
300
+ "MOTOR_EN": [
301
+ "x-",
302
+ "x+"
303
+ ],
304
+ "IMU_INT": [
305
+ "x-",
306
+ "x+"
307
+ ],
308
+ "GPIO15": [
309
+ "x-",
310
+ "x+"
311
+ ],
312
+ "GPIO16": [
313
+ "x-",
314
+ "x+"
315
+ ],
316
+ "GPIO17": [
317
+ "x-",
318
+ "x+"
319
+ ],
320
+ "OUT0A": [
321
+ "x-",
322
+ "x+"
323
+ ],
324
+ "OUT0B": [
325
+ "x-",
326
+ "x+"
327
+ ],
328
+ "OUT1A": [
329
+ "x-",
330
+ "x+"
331
+ ],
332
+ "OUT1B": [
333
+ "x-",
334
+ "x+"
335
+ ],
336
+ "VSYS": [
337
+ "y+"
338
+ ],
339
+ "ADC_VREF": [
340
+ "x-",
341
+ "x+"
342
+ ],
343
+ "V1V1": [
344
+ "y+"
345
+ ]
346
+ },
347
+ "maxMspPairDistance": 2.4,
348
+ "_hideRatsNet": false
349
+ }
@@ -71,7 +71,19 @@ test("repro BatteryManagement_BQ24073 schematic traces", () => {
71
71
  )
72
72
  .sort((first, second) => first.anchorPoint.y - second.anchorPoint.y)[0]!
73
73
 
74
- expect(solver.inlineNetLabelSolver!.stats.pushedAnchoredNetLabelCount).toBe(1)
74
+ // An inline label farther down the same column must not pull this ground
75
+ // label off its existing trace and create an unnecessary dangling branch.
76
+ const upperGroundLabel = output.netLabelPlacements.find(
77
+ (label) =>
78
+ label.pinIds.includes("schematic_port_1") &&
79
+ label.pinIds.includes("schematic_port_8"),
80
+ )!
81
+ expect(upperGroundLabel.anchorPoint.y).toBeCloseTo(0.1)
82
+ expect(
83
+ output.traces.some(
84
+ (trace) => trace.mspPairId === "inline-net-label-clearance-0-GND",
85
+ ),
86
+ ).toBe(false)
75
87
  expect(bottomGroundLabel.anchorPoint.x).toBeCloseTo(
76
88
  EXPECTED_BOTTOM_GROUND_LABEL_X,
77
89
  )
@@ -1,4 +1,5 @@
1
1
  import { expect, test } from "bun:test"
2
+ import { getOutputLabelCollisions } from "lib/solvers/InlineNetLabelSolver/getOutputLabelCollisions"
2
3
  import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
3
4
  import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
4
5
  import { isVertical } from "lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry"
@@ -52,5 +53,29 @@ test("repro hub USB sheet schematic trace routing", () => {
52
53
  expect(sameChipGroundLabel.orientation).toBe("y-")
53
54
  expect(sameChipGroundLabel.anchorPoint.x).toBeCloseTo(verticalHostPoint.x)
54
55
  expect(tracesAtSameChipGroundLabel).toHaveLength(1)
56
+ const txLabel = solver
57
+ .inlineNetLabelSolver!.getOutput()
58
+ .inlineNetLabelPlacements.find((label) =>
59
+ label.pinIds.includes("schematic_port_434"),
60
+ )
61
+ expect(txLabel?.axis).toBe("y")
62
+ expect(txLabel?.side).toBe("x+")
63
+ expect(
64
+ solver
65
+ .inlineNetLabelSolver!.getOutput()
66
+ .netLabelPlacements.some((label) =>
67
+ label.pinIds.includes("schematic_port_434"),
68
+ ),
69
+ ).toBe(false)
70
+ expect(
71
+ [
72
+ ...getOutputLabelCollisions(solver.inlineNetLabelSolver!.getOutput()),
73
+ ].filter(
74
+ (collision) =>
75
+ collision.kind === "trace-label" &&
76
+ collision.trace.kind === "routed" &&
77
+ collision.trace.id === "schematic_port_375-schematic_port_373",
78
+ ),
79
+ ).toEqual([])
55
80
  expect(solver).toMatchSolverSnapshot(import.meta.path)
56
81
  })
@@ -0,0 +1,83 @@
1
+ import { expect, test } from "bun:test"
2
+ import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
3
+ import {
4
+ EPS,
5
+ tracePathContainsPoint,
6
+ } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
7
+ import type { InputProblem } from "lib/types/InputProblem"
8
+ import { convertSolverOutputToCircuitJson } from "tests/fixtures/convertSolverOutputToCircuitJson"
9
+ import "tests/fixtures/matcher"
10
+ import inputProblem from "./assets/repro-robot-controller-imu-tof.input.json"
11
+
12
+ // IMU/ToF section from ROVER, captured with @tscircuit/core 0.0.1861.
13
+ // Other sections are removed; retained placement and routing options are unchanged.
14
+ test("repro robot controller IMU and ToF trace routing", async () => {
15
+ expect([
16
+ ...new Set(inputProblem.chips.map((chip) => chip.sectionId)),
17
+ ]).toEqual(["07_IMU_AND_TOF"])
18
+
19
+ const solver = new SchematicTracePipelineSolver(
20
+ inputProblem as InputProblem,
21
+ { hideRatsNet: true },
22
+ )
23
+
24
+ solver.solve()
25
+
26
+ expect(solver.solved).toBe(true)
27
+ expect(solver.failed).toBe(false)
28
+ const sclConnection = inputProblem.netConnections.find(
29
+ (connection) => connection.netId === "I2C_SCL",
30
+ )!
31
+ const sclTrace = solver
32
+ .netLabelToTraceSolver!.getOutput()
33
+ .traces.find(
34
+ (trace) =>
35
+ trace.pinIds.includes("schematic_port_275") &&
36
+ trace.pinIds.includes("schematic_port_288"),
37
+ )!
38
+ const connectedSclPins = inputProblem.chips.flatMap((chip) =>
39
+ chip.pins.filter(
40
+ (pin) =>
41
+ sclConnection.pinIds.includes(pin.pinId) &&
42
+ tracePathContainsPoint(sclTrace.tracePath, pin),
43
+ ),
44
+ )
45
+ expect(connectedSclPins).toHaveLength(2)
46
+ const sdaLabel = solver
47
+ .inlineNetLabelSolver!.getOutput()
48
+ .inlineNetLabelPlacements.find((label) =>
49
+ label.pinIds.includes("schematic_port_276"),
50
+ )!
51
+ expect(sdaLabel.side).toBe("y+")
52
+ expect(sdaLabel.center.y).toBeGreaterThan(sdaLabel.anchorPoint.y)
53
+ const output = solver.netLabelToTraceSolver!.getOutput()
54
+ const connectorIds =
55
+ solver.inlineNetLabelSolver!.getOutput().netLabelConnectorTraceIds!
56
+ const connectorTraces = output.traces.filter((trace) =>
57
+ connectorIds.has(trace.mspPairId),
58
+ )
59
+ expect(connectorTraces).toHaveLength(4)
60
+ for (const trace of connectorTraces) {
61
+ const endpoint = trace.tracePath.at(-1)!
62
+ expect(
63
+ output.netLabelPlacements.some(
64
+ (label) =>
65
+ label.globalConnNetId === trace.globalConnNetId &&
66
+ Math.abs(label.anchorPoint.x - endpoint.x) < EPS &&
67
+ Math.abs(label.anchorPoint.y - endpoint.y) < EPS,
68
+ ),
69
+ ).toBe(true)
70
+ }
71
+ const circuitJson = convertSolverOutputToCircuitJson(solver)
72
+ expect(
73
+ circuitJson
74
+ .filter((element) => element.type === "schematic_text")
75
+ .map((element) => element.text),
76
+ ).toContain("I2C_SDA")
77
+ expect(
78
+ circuitJson
79
+ .filter((element) => element.type === "schematic_net_label")
80
+ .map((element) => element.text),
81
+ ).toContain("V3V3_IMU")
82
+ await expect(solver).toMatchSolverSnapshot(import.meta.path)
83
+ })
@@ -1,3 +1,4 @@
1
+ import { getOutputLabelCollisions } from "lib/solvers/InlineNetLabelSolver/getOutputLabelCollisions"
1
2
  import { expect, test } from "bun:test"
2
3
  import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
3
4
  import type { InputProblem } from "lib/types/InputProblem"
@@ -6,7 +7,7 @@ import inputProblemJson from "./assets/repro-usb-power-vbus-label-detour.input.j
6
7
 
7
8
  const VBUS_PIN_IDS = new Set(["schematic_port_224", "schematic_port_226"])
8
9
 
9
- test("routes the usb power vbus connection without a label detour", () => {
10
+ test("keeps the compact VBUS route and USB labels clear of neighboring wires", () => {
10
11
  const inputProblem: InputProblem = JSON.parse(
11
12
  JSON.stringify(inputProblemJson),
12
13
  )
@@ -14,11 +15,13 @@ test("routes the usb power vbus connection without a label detour", () => {
14
15
 
15
16
  solver.solve()
16
17
 
17
- const vbusTrace = solver.sameNetJunctionAlignmentSolver!.outputTraces.find(
18
- (trace) =>
19
- trace.pins.length === VBUS_PIN_IDS.size &&
20
- trace.pins.every((pin) => VBUS_PIN_IDS.has(pin.pinId)),
21
- )
18
+ const vbusTrace = solver
19
+ .inlineNetLabelSolver!.getOutput()
20
+ .traces.find(
21
+ (trace) =>
22
+ trace.pins.length === VBUS_PIN_IDS.size &&
23
+ trace.pins.every((pin) => VBUS_PIN_IDS.has(pin.pinId)),
24
+ )
22
25
  const gndLabel =
23
26
  solver.availableNetOrientationSolver!.outputNetLabelPlacements.find(
24
27
  (label) =>
@@ -27,7 +30,9 @@ test("routes the usb power vbus connection without a label detour", () => {
27
30
  )
28
31
  const gndLabelConnector = solver.availableNetOrientationSolver!.traces.find(
29
32
  (trace) =>
30
- trace.mspPairId.startsWith("available-net-orientation-") &&
33
+ solver.availableNetOrientationSolver!.netLabelConnectorTraceIds.has(
34
+ trace.mspPairId,
35
+ ) &&
31
36
  trace.pinIds.includes("schematic_port_37") &&
32
37
  trace.pinIds.includes("schematic_port_46"),
33
38
  )
@@ -60,12 +65,53 @@ test("routes the usb power vbus connection without a label detour", () => {
60
65
  { x: 12.8, y: -5.8 },
61
66
  { x: 13, y: -5.8 },
62
67
  ])
63
- // The available inline side of USB_HS_DM overlaps the retained USB_HS_DP
64
- // tag. Keep both differential-pair nets anchored instead of producing a
65
- // mixed, overlapping representation.
66
- expect(usbHighSpeedInlineLabels).toHaveLength(0)
68
+ expect(usbHighSpeedAnchoredLabels).toEqual([])
69
+ expect(usbHighSpeedInlineLabels).toHaveLength(4)
70
+ expect(usbHighSpeedInlineLabels.every((label) => label.side === "y+")).toBe(
71
+ true,
72
+ )
73
+ const dm = usbHighSpeedInlineLabels.find((label) =>
74
+ label.pinIds.includes("schematic_port_191"),
75
+ )!
76
+ const vbusConnector = inlineOutput.traces.find((trace) =>
77
+ trace.pinIds.includes("schematic_port_192"),
78
+ )!
79
+ expect(dm.center.y - dm.height / 2).toBeGreaterThan(dm.stubTracePath![0].y)
80
+ expect(dm.center.y + dm.height / 2).toBeLessThan(
81
+ vbusConnector.tracePath[0]!.y,
82
+ )
83
+ for (const pinId of ["schematic_port_188", "schematic_port_190"]) {
84
+ expect(
85
+ inputProblem.directConnections
86
+ .filter((connection) => connection.pinIds.includes(pinId))
87
+ .every((connection) => !connection.allowInlineNetLabel),
88
+ ).toBe(true)
89
+ expect(
90
+ inlineOutput.netLabelPlacements.some((label) =>
91
+ label.pinIds.includes(pinId),
92
+ ),
93
+ ).toBe(true)
94
+ }
67
95
  expect(
68
- usbHighSpeedAnchoredLabels.map((placement) => placement.netId).sort(),
69
- ).toEqual(["USB_HS_DM", "USB_HS_DM", "USB_HS_DP", "USB_HS_DP"])
96
+ getOutputLabelCollisions(inlineOutput).filter((collision) => {
97
+ const labels =
98
+ collision.kind === "label-label" ? collision.labels : [collision.label]
99
+ return labels.some((label) =>
100
+ usbHighSpeedInlineLabels.some((usb) => usb === label),
101
+ )
102
+ }),
103
+ ).toEqual([])
104
+ for (const pinId of [
105
+ "schematic_port_83",
106
+ "schematic_port_84",
107
+ "schematic_port_191",
108
+ "schematic_port_193",
109
+ ]) {
110
+ expect(
111
+ [...usbHighSpeedInlineLabels, ...usbHighSpeedAnchoredLabels].filter(
112
+ (label) => label.pinIds.includes(pinId),
113
+ ),
114
+ ).toHaveLength(1)
115
+ }
70
116
  expect(solver).toMatchSolverSnapshot(import.meta.path)
71
117
  })
@@ -0,0 +1,59 @@
1
+ import { expect, test } from "bun:test"
2
+ import { convertCircuitJsonToSchematicSvg } from "circuit-to-svg"
3
+ import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
4
+ import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
5
+ import type { InputProblem } from "lib/types/InputProblem"
6
+ import { convertSolverOutputToCircuitJson } from "tests/fixtures/convertSolverOutputToCircuitJson"
7
+ import { readFileSync } from "node:fs"
8
+
9
+ const inputProblem: InputProblem = JSON.parse(
10
+ readFileSync(
11
+ new URL("./assets/rp2040-robot-controller.input.json", import.meta.url),
12
+ "utf8",
13
+ ),
14
+ )
15
+
16
+ // Captured from core rendering https://tscircuit.com/mohan-bee/rp2040-robot-controller.
17
+ // U_M0 pin4 (GND) and pin9 (EP) share a generated ground-label connector.
18
+ test("rp2040 robot controller keeps GND connected after junction alignment", () => {
19
+ const solver = new SchematicTracePipelineSolver(inputProblem)
20
+ solver.solve()
21
+ expect(solver.solved).toBe(true)
22
+ expect(solver.failed).toBe(false)
23
+
24
+ const before = solver.netLabelNetLabelCollisionSolver!
25
+ const after = solver.inlineNetLabelSolver!.getOutput()
26
+ const groundLabel = after.netLabelPlacements.find((label) =>
27
+ label.pinIds.includes("schematic_port_162"),
28
+ )!
29
+ const beforeConnector = before.traces.find(
30
+ (trace) =>
31
+ solver.availableNetOrientationSolver!.netLabelConnectorTraceIds.has(
32
+ trace.mspPairId,
33
+ ) && trace.pinIds.includes("schematic_port_162"),
34
+ )!
35
+ const afterConnector = after.traces.find(
36
+ (trace) => trace.mspPairId === beforeConnector.mspPairId,
37
+ )!
38
+ const beforeGroundLabel = before
39
+ .getOutput()
40
+ .netLabelPlacements.find((label) =>
41
+ label.pinIds.includes("schematic_port_162"),
42
+ )!
43
+ expect(
44
+ tracePathContainsPoint(
45
+ beforeConnector.tracePath,
46
+ beforeGroundLabel.anchorPoint,
47
+ ),
48
+ ).toBe(true)
49
+ expect(
50
+ tracePathContainsPoint(afterConnector.tracePath, groundLabel.anchorPoint),
51
+ ).toBe(true)
52
+
53
+ expect(
54
+ convertCircuitJsonToSchematicSvg(convertSolverOutputToCircuitJson(solver), {
55
+ width: 1000,
56
+ height: 800,
57
+ }).replace(/[ \t]+$/gm, ""),
58
+ ).toMatchSvgSnapshot(import.meta.path)
59
+ })
@@ -16,6 +16,30 @@ const createPlacement = (
16
16
  ...overrides,
17
17
  })
18
18
 
19
+ test("orients named vertical tags while preserving width-only placeholders", () => {
20
+ for (const orientation of ["y+", "y-"] as const) {
21
+ const placement = createPlacement({
22
+ orientation,
23
+ width: 1.2,
24
+ height: 0.42,
25
+ center: { x: 2, y: 3 },
26
+ })
27
+ const placeholderBounds = getAnchoredNetLabelRenderedBounds(placement)
28
+ expect(placeholderBounds.maxX - placeholderBounds.minX).toBeCloseTo(1.2)
29
+ expect(placeholderBounds.maxY - placeholderBounds.minY).toBeCloseTo(0.42)
30
+
31
+ const namedBounds = getAnchoredNetLabelRenderedBounds({
32
+ ...placement,
33
+ netLabelText: "SUPPLY",
34
+ })
35
+ expect(namedBounds.minX).toBeCloseTo(1.79)
36
+ expect(namedBounds.maxX).toBeCloseTo(2.21)
37
+ expect(namedBounds.maxY - namedBounds.minY).toBeCloseTo(1.2)
38
+ if (orientation === "y+") expect(namedBounds.minY).toBeCloseTo(3)
39
+ else expect(namedBounds.maxY).toBeCloseTo(3)
40
+ }
41
+ })
42
+
19
43
  test("uses the horizontal renderer envelope for x-facing labels", () => {
20
44
  const horizontalBounds = getAnchoredNetLabelRenderedBounds(
21
45
  createPlacement({}),