@tscircuit/schematic-trace-solver 0.0.164 → 0.0.166

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 (23) hide show
  1. package/dist/index.d.ts +5 -1
  2. package/dist/index.js +370 -24
  3. package/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts +225 -24
  4. package/lib/solvers/SameNetJunctionAlignmentSolver/SameNetJunctionAlignmentSolver.ts +11 -5
  5. package/lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts +1 -1
  6. package/lib/solvers/SameNetJunctionAlignmentSolver/collapseSameNetCycles.ts +310 -0
  7. package/package.json +1 -1
  8. package/tests/assets/example51.json +6 -0
  9. package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +2 -2
  10. package/tests/examples/__snapshots__/example51.snap.svg +42 -78
  11. package/tests/examples/example51.test.ts +24 -0
  12. package/tests/fixtures/convertSolverOutputToCircuitJson.ts +22 -3
  13. package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +2 -2
  14. package/tests/repros/__snapshots__/repro-bluetooth-controller-ground-decoupling-groups.snap.svg +1 -15
  15. package/tests/repros/__snapshots__/repro-core-ground-inline-label-fallback.snap.svg +2 -16
  16. package/tests/repros/__snapshots__/repro-isolated-rs485-isow7841.snap.svg +10 -10
  17. package/tests/repros/__snapshots__/repro-nrf52810-clock-routing.snap.svg +18 -18
  18. package/tests/repros/__snapshots__/repro-pga300-redundant-ground-traces.snap.svg +1 -15
  19. package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +33 -47
  20. package/tests/repros/repro-board-648-esp12f-section.test.ts +3 -0
  21. package/tests/repros/repro-isolated-rs485-isow7841.test.ts +33 -1
  22. package/tests/repros/repro-nrf52810-clock-routing.test.ts +26 -1
  23. package/tests/solvers/SameNetJunctionAlignmentSolver/collapse-same-net-cycles.test.ts +154 -0
