@tscircuit/schematic-trace-solver 0.0.120 → 0.0.122

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 (40) hide show
  1. package/dist/index.d.ts +10 -3
  2. package/dist/index.js +350 -78
  3. package/lib/solvers/RailNetLabelCornerPlacementSolver/RailNetLabelCornerPlacementSolver.ts +223 -21
  4. package/lib/solvers/RailNetLabelCornerPlacementSolver/types.ts +3 -3
  5. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +34 -9
  6. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/evaluateRailGroup.ts +79 -59
  7. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getRailAlignmentFallbackCoordinates.ts +96 -0
  8. package/lib/utils/doesPathCoincideWithTraces.ts +71 -1
  9. package/package.json +1 -1
  10. package/site/bug-reports/bug-report-20260804T095800Z.page.tsx +4 -0
  11. package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +4 -4
  12. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +5 -5
  13. package/tests/bug-reports/bug-report-20260717T042845Z/__snapshots__/bug-report-20260717T042845Z.snap.svg +12 -12
  14. package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +2 -2
  15. package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +2 -2
  16. package/tests/bug-reports/bug-report-20260804T095800Z/__snapshots__/bug-report-20260804T095800Z.snap.svg +135 -0
  17. package/tests/bug-reports/bug-report-20260804T095800Z/bug-report-20260804T095800Z.json +634 -0
  18. package/tests/bug-reports/bug-report-20260804T095800Z/bug-report-20260804T095800Z.test.ts +12 -0
  19. package/tests/examples/__snapshots__/example12.snap.svg +2 -2
  20. package/tests/examples/__snapshots__/example13.snap.svg +4 -4
  21. package/tests/examples/__snapshots__/example14.snap.svg +36 -36
  22. package/tests/examples/__snapshots__/example31.snap.svg +6 -6
  23. package/tests/examples/__snapshots__/example43.snap.svg +2 -2
  24. package/tests/examples/__snapshots__/example44.snap.svg +2 -2
  25. package/tests/repros/__snapshots__/board-15984-same-net-junction.snap.svg +2 -2
  26. package/tests/repros/__snapshots__/bugreport-001-gnd-overlap.snap.svg +26 -26
  27. package/tests/repros/__snapshots__/netlabel-connector-through-rail-label.snap.svg +2 -2
  28. package/tests/repros/__snapshots__/repro-atmega328p-fault-pullup.snap.svg +2 -2
  29. package/tests/repros/__snapshots__/repro-atmega328p-missing-gnd-netlabel.snap.svg +2 -2
  30. package/tests/repros/__snapshots__/repro-bq25895-cross-net-trace-overlap.snap.svg +101 -0
  31. package/tests/repros/__snapshots__/repro-missing-trace-netlabel.snap.svg +18 -18
  32. package/tests/repros/__snapshots__/repro-netlabel-overlap-trace.snap.svg +3 -3
  33. package/tests/repros/__snapshots__/repro-nrf52810-clock-routing.snap.svg +82 -0
  34. package/tests/repros/assets/repro-bq25895-cross-net-trace-overlap.input.json +498 -0
  35. package/tests/repros/assets/repro-nrf52810-clock-routing.input.json +234 -0
  36. package/tests/repros/repro-bq25895-cross-net-trace-overlap.test.ts +88 -0
  37. package/tests/repros/repro-missing-trace-netlabel.test.ts +12 -0
  38. package/tests/repros/repro-netlabel-overlap-trace.test.ts +38 -0
  39. package/tests/repros/repro-nrf52810-clock-routing.test.ts +28 -0
  40. package/tests/solvers/TraceCleanupSolver/alignSameNetRails-trace-clearance.test.ts +68 -0
