@tscircuit/schematic-trace-solver 0.0.202 → 0.0.204

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.js +54 -7
  2. package/lib/solvers/TraceCleanupSolver/getInteriorShortcutPaths.ts +41 -0
  3. package/lib/solvers/TraceCleanupSolver/rerouteGeneratedNetLabelConnectorCrossings.ts +7 -0
  4. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getFixedLabelCoordinate.ts +17 -8
  5. package/lib/solvers/TraceElbowTransitionSimplificationSolver/TraceElbowTransitionSimplificationSolver.ts +25 -3
  6. package/package.json +1 -1
  7. package/site/SchematicTracePipelineSolver/repro-smart-lock-ble-module.page.tsx +7 -0
  8. package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +78 -60
  9. package/tests/bug-reports/bug-report-20260707T134722Z/__snapshots__/bug-report-20260707T134722Z.snap.svg +62 -62
  10. package/tests/bug-reports/bug-report-20260708T055430Z/__snapshots__/bug-report-20260708T055430Z.snap.svg +2 -2
  11. package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +2 -2
  12. package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +2 -2
  13. package/tests/bug-reports/bug-report-20260901T055358Z/__snapshots__/bug-report-20260901T055358Z.snap.svg +10 -10
  14. package/tests/bug-reports/bug-report-20260905T041712Z/__snapshots__/bug-report-20260905T041712Z.snap.svg +10 -10
  15. package/tests/bug-reports/bug-report-20260907T110144Z/__snapshots__/bug-report-20260907T110144Z.snap.svg +9 -9
  16. package/tests/bug-reports/bug-report-20260907T145640Z/__snapshots__/bug-report-20260907T145640Z.snap.svg +8 -8
  17. package/tests/bug-reports/bug-report-20260911T195723Z/__snapshots__/bug-report-20260911T195723Z.snap.svg +19 -19
  18. package/tests/bug-reports/bug-report-20260916T054012Z/__snapshots__/bug-report-20260916T054012Z.snap.svg +292 -292
  19. package/tests/examples/__snapshots__/example30.snap.svg +123 -123
  20. package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +18 -18
  21. package/tests/repros/__snapshots__/board-15984-same-net-junction.snap.svg +2 -2
  22. package/tests/repros/__snapshots__/repro-am3352-06-boot.snap.svg +448 -0
  23. package/tests/repros/__snapshots__/repro-am3352-sheet3-power-rails.snap.svg +2 -2
  24. package/tests/repros/__snapshots__/repro-hub-usb-sheet.snap.svg +2 -2
  25. package/tests/repros/__snapshots__/repro-ina237-current-monitor.snap.svg +2 -2
  26. package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +2 -2
  27. package/tests/repros/__snapshots__/repro-robot-controller-imu-tof.snap.svg +7 -7
  28. package/tests/repros/__snapshots__/repro-smart-lock-ble-module.snap.svg +465 -0
  29. package/tests/repros/__snapshots__/repro-tida010076-page02.snap.svg +10 -10
  30. package/tests/repros/assets/repro-am3352-06-boot.input.json +1875 -0
  31. package/tests/repros/assets/repro-smart-lock-ble-module.input.json +2183 -0
  32. package/tests/repros/repro-am3352-06-boot.test.ts +43 -0
  33. package/tests/repros/repro-smart-lock-ble-module.test.ts +69 -0
  34. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-fixed-backbone.test.ts +135 -0
  35. package/tests/solvers/TraceCleanupSolver/reroute-generated-net-label-connector-crossings.test.ts +49 -0
  36. package/tests/solvers/TraceElbowTransitionSimplificationSolver/interior-staircase.test.ts +146 -0