@@ -0,0 +1,310 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import { getSegmentIntersection } from "@tscircuit/math-utils/line-intersections"
3
+ import { moveAttachedLabelsToReroutedTrace } from "lib/solvers/Example28Solver/labelMovement"
4
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
5
+ import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
6
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
7
+ import {
8
+ getRailOrientation,
9
+ getVisibleTraceLength,
10
+ getVisibleTraceSegmentCount,
11
+ pointsEqual,
12
+ } from "lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry"
13
+ import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
14
+ import { getSharedPin } from "./alignSameNetJunctions"
15
+ import {
16
+ pathEntersAnyNetLabel,
17
+ pathIntersectsAnyNetLabel,
18
+ } from "./pathIntersectsAnyNetLabel"
19
+
20
+ interface CollapseSameNetCyclesInput {
21
+ traces: SolvedTracePath[]
22
+ netLabelPlacements: NetLabelPlacement[]
23
+ }
24
+
25
+ interface PathCrossing {
26
+ intersectionPoint: Point
27
+ targetSegmentIndex: number
28
+ donorSegmentIndex: number
29
+ }
30
+
31
+ interface CycleCollapseCandidate {
32
+ tracePath: Point[]
33
+ netLabelPlacements: NetLabelPlacement[]
34
+ netVisibleLength: number
35
+ netVisibleSegmentCount: number
36
+ }
37
+
38
+ const TRACE_LENGTH_EPSILON = 1e-6
39
+
40
+ const getTracePathStartingAtPin = (
41
+ trace: SolvedTracePath,
42
+ pin: Point,
43
+ ): Point[] | null => {
44
+ const pathStart = trace.tracePath[0]
45
+ const pathEnd = trace.tracePath.at(-1)
46
+ if (!pathStart || !pathEnd) return null
47
+ if (pointsEqual(pathStart, pin)) return trace.tracePath
48
+ if (pointsEqual(pathEnd, pin)) return [...trace.tracePath].reverse()
49
+ return null
50
+ }
51
+
52
+ const getPerpendicularPathCrossings = (
53
+ targetPath: Point[],
54
+ donorPath: Point[],
55
+ ): PathCrossing[] => {
56
+ const crossings: PathCrossing[] = []
57
+ const sharedEndpoint = targetPath[0]!
58
+
59
+ for (
60
+ let targetSegmentIndex = 0;
61
+ targetSegmentIndex < targetPath.length - 1;
62
+ targetSegmentIndex++
63
+ ) {
64
+ const targetStart = targetPath[targetSegmentIndex]!
65
+ const targetEnd = targetPath[targetSegmentIndex + 1]!
66
+ const targetOrientation = getRailOrientation(targetStart, targetEnd)
67
+ if (!targetOrientation) continue
68
+
69
+ for (
70
+ let donorSegmentIndex = 0;
71
+ donorSegmentIndex < donorPath.length - 1;
72
+ donorSegmentIndex++
73
+ ) {
74
+ const donorStart = donorPath[donorSegmentIndex]!
75
+ const donorEnd = donorPath[donorSegmentIndex + 1]!
76
+ const donorOrientation = getRailOrientation(donorStart, donorEnd)
77
+ if (!donorOrientation || donorOrientation === targetOrientation) continue
78
+
79
+ const intersectionPoint = getSegmentIntersection(
80
+ targetStart,
81
+ targetEnd,
82
+ donorStart,
83
+ donorEnd,
84
+ )
85
+ if (
86
+ !intersectionPoint ||
87
+ pointsEqual(intersectionPoint, sharedEndpoint)
88
+ ) {
89
+ continue
90
+ }
91
+
92
+ crossings.push({
93
+ intersectionPoint,
94
+ targetSegmentIndex,
95
+ donorSegmentIndex,
96
+ })
97
+ }
98
+ }
99
+
100
+ return crossings
101
+ }
102
+
103
+ const buildCollapsedCyclePath = ({
104
+ targetTrace,
105
+ targetPath,
106
+ donorPath,
107
+ crossing,
108
+ }: {
109
+ targetTrace: SolvedTracePath
110
+ targetPath: Point[]
111
+ donorPath: Point[]
112
+ crossing: PathCrossing
113
+ }) => {
114
+ const pathFromSharedPin = simplifyPath([
115
+ ...donorPath.slice(0, crossing.donorSegmentIndex + 1),
116
+ crossing.intersectionPoint,
117
+ ...targetPath.slice(crossing.targetSegmentIndex + 1),
118
+ ])
119
+ if (pointsEqual(targetTrace.tracePath[0]!, targetPath[0]!)) {
120
+ return pathFromSharedPin
121
+ }
122
+ return pathFromSharedPin.reverse()
123
+ }
124
+
125
+ const getNetLabelPlacementsForCycleCollapse = ({
126
+ targetTrace,
127
+ tracePath,
128
+ netLabelPlacements,
129
+ }: {
130
+ targetTrace: SolvedTracePath
131
+ tracePath: Point[]
132
+ netLabelPlacements: NetLabelPlacement[]
133
+ }): NetLabelPlacement[] | null => {
134
+ const candidateNetLabelPlacements = moveAttachedLabelsToReroutedTrace({
135
+ trace: targetTrace,
136
+ originalTracePath: targetTrace.tracePath,
137
+ reroutedTracePath: tracePath,
138
+ netLabelPlacements,
139
+ })
140
+
141
+ for (
142
+ let labelIndex = 0;
143
+ labelIndex < netLabelPlacements.length;
144
+ labelIndex++
145
+ ) {
146
+ const label = netLabelPlacements[labelIndex]!
147
+ if (label.globalConnNetId !== targetTrace.globalConnNetId) continue
148
+ if (!tracePathContainsPoint(targetTrace.tracePath, label.anchorPoint)) {
149
+ continue
150
+ }
151
+ const candidateLabel = candidateNetLabelPlacements[labelIndex]!
152
+ if (!tracePathContainsPoint(tracePath, candidateLabel.anchorPoint)) {
153
+ return null
154
+ }
155
+ }
156
+
157
+ const otherNetLabelPlacements = candidateNetLabelPlacements.filter(
158
+ (label) => label.globalConnNetId !== targetTrace.globalConnNetId,
159
+ )
160
+ if (
161
+ pathIntersectsAnyNetLabel({
162
+ path: tracePath,
163
+ netLabelPlacements: otherNetLabelPlacements,
164
+ })
165
+ ) {
166
+ return null
167
+ }
168
+
169
+ const sameNetLabelPlacements = candidateNetLabelPlacements.filter(
170
+ (label) => label.globalConnNetId === targetTrace.globalConnNetId,
171
+ )
172
+ if (
173
+ pathEntersAnyNetLabel({
174
+ path: tracePath,
175
+ netLabelPlacements: sameNetLabelPlacements,
176
+ })
177
+ ) {
178
+ return null
179
+ }
180
+ return candidateNetLabelPlacements
181
+ }
182
+
183
+ const candidateIsBetter = (
184
+ candidate: CycleCollapseCandidate,
185
+ bestCandidate: CycleCollapseCandidate | null,
186
+ ) => {
187
+ if (!bestCandidate) return true
188
+ const visibleLengthDelta =
189
+ candidate.netVisibleLength - bestCandidate.netVisibleLength
190
+ if (Math.abs(visibleLengthDelta) > TRACE_LENGTH_EPSILON) {
191
+ return visibleLengthDelta < 0
192
+ }
193
+ return candidate.netVisibleSegmentCount < bestCandidate.netVisibleSegmentCount
194
+ }
195
+
196
+ const getBestCycleCollapseCandidate = ({
197
+ targetTrace,
198
+ traces,
199
+ netLabelPlacements,
200
+ }: {
201
+ targetTrace: SolvedTracePath
202
+ traces: SolvedTracePath[]
203
+ netLabelPlacements: NetLabelPlacement[]
204
+ }): CycleCollapseCandidate | null => {
205
+ const sameNetTraces = traces.filter(
206
+ (trace) => trace.globalConnNetId === targetTrace.globalConnNetId,
207
+ )
208
+ const baselineNetVisibleLength = getVisibleTraceLength(sameNetTraces)
209
+ const baselineTargetVisibleLength = getVisibleTraceLength([targetTrace])
210
+ let bestCandidate: CycleCollapseCandidate | null = null
211
+
212
+ for (const donorTrace of sameNetTraces) {
213
+ if (donorTrace.mspPairId === targetTrace.mspPairId) continue
214
+ const sharedPin = getSharedPin({
215
+ donorTrace,
216
+ branchTrace: targetTrace,
217
+ })
218
+ if (!sharedPin) continue
219
+ const targetPath = getTracePathStartingAtPin(targetTrace, sharedPin)
220
+ const donorPath = getTracePathStartingAtPin(donorTrace, sharedPin)
221
+ if (!targetPath || !donorPath) continue
222
+
223
+ const crossings = getPerpendicularPathCrossings(targetPath, donorPath)
224
+ for (const crossing of crossings) {
225
+ const tracePath = buildCollapsedCyclePath({
226
+ targetTrace,
227
+ targetPath,
228
+ donorPath,
229
+ crossing,
230
+ })
231
+ const candidateTrace = { ...targetTrace, tracePath }
232
+ const candidateTargetVisibleLength = getVisibleTraceLength([
233
+ candidateTrace,
234
+ ])
235
+ if (
236
+ candidateTargetVisibleLength >
237
+ baselineTargetVisibleLength + TRACE_LENGTH_EPSILON
238
+ ) {
239
+ continue
240
+ }
241
+ const candidateNetLabelPlacements = getNetLabelPlacementsForCycleCollapse(
242
+ {
243
+ targetTrace,
244
+ tracePath,
245
+ netLabelPlacements,
246
+ },
247
+ )
248
+ if (!candidateNetLabelPlacements) continue
249
+
250
+ const candidateNetTraces = sameNetTraces.map((trace) => {
251
+ if (trace.mspPairId === targetTrace.mspPairId) return candidateTrace
252
+ return trace
253
+ })
254
+ const netVisibleLength = getVisibleTraceLength(candidateNetTraces)
255
+ if (netVisibleLength >= baselineNetVisibleLength - TRACE_LENGTH_EPSILON) {
256
+ continue
257
+ }
258
+ const netVisibleSegmentCount =
259
+ getVisibleTraceSegmentCount(candidateNetTraces)
260
+ const candidate = {
261
+ tracePath,
262
+ netLabelPlacements: candidateNetLabelPlacements,
263
+ netVisibleLength,
264
+ netVisibleSegmentCount,
265
+ }
266
+ if (candidateIsBetter(candidate, bestCandidate)) {
267
+ bestCandidate = candidate
268
+ }
269
+ }
270
+ }
271
+
272
+ return bestCandidate
273
+ }
274
+
275
+ export const collapseSameNetCycles = ({
276
+ traces,
277
+ netLabelPlacements,
278
+ }: CollapseSameNetCyclesInput) => {
279
+ const outputTraces = [...traces]
280
+ let outputNetLabelPlacements = [...netLabelPlacements]
281
+ let collapsedCycleCount = 0
282
+ let traceIndex = 0
283
+
284
+ while (traceIndex < outputTraces.length) {
285
+ const targetTrace = outputTraces[traceIndex]!
286
+ const candidate = getBestCycleCollapseCandidate({
287
+ targetTrace,
288
+ traces: outputTraces,
289
+ netLabelPlacements: outputNetLabelPlacements,
290
+ })
291
+ if (!candidate) {
292
+ traceIndex++
293
+ continue
294
+ }
295
+
296
+ outputTraces[traceIndex] = {
297
+ ...targetTrace,
298
+ tracePath: candidate.tracePath,
299
+ }
300
+ outputNetLabelPlacements = candidate.netLabelPlacements
301
+ collapsedCycleCount++
302
+ traceIndex = 0
303
+ }
304
+
305
+ return {
306
+ traces: outputTraces,
307
+ netLabelPlacements: outputNetLabelPlacements,
308
+ collapsedCycleCount,
309
+ }
310
+ }
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "https://github.com/tscircuit/schematic-trace-solver.git"
6
6
  },