@@ -0,0 +1,234 @@
1
+ {
2
+ "chips": [
3
+ {
4
+ "chipId": "schematic_component_30",
5
+ "center": { "x": -7, "y": 5 },
6
+ "width": 3.1999999999999993,
7
+ "height": 0.5999999999999996,
8
+ "pins": [
9
+ { "pinId": "schematic_port_38", "x": -5.4, "y": 5.1 },
10
+ { "pinId": "schematic_port_39", "x": -5.4, "y": 4.9 }
11
+ ],
12
+ "sectionId": "hf-clock"
13
+ },
14
+ {
15
+ "chipId": "schematic_component_31",
16
+ "center": { "x": -4, "y": 5 },
17
+ "width": 1.08,
18
+ "height": 1.42,
19
+ "pins": [
20
+ {
21
+ "pinId": "schematic_port_40",
22
+ "x": -4,
23
+ "y": 5.71,
24
+ "_facingDirection": "y+"
25
+ },
26
+ {
27
+ "pinId": "schematic_port_41",
28
+ "x": -4.02,
29
+ "y": 4.29,
30
+ "_facingDirection": "y-"
31
+ },
32
+ {
33
+ "pinId": "schematic_port_42",
34
+ "x": -4.54,
35
+ "y": 4.99,
36
+ "_facingDirection": "x-"
37
+ },
38
+ {
39
+ "pinId": "schematic_port_43",
40
+ "x": -3.46,
41
+ "y": 4.99,
42
+ "_facingDirection": "x+"
43
+ }
44
+ ],
45
+ "sectionId": "hf-clock"
46
+ },
47
+ {
48
+ "chipId": "schematic_component_32",
49
+ "center": { "x": -4.9275, "y": 3 },
50
+ "width": 1.0450000000000008,
51
+ "height": 0.7600000000000007,
52
+ "pins": [
53
+ {
54
+ "pinId": "schematic_port_175",
55
+ "x": -5,
56
+ "y": 3.3800000000000003,
57
+ "_facingDirection": "y+"
58
+ },
59
+ {
60
+ "pinId": "schematic_port_176",
61
+ "x": -5,
62
+ "y": 2.6199999999999997,
63
+ "_facingDirection": "y-"
64
+ }
65
+ ],
66
+ "sectionId": "hf-clock"
67
+ },
68
+ {
69
+ "chipId": "schematic_component_33",
70
+ "center": { "x": -2.9275, "y": 3 },
71
+ "width": 1.0450000000000004,
72
+ "height": 0.7600000000000007,
73
+ "pins": [
74
+ {
75
+ "pinId": "schematic_port_177",
76
+ "x": -3,
77
+ "y": 3.3800000000000003,
78
+ "_facingDirection": "y+"
79
+ },
80
+ {
81
+ "pinId": "schematic_port_178",
82
+ "x": -3,
83
+ "y": 2.6199999999999997,
84
+ "_facingDirection": "y-"
85
+ }
86
+ ],
87
+ "sectionId": "hf-clock"
88
+ },
89
+ {
90
+ "chipId": "schematic_component_34",
91
+ "center": { "x": 3, "y": 5 },
92
+ "width": 3.1999999999999993,
93
+ "height": 0.5999999999999996,
94
+ "pins": [
95
+ { "pinId": "schematic_port_44", "x": 4.6, "y": 5.1 },
96
+ { "pinId": "schematic_port_45", "x": 4.6, "y": 4.9 }
97
+ ],
98
+ "sectionId": "lf-clock"
99
+ },
100
+ {
101
+ "chipId": "schematic_component_35",
102
+ "center": { "x": 6, "y": 5 },
103
+ "width": 1.0999999999999996,
104
+ "height": 0.8499999999999996,
105
+ "pins": [
106
+ {
107
+ "pinId": "schematic_port_46",
108
+ "x": 5.45,
109
+ "y": 5,
110
+ "_facingDirection": "x-"
111
+ },
112
+ {
113
+ "pinId": "schematic_port_47",
114
+ "x": 6.55,
115
+ "y": 5,
116
+ "_facingDirection": "x+"
117
+ }
118
+ ],
119
+ "sectionId": "lf-clock"
120
+ },
121
+ {
122
+ "chipId": "schematic_component_36",
123
+ "center": { "x": 5.0725, "y": 3 },
124
+ "width": 1.0450000000000008,
125
+ "height": 0.7600000000000007,
126
+ "pins": [
127
+ {
128
+ "pinId": "schematic_port_181",
129
+ "x": 5,
130
+ "y": 3.3800000000000003,
131
+ "_facingDirection": "y+"
132
+ },
133
+ {
134
+ "pinId": "schematic_port_182",
135
+ "x": 5,
136
+ "y": 2.6199999999999997,
137
+ "_facingDirection": "y-"
138
+ }
139
+ ],
140
+ "sectionId": "lf-clock"
141
+ },
142
+ {
143
+ "chipId": "schematic_component_37",
144
+ "center": { "x": 7.0725, "y": 3 },
145
+ "width": 1.0450000000000008,
146
+ "height": 0.7600000000000007,
147
+ "pins": [
148
+ {
149
+ "pinId": "schematic_port_183",
150
+ "x": 7,
151
+ "y": 3.3800000000000003,
152
+ "_facingDirection": "y+"
153
+ },
154
+ {
155
+ "pinId": "schematic_port_184",
156
+ "x": 7,
157
+ "y": 2.6199999999999997,
158
+ "_facingDirection": "y-"
159
+ }
160
+ ],
161
+ "sectionId": "lf-clock"
162
+ }
163
+ ],
164
+ "directConnections": [
165
+ {
166
+ "netId": ".U1 > .XC1 to .X1 > .XTAL1",
167
+ "pinIds": ["schematic_port_38", "schematic_port_42"]
168
+ },
169
+ {
170
+ "netId": ".U1 > .XC2 to .X1 > .XTAL2",
171
+ "pinIds": ["schematic_port_39", "schematic_port_43"]
172
+ },
173
+ {
174
+ "netId": ".X1 > .XTAL1 to .C1 > .pin1",
175
+ "pinIds": ["schematic_port_42", "schematic_port_175"]
176
+ },
177
+ {
178
+ "netId": ".X1 > .XTAL2 to .C2 > .pin1",
179
+ "pinIds": ["schematic_port_43", "schematic_port_177"]
180
+ },
181
+ {
182
+ "netId": ".U1 > .XL1 to .X2 > .OSC1",
183
+ "pinIds": ["schematic_port_44", "schematic_port_46"]
184
+ },
185
+ {
186
+ "netId": ".U1 > .XL2 to .X2 > .OSC2",
187
+ "pinIds": ["schematic_port_45", "schematic_port_47"]
188
+ },
189
+ {
190
+ "netId": ".X2 > .OSC1 to .C11 > .pin1",
191
+ "pinIds": ["schematic_port_46", "schematic_port_181"]
192
+ },
193
+ {
194
+ "netId": ".X2 > .OSC2 to .C12 > .pin1",
195
+ "pinIds": ["schematic_port_47", "schematic_port_183"]
196
+ }
197
+ ],
198
+ "netConnections": [
199
+ {
200
+ "netId": "GND",
201
+ "netLabelWidth": 0.42,
202
+ "netLabelHeight": 0.48,
203
+ "pinIds": [
204
+ "schematic_port_40",
205
+ "schematic_port_41",
206
+ "schematic_port_176",
207
+ "schematic_port_178",
208
+ "schematic_port_182",
209
+ "schematic_port_184"
210
+ ]
211
+ }
212
+ ],
213
+ "textBoxes": [
214
+ {
215
+ "chipId": "schematic_component_30",
216
+ "center": { "x": -7.539999999999999, "y": 5.43 },
217
+ "width": 1.3200000000000003,
218
+ "height": 0.17999999999999972,
219
+ "text": "U1 HF Clock"
220
+ },
221
+ {
222
+ "chipId": "schematic_component_34",
223
+ "center": { "x": 2.46, "y": 5.43 },
224
+ "width": 1.32,
225
+ "height": 0.17999999999999972,
226
+ "text": "U1 LF Clock"
227
+ }
228
+ ],
229
+ "availableNetLabelOrientations": {
230
+ "GND": ["y-"]
231
+ },
232
+ "maxMspPairDistance": 2.4,
233
+ "_hideRatsNet": false
234
+ }
@@ -0,0 +1,88 @@
1
+ import { expect, test } from "bun:test"
2
+ import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
3
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
4
+ import { getTraceCorners } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
5
+ import { SCHEMATIC_TRACE_MIN_VISUAL_CENTERLINE_CLEARANCE } from "lib/utils/doesPathCoincideWithTraces"
6
+ import "tests/fixtures/matcher"
7
+ import inputProblem from "./assets/repro-bq25895-cross-net-trace-overlap.input.json"
8
+
9
+ const EPSILON = 1e-6
10
+
11
+ const tracesHaveInsufficientParallelClearance = (
12
+ firstTrace: SolvedTracePath,
13
+ secondTrace: SolvedTracePath,
14
+ ) => {
15
+ for (
16
+ let firstPointIndex = 0;
17
+ firstPointIndex < firstTrace.tracePath.length - 1;
18
+ firstPointIndex++
19
+ ) {
20
+ const firstStart = firstTrace.tracePath[firstPointIndex]!
21
+ const firstEnd = firstTrace.tracePath[firstPointIndex + 1]!
22
+ const firstIsVertical = Math.abs(firstStart.x - firstEnd.x) < EPSILON
23
+ if (!firstIsVertical) continue
24
+
25
+ for (
26
+ let secondPointIndex = 0;
27
+ secondPointIndex < secondTrace.tracePath.length - 1;
28
+ secondPointIndex++
29
+ ) {
30
+ const secondStart = secondTrace.tracePath[secondPointIndex]!
31
+ const secondEnd = secondTrace.tracePath[secondPointIndex + 1]!
32
+ const secondIsVertical = Math.abs(secondStart.x - secondEnd.x) < EPSILON
33
+ if (!secondIsVertical) continue
34
+
35
+ const xSeparation = Math.abs(firstStart.x - secondStart.x)
36
+ const verticalOverlap =
37
+ Math.min(
38
+ Math.max(firstStart.y, firstEnd.y),
39
+ Math.max(secondStart.y, secondEnd.y),
40
+ ) -
41
+ Math.max(
42
+ Math.min(firstStart.y, firstEnd.y),
43
+ Math.min(secondStart.y, secondEnd.y),
44
+ )
45
+
46
+ if (
47
+ xSeparation + EPSILON <
48
+ SCHEMATIC_TRACE_MIN_VISUAL_CENTERLINE_CLEARANCE &&
49
+ verticalOverlap > EPSILON
50
+ ) {
51
+ return true
52
+ }
53
+ }
54
+ }
55
+
56
+ return false
57
+ }
58
+
59
+ test("BQ25895 CE and GND traces keep visible parallel clearance", () => {
60
+ const solver = new SchematicTracePipelineSolver(inputProblem as any)
61
+ solver.solve()
62
+
63
+ const traces = solver.netLabelTraceCollisionSolver!.getOutput().traces
64
+ const ceTraces = traces.filter(
65
+ (trace) => trace.userNetId === "J3.pin6 to U1.CE",
66
+ )
67
+ const groundTraces = traces.filter((trace) => trace.userNetId === "GND")
68
+ const hasInsufficientCeAndGroundClearance = ceTraces.some((ceTrace) =>
69
+ groundTraces.some((groundTrace) =>
70
+ tracesHaveInsufficientParallelClearance(ceTrace, groundTrace),
71
+ ),
72
+ )
73
+
74
+ expect(hasInsufficientCeAndGroundClearance).toBe(false)
75
+
76
+ const output = solver.netLabelTraceCollisionSolver!.getOutput()
77
+ const groundLabel = output.netLabelPlacements.find(
78
+ (label) => label.netId === "GND",
79
+ )!
80
+ const groundLabelTrace = output.traces.find((trace) =>
81
+ groundLabel.mspConnectionPairIds.includes(trace.mspPairId),
82
+ )!
83
+
84
+ expect(getTraceCorners(groundLabelTrace.tracePath)).toContainEqual(
85
+ groundLabel.anchorPoint,
86
+ )
87
+ expect(solver).toMatchSolverSnapshot(import.meta.path)
88
+ })
@@ -1,4 +1,5 @@
1
1
  import { expect, test } from "bun:test"
