@tscircuit/schematic-trace-solver 0.0.51 → 0.0.52

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.
@@ -23,6 +23,7 @@ import { TraceCleanupSolver } from "../TraceCleanupSolver/TraceCleanupSolver"
23
23
  import { Example28Solver } from "../Example28Solver/Example28Solver"
24
24
  import { AvailableNetOrientationSolver } from "../AvailableNetOrientationSolver/AvailableNetOrientationSolver"
25
25
  import { VccNetLabelCornerPlacementSolver } from "../VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver"
26
+ import { TraceAnchoredNetLabelOverlapSolver } from "../TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver"
26
27
 
27
28
  type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
28
29
  solverName: string
@@ -75,6 +76,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
75
76
  example28Solver?: Example28Solver
76
77
  availableNetOrientationSolver?: AvailableNetOrientationSolver
77
78
  vccNetLabelCornerPlacementSolver?: VccNetLabelCornerPlacementSolver
79
+ traceAnchoredNetLabelOverlapSolver?: TraceAnchoredNetLabelOverlapSolver
78
80
 
79
81
  startTimeOfPhase: Record<string, number>
80
82
  endTimeOfPhase: Record<string, number>
@@ -270,6 +272,18 @@ export class SchematicTracePipelineSolver extends BaseSolver {
270
272
  ]
271
273
  },
272
274
  ),
275
+ definePipelineStep(
276
+ "traceAnchoredNetLabelOverlapSolver",
277
+ TraceAnchoredNetLabelOverlapSolver,
278
+ (instance) => [
279
+ {
280
+ inputProblem: instance.inputProblem,
281
+ traces: instance.availableNetOrientationSolver!.traces,
282
+ netLabelPlacements:
283
+ instance.vccNetLabelCornerPlacementSolver!.outputNetLabelPlacements,
284
+ },
285
+ ],
286
+ ),
273
287
  ]
274
288
 
