@tscircuit/schematic-trace-solver 0.0.49 → 0.0.51

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 +133 -1
  2. package/dist/index.js +1084 -41
  3. package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +682 -0
  4. package/lib/solvers/AvailableNetOrientationSolver/constants.ts +6 -0
  5. package/lib/solvers/AvailableNetOrientationSolver/geometry.ts +192 -0
  6. package/lib/solvers/AvailableNetOrientationSolver/traces.ts +43 -0
  7. package/lib/solvers/AvailableNetOrientationSolver/types.ts +44 -0
  8. package/lib/solvers/AvailableNetOrientationSolver/visualize.ts +123 -0
  9. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +30 -0
  10. package/lib/solvers/VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver.ts +268 -0
  11. package/lib/solvers/VccNetLabelCornerPlacementSolver/geometry.ts +59 -0
  12. package/lib/solvers/VccNetLabelCornerPlacementSolver/types.ts +37 -0
  13. package/lib/solvers/VccNetLabelCornerPlacementSolver/visualize.ts +114 -0
  14. package/package.json +1 -1
  15. package/tests/assets/example22.json +1 -1
  16. package/tests/examples/__snapshots__/example12.snap.svg +33 -30
  17. package/tests/examples/__snapshots__/example13.snap.svg +10 -4
  18. package/tests/examples/__snapshots__/example14.snap.svg +72 -66
  19. package/tests/examples/__snapshots__/example16.snap.svg +33 -27
  20. package/tests/examples/__snapshots__/example20.snap.svg +29 -23
  21. package/tests/examples/__snapshots__/example21.snap.svg +77 -65
  22. package/tests/examples/__snapshots__/example22.snap.svg +31 -25
  23. package/tests/examples/__snapshots__/example31.snap.svg +12 -12