2
+ import { getTraceCorners } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
2
3
  import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
3
4
  import inputProblem from "./assets/repro-missing-trace-netlabel.input.json"
4
5
  import "tests/fixtures/matcher"
@@ -10,5 +11,16 @@ test("repro148 missing trace becomes net labels", () => {
10
11
 
11
12
  solver.solve()
12
13
 
14
+ const output = solver.sameNetJunctionAlignmentSolver!.getOutput()
15
+ const powerLabel = output.netLabelPlacements.find(
16
+ (label) => label.netId === "V3V3",
17
+ )!
18
+ const powerTrace = output.traces.find((trace) =>
19
+ powerLabel.mspConnectionPairIds.includes(trace.mspPairId),
20
+ )!
21
+
22
+ expect(getTraceCorners(powerTrace.tracePath)).toContainEqual(
23
+ powerLabel.anchorPoint,
24
+ )
13
25
  expect(solver).toMatchSolverSnapshot(import.meta.path)
14
26
  })
@@ -1,4 +1,10 @@
1
1
  import { expect, test } from "bun:test"
2
+ import { traceCrossesBoundsInterior } from "lib/solvers/AvailableNetOrientationSolver/geometry"
3
+ import {
4
+ getTraceCorners,
5
+ rectsOverlap,
6
+ } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
7
+ import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
2
8
  import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
