@tscircuit/schematic-trace-solver 0.0.165 → 0.0.167
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.
- package/dist/index.d.ts +5 -1
- package/dist/index.js +243 -8
- package/lib/solvers/MspConnectionPairSolver/getGroundConnectionPolicy.ts +38 -4
- package/lib/solvers/SameNetJunctionAlignmentSolver/SameNetJunctionAlignmentSolver.ts +11 -5
- package/lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts +1 -1
- package/lib/solvers/SameNetJunctionAlignmentSolver/collapseSameNetCycles.ts +310 -0
- package/lib/types/InputProblem.ts +5 -1
- package/package.json +1 -1
- package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +2 -2
- package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-isolated-rs485-isow7841.snap.svg +10 -10
- package/tests/repros/__snapshots__/repro-nrf52810-clock-routing.snap.svg +18 -18
- package/tests/repros/__snapshots__/repro-pga300-redundant-ground-traces.snap.svg +44 -36
- package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +18 -18
- package/tests/repros/repro-board-648-esp12f-section.test.ts +3 -0
- package/tests/repros/repro-isolated-rs485-isow7841.test.ts +33 -1
- package/tests/repros/repro-nrf52810-clock-routing.test.ts +26 -1
- package/tests/repros/repro-pga300-redundant-ground-traces.test.ts +35 -7
- package/tests/solvers/MspConnectionPairSolver/ground-direct-connection-groups.test.ts +120 -0
- 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
|
+
}
|
|
@@ -98,7 +98,11 @@ export interface InputNetConnection {
|
|
|
98
98
|
netId: string
|
|
99
99
|
pinIds: Array<PinId>
|
|
100
100
|
|
|
101
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Preserve explicitly wired islands on this ground net, joining the islands
|
|
103
|
+
* electrically through labels instead of routing between them. Nets without
|
|
104
|
+
* explicit traces, or without this hint, retain automatic bus routing.
|
|
105
|
+
*/
|
|
102
106
|
isGround?: boolean
|
|
103
107
|
|
|
104
108
|
/**
|