@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
@@ -12,6 +12,7 @@ import {
12
12
  getTraceRecoveryConnectivityMaps,
13
13
  type TraceRecoveryPin,
14
14
  } from "lib/solvers/NetLabelTraceRecovery/getTraceRecoveryConnectivityMaps"
15
+ import { getTraceConnectedPinComponents } from "lib/solvers/SchematicTraceLinesSolver/getTraceConnectedPinComponents"
15
16
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
16
17
  import { SchematicTraceSingleLineSolver2 } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2"
17
18
  import type {
@@ -37,10 +38,14 @@ interface CandidatePair {
37
38
  perpendicularOffset: number
38
39
  routeDistance: number
39
40
  key: string
41
+ recoveryMode: "fallback_labels" | "routed_components"
42
+ netConnectionPinIds?: PinId[]
40
43
  }
41
44
 
42
45
  const AVAILABLE_NET_ORIENTATION_PREFIX = "available-net-orientation-"
43
46
  const RECOVERED_TRACE_PREFIX = "net-label-to-trace-"
47
+ const MAX_NAMED_NET_RECOVERY_PERPENDICULAR_OFFSET = 0.05
48
+ const MAX_ROUTED_COMPONENT_RECOVERY_PERPENDICULAR_OFFSET = 0.25
44
49
 
45
50
  const getCanonicalPairKey = (firstPinId: PinId, secondPinId: PinId) =>
46
51
  [firstPinId, secondPinId].sort().join("--")
@@ -70,14 +75,28 @@ export const pathIntersectsRenderedLabel = (
70
75
  return false
71
76
  }
72
77
 
73
- const getPerpendicularOffset = (
78
+ const getPerpendicularOffset = (firstPoint: Point, secondPoint: Point) => {
79
+ const xDistance = Math.abs(firstPoint.x - secondPoint.x)
80
+ const yDistance = Math.abs(firstPoint.y - secondPoint.y)
81
+ if (xDistance >= yDistance) return yDistance
82
+ return xDistance
83
+ }
84
+
85
+ const arePinsCoFacingAlongSeparationAxis = (
74
86
  firstPin: TraceRecoveryPin,
75
87
  secondPin: TraceRecoveryPin,
76
88
  ) => {
89
+ if (firstPin._facingDirection !== secondPin._facingDirection) return false
77
90
  const xDistance = Math.abs(firstPin.x - secondPin.x)
78
91
  const yDistance = Math.abs(firstPin.y - secondPin.y)
79
- if (xDistance >= yDistance) return yDistance
80
- return xDistance
92
+ if (xDistance >= yDistance) {
93
+ return (
94
+ firstPin._facingDirection === "x+" || firstPin._facingDirection === "x-"
95
+ )
96
+ }
97
+ return (
98
+ firstPin._facingDirection === "y+" || firstPin._facingDirection === "y-"
99
+ )
81
100
  }
82
101
 
83
102
  export class NetLabelToTraceSolver extends BaseSolver {
@@ -113,20 +132,20 @@ export class NetLabelToTraceSolver extends BaseSolver {
113
132
  return [this.input]
114
133
  }
115
134
 
116
- private isEligiblePortOnlyDirectConnectionLabel(
135
+ private isPortOnlyFallbackLabel(
117
136
  label: NetLabelPlacement,
118
- groundGlobalConnNetId?: GlobalConnNetId,
137
+ groundGlobalConnNetIds: Set<GlobalConnNetId>,
119
138
  ) {
120
- if (
139
+ return !(
121
140
  label.pinIds.length !== 1 ||
122
141
  label.mspConnectionPairIds.length !== 0 ||
123
142
  !label.netId ||
124
- label.netId === "GND" ||
125
- label.globalConnNetId === groundGlobalConnNetId
126
- ) {
127
- return false
128
- }
143
+ groundGlobalConnNetIds.has(label.globalConnNetId)
144
+ )
145
+ }
129
146
 
147
+ private isDirectConnectionLabel(label: NetLabelPlacement) {
148
+ if (!label.netId || label.pinIds.length !== 1) return false
130
149
  const pinId = label.pinIds[0]!
131
150
  return this.inputProblem.directConnections.some(
132
151
  (connection) =>
@@ -134,20 +153,35 @@ export class NetLabelToTraceSolver extends BaseSolver {
134
153
  )
135
154
  }
136
155
 
156
+ private getMultiPinNetConnection(label: NetLabelPlacement) {
157
+ if (!label.netId || label.pinIds.length !== 1) return undefined
158
+ const pinId = label.pinIds[0]!
159
+ return this.inputProblem.netConnections.find(
160
+ (connection) =>
161
+ connection.isGround === false &&
162
+ connection.pinIds.length > 2 &&
163
+ connection.netId === label.netId &&
164
+ connection.pinIds.includes(pinId),
165
+ )
166
+ }
167
+
137
168
  private buildCandidatePairs() {
138
169
  const { netConnMap } = getConnectivityMapsFromInputProblem(
139
170
  this.inputProblem,
140
171
  )
141
- const groundGlobalConnNetId =
142
- netConnMap.getNetConnectedToId("GND") ?? undefined
172
+ const groundGlobalConnNetIds = new Set<GlobalConnNetId>()
173
+ for (const connection of this.inputProblem.netConnections) {
174
+ if (!connection.isGround) continue
175
+ const globalConnNetId = netConnMap.getNetConnectedToId(connection.netId)
176
+ if (globalConnNetId) groundGlobalConnNetIds.add(globalConnNetId)
177
+ }
143
178
  const labelsByGlobalNet = new Map<GlobalConnNetId, NetLabelPlacement[]>()
144
179
 
145
180
  for (const label of this.input.netLabelPlacements) {
146
181
  if (
147
- !this.isEligiblePortOnlyDirectConnectionLabel(
148
- label,
149
- groundGlobalConnNetId,
150
- )
182
+ !this.isPortOnlyFallbackLabel(label, groundGlobalConnNetIds) ||
183
+ (!this.isDirectConnectionLabel(label) &&
184
+ !this.getMultiPinNetConnection(label))
151
185
  ) {
152
186
  continue
153
187
  }
@@ -169,6 +203,25 @@ export class NetLabelToTraceSolver extends BaseSolver {
169
203
  const firstPin = this.pinMap.get(firstLabel.pinIds[0]!)
170
204
  const secondPin = this.pinMap.get(secondLabel.pinIds[0]!)
171
205
  if (!firstPin || !secondPin) continue
206
+ const perpendicularOffset = getPerpendicularOffset(
207
+ firstPin,
208
+ secondPin,
209
+ )
210
+ const bothLabelsBelongToDirectConnections =
211
+ this.isDirectConnectionLabel(firstLabel) &&
212
+ this.isDirectConnectionLabel(secondLabel)
213
+ const firstMultiPinNetConnection =
214
+ this.getMultiPinNetConnection(firstLabel)
215
+ const secondMultiPinNetConnection =
216
+ this.getMultiPinNetConnection(secondLabel)
217
+ if (
218
+ !bothLabelsBelongToDirectConnections &&
219
+ (!firstMultiPinNetConnection ||
220
+ firstMultiPinNetConnection !== secondMultiPinNetConnection ||
221
+ perpendicularOffset > MAX_NAMED_NET_RECOVERY_PERPENDICULAR_OFFSET)
222
+ ) {
223
+ continue
224
+ }
172
225
  if (
173
226
  arePinsInDifferentSchematicSections(
174
227
  this.inputProblem,
@@ -194,16 +247,19 @@ export class NetLabelToTraceSolver extends BaseSolver {
194
247
  firstLabel,
195
248
  secondLabel,
196
249
  pins: [firstPin, secondPin],
197
- perpendicularOffset: getPerpendicularOffset(firstPin, secondPin),
250
+ perpendicularOffset,
198
251
  routeDistance:
199
252
  Math.abs(firstPin.x - secondPin.x) +
200
253
  Math.abs(firstPin.y - secondPin.y),
201
254
  key: getCanonicalPairKey(firstPin.pinId, secondPin.pinId),
255
+ recoveryMode: "fallback_labels",
202
256
  })
203
257
  }
204
258
  }
205
259
  }
206
260
 
261
+ candidates.push(...this.buildRoutedComponentCandidates())
262
+
207
263
  candidates.sort(
208
264
  (first, second) =>
209
265
  first.perpendicularOffset - second.perpendicularOffset ||
@@ -213,6 +269,129 @@ export class NetLabelToTraceSolver extends BaseSolver {
213
269
  return candidates
214
270
  }
215
271
 
272
+ private buildRoutedComponentCandidates() {
273
+ const { netConnMap } = getConnectivityMapsFromInputProblem(
274
+ this.inputProblem,
275
+ )
276
+ const candidates: CandidatePair[] = []
277
+
278
+ for (const connection of this.inputProblem.netConnections) {
279
+ if (connection.pinIds.length <= 2 || connection.isGround !== false)
280
+ continue
281
+ const globalConnNetId = netConnMap.getNetConnectedToId(connection.netId)
282
+ if (!globalConnNetId) continue
283
+
284
+ const traceConnectedPinComponents = getTraceConnectedPinComponents({
285
+ pinIds: connection.pinIds,
286
+ traces: this.outputTraces.filter(
287
+ (trace) => trace.globalConnNetId === globalConnNetId,
288
+ ),
289
+ }).filter((component) => component.traces.length > 0)
290
+
291
+ for (
292
+ let firstIndex = 0;
293
+ firstIndex < traceConnectedPinComponents.length;
294
+ firstIndex++
295
+ ) {
296
+ for (
297
+ let secondIndex = firstIndex + 1;
298
+ secondIndex < traceConnectedPinComponents.length;
299
+ secondIndex++
300
+ ) {
301
+ const firstComponent = traceConnectedPinComponents[firstIndex]!
302
+ const secondComponent = traceConnectedPinComponents[secondIndex]!
303
+ const firstLabel = this.input.netLabelPlacements.find(
304
+ (label) =>
305
+ label.globalConnNetId === globalConnNetId &&
306
+ label.mspConnectionPairIds.length > 0 &&
307
+ label.pinIds.some((pinId) =>
308
+ firstComponent.pinIds.includes(pinId),
309
+ ),
310
+ )
311
+ const secondLabel = this.input.netLabelPlacements.find(
312
+ (label) =>
313
+ label.globalConnNetId === globalConnNetId &&
314
+ label.mspConnectionPairIds.length > 0 &&
315
+ label.pinIds.some((pinId) =>
316
+ secondComponent.pinIds.includes(pinId),
317
+ ),
318
+ )
319
+ if (!firstLabel || !secondLabel || firstLabel === secondLabel)
320
+ continue
321
+ if (
322
+ getPerpendicularOffset(
323
+ firstLabel.anchorPoint,
324
+ secondLabel.anchorPoint,
325
+ ) > MAX_ROUTED_COMPONENT_RECOVERY_PERPENDICULAR_OFFSET
326
+ ) {
327
+ continue
328
+ }
329
+
330
+ let bestCandidate: CandidatePair | undefined
331
+ for (const firstPinId of firstComponent.pinIds) {
332
+ for (const secondPinId of secondComponent.pinIds) {
333
+ const firstPin = this.pinMap.get(firstPinId)
334
+ const secondPin = this.pinMap.get(secondPinId)
335
+ if (!firstPin || !secondPin) continue
336
+ const perpendicularOffset = getPerpendicularOffset(
337
+ firstPin,
338
+ secondPin,
339
+ )
340
+ if (
341
+ perpendicularOffset >
342
+ MAX_ROUTED_COMPONENT_RECOVERY_PERPENDICULAR_OFFSET ||
343
+ arePinsCoFacingAlongSeparationAxis(firstPin, secondPin) ||
344
+ arePinsInDifferentSchematicSections(
345
+ this.inputProblem,
346
+ firstPin,
347
+ secondPin,
348
+ ) ||
349
+ doesPairCrossRestrictedCenterLines({
350
+ inputProblem: this.inputProblem,
351
+ chipMap: this.chipMap,
352
+ pinIdMap: this.pinMap,
353
+ p1: firstPin,
354
+ p2: secondPin,
355
+ })
356
+ ) {
357
+ continue
358
+ }
359
+
360
+ const routeDistance =
361
+ Math.abs(firstPin.x - secondPin.x) +
362
+ Math.abs(firstPin.y - secondPin.y)
363
+ const candidate: CandidatePair = {
364
+ firstLabel,
365
+ secondLabel,
366
+ pins: [firstPin, secondPin],
367
+ perpendicularOffset,
368
+ routeDistance,
369
+ key: getCanonicalPairKey(firstPin.pinId, secondPin.pinId),
370
+ recoveryMode: "routed_components",
371
+ netConnectionPinIds: connection.pinIds,
372
+ }
373
+ if (
374
+ !bestCandidate ||
375
+ candidate.routeDistance < bestCandidate.routeDistance ||
376
+ (candidate.routeDistance === bestCandidate.routeDistance &&
377
+ (candidate.perpendicularOffset <
378
+ bestCandidate.perpendicularOffset ||
379
+ (candidate.perpendicularOffset ===
380
+ bestCandidate.perpendicularOffset &&
381
+ candidate.key.localeCompare(bestCandidate.key) < 0)))
382
+ ) {
383
+ bestCandidate = candidate
384
+ }
385
+ }
386
+ }
387
+ if (bestCandidate) candidates.push(bestCandidate)
388
+ }
389
+ }
390
+ }
391
+
392
+ return candidates
393
+ }
394
+
216
395
  private isSupersededConnectorTrace(
217
396
  trace: SolvedTracePath,
218
397
  candidate: CandidatePair,
@@ -247,8 +426,15 @@ export class NetLabelToTraceSolver extends BaseSolver {
247
426
  const retainedTraces = this.outputTraces.filter(
248
427
  (trace) => !this.isSupersededConnectorTrace(trace, candidate),
249
428
  )
429
+ let collisionTraces = retainedTraces
430
+ if (candidate.recoveryMode === "routed_components") {
431
+ collisionTraces = retainedTraces.filter(
432
+ (trace) =>
433
+ trace.globalConnNetId !== candidate.firstLabel.globalConnNetId,
434
+ )
435
+ }
250
436
  if (
251
- doesTraceOverlapWithExistingTraces(tracePath, retainedTraces) ||
437
+ doesTraceOverlapWithExistingTraces(tracePath, collisionTraces) ||
252
438
  this.routeIntersectsRemainingLabels(tracePath, candidate)
253
439
  ) {
254
440
  return
@@ -267,13 +453,27 @@ export class NetLabelToTraceSolver extends BaseSolver {
267
453
  }
268
454
 
269
455
  this.outputTraces = [...retainedTraces, recoveredTrace]
270
- this.outputNetLabelPlacements = this.outputNetLabelPlacements.filter(
271
- (label) =>
272
- label !== candidate.firstLabel && label !== candidate.secondLabel,
273
- )
456
+ if (candidate.recoveryMode === "fallback_labels") {
457
+ this.outputNetLabelPlacements = this.outputNetLabelPlacements.filter(
458
+ (label) =>
459
+ label !== candidate.firstLabel && label !== candidate.secondLabel,
460
+ )
461
+ }
274
462
  this.stats.recoveredTraceCount++
275
463
  }
276
464
 
465
+ private areCandidatePinsAlreadyConnected(candidate: CandidatePair) {
466
+ if (!candidate.netConnectionPinIds) return false
467
+ return getTraceConnectedPinComponents({
468
+ pinIds: candidate.netConnectionPinIds,
469
+ traces: this.outputTraces,
470
+ }).some(
471
+ (component) =>
472
+ component.pinIds.includes(candidate.pins[0].pinId) &&
473
+ component.pinIds.includes(candidate.pins[1].pinId),
474
+ )
475
+ }
476
+
277
477
  override _step() {
278
478
  if (this.activeSubSolver) {
279
479
  this.activeSubSolver.step()
@@ -292,7 +492,8 @@ export class NetLabelToTraceSolver extends BaseSolver {
292
492
  while (
293
493
  candidate &&
294
494
  (!this.outputNetLabelPlacements.includes(candidate.firstLabel) ||
295
- !this.outputNetLabelPlacements.includes(candidate.secondLabel))
495
+ !this.outputNetLabelPlacements.includes(candidate.secondLabel) ||
496
+ this.areCandidatePinsAlreadyConnected(candidate))
296
497
  ) {
297
498
  candidate = this.queuedCandidates.shift()
298
499
  }
@@ -6,6 +6,7 @@ import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/
6
6
  import type { InputProblem } from "lib/types/InputProblem"
7
7
  import { getColorFromString } from "lib/utils/getColorFromString"
8
8
  import { alignSameNetJunctions } from "./alignSameNetJunctions"
9
+ import { collapseSameNetCycles } from "./collapseSameNetCycles"
9
10
  import { placeGroundRailLabelsAtOuterEnd } from "./placeGroundRailLabelsAtOuterEnd"
10
11
 
11
12
  interface SameNetJunctionAlignmentSolverInput {
@@ -28,14 +29,19 @@ export class SameNetJunctionAlignmentSolver extends BaseSolver {
28
29
  }
29
30
 
30
31
  override _step() {
31
- const result = alignSameNetJunctions(this.input)
32
- this.outputTraces = result.traces
32
+ const alignment = alignSameNetJunctions(this.input)
33
+ const cycleCollapse = collapseSameNetCycles({
34
+ traces: alignment.traces,
35
+ netLabelPlacements: alignment.netLabelPlacements,
36
+ })
37
+ this.outputTraces = cycleCollapse.traces
33
38
  this.outputNetLabelPlacements = placeGroundRailLabelsAtOuterEnd({
34
39
  inputProblem: this.input.inputProblem,
35
- traces: result.traces,
36
- netLabelPlacements: result.netLabelPlacements,
40
+ traces: cycleCollapse.traces,
41
+ netLabelPlacements: cycleCollapse.netLabelPlacements,
37
42
  })
38
- this.stats.alignedJunctionCount = result.alignedJunctionCount
43
+ this.stats.alignedJunctionCount = alignment.alignedJunctionCount
44
+ this.stats.collapsedCycleCount = cycleCollapse.collapsedCycleCount
39
45
  this.solved = true
40
46
  }
41
47
 
@@ -41,7 +41,7 @@ const MAX_SAME_NET_LABEL_BOUNDARY_RAIL_OFFSET = 0.2
41
41
  const MAX_SHARED_PIN_RAIL_OFFSET = 0.05
42
42
  const MIN_RETURN_STEM_LENGTH = 0.05
43
43
 
44
- const getSharedPin = ({
44
+ export const getSharedPin = ({
45
45
  donorTrace,
46
46
  branchTrace,
47
47
  }: {