3
9
  import inputProblem from "./assets/repro-netlabel-overlap-trace.input.json"
4
10
  import "tests/fixtures/matcher"
@@ -10,5 +16,37 @@ test("repro147 netlabel overlaps trace", () => {
10
16
 
11
17
  solver.solve()
12
18
 
19
+ const output = solver.sameNetJunctionAlignmentSolver!.getOutput()
20
+ const groundLabel = output.netLabelPlacements.find(
21
+ (label) => label.netId === "GND",
22
+ )!
23
+ const groundTrace = output.traces.find((trace) =>
24
+ groundLabel.mspConnectionPairIds.includes(trace.mspPairId),
25
+ )!
26
+ const groundLabelBounds = getRectBounds(
27
+ groundLabel.center,
28
+ groundLabel.width,
29
+ groundLabel.height,
30
+ )
31
+
32
+ expect(getTraceCorners(groundTrace.tracePath)).toContainEqual(
33
+ groundLabel.anchorPoint,
34
+ )
35
+ expect(
36
+ inputProblem.chips.some((chip) =>
37
+ rectsOverlap(
38
+ groundLabelBounds,
39
+ getRectBounds(chip.center, chip.width, chip.height),
40
+ ),
41
+ ),
42
+ ).toBe(false)
43
+ expect(
44
+ traceCrossesBoundsInterior(
45
+ groundLabelBounds,
46
+ Object.fromEntries(
47
+ output.traces.map((trace) => [trace.mspPairId, trace]),
48
+ ),
49
+ ),
50
+ ).toBe(false)
13
51
  expect(solver).toMatchSolverSnapshot(import.meta.path)
14
52
  })