275
289
  constructor(inputProblem: InputProblem) {
@@ -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
+ traceCrossesBoundsInterior,
13
+ } from "./geometry"
14
+ import type {
15
+ Bounds,
16
+ CandidateStatus,
17
+ LabelCandidate,
18
+ LabelOverlap,
19
+ TraceAnchoredNetLabelOverlapSolverParams,
20
+ } from "./types"
21
+ import { visualizeTraceAnchoredNetLabelOverlapSolver } from "./visualize"
22
+
23
+ type ActiveOverlapSearch = {
24
+ overlap: LabelOverlap
25
+ queuedLabelIndices: number[]
26
+ labelIndex: number | null
27
+ candidates: LabelCandidate[]
28
+ candidateIndex: number
29
+ candidateResults: LabelCandidate[]
30
+ completed: boolean
31
+ }
32
+
33
+ export class TraceAnchoredNetLabelOverlapSolver extends BaseSolver {
34
+ inputProblem: InputProblem
35
+ traces: SolvedTracePath[]
36
+ netLabelPlacements: NetLabelPlacement[]
37
+
38
+ outputNetLabelPlacements: NetLabelPlacement[]
39
+ currentOverlap: LabelOverlap | null = null
40
+ currentCandidateResults: LabelCandidate[] = []
41
+
42
+ private activeSearch: ActiveOverlapSearch | null = null
43
+ private skippedOverlapKeys = new Set<string>()
44
+
45
+ constructor(params: TraceAnchoredNetLabelOverlapSolverParams) {
46
+ super()
47
+ this.inputProblem = params.inputProblem
48
+ this.traces = params.traces
49
+ this.netLabelPlacements = params.netLabelPlacements
50
+ this.outputNetLabelPlacements = [...params.netLabelPlacements]
51
+ }
52
+
53
+ override getConstructorParams(): ConstructorParameters<
54
+ typeof TraceAnchoredNetLabelOverlapSolver
55
+ >[0] {
56
+ return {
57
+ inputProblem: this.inputProblem,
58
+ traces: this.traces,
59
+ netLabelPlacements: this.netLabelPlacements,
60
+ }
61
+ }
62
+
63
+ override _step() {
64
+ if (this.activeSearch?.completed) this.clearActiveSearch()
65
+
66
+ const search = this.getNextSearchWithCandidates()
67
+ if (!search) {
68
+ this.finish()
69
+ return
70
+ }
71
+
72
+ this.evaluateNextCandidate(search)
73
+ }
74
+
75
+ getOutput() {
76
+ return {
77
+ netLabelPlacements: this.outputNetLabelPlacements,
78
+ }
79
+ }
80
+
81
+ private findNextOverlap() {
82
+ for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
83
+ for (let j = i + 1; j < this.outputNetLabelPlacements.length; j++) {
84
+ if (!this.isLabelEligible(i) && !this.isLabelEligible(j)) continue
85
+
86
+ const overlap = {
87
+ firstLabelIndex: i,
88
+ secondLabelIndex: j,
89
+ }
90
+ if (this.skippedOverlapKeys.has(this.getOverlapKey(overlap))) continue
91
+ if (this.labelsOverlap(overlap)) return overlap
92
+ }
93
+ }
94
+
95
+ return null
96
+ }
97
+
98
+ private isLabelEligible(labelIndex: number) {
99
+ return this.getTraceLocationsForLabel(labelIndex).length > 0
100
+ }
101
+
102
+ private labelsOverlap(overlap: LabelOverlap) {
103
+ const first = this.outputNetLabelPlacements[overlap.firstLabelIndex]
104
+ const second = this.outputNetLabelPlacements[overlap.secondLabelIndex]
105
+ if (!first || !second) return false
106
+
107
+ return rectsOverlap(getLabelBounds(first), getLabelBounds(second))
108
+ }
109
+
110
+ private startNextOverlapSearch() {
111
+ const overlap = this.findNextOverlap()
112
+ if (!overlap) return null
113
+
114
+ const search: ActiveOverlapSearch = {
115
+ overlap,
116
+ queuedLabelIndices: this.getMoveLabelIndices(overlap),
117
+ labelIndex: null,
118
+ candidates: [],
119
+ candidateIndex: 0,
120
+ candidateResults: [],
121
+ completed: false,
122
+ }
123
+
124
+ this.setActiveSearch(search)
125
+ return search
126
+ }
127
+
128
+ private getMoveLabelIndices(overlap: LabelOverlap) {
129
+ const labelIndices: number[] = []
130
+
131
+ if (this.isLabelEligible(overlap.secondLabelIndex)) {
132
+ labelIndices.push(overlap.secondLabelIndex)
133
+ }
134
+ if (this.isLabelEligible(overlap.firstLabelIndex)) {
135
+ labelIndices.push(overlap.firstLabelIndex)
136
+ }
137
+
138
+ return labelIndices
139
+ }
140
+
141
+ private getNextSearchWithCandidates() {
142
+ while (true) {
143
+ const search = this.activeSearch ?? this.startNextOverlapSearch()
144
+ if (!search) return null
145
+ if (this.activateNextLabelSearch(search)) return search
146
+
147
+ this.skipSearch(search)
148
+ }
149
+ }
150
+
151
+ private activateNextLabelSearch(search: ActiveOverlapSearch) {
152
+ if (
153
+ search.labelIndex !== null &&
154
+ search.candidateIndex < search.candidates.length
155
+ ) {
156
+ return true
157
+ }
158
+
159
+ while (search.queuedLabelIndices.length > 0) {
160
+ const labelIndex = search.queuedLabelIndices.shift()!
161
+ const label = this.outputNetLabelPlacements[labelIndex]
162
+ if (!label) continue
163
+
164
+ search.labelIndex = labelIndex
165
+ search.candidates = this.getCandidatesForLabel(label)
166
+ search.candidateIndex = 0
167
+ if (search.candidates.length > 0) return true
168
+ }
169
+
170
+ search.labelIndex = null
171
+ search.candidates = []
172
+ search.candidateIndex = 0
173
+ return false
174
+ }
175
+
176
+ private skipSearch(search: ActiveOverlapSearch) {
177
+ this.skippedOverlapKeys.add(this.getOverlapKey(search.overlap))
178
+ this.clearActiveSearch()
179
+ }
180
+
181
+ private evaluateNextCandidate(search: ActiveOverlapSearch) {
182
+ const labelIndex = search.labelIndex
183
+ if (labelIndex === null) return
184
+
185
+ const label = this.outputNetLabelPlacements[labelIndex]
186
+ const candidate = search.candidates[search.candidateIndex]
187
+ if (!label || !candidate) return
188
+
189
+ search.candidateIndex += 1
190
+
191
+ const evaluatedCandidate = {
192
+ ...candidate,
193
+ status: this.getCandidateStatus(candidate, labelIndex),
194
+ }
195
+ search.candidateResults.push(evaluatedCandidate)
196
+
197
+ if (evaluatedCandidate.status !== "valid") return
198
+
199
+ evaluatedCandidate.selected = true
200
+ this.applyCandidate(labelIndex, label, evaluatedCandidate)
201
+ search.completed = true
202
+ }
203
+
204
+ private getCandidatesForLabel(label: NetLabelPlacement) {
205
+ const candidates: LabelCandidate[] = []
206
+
207
+ for (const traceLocation of getTraceLocationsForPoint(
208
+ label.anchorPoint,
209
+ this.traces,
210
+ ).filter(({ trace }) => this.isTraceAssociatedWithLabel(trace, label))) {
211
+ candidates.push(
212
+ ...generateCandidatesAlongTrace({
213
+ inputProblem: this.inputProblem,
214
+ label,
215
+ traceLocation,
216
+ }),
217
+ )
218
+ }
219
+
220
+ return candidates.sort(
221
+ (a, b) =>
222
+ a.distanceFromOriginal - b.distanceFromOriginal ||
223
+ a.pathDistance - b.pathDistance,
224
+ )
225
+ }
226
+
227
+ private getCandidateStatus(
228
+ candidate: LabelCandidate,
229
+ labelIndex: number,
230
+ ): CandidateStatus {
231
+ const bounds = getLabelBounds(candidate)
232
+ if (this.intersectsAnyChip(bounds)) return "chip-collision"
233
+ if (traceCrossesBoundsInterior(bounds, this.traces))
234
+ return "trace-collision"
235
+ if (this.intersectsAnyOtherLabel(bounds, labelIndex)) {
236
+ return "netlabel-collision"
237
+ }
238
+
239
+ return "valid"
240
+ }
241
+
242
+ private applyCandidate(
243
+ labelIndex: number,
244
+ label: NetLabelPlacement,
245
+ candidate: LabelCandidate,
246
+ ) {
247
+ this.outputNetLabelPlacements[labelIndex] = {
248
+ ...label,
249
+ anchorPoint: candidate.anchorPoint,
250
+ center: candidate.center,
251
+ width: candidate.width,
252
+ height: candidate.height,
253
+ orientation: candidate.orientation,
254
+ }
255
+ }
256
+
257
+ private intersectsAnyChip(bounds: Bounds) {
258
+ return this.inputProblem.chips.some((chip) =>
259
+ rectsOverlap(bounds, getRectBounds(chip.center, chip.width, chip.height)),
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
+ }