@@ -0,0 +1,43 @@
1
+ import { expect, test } from "bun:test"
2
+ import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
3
+ import type { InputProblem } from "lib/types/InputProblem"
4
+ import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
5
+ import { findPerpendicularPathCrossings } from "lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles"
6
+ import "tests/fixtures/matcher"
7
+ import input from "./assets/repro-am3352-06-boot.input.json"
8
+
9
+ // Full sheet from am3352-dev-board-4layer-dogbone.json; routing uses core defaults.
10
+ test("repro complete AM3352 06-Boot sheet", async () => {
11
+ const problem = structuredClone(input) as InputProblem
12
+ const solver = new SchematicTracePipelineSolver(problem, {
13
+ hideRatsNet: true,
14
+ })
15
+ solver.solve()
16
+ expect(solver.solved).toBe(true)
17
+ expect(solver.failed).toBe(false)
18
+ const { traces, inlineNetLabelPlacements } =
19
+ solver.netLabelToTraceSolver!.getOutput()
20
+ for (const netId of ["BOOT_SEL1", "BOOT_SEL2", "BOOT_SEL3"]) {
21
+ const trace = traces.find((trace) => trace.userNetId === netId)!
22
+ expect(trace).toBeDefined()
23
+ expect(trace.tracePath).toHaveLength(4)
24
+ expect(trace.tracePath[1]!.y).toBe(trace.tracePath[0]!.y)
25
+ expect(trace.tracePath[2]!.y).toBe(trace.tracePath[3]!.y)
26
+ const label = inlineNetLabelPlacements.find(
27
+ (label) => label.globalConnNetId === trace.globalConnNetId,
28
+ )!
29
+ expect(label).toBeDefined()
30
+ expect(tracePathContainsPoint(trace.tracePath, label.anchorPoint)).toBe(
31
+ true,
32
+ )
33
+ for (const otherTrace of traces) {
34
+ if (otherTrace.globalConnNetId === trace.globalConnNetId) continue
35
+ expect(
36
+ findPerpendicularPathCrossings(trace.tracePath, otherTrace.tracePath, {
37
+ includeTerminalSegments: true,
38
+ }),
39
+ ).toEqual([])
40
+ }
41
+ }
42
+ await expect(solver).toMatchSolverSnapshot(import.meta.path)
43
+ })
@@ -0,0 +1,69 @@
1
+ import { expect, test } from "bun:test"
2
+ import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
3
+ import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
4
+ import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
5
+ import { detectTraceLabelOverlap } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap"
6
+ import type { InputProblem } from "lib/types/InputProblem"
7
+ import "tests/fixtures/matcher"
8
+ import input from "./assets/repro-smart-lock-ble-module.input.json"
9
+
10
+ // Full ble_module sheet from imrishabh18/smart-lock v0.0.1.
11
+ // Reconstructed from deployed Circuit JSON with current core routing defaults.
12
+ test("repro smart-lock BLE module schematic sheet", async () => {
13
+ const solver = new SchematicTracePipelineSolver(
14
+ structuredClone(input) as unknown as InputProblem,
15
+ { hideRatsNet: true },
16
+ )
17
+ solver.solve()
18
+
19
+ expect(solver.solved).toBe(true)
20
+ expect(solver.failed).toBe(false)
21
+
22
+ const { traces, netLabelPlacements } =
23
+ solver.netLabelToTraceSolver!.getOutput()
24
+ const supplyLabel = netLabelPlacements.find((label) =>
25
+ label.mspConnectionPairIds.includes(
26
+ "schematic_port_246-schematic_port_243",
27
+ ),
28
+ )!
29
+ // C99, C104, C103, and C102 must share C101's labeled VDDS rail.
30
+ for (const traceId of [
31
+ "schematic_port_244-schematic_port_246",
32
+ "schematic_port_248-schematic_port_246",
33
+ "schematic_port_250-schematic_port_248",
34
+ "schematic_port_252-schematic_port_250",
35
+ ]) {
36
+ const trace = traces.find((trace) => trace.mspPairId === traceId)!
37
+ expect(trace.tracePath[1]!.y).toBeCloseTo(supplyLabel.anchorPoint.y)
38
+ expect(trace.tracePath[2]!.y).toBeCloseTo(supplyLabel.anchorPoint.y)
39
+ }
40
+
41
+ // The C92–L34 ground trace needs one offset around U1's label column.
42
+ const groundTrace = traces.find(
43
+ (trace) => trace.mspPairId === "schematic_port_291-schematic_port_297",
44
+ )!
45
+ const path = groundTrace.tracePath
46
+ expect(path).toHaveLength(6)
47
+ expect(path[2]!.x).toBeCloseTo(path[3]!.x)
48
+ expect(path[3]!.y).toBeCloseTo(path[4]!.y)
49
+ for (const [endpoint, neighbor, pin] of [
50
+ [path[0]!, path[1]!, groundTrace.pins[0]!],
51
+ [path.at(-1)!, path.at(-2)!, groundTrace.pins[1]!],
52
+ ] as const) {
53
+ expect(endpoint.x).toBeCloseTo(pin.x)
54
+ expect(endpoint.y).toBeCloseTo(pin.y)
55
+ expect(neighbor.x).toBeCloseTo(endpoint.x)
56
+ expect(neighbor.y).toBeLessThan(endpoint.y)
57
+ }
58
+ expect(
59
+ isPathCollidingWithObstacles(path, getObstacleRects(solver.inputProblem)),
60
+ ).toBe(false)
61
+ expect(
62
+ detectTraceLabelOverlap({
63
+ traces: [groundTrace],
64
+ netLabels: netLabelPlacements,
65
+ }),
66
+ ).toHaveLength(0)
67
+
68
+ await expect(solver).toMatchSolverSnapshot(import.meta.path)
69
+ })
@@ -0,0 +1,135 @@
1
+ import { expect, test } from "bun:test"
2
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
3
+ import type { InputProblem } from "lib/types/InputProblem"
4
+ import { align, createTrace } from "./fixtures/alignSameNetRails"
5
+
6
+ const createFixture = () => {
7
+ const firstPin = {
8
+ pinId: "C1.1",
9
+ chipId: "C1",
10
+ x: -2,
11
+ y: 0,
12
+ _facingDirection: "y+" as const,
13
+ }
14
+ const sharedPin = {
15
+ pinId: "C2.1",
16
+ chipId: "C2",
17
+ x: 0,
18
+ y: 0,
19
+ _facingDirection: "y+" as const,
20
+ }
21
+ const supplyPin = {
22
+ pinId: "F1.1",
23
+ chipId: "F1",
24
+ x: 1,
25
+ y: 3,
26
+ _facingDirection: "x-" as const,
27
+ }
28
+ const problem: InputProblem = {
29
+ maxMspPairDistance: 2.4,
30
+ chips: [
31
+ {
32
+ chipId: "C1",
33
+ center: { x: -2, y: -0.5 },
34
+ width: 0.8,
35
+ height: 1,
36
+ pins: [firstPin],
37
+ },
38
+ {
39
+ chipId: "C2",
40
+ center: { x: 0, y: -0.5 },
41
+ width: 0.8,
42
+ height: 1,
43
+ pins: [sharedPin],
44
+ },
45
+ {
46
+ chipId: "F1",
47
+ center: { x: 1.5, y: 3 },
48
+ width: 1,
49
+ height: 0.8,
50
+ pins: [supplyPin],
51
+ },
52
+ ],
53
+ directConnections: [],
54
+ netConnections: [],
55
+ availableNetLabelOrientations: {},
56
+ }
57
+ const branch = createTrace(
58
+ "branch",
59
+ [
60
+ { x: -2, y: 0 },
61
+ { x: -2, y: 1 },
62
+ { x: 0, y: 1 },
63
+ { x: 0, y: 0 },
64
+ ],
65
+ [firstPin, sharedPin],
66
+ )
67
+ const backbone = createTrace(
68
+ "backbone",
69
+ [
70
+ { x: 0, y: 0 },
71
+ { x: 0, y: 2 },
72
+ { x: 0.5, y: 2 },
73
+ { x: 0.5, y: 3 },
74
+ { x: 1, y: 3 },
75
+ ],
76
+ [sharedPin, supplyPin],
77
+ )
78
+ const label: NetLabelPlacement = {
79
+ globalConnNetId: "power-net",
80
+ netId: "POWER",
81
+ mspConnectionPairIds: ["backbone"],
82
+ pinIds: backbone.pinIds,
83
+ orientation: "y+",
84
+ anchorPoint: { x: 0, y: 2 },
85
+ center: { x: 0, y: 2.1 },
86
+ width: 0.4,
87
+ height: 0.2,
88
+ }
89
+ return { problem, branch, backbone, label }
90
+ }
91
+
92
+ test("aligns a branch to a labeled backbone without moving its elbows", () => {
93
+ const { problem, branch, backbone, label } = createFixture()
94
+ const result = align([branch, backbone], {
95
+ inputProblem: problem,
96
+ netLabelPlacements: [label],
97
+ })
98
+
99
+ expect(result.traces[0]!.tracePath).toEqual([
100
+ { x: -2, y: 0 },
101
+ { x: -2, y: 2 },
102
+ { x: 0, y: 2 },
103
+ { x: 0, y: 0 },
104
+ ])
105
+ expect(result.traces[1]).toEqual(backbone)
106
+ })
107
+
108
+ test("keeps the branch below an obstacle on the fixed backbone coordinate", () => {
109
+ const { problem, branch, backbone, label } = createFixture()
110
+ problem.chips.push({
111
+ chipId: "obstacle",
112
+ center: { x: -1, y: 2 },
113
+ width: 0.4,
114
+ height: 0.4,
115
+ pins: [],
116
+ })
117
+ const result = align([branch, backbone], {
118
+ inputProblem: problem,
119
+ netLabelPlacements: [label],
120
+ })
121
+
122
+ expect(result.traces).toEqual([branch, backbone])
123
+ })
124
+
125
+ test("does not pull a multi-turn backbone toward a label on a different coordinate", () => {
126
+ const { problem, branch, backbone, label } = createFixture()
127
+ label.anchorPoint.y = 1.5
128
+ label.center.y = 1.6
129
+ const result = align([branch, backbone], {
130
+ inputProblem: problem,
131
+ netLabelPlacements: [label],
132
+ })
133
+
134
+ expect(result.traces).toEqual([branch, backbone])
135
+ })
@@ -158,3 +158,52 @@ test("allows crossings between generated label connectors", () => {
158
158
  expect(result.reroutedTraceCount).toBe(0)
159
159
  expect(result.traces).toEqual([connectorTrace, otherConnector])
160
160
  })