@@ -0,0 +1,28 @@
1
+ import { expect, test } from "bun:test"
2
+ import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
3
+ import "tests/fixtures/matcher"
4
+ import inputProblem from "./assets/repro-nrf52810-clock-routing.input.json"
5
+
6
+ // Extracted from the nRF52810 clock-source schematic shown in the reproduction:
7
+ // a four-pin 32 MHz crystal and a two-pin 32.768 kHz crystal, each connected
8
+ // to an MCU schematic box, two load capacitors, and section-local GND labels.
9
+ test("repro nRF52810 HF and LF crystal trace/net-label routing", () => {
10
+ const solver = new SchematicTracePipelineSolver(inputProblem as any)
11
+
12
+ solver.solve()
13
+
14
+ expect(solver.mspConnectionPairSolver?.mspConnectionPairs).toHaveLength(11)
15
+ expect(
16
+ solver.schematicTraceLinesSolver?.failedConnectionPairs.map(
17
+ (pair) => pair.mspPairId,
18
+ ),
19
+ ).toEqual([
20
+ "schematic_port_39-schematic_port_43", // U1.XC2 -> X1.XTAL2
21
+ "schematic_port_45-schematic_port_47", // U1.XL2 -> X2.OSC2
22
+ "schematic_port_41-schematic_port_40", // X1.GND2 -> X1.GND1
23
+ ])
24
+ expect(
25
+ solver.sameNetJunctionAlignmentSolver?.getOutput().netLabelPlacements,
26
+ ).toHaveLength(10)
27
+ expect(solver).toMatchSolverSnapshot(import.meta.path)
28
+ })
@@ -0,0 +1,68 @@
1
+ import { expect, test } from "bun:test"
2
+ import {
3
+ SCHEMATIC_TRACE_MIN_VISUAL_CENTERLINE_CLEARANCE,
4
+ SCHEMATIC_TRACE_STROKE_WIDTH,
5
+ } from "lib/utils/doesPathCoincideWithTraces"
6
+ import {
7
+ align,
8
+ createTrace,
9
+ getVerticalRailTraces,
10
+ } from "./fixtures/alignSameNetRails"
11
+
12
+ test("aligns same-net rails at a safe coordinate beside different-net traces", () => {
13
+ const rails = getVerticalRailTraces()
14
+ const foreignLowerTrace = createTrace(
15
+ "foreign-lower",
16
+ [
17
+ { x: -2.02, y: -2 },
18
+ { x: -2.02, y: -0.2 },
19
+ ],
20
+ [
21
+ { pinId: "X1.1", chipId: "X1", x: -2.02, y: -2 },
22
+ { pinId: "X2.1", chipId: "X2", x: -2.02, y: -0.2 },
23
+ ],
24
+ "foreign-lower-net",
25
+ )
26
+ const foreignUpperTrace = createTrace(
27
+ "foreign-upper",
28
+ [
29
+ { x: -3.02, y: 0.2 },
30
+ { x: -3.02, y: 2 },
31
+ ],
32
+ [
33
+ { pinId: "X3.1", chipId: "X3", x: -3.02, y: 0.2 },
34
+ { pinId: "X4.1", chipId: "X4", x: -3.02, y: 2 },
35
+ ],
36
+ "foreign-upper-net",
37
+ )
38
+ const traces = [...rails, foreignLowerTrace, foreignUpperTrace]
39
+
40
+ const result = align(traces, {
41
+ eligibleTraceIds: new Set(rails.map((trace) => trace.mspPairId)),
42
+ })
43
+
44
+ expect(result.alignedRailGroupCount).toBe(1)
45
+
46
+ const alignedUpper = result.traces.find(
47
+ (trace) => trace.mspPairId === "upper",
48
+ )!
49
+ const alignedLower = result.traces.find(
50
+ (trace) => trace.mspPairId === "lower",
51
+ )!
52
+ const upperRailX = alignedUpper.tracePath[1]!.x
53
+ const lowerRailX = alignedLower.tracePath[1]!.x
54
+
55
+ expect(upperRailX).toBe(lowerRailX)
56
+ expect(
57
+ Math.abs(upperRailX - foreignLowerTrace.tracePath[0]!.x),
58
+ ).toBeGreaterThan(SCHEMATIC_TRACE_STROKE_WIDTH)
59
+ expect(
60
+ Math.abs(upperRailX - foreignUpperTrace.tracePath[0]!.x),
61
+ ).toBeGreaterThan(SCHEMATIC_TRACE_STROKE_WIDTH)
62
+ expect(
63
+ Math.min(
64
+ Math.abs(upperRailX - foreignLowerTrace.tracePath[0]!.x),
65
+ Math.abs(upperRailX - foreignUpperTrace.tracePath[0]!.x),
66
+ ),
67
+ ).toBeCloseTo(SCHEMATIC_TRACE_MIN_VISUAL_CENTERLINE_CLEARANCE, 6)
68
+ })