7
7
  "main": "dist/index.js",
8
- "version": "0.0.164",
8
+ "version": "0.0.166",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "cosmos",
@@ -2589,6 +2589,7 @@
2589
2589
  "netConnections": [
2590
2590
  {
2591
2591
  "netId": "NET_01",
2592
+ "isGround": false,
2592
2593
  "netLabelWidth": 0.42,
2593
2594
  "netLabelHeight": 0.84,
2594
2595
  "pinIds": [
@@ -2649,6 +2650,7 @@
2649
2650
  },
2650
2651
  {
2651
2652
  "netId": "NET_08",
2653
+ "isGround": false,
2652
2654
  "netLabelWidth": 0.42,
2653
2655
  "netLabelHeight": 0.84,
2654
2656
  "pinIds": [
@@ -2666,6 +2668,7 @@
2666
2668
  },
2667
2669
  {
2668
2670
  "netId": "BAT",
2671
+ "isGround": false,
2669
2672
  "netLabelWidth": 0.42,
2670
2673
  "netLabelHeight": 0.48,
2671
2674
  "pinIds": [
@@ -2678,6 +2681,7 @@
2678
2681
  },
2679
2682
  {
2680
2683
  "netId": "NET_03",
2684
+ "isGround": false,
2681
2685
  "netLabelWidth": 0.42,
2682
2686
  "netLabelHeight": 0.84,
2683
2687
  "pinIds": [
@@ -2690,6 +2694,7 @@
2690
2694
  },
2691
2695
  {
2692
2696
  "netId": "NET_05",
2697
+ "isGround": false,
2693
2698
  "netLabelWidth": 0.42,
2694
2699
  "netLabelHeight": 0.84,
2695
2700
  "pinIds": [
@@ -2725,6 +2730,7 @@
2725
2730
  },
2726
2731
  {
2727
2732
  "netId": "PPHV",
2733
+ "isGround": false,
2728
2734
  "netLabelWidth": 0.42,
2729
2735
  "netLabelHeight": 0.6,
2730
2736
  "pinIds": [