@@ -0,0 +1,682 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import type { GraphicsObject } from "graphics-debug"
3
+ import { ChipObstacleSpatialIndex } from "lib/data-structures/ChipObstacleSpatialIndex"
4
+ import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
5
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
6
+ import {
7
+ getCenterFromAnchor,
8
+ getDimsForOrientation,
9
+ getRectBounds,
10
+ } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
11
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
12
+ import type { InputPin, InputProblem } from "lib/types/InputProblem"
13
+ import { dir, type FacingDirection } from "lib/utils/dir"
14
+ import { EPS, LABEL_SEARCH_STEP, WICK_CLEARANCE } from "./constants"
15
+ import {
16
+ getConnectorTracePath,
17
+ getMaxSearchDistance,
18
+ getSideDistances,
19
+ isXOrientation,
20
+ isYOrientation,
21
+ rangesOverlap,
22
+ rectsOverlap,
23
+ traceCrossesBoundsInterior,
24
+ tracePathCrossesAnyTrace,
25
+ } from "./geometry"
26
+ import { getPinMap, getTracePins, toNetLabelPlacementPatch } from "./traces"
27
+ import type {
28
+ AvailableNetOrientationSolverParams,
29
+ Bounds,
30
+ CandidateLabel,
31
+ CandidatePhase,
32
+ CandidateStatus,
33
+ ChipSide,
34
+ EvaluatedCandidate,
35
+ } from "./types"
36
+ import { visualizeAvailableNetOrientationSolver } from "./visualize"
37
+
38
+ export class AvailableNetOrientationSolver extends BaseSolver {
39
+ inputProblem: InputProblem
40
+ traces: SolvedTracePath[]
41
+ netLabelPlacements: NetLabelPlacement[]
42
+
43
+ outputNetLabelPlacements: NetLabelPlacement[]
44
+ queuedLabelIndices: number[] = []
45
+ currentLabelIndex: number | null = null
46
+ currentLabel: NetLabelPlacement | null = null
47
+ currentCandidateResults: EvaluatedCandidate[] = []
48
+
49
+ private traceMap: Record<string, SolvedTracePath>
50
+ private chipObstacleSpatialIndex: ChipObstacleSpatialIndex
51
+ private maxSearchDistance: number
52
+ private pinMap: Record<string, InputPin & { chipId: string }>
53
+
54
+ constructor(params: AvailableNetOrientationSolverParams) {
55
+ super()
56
+ this.inputProblem = params.inputProblem
57
+ this.traces = [...params.traces]
58
+ this.netLabelPlacements = params.netLabelPlacements
59
+ this.outputNetLabelPlacements = [...params.netLabelPlacements]
60
+ this.traceMap = Object.fromEntries(
61
+ this.traces.map((trace) => [trace.mspPairId, trace]),
62
+ )
63
+ this.pinMap = getPinMap(params.inputProblem)
64
+ this.chipObstacleSpatialIndex =
65
+ params.inputProblem._chipObstacleSpatialIndex ??
66
+ new ChipObstacleSpatialIndex(params.inputProblem.chips)
67
+ this.maxSearchDistance = getMaxSearchDistance(params.inputProblem)
68
+ this.queuedLabelIndices = this.getProcessableLabelIndices()
69
+ this.setCurrentLabel(this.queuedLabelIndices[0] ?? null)
70
+ }
71
+
72
+ override getConstructorParams(): ConstructorParameters<
73
+ typeof AvailableNetOrientationSolver
74
+ >[0] {
75
+ return {
76
+ inputProblem: this.inputProblem,
77
+ traces: this.traces,
78
+ netLabelPlacements: this.netLabelPlacements,
79
+ }
80
+ }
81
+
82
+ override _step() {
83
+ const labelIndex = this.queuedLabelIndices.shift()
84
+ if (labelIndex === undefined) {
85
+ this.finish()
86
+ return
87
+ }
88
+
89
+ this.processLabel(labelIndex)
90
+ }
91
+
92
+ getOutput() {
93
+ return {
94
+ traces: this.traces,
95
+ netLabelPlacements: this.outputNetLabelPlacements,
96
+ }
97
+ }
98
+
99
+ private getProcessableLabelIndices() {
100
+ const indices: number[] = []
101
+
102
+ for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
103
+ if (this.shouldProcessLabel(this.outputNetLabelPlacements[i]!, i)) {
104
+ indices.push(i)
105
+ }
106
+ }
107
+
108
+ return indices
109
+ }
110
+
111
+ private shouldProcessLabel(label: NetLabelPlacement, labelIndex: number) {
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
+ )
119
+ }
120
+
121
+ private processLabel(labelIndex: number) {
122
+ const label = this.outputNetLabelPlacements[labelIndex]!
123
+ this.setCurrentLabel(labelIndex)
124
+ this.currentCandidateResults = []
125
+
126
+ const candidate = this.findCorrectedCandidate(label, labelIndex)
127
+ if (!candidate) return
128
+
129
+ this.applyCandidate(label, candidate, labelIndex)
130
+ }
131
+
132
+ private applyCandidate(
133
+ label: NetLabelPlacement,
134
+ candidate: CandidateLabel,
135
+ labelIndex: number,
136
+ ) {
137
+ this.outputNetLabelPlacements[labelIndex] = {
138
+ ...label,
139
+ ...toNetLabelPlacementPatch(candidate),
140
+ }
141
+ this.addConnectorTrace(label, candidate, labelIndex)
142
+ }
143
+
144
+ private addConnectorTrace(
145
+ label: NetLabelPlacement,
146
+ candidate: CandidateLabel,
147
+ labelIndex: number,
148
+ ) {
149
+ const tracePath = getConnectorTracePath(
150
+ label.anchorPoint,
151
+ candidate.anchorPoint,
152
+ candidate.orientation,
153
+ )
154
+ if (tracePath.length < 2) return
155
+
156
+ const mspPairId = `available-net-orientation-${labelIndex}-${label.netId ?? label.globalConnNetId}`
157
+ const connectorTrace: SolvedTracePath = {
158
+ mspPairId,
159
+ dcConnNetId: label.dcConnNetId ?? label.globalConnNetId,
160
+ globalConnNetId: label.globalConnNetId,
161
+ userNetId: label.netId,
162
+ pins: getTracePins(label, this.pinMap),
163
+ tracePath,
164
+ mspConnectionPairIds: [mspPairId],
165
+ pinIds: label.pinIds,
166
+ }
167
+
168
+ this.traces.push(connectorTrace)
169
+ this.traceMap[mspPairId] = connectorTrace
170
+ }
171
+
172
+ private finish() {
173
+ this.currentLabelIndex = null
174
+ this.currentLabel = null
175
+ this.currentCandidateResults = []
176
+ this.solved = true
177
+ }
178
+
179
+ private setCurrentLabel(labelIndex: number | null) {
180
+ this.currentLabelIndex = labelIndex
181
+ this.currentLabel =
182
+ labelIndex === null ? null : this.outputNetLabelPlacements[labelIndex]!
183
+ }
184
+
185
+ private findCorrectedCandidate(label: NetLabelPlacement, labelIndex: number) {
186
+ const orientations = this.getAvailableOrientations(label)
187
+ const rotatedCandidate = this.findValidRotatedCandidate(
188
+ label,
189
+ labelIndex,
190
+ orientations,
191
+ )
192
+
193
+ return (
194
+ rotatedCandidate ??
195
+ this.findValidShiftedCandidate(label, orientations[0]!, labelIndex)
196
+ )
197
+ }
198
+
199
+ private findValidRotatedCandidate(
200
+ label: NetLabelPlacement,
201
+ labelIndex: number,
202
+ orientations: FacingDirection[],
203
+ ) {
204
+ for (const orientation of orientations) {
205
+ const candidate = this.createCandidate(
206
+ label,
207
+ this.getSearchStartAnchor(label, orientation),
208
+ orientation,
209
+ )
210
+ const result = this.evaluateCandidate(
211
+ candidate,
212
+ label,
213
+ labelIndex,
214
+ "rotate",
215
+ )
216
+ this.currentCandidateResults.push(result)
217
+ if (result.status === "valid") {
218
+ result.selected = true
219
+ return result
220
+ }
221
+ }
222
+
223
+ return null
224
+ }
225
+
226
+ private getAvailableOrientations(label: NetLabelPlacement) {
227
+ const effectiveNetId = label.netId ?? label.globalConnNetId
228
+ return this.inputProblem.availableNetLabelOrientations[effectiveNetId] ?? []
229
+ }
230
+
231
+ private findValidShiftedCandidate(
232
+ label: NetLabelPlacement,
233
+ orientation: FacingDirection,
234
+ labelIndex: number,
235
+ ) {
236
+ const direction = dir(orientation)
237
+ const initialBaseAnchor = this.getSearchStartAnchor(label, orientation)
238
+ const outwardDirection = this.getPerpendicularOutwardDirection(
239
+ label.anchorPoint,
240
+ orientation,
241
+ )
242
+ const maxSearchDistance = this.getSearchDistanceLimit(label, orientation)
243
+ const maxOutwardDistance =
244
+ outwardDirection.x === 0 && outwardDirection.y === 0
245
+ ? 0
246
+ : this.maxSearchDistance
247
+
248
+ for (
249
+ let outwardDistance = 0;
250
+ outwardDistance <= maxOutwardDistance + EPS;
251
+ outwardDistance += LABEL_SEARCH_STEP
252
+ ) {
253
+ const baseAnchor = {
254
+ x: initialBaseAnchor.x + outwardDirection.x * outwardDistance,
255
+ y: initialBaseAnchor.y + outwardDirection.y * outwardDistance,
256
+ }
257
+ const candidate = this.findValidCandidateInShiftColumn({
258
+ label,
259
+ labelIndex,
260
+ orientation,
261
+ direction,
262
+ baseAnchor,
263
+ maxSearchDistance,
264
+ outwardDistance,
265
+ })
266
+
267
+ if (candidate) return candidate
268
+ }
269
+
270
+ return null
271
+ }
272
+
273
+ private findValidCandidateInShiftColumn(params: {
274
+ label: NetLabelPlacement
275
+ labelIndex: number
276
+ orientation: FacingDirection
277
+ direction: Point
278
+ baseAnchor: Point
279
+ maxSearchDistance: number
280
+ outwardDistance: number
281
+ }) {
282
+ const {
283
+ label,
284
+ labelIndex,
285
+ orientation,
286
+ direction,
287
+ baseAnchor,
288
+ maxSearchDistance,
289
+ outwardDistance,
290
+ } = params
291
+
292
+ for (
293
+ let distance = LABEL_SEARCH_STEP;
294
+ distance <= maxSearchDistance + EPS;
295
+ distance += LABEL_SEARCH_STEP
296
+ ) {
297
+ const anchorPoint = {
298
+ x: baseAnchor.x + direction.x * distance,
299
+ y: baseAnchor.y + direction.y * distance,
300
+ }
301
+ const candidate = this.createCandidate(label, anchorPoint, orientation)
302
+ const result = this.evaluateCandidate(
303
+ candidate,
304
+ label,
305
+ labelIndex,
306
+ "shift",
307
+ distance,
308
+ outwardDistance,
309
+ )
310
+ this.currentCandidateResults.push(result)
311
+
312
+ if (result.status === "valid") {
313
+ result.selected = true
314
+ return result
315
+ }
316
+ if (result.status === "trace-collision") break
317
+ }
318
+
319
+ return null
320
+ }
321
+
322
+ private evaluateCandidate(
323
+ candidate: CandidateLabel,
324
+ label: NetLabelPlacement,
325
+ labelIndex: number,
326
+ phase: CandidatePhase,
327
+ distance?: number,
328
+ outwardDistance?: number,
329
+ ): EvaluatedCandidate {
330
+ return {
331
+ ...candidate,
332
+ phase,
333
+ distance,
334
+ outwardDistance,
335
+ selected: false,
336
+ status: this.getCandidateStatus(candidate, label, labelIndex),
337
+ }
338
+ }
339
+
340
+ private getSearchStartAnchor(
341
+ label: NetLabelPlacement,
342
+ orientation: FacingDirection,
343
+ ) {
344
+ const anchorPoint = this.getWickOffsetAnchor(label.anchorPoint, orientation)
345
+ const { width, height } = getDimsForOrientation({
346
+ orientation,
347
+ netLabelWidth: this.getNetLabelWidth(label),
348
+ })
349
+
350
+ return this.getSideOffsetAnchor({
351
+ anchorPoint,
352
+ labelAnchorPoint: label.anchorPoint,
353
+ orientation,
354
+ width,
355
+ height,
356
+ })
357
+ }
358
+
359
+ private getWickOffsetAnchor(
360
+ anchorPoint: Point,
361
+ orientation: FacingDirection,
362
+ ) {
363
+ const direction = dir(orientation)
364
+ return {
365
+ x: anchorPoint.x + direction.x * WICK_CLEARANCE,
366
+ y: anchorPoint.y + direction.y * WICK_CLEARANCE,
367
+ }
368
+ }
369
+
370
+ private getSideOffsetAnchor(params: {
371
+ anchorPoint: Point
372
+ labelAnchorPoint: Point
373
+ orientation: FacingDirection
374
+ width: number
375
+ height: number
376
+ }) {
377
+ const { anchorPoint, labelAnchorPoint, orientation, width, height } = params
378
+ const chipSide = this.getChipSideForPoint(labelAnchorPoint)
379
+ if (isYOrientation(orientation) && chipSide === "left") {
380
+ return {
381
+ x: anchorPoint.x - width / 2 - WICK_CLEARANCE,
382
+ y: anchorPoint.y,
383
+ }
384
+ }
385
+ if (isYOrientation(orientation) && chipSide === "right") {
386
+ return {
387
+ x: anchorPoint.x + width / 2 + WICK_CLEARANCE,
388
+ y: anchorPoint.y,
389
+ }
390
+ }
391
+ if (isXOrientation(orientation) && chipSide === "bottom") {
392
+ return {
393
+ x: anchorPoint.x,
394
+ y: anchorPoint.y - height / 2 - WICK_CLEARANCE,
395
+ }
396
+ }
397
+ if (isXOrientation(orientation) && chipSide === "top") {
398
+ return {
399
+ x: anchorPoint.x,
400
+ y: anchorPoint.y + height / 2 + WICK_CLEARANCE,
401
+ }
402
+ }
403
+ return anchorPoint
404
+ }
405
+
406
+ private getSearchDistanceLimit(
407
+ label: NetLabelPlacement,
408
+ orientation: FacingDirection,
409
+ ) {
410
+ const { width, height } = getDimsForOrientation({
411
+ orientation,
412
+ netLabelWidth: this.getNetLabelWidth(label),
413
+ })
414
+ const labelLength =
415
+ orientation === "y+" || orientation === "y-" ? height : width
416
+ return Math.min(this.maxSearchDistance, labelLength * 2)
417
+ }
418
+
419
+ private createCandidate(
420
+ label: NetLabelPlacement,
421
+ anchorPoint: Point,
422
+ orientation: FacingDirection,
423
+ ): CandidateLabel {
424
+ const { width, height } = getDimsForOrientation({
425
+ orientation,
426
+ netLabelWidth: this.getNetLabelWidth(label),
427
+ })
428
+ return {
429
+ orientation,
430
+ anchorPoint,
431
+ width,
432
+ height,
433
+ center: getCenterFromAnchor(anchorPoint, orientation, width, height),
434
+ }
435
+ }
436
+
437
+ private getNetLabelWidth(label: NetLabelPlacement) {
438
+ if (!label.netId) return undefined
439
+ return this.inputProblem.netConnections.find(
440
+ (connection) => connection.netId === label.netId,
441
+ )?.netLabelWidth
442
+ }
443
+
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
+ private getCandidateStatus(
466
+ candidate: CandidateLabel,
467
+ label: NetLabelPlacement,
468
+ labelIndex: number,
469
+ ) {
470
+ const boundsStatus = this.getBoundsStatus(
471
+ getRectBounds(candidate.center, candidate.width, candidate.height),
472
+ labelIndex,
473
+ )
474
+ if (boundsStatus !== "valid") return boundsStatus
475
+
476
+ if (
477
+ tracePathCrossesAnyTrace(
478
+ getConnectorTracePath(
479
+ label.anchorPoint,
480
+ candidate.anchorPoint,
481
+ candidate.orientation,
482
+ ),
483
+ this.traceMap,
484
+ )
485
+ ) {
486
+ return "trace-collision"
487
+ }
488
+
489
+ return "valid"
490
+ }
491
+
492
+ private getBoundsStatus(bounds: Bounds, labelIndex: number): CandidateStatus {
493
+ if (this.chipObstacleSpatialIndex.getChipsInBounds(bounds).length > 0) {
494
+ return "chip-collision"
495
+ }
496
+ if (this.sharesChipBoundary(bounds)) {
497
+ return "chip-collision"
498
+ }
499
+ if (traceCrossesBoundsInterior(bounds, this.traceMap)) {
500
+ return "trace-collision"
501
+ }
502
+ if (this.intersectsAnyOtherNetLabel(bounds, labelIndex)) {
503
+ return "netlabel-collision"
504
+ }
505
+ return "valid"
506
+ }
507
+
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
+ private intersectsAnyOtherNetLabel(bounds: Bounds, labelIndex: number) {
519
+ for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
520
+ if (i === labelIndex) continue
521
+ const label = this.outputNetLabelPlacements[i]!
522
+ const otherBounds = getRectBounds(label.center, label.width, label.height)
523
+ if (rectsOverlap(bounds, otherBounds)) return true
524
+ }
525
+ return false
526
+ }
527
+
528
+ private sharesChipBoundary(bounds: Bounds) {
529
+ for (const chip of this.chipObstacleSpatialIndex.chips) {
530
+ const chipBounds = chip.bounds
531
+ const adjacentToVerticalSide =
532
+ Math.abs(bounds.minX - chipBounds.maxX) <= WICK_CLEARANCE + EPS ||
533
+ Math.abs(bounds.maxX - chipBounds.minX) <= WICK_CLEARANCE + EPS
534
+ const adjacentToHorizontalSide =
535
+ Math.abs(bounds.minY - chipBounds.maxY) <= WICK_CLEARANCE + EPS ||
536
+ Math.abs(bounds.maxY - chipBounds.minY) <= WICK_CLEARANCE + EPS
537
+
538
+ if (
539
+ adjacentToVerticalSide &&
540
+ rangesOverlap(
541
+ bounds.minY,
542
+ bounds.maxY,
543
+ chipBounds.minY,
544
+ chipBounds.maxY,
545
+ )
546
+ ) {
547
+ return true
548
+ }
549
+ if (
550
+ adjacentToHorizontalSide &&
551
+ rangesOverlap(
552
+ bounds.minX,
553
+ bounds.maxX,
554
+ chipBounds.minX,
555
+ chipBounds.maxX,
556
+ )
557
+ ) {
558
+ return true
559
+ }
560
+ }
561
+
562
+ return false
563
+ }
564
+
565
+ private getChipSideForPoint(point: Point) {
566
+ const containingSide = this.getContainingChipSide(point)
567
+ if (containingSide) return containingSide
568
+
569
+ const side = this.getOutsideChipSide(point)
570
+ if (side) return side
571
+
572
+ return this.getNearestChipSide(point)
573
+ }
574
+
575
+ private getPerpendicularOutwardDirection(
576
+ point: Point,
577
+ orientation: FacingDirection,
578
+ ) {
579
+ const chipSide = this.getChipSideForPoint(point)
580
+ if (isYOrientation(orientation) && chipSide === "left") {
581
+ return { x: -1, y: 0 }
582
+ }
583
+ if (isYOrientation(orientation) && chipSide === "right") {
584
+ return { x: 1, y: 0 }
585
+ }
586
+ if (isXOrientation(orientation) && chipSide === "bottom") {
587
+ return { x: 0, y: -1 }
588
+ }
589
+ if (isXOrientation(orientation) && chipSide === "top") {
590
+ return { x: 0, y: 1 }
591
+ }
592
+ return { x: 0, y: 0 }
593
+ }
594
+
595
+ private getContainingChipSide(point: Point) {
596
+ let nearestSide: ChipSide | null = null
597
+ let nearestDistance = Number.POSITIVE_INFINITY
598
+
599
+ for (const chip of this.chipObstacleSpatialIndex.chips) {
600
+ const bounds = chip.bounds
601
+ if (
602
+ point.x < bounds.minX - EPS ||
603
+ point.x > bounds.maxX + EPS ||
604
+ point.y < bounds.minY - EPS ||
605
+ point.y > bounds.maxY + EPS
606
+ ) {
607
+ continue
608
+ }
609
+
610
+ for (const [side, distance] of getSideDistances(point, bounds)) {
611
+ if (distance < nearestDistance) {
612
+ nearestSide = side
613
+ nearestDistance = distance
614
+ }
615
+ }
616
+ }
617
+
618
+ return nearestSide
619
+ }
620
+
621
+ private getOutsideChipSide(point: Point) {
622
+ let nearestSide: ChipSide | null = null
623
+ let nearestDistanceSq = Number.POSITIVE_INFINITY
624
+
625
+ for (const chip of this.chipObstacleSpatialIndex.chips) {
626
+ const bounds = chip.bounds
627
+ const dx =
628
+ point.x < bounds.minX - EPS
629
+ ? bounds.minX - point.x
630
+ : point.x > bounds.maxX + EPS
631
+ ? point.x - bounds.maxX
632
+ : 0
633
+ const dy =
634
+ point.y < bounds.minY - EPS
635
+ ? bounds.minY - point.y
636
+ : point.y > bounds.maxY + EPS
637
+ ? point.y - bounds.maxY
638
+ : 0
639
+
640
+ if (dx === 0 && dy === 0) continue
641
+
642
+ const distanceSq = dx ** 2 + dy ** 2
643
+ if (distanceSq >= nearestDistanceSq) continue
644
+
645
+ nearestDistanceSq = distanceSq
646
+ if (dx > 0) {
647
+ nearestSide = point.x < bounds.minX ? "left" : "right"
648
+ } else {
649
+ nearestSide = point.y < bounds.minY ? "bottom" : "top"
650
+ }
651
+ }
652
+
653
+ return nearestSide
654
+ }
655
+
656
+ private getNearestChipSide(point: Point) {
657
+ let nearestSide: ChipSide | null = null
658
+ let nearestDistance = Number.POSITIVE_INFINITY
659
+
660
+ for (const chip of this.chipObstacleSpatialIndex.chips) {
661
+ for (const [side, distance] of getSideDistances(point, chip.bounds)) {
662
+ if (distance < nearestDistance) {
663
+ nearestSide = side
664
+ nearestDistance = distance
665
+ }
666
+ }
667
+ }
668
+
669
+ return nearestSide
670
+ }
671
+
672
+ override visualize(): GraphicsObject {
673
+ return visualizeAvailableNetOrientationSolver({
674
+ inputProblem: this.inputProblem,
675
+ traces: this.traces,
676
+ outputNetLabelPlacements: this.outputNetLabelPlacements,
677
+ currentLabel: this.currentLabel,
678
+ currentCandidateResults: this.currentCandidateResults,
679
+ solved: this.solved,
680
+ })
681
+ }
682
+ }
@@ -0,0 +1,6 @@
1
+ export const LABEL_SEARCH_STEP = 0.05
2
+ export const WICK_CLEARANCE = 0.001
3
+ export const EPS = 1e-9
4
+ export const TRACE_BOUNDARY_TOLERANCE = WICK_CLEARANCE + EPS
5
+ export const CANDIDATE_SELECTED_COLOR = "blue"
6
+ export const CANDIDATE_REJECTED_COLOR = "red"