@tscircuit/schematic-trace-solver 0.0.51 → 0.0.53

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 (30) hide show
  1. package/dist/index.d.ts +65 -6
  2. package/dist/index.js +835 -80
  3. package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +3 -39
  4. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +16 -0
  5. package/lib/solvers/SchematicTracePipelineSolver/colorAvailableNetOrientationLabels.ts +54 -0
  6. package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver.ts +331 -0
  7. package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/candidates.ts +387 -0
  8. package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/geometry.ts +254 -0
  9. package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/types.ts +47 -0
  10. package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/visualize.ts +159 -0
  11. package/package.json +1 -1
  12. package/tests/assets/example30.json +4 -1
  13. package/tests/examples/__snapshots__/example01.snap.svg +6 -3
  14. package/tests/examples/__snapshots__/example02.snap.svg +8 -4
  15. package/tests/examples/__snapshots__/example04.snap.svg +4 -2
  16. package/tests/examples/__snapshots__/example07.snap.svg +10 -5
  17. package/tests/examples/__snapshots__/example12.snap.svg +8 -4
  18. package/tests/examples/__snapshots__/example13.snap.svg +16 -8
  19. package/tests/examples/__snapshots__/example14.snap.svg +16 -8
  20. package/tests/examples/__snapshots__/example15.snap.svg +18 -9
  21. package/tests/examples/__snapshots__/example16.snap.svg +8 -4
  22. package/tests/examples/__snapshots__/example17.snap.svg +9 -5
  23. package/tests/examples/__snapshots__/example18.snap.svg +10 -5
  24. package/tests/examples/__snapshots__/example20.snap.svg +6 -3
  25. package/tests/examples/__snapshots__/example21.snap.svg +17 -12
  26. package/tests/examples/__snapshots__/example22.snap.svg +8 -4
  27. package/tests/examples/__snapshots__/example25.snap.svg +18 -9
  28. package/tests/examples/__snapshots__/example30.snap.svg +74 -62
  29. package/tests/examples/__snapshots__/example31.snap.svg +2 -1
  30. package/tests/fixtures/matcher.ts +21 -0
@@ -100,7 +100,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
100
100
  const indices: number[] = []
101
101
 
102
102
  for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
103
- if (this.shouldProcessLabel(this.outputNetLabelPlacements[i]!, i)) {
103
+ if (this.shouldProcessLabel(this.outputNetLabelPlacements[i]!)) {
104
104
  indices.push(i)
105
105
  }
106
106
  }
@@ -108,14 +108,9 @@ export class AvailableNetOrientationSolver extends BaseSolver {
108
108
  return indices
109
109
  }
110
110
 