161
+
162
+ test.each([false, true])(
163
+ "removes the old loop when rerouting a connector crossing (reversed: %s)",
164
+ (reversed) => {
165
+ let tracePath = [
166
+ { x: 0, y: 0 },
167
+ { x: 1, y: 0 },
168
+ { x: 1, y: 3 },
169
+ { x: 4, y: 3 },
170
+ { x: 4, y: -3 },
171
+ { x: 0, y: -3 },
172
+ ]
173
+ if (reversed) tracePath = tracePath.toReversed()
174
+ const trace = makeTrace({
175
+ mspPairId: "loop",
176
+ globalConnNetId: "signal",
177
+ pinIds: ["a", "b"],
178
+ tracePath,
179
+ })
180
+ const connector = makeTrace({
181
+ mspPairId: "connector",
182
+ globalConnNetId: "power",
183
+ pinIds: ["c"],
184
+ tracePath: [
185
+ { x: 0, y: 1 },
186
+ { x: 2, y: 1 },
187
+ ],
188
+ })
189
+ const result = rerouteGeneratedNetLabelConnectorCrossings({
190
+ inputProblem,
191
+ traces: [trace, connector],
192
+ netLabelPlacements: [],
193
+ mergedLabelNetIdMap: {},
194
+ clearance: 0.1,
195
+ connectorTraceIds: new Set([connector.mspPairId]),
196
+ })
197
+ const path = result.traces[0]!.tracePath
198
+ expect(result.reroutedTraceCount).toBe(1)
199
+ expect(path).toHaveLength(4)
200
+ expect(path[0]).toEqual(tracePath[0])
201
+ expect(path.at(-1)).toEqual(tracePath.at(-1))
202
+ expect(getPathLength(path)).toBeLessThan(getPathLength(tracePath))
203
+ expect(
204
+ findPerpendicularPathCrossings(path, connector.tracePath, {
205
+ includeTerminalSegments: true,
206
+ }),
207
+ ).toEqual([])
208
+ },
209
+ )
@@ -0,0 +1,146 @@
1
+ import { expect, test } from "bun:test"
2
+ import { getPathLength } from "lib/solvers/Example28Solver/geometry"
3
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
4
+ import { TraceElbowTransitionSimplificationSolver } from "lib/solvers/TraceElbowTransitionSimplificationSolver/TraceElbowTransitionSimplificationSolver"
5
+ import type { InputProblem } from "lib/types/InputProblem"
6
+ import { createTrace } from "../TraceCleanupSolver/fixtures/alignSameNetRails"
7
+
8
+ const createFixture = () => {
9
+ const path = [
10
+ { x: 5, y: 0 },
11
+ { x: 5, y: -1 },
12
+ { x: 2.5, y: -1 },
13
+ { x: 2.5, y: 2 },
14
+ { x: 2, y: 2 },
15
+ { x: 2, y: 3 },
16
+ { x: 1, y: 3 },
17
+ { x: 1, y: 4 },
18
+ ]
19
+ const trace = createTrace("rerouted", path, [
20
+ { pinId: "C1.1", chipId: "C1", x: 5, y: 0, _facingDirection: "y-" },
21
+ { pinId: "L1.1", chipId: "L1", x: 1, y: 4, _facingDirection: "y-" },
22
+ ])
23
+ const labels: NetLabelPlacement[] = [
24
+ {
25
+ globalConnNetId: "signal",
26
+ netId: "SIGNAL",
27
+ mspConnectionPairIds: [],
28
+ pinIds: [],
29
+ orientation: "x+",
30
+ anchorPoint: { x: 0.5, y: 0.7 },
31
+ center: { x: 1.45, y: 0.7 },
32
+ width: 1.9,
33
+ height: 2.4,
34
+ },
35
+ {
36
+ globalConnNetId: "signal2",
37
+ netId: "SIGNAL2",
38
+ mspConnectionPairIds: [],
39
+ pinIds: [],
40
+ orientation: "x+",
41
+ anchorPoint: { x: 0.5, y: 2.45 },
42
+ center: { x: 1.2, y: 2.45 },
43
+ width: 1.4,
44
+ height: 0.9,
45
+ },
46
+ ]
47
+ const problem: InputProblem = {
48
+ chips: [],
49
+ directConnections: [],
50
+ netConnections: [],
51
+ availableNetLabelOrientations: {},
52
+ }
53
+ return {
54
+ inputProblem: problem,
55
+ traces: [trace],
56
+ completedReroutes: [
57
+ {
58
+ initialTrace: trace,
59
+ reroutedTracePath: path,
60
+ label: labels[0]!,
61
+ detourCount: 0,
62
+ },
63
+ ],
64
+ netLabelPlacements: labels,
65
+ paddingBuffer: 0.1,
66
+ }
67
+ }
68
+
69
+ test("merges successive label detours without changing length or terminal directions", () => {
70
+ const input = createFixture()
71
+ const solver = new TraceElbowTransitionSimplificationSolver(input)
72
+ solver.solve()
73
+ const path = solver.getOutput().traces[0]!.tracePath
74
+
75
+ expect(path).toEqual([
76
+ { x: 5, y: 0 },
77
+ { x: 5, y: -1 },
78
+ { x: 2.5, y: -1 },
79
+ { x: 2.5, y: 3 },
80
+ { x: 1, y: 3 },
81
+ { x: 1, y: 4 },
82
+ ])
83
+ expect(getPathLength(path)).toBeCloseTo(
84
+ getPathLength(input.traces[0]!.tracePath),
85
+ )
86
+ })
87
+
88
+ test.each([
89
+ "component",
90
+ "label",
91
+ "parallel_trace",
92
+ "crossing_trace",
93
+ "anchor",
94
+ ] as const)("preserves a necessary staircase blocked by %s", (blocker) => {
95
+ const input = createFixture()
96
+ if (blocker === "component") {
97
+ input.inputProblem.chips.push({
98
+ chipId: "obstacle",
99
+ center: { x: 2.5, y: 2.5 },
100
+ width: 0.2,
101
+ height: 0.4,
102
+ pins: [],
103
+ })
104
+ } else if (blocker === "parallel_trace" || blocker === "crossing_trace") {
105
+ const path =
106
+ blocker === "parallel_trace"
107
+ ? [
108
+ { x: 2.5, y: 2.2 },
109
+ { x: 2.5, y: 2.8 },
110
+ ]
111
+ : [
112
+ { x: 2.3, y: 2.5 },
113
+ { x: 2.7, y: 2.5 },
114
+ ]
115
+ input.traces.push(
116
+ createTrace(
117
+ "foreign",
118
+ path,
119
+ [
120
+ { pinId: "X1.1", chipId: "X1", ...path[0]! },
121
+ { pinId: "X2.1", chipId: "X2", ...path[1]! },
122
+ ],
123
+ "foreign",
124
+ ),
125
+ )
126
+ } else {
127
+ const x = blocker === "anchor" ? 2 : 2.5
128
+ input.netLabelPlacements.push({
129
+ globalConnNetId: blocker === "anchor" ? "power-net" : "foreign",
130
+ netId: "BLOCKER",
131
+ mspConnectionPairIds: ["rerouted"],
132
+ pinIds: [],
133
+ orientation: "x+",
134
+ anchorPoint: { x, y: 2.5 },
135
+ center: { x: x + 0.1, y: 2.5 },
136
+ width: 0.2,
137
+ height: 0.4,
138
+ })
139
+ }
140
+ const solver = new TraceElbowTransitionSimplificationSolver(input)
141
+ solver.solve()
142
+
143
+ expect(solver.getOutput().traces[0]!.tracePath).toEqual(
144
+ input.traces[0]!.tracePath,
145
+ )
146
+ })