111
- private shouldProcessLabel(label: NetLabelPlacement, labelIndex: number) {
111
+ private shouldProcessLabel(label: NetLabelPlacement) {
112
112
  const orientations = this.getAvailableOrientations(label)
113
- if (orientations.length === 0) return false
114
-
115
- return !(
116
- orientations.includes(label.orientation) &&
117
- this.isLabelPlacementValid(label, labelIndex)
118
- )
113
+ return orientations.length > 0 && !orientations.includes(label.orientation)
119
114
  }
120
115
 
121
116
  private processLabel(labelIndex: number) {
@@ -441,27 +436,6 @@ export class AvailableNetOrientationSolver extends BaseSolver {
441
436
  )?.netLabelWidth
442
437
  }
443
438
 
444
- private isLabelPlacementValid(label: NetLabelPlacement, labelIndex: number) {
445
- return this.isCandidateValidWithOffset(
446
- {
447
- orientation: label.orientation,
448
- anchorPoint: label.anchorPoint,
449
- center: label.center,
450
- width: label.width,
451
- height: label.height,
452
- },
453
- labelIndex,
454
- )
455
- }
456
-
457
- private isCandidateValidWithOffset(
458
- candidate: CandidateLabel,
459
- labelIndex: number,
460
- ) {
461
- const bounds = this.getOffsetCollisionBounds(candidate)
462
- return this.getBoundsStatus(bounds, labelIndex) === "valid"
463
- }
464
-
465
439
  private getCandidateStatus(
466
440
  candidate: CandidateLabel,
467
441
  label: NetLabelPlacement,
@@ -505,16 +479,6 @@ export class AvailableNetOrientationSolver extends BaseSolver {
505
479
  return "valid"
506
480
  }
507
481
 
508
- private getOffsetCollisionBounds(candidate: CandidateLabel) {
509
- const direction = dir(candidate.orientation)
510
- const collisionTestOffset = 1e-4
511
- const testCenter = {
512
- x: candidate.center.x + direction.x * collisionTestOffset,
513
- y: candidate.center.y + direction.y * collisionTestOffset,
514
- }
515
- return getRectBounds(testCenter, candidate.width, candidate.height)
516
- }
517
-
518
482
  private intersectsAnyOtherNetLabel(bounds: Bounds, labelIndex: number) {
519
483
  for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
520
484
  if (i === labelIndex) continue
@@ -13,6 +13,7 @@ import {
13
13
  } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
14
14
  import { TraceOverlapShiftSolver } from "../TraceOverlapShiftSolver/TraceOverlapShiftSolver"
15
15
  import { NetLabelPlacementSolver } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
16
+ import { colorAvailableNetOrientationLabels } from "./colorAvailableNetOrientationLabels"
16
17
  import { visualizeInputProblem } from "./visualizeInputProblem"
17
18
  import { TraceLabelOverlapAvoidanceSolver } from "../TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver"
18
19
  import { correctPinsInsideChips } from "./correctPinsInsideChip"
@@ -23,6 +24,7 @@ import { TraceCleanupSolver } from "../TraceCleanupSolver/TraceCleanupSolver"
23
24
  import { Example28Solver } from "../Example28Solver/Example28Solver"
24
25
  import { AvailableNetOrientationSolver } from "../AvailableNetOrientationSolver/AvailableNetOrientationSolver"
25
26
  import { VccNetLabelCornerPlacementSolver } from "../VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver"
27
+ import { TraceAnchoredNetLabelOverlapSolver } from "../TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver"
26
28
 
27
29
  type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
28
30
  solverName: string
@@ -75,6 +77,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
75
77
  example28Solver?: Example28Solver
76
78
  availableNetOrientationSolver?: AvailableNetOrientationSolver
77
79
  vccNetLabelCornerPlacementSolver?: VccNetLabelCornerPlacementSolver
80
+ traceAnchoredNetLabelOverlapSolver?: TraceAnchoredNetLabelOverlapSolver
78
81
 
79
82
  startTimeOfPhase: Record<string, number>
80
83
  endTimeOfPhase: Record<string, number>
@@ -270,6 +273,18 @@ export class SchematicTracePipelineSolver extends BaseSolver {
270
273
  ]
271
274
  },
272
275
  ),
276
+ definePipelineStep(
277
+ "traceAnchoredNetLabelOverlapSolver",
278
+ TraceAnchoredNetLabelOverlapSolver,
279
+ (instance) => [
280
+ {
281
+ inputProblem: instance.inputProblem,
282
+ traces: instance.availableNetOrientationSolver!.traces,
283
+ netLabelPlacements:
284
+ instance.vccNetLabelCornerPlacementSolver!.outputNetLabelPlacements,
285
+ },
286
+ ],
287
+ ),
273
288
  ]
274
289
 
275
290
  constructor(inputProblem: InputProblem) {
@@ -389,6 +404,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
389
404
  circles: visualizations.flatMap((v) => v.circles || []),
390
405
  texts: visualizations.flatMap((v) => v.texts || []),
391
406
  }
407
+ colorAvailableNetOrientationLabels(finalGraphics, this.inputProblem)
392
408
  return finalGraphics
393
409
  }
394
410
 
@@ -0,0 +1,54 @@
1
+ import type { GraphicsObject } from "graphics-debug"
2
+ import type { InputProblem } from "lib/types/InputProblem"
3
+
4
+ const AVAILABLE_Y_PLUS_COLOR = "#93c5fd"
5
+ const AVAILABLE_Y_MINUS_COLOR = "#fca5a5"
6
+ const AVAILABLE_ORIENTATION_FILL_ALPHA = "66"
7
+
8
+ export const colorAvailableNetOrientationLabels = (
9
+ graphicsObject: GraphicsObject,
10
+ inputProblem: InputProblem,
11
+ ) => {
12
+ const availableOrientations = inputProblem.availableNetLabelOrientations
13
+ if (!availableOrientations) return
14
+
15
+ for (const rect of graphicsObject.rects ?? []) {
16
+ const orientations = getAvailableOrientationsForRect(
17
+ rect.label,
18
+ availableOrientations,
19
+ )
20
+ if (!orientations) continue
21
+
22
+ const hasYPlus = orientations.includes("y+")
23
+ const hasYMinus = orientations.includes("y-")
24
+ if (hasYPlus === hasYMinus) continue
25
+
26
+ const color = hasYPlus ? AVAILABLE_Y_PLUS_COLOR : AVAILABLE_Y_MINUS_COLOR
27
+
28
+ rect.fill = `${color}${AVAILABLE_ORIENTATION_FILL_ALPHA}`
29
+ rect.stroke = color
30
+ rect.color = color
31
+ }
32
+ }
33
+
34
+ const getAvailableOrientationsForRect = (
35
+ label: string | undefined,
36
+ availableOrientations: InputProblem["availableNetLabelOrientations"],
37
+ ) => {
38
+ for (const netId of getNetIdsFromRectLabel(label)) {
39
+ if (Object.hasOwn(availableOrientations, netId)) {
40
+ return availableOrientations[netId]
41
+ }
42
+ }
43
+
44
+ return undefined
45
+ }
46
+
47
+ const getNetIdsFromRectLabel = (label: string | undefined) => {
48
+ if (!label) return []
49
+
50
+ return [
51
+ label.match(/^netId: (.+)$/m)?.[1],
52
+ label.match(/^globalConnNetId: (.+)$/m)?.[1],
53
+ ].filter((netId): netId is string => Boolean(netId && netId !== "undefined"))
54
+ }
@@ -0,0 +1,331 @@
1
+ import type { GraphicsObject } from "graphics-debug"
2
+ import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
3
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
4
+ import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
5
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
6
+ import type { InputProblem } from "lib/types/InputProblem"
7
+ import { generateCandidatesAlongTrace } from "./candidates"
8
+ import {
9
+ getLabelBounds,
10
+ getTraceLocationsForPoint,
11
+ rectsOverlap,
12
+ rectsTouchOrOverlap,
13
+ traceCrossesBoundsInterior,
14
+ } from "./geometry"
15
+ import type {
16
+ Bounds,
17
+ CandidateStatus,
18
+ LabelCandidate,
19
+ LabelOverlap,
20
+ TraceAnchoredNetLabelOverlapSolverParams,
21
+ } from "./types"
22
+ import { visualizeTraceAnchoredNetLabelOverlapSolver } from "./visualize"
23
+
24
+ type ActiveOverlapSearch = {
25
+ overlap: LabelOverlap
26
+ queuedLabelIndices: number[]
27
+ labelIndex: number | null
28
+ candidates: LabelCandidate[]
29
+ candidateIndex: number
30
+ candidateResults: LabelCandidate[]
31
+ completed: boolean
32
+ }
33
+
34
+ export class TraceAnchoredNetLabelOverlapSolver extends BaseSolver {
35
+ inputProblem: InputProblem
36
+ traces: SolvedTracePath[]
37
+ netLabelPlacements: NetLabelPlacement[]
38
+
39
+ outputNetLabelPlacements: NetLabelPlacement[]
40
+ currentOverlap: LabelOverlap | null = null
41
+ currentCandidateResults: LabelCandidate[] = []
42
+
43
+ private activeSearch: ActiveOverlapSearch | null = null
44
+ private skippedOverlapKeys = new Set<string>()
45
+
46
+ constructor(params: TraceAnchoredNetLabelOverlapSolverParams) {
47
+ super()
48
+ this.inputProblem = params.inputProblem
49
+ this.traces = params.traces
50
+ this.netLabelPlacements = params.netLabelPlacements
51
+ this.outputNetLabelPlacements = [...params.netLabelPlacements]
52
+ }
53
+
54
+ override getConstructorParams(): ConstructorParameters<
55
+ typeof TraceAnchoredNetLabelOverlapSolver
56
+ >[0] {
57
+ return {
58
+ inputProblem: this.inputProblem,
59
+ traces: this.traces,
60
+ netLabelPlacements: this.netLabelPlacements,
61
+ }
62
+ }
63
+
64
+ override _step() {
65
+ if (this.activeSearch?.completed) this.clearActiveSearch()
66
+
67
+ const search = this.getNextSearchWithCandidates()
68
+ if (!search) {
69
+ this.finish()
70
+ return
71
+ }
72
+
73
+ this.evaluateNextCandidate(search)
74
+ }
75
+
76
+ getOutput() {
77
+ return {
78
+ netLabelPlacements: this.outputNetLabelPlacements,
79
+ }
80
+ }
81
+
82
+ private findNextOverlap() {
83
+ for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
84
+ for (let j = i + 1; j < this.outputNetLabelPlacements.length; j++) {
85
+ if (!this.isLabelEligible(i) && !this.isLabelEligible(j)) continue
86
+
87
+ const overlap = {
88
+ firstLabelIndex: i,
89
+ secondLabelIndex: j,
90
+ }
91
+ if (this.skippedOverlapKeys.has(this.getOverlapKey(overlap))) continue
92
+ if (this.labelsOverlap(overlap)) return overlap
93
+ }
94
+ }
95
+
96
+ return null
97
+ }
98
+
99
+ private isLabelEligible(labelIndex: number) {
100
+ return this.getTraceLocationsForLabel(labelIndex).length > 0
101
+ }
102
+
103
+ private labelsOverlap(overlap: LabelOverlap) {
104
+ const first = this.outputNetLabelPlacements[overlap.firstLabelIndex]
105
+ const second = this.outputNetLabelPlacements[overlap.secondLabelIndex]
106
+ if (!first || !second) return false
107
+
108
+ return rectsOverlap(getLabelBounds(first), getLabelBounds(second))
109
+ }
110
+
111
+ private startNextOverlapSearch() {
112
+ const overlap = this.findNextOverlap()
113
+ if (!overlap) return null
114
+
115
+ const search: ActiveOverlapSearch = {
116
+ overlap,
117
+ queuedLabelIndices: this.getMoveLabelIndices(overlap),
118
+ labelIndex: null,
119
+ candidates: [],
120
+ candidateIndex: 0,
121
+ candidateResults: [],
122
+ completed: false,
123
+ }
124
+
125
+ this.setActiveSearch(search)
126
+ return search
127
+ }
128
+
129
+ private getMoveLabelIndices(overlap: LabelOverlap) {
130
+ const labelIndices: number[] = []
131
+
132
+ if (this.isLabelEligible(overlap.secondLabelIndex)) {
133
+ labelIndices.push(overlap.secondLabelIndex)
134
+ }
135
+ if (this.isLabelEligible(overlap.firstLabelIndex)) {
136
+ labelIndices.push(overlap.firstLabelIndex)
137
+ }
138
+
139
+ return labelIndices
140
+ }
141
+
142
+ private getNextSearchWithCandidates() {
143
+ while (true) {
144
+ const search = this.activeSearch ?? this.startNextOverlapSearch()
145
+ if (!search) return null
146
+ if (this.activateNextLabelSearch(search)) return search
147
+
148
+ this.skipSearch(search)
149
+ }
150
+ }
151
+
152
+ private activateNextLabelSearch(search: ActiveOverlapSearch) {
153
+ if (
154
+ search.labelIndex !== null &&
155
+ search.candidateIndex < search.candidates.length
156
+ ) {
157
+ return true
158
+ }
159
+
160
+ while (search.queuedLabelIndices.length > 0) {
161
+ const labelIndex = search.queuedLabelIndices.shift()!
162
+ const label = this.outputNetLabelPlacements[labelIndex]
163
+ if (!label) continue
164
+
165
+ search.labelIndex = labelIndex
166
+ search.candidates = this.getCandidatesForLabel(label)
167
+ search.candidateIndex = 0
168
+ if (search.candidates.length > 0) return true
169
+ }
170
+
171
+ search.labelIndex = null
172
+ search.candidates = []
173
+ search.candidateIndex = 0
174
+ return false
175
+ }
176
+
177
+ private skipSearch(search: ActiveOverlapSearch) {
178
+ this.skippedOverlapKeys.add(this.getOverlapKey(search.overlap))
179
+ this.clearActiveSearch()
180
+ }
181
+
182
+ private evaluateNextCandidate(search: ActiveOverlapSearch) {
183
+ const labelIndex = search.labelIndex
184
+ if (labelIndex === null) return
185
+
186
+ const label = this.outputNetLabelPlacements[labelIndex]
187
+ const candidate = search.candidates[search.candidateIndex]
188
+ if (!label || !candidate) return
189
+
190
+ search.candidateIndex += 1
191
+
192
+ const evaluatedCandidate = {
193
+ ...candidate,
194
+ status: this.getCandidateStatus(candidate, labelIndex),
195
+ }
196
+ search.candidateResults.push(evaluatedCandidate)
197
+
198
+ if (evaluatedCandidate.status !== "valid") return
199
+
200
+ evaluatedCandidate.selected = true
201
+ this.applyCandidate(labelIndex, label, evaluatedCandidate)
202
+ search.completed = true
203
+ }
204
+
205
+ private getCandidatesForLabel(label: NetLabelPlacement) {
206
+ const candidates: LabelCandidate[] = []
207
+
208
+ for (const traceLocation of getTraceLocationsForPoint(
209
+ label.anchorPoint,
210
+ this.traces,
211
+ ).filter(({ trace }) => this.isTraceAssociatedWithLabel(trace, label))) {
212
+ candidates.push(
213
+ ...generateCandidatesAlongTrace({
214
+ inputProblem: this.inputProblem,
215
+ label,
216
+ traceLocation,
217
+ }),
218
+ )
219
+ }
220
+
221
+ return candidates
222
+ }
223
+
224
+ private getCandidateStatus(
225
+ candidate: LabelCandidate,
226
+ labelIndex: number,
227
+ ): CandidateStatus {
228
+ const bounds = getLabelBounds(candidate)
229
+ if (this.intersectsAnyChip(bounds)) return "chip-collision"
230
+ if (traceCrossesBoundsInterior(bounds, this.traces))
231
+ return "trace-collision"
232
+ if (this.intersectsAnyOtherLabel(bounds, labelIndex)) {
233
+ return "netlabel-collision"
234
+ }
235
+
236
+ return "valid"
237
+ }
238
+
239
+ private applyCandidate(
240
+ labelIndex: number,
241
+ label: NetLabelPlacement,
242
+ candidate: LabelCandidate,
243
+ ) {
244
+ this.outputNetLabelPlacements[labelIndex] = {
245
+ ...label,
246
+ anchorPoint: candidate.anchorPoint,
247
+ center: candidate.center,
248
+ width: candidate.width,
249
+ height: candidate.height,
250
+ orientation: candidate.orientation,
251
+ }
252
+ }
253
+
254
+ private intersectsAnyChip(bounds: Bounds) {
255
+ return this.inputProblem.chips.some((chip) =>
256
+ rectsTouchOrOverlap(
257
+ bounds,
258
+ getRectBounds(chip.center, chip.width, chip.height),
259
+ ),
260
+ )
261
+ }
262
+
263
+ private intersectsAnyOtherLabel(bounds: Bounds, labelIndex: number) {
264
+ return this.outputNetLabelPlacements.some((label, index) => {
265
+ if (index === labelIndex) return false
266
+ return rectsOverlap(bounds, getLabelBounds(label))
267
+ })
268
+ }
269
+
270
+ private getTraceLocationsForLabel(labelIndex: number) {
271
+ const label = this.outputNetLabelPlacements[labelIndex]
272
+ if (!label) return []
273
+
274
+ return getTraceLocationsForPoint(label.anchorPoint, this.traces).filter(
275
+ ({ trace }) => this.isTraceAssociatedWithLabel(trace, label),
276
+ )
277
+ }
278
+
279
+ private isTraceAssociatedWithLabel(
280
+ trace: SolvedTracePath,
281
+ label: NetLabelPlacement,
282
+ ) {
283
+ if (label.mspConnectionPairIds.includes(trace.mspPairId)) return true
284
+ if (label.netId !== undefined && label.netId === trace.userNetId) {
285
+ return true
286
+ }
287
+
288
+ return label.globalConnNetId === trace.globalConnNetId
289
+ }
290
+
291
+ private getOverlapKey(overlap: LabelOverlap) {
292
+ const first = this.outputNetLabelPlacements[overlap.firstLabelIndex]
293
+ const second = this.outputNetLabelPlacements[overlap.secondLabelIndex]
294
+ return [
295
+ overlap.firstLabelIndex,
296
+ overlap.secondLabelIndex,
297
+ first?.anchorPoint.x,
298
+ first?.anchorPoint.y,
299
+ second?.anchorPoint.x,
300
+ second?.anchorPoint.y,
301
+ ].join(":")
302
+ }
303
+
304
+ private finish() {
305
+ this.clearActiveSearch()
306
+ this.solved = true
307
+ }
308
+
309
+ private setActiveSearch(search: ActiveOverlapSearch) {
310
+ this.activeSearch = search
311
+ this.currentOverlap = search.overlap
312
+ this.currentCandidateResults = search.candidateResults
313
+ }
314
+
315
+ private clearActiveSearch() {
316
+ this.activeSearch = null
317
+ this.currentOverlap = null
318
+ this.currentCandidateResults = []
319
+ }
320
+
321
+ override visualize(): GraphicsObject {
322
+ return visualizeTraceAnchoredNetLabelOverlapSolver({
323
+ inputProblem: this.inputProblem,
324
+ traces: this.traces,
325
+ outputNetLabelPlacements: this.outputNetLabelPlacements,
326
+ currentOverlap: this.currentOverlap,
327
+ currentCandidateResults: this.currentCandidateResults,
328
+ solved: this.solved,
329
+ })
330
+ }
331
+ }