@tscircuit/schematic-trace-solver 0.0.60 → 0.0.62

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 (29) hide show
  1. package/dist/index.d.ts +77 -6
  2. package/dist/index.js +445 -8
  3. package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +131 -11
  4. package/lib/solvers/AvailableNetOrientationSolver/geometry.ts +1 -1
  5. package/lib/solvers/AvailableNetOrientationSolver/types.ts +1 -1
  6. package/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts +441 -0
  7. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +15 -0
  8. package/package.json +1 -1
  9. package/site/examples/example37.page.tsx +4 -0
  10. package/site/examples/example38.page.tsx +4 -0
  11. package/site/examples/example39.page.tsx +4 -0
  12. package/site/examples/example40.page.tsx +4 -0
  13. package/site/examples/example41.page.tsx +4 -0
  14. package/tests/assets/example37.json +173 -0
  15. package/tests/assets/example38.json +32 -0
  16. package/tests/assets/example39.json +71 -0
  17. package/tests/assets/example40.json +157 -0
  18. package/tests/assets/example41.json +81 -0
  19. package/tests/examples/__snapshots__/example04.snap.svg +16 -17
  20. package/tests/examples/__snapshots__/example37.snap.svg +320 -0
  21. package/tests/examples/__snapshots__/example38.snap.svg +77 -0
  22. package/tests/examples/__snapshots__/example39.snap.svg +107 -0
  23. package/tests/examples/__snapshots__/example40.snap.svg +231 -0
  24. package/tests/examples/__snapshots__/example41.snap.svg +119 -0
  25. package/tests/examples/example37.test.ts +12 -0
  26. package/tests/examples/example38.test.ts +12 -0
  27. package/tests/examples/example39.test.ts +12 -0
  28. package/tests/examples/example40.test.ts +12 -0
  29. package/tests/examples/example41.test.ts +12 -0
@@ -20,6 +20,7 @@ import {
20
20
  isYOrientation,
21
21
  rangesOverlap,
22
22
  rectsOverlap,
23
+ simplifyOrthogonalPath,
23
24
  traceCrossesBoundsInterior,
24
25
  tracePathCrossesAnyBounds,
25
26
  tracePathCrossesAnyTrace,
@@ -127,7 +128,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
127
128
 
128
129
  private applyCandidate(
129
130
  label: NetLabelPlacement,
130
- candidate: CandidateLabel,
131
+ candidate: EvaluatedCandidate,
131
132
  labelIndex: number,
132
133
  ) {
133
134
  this.outputNetLabelPlacements[labelIndex] = {
@@ -139,14 +140,33 @@ export class AvailableNetOrientationSolver extends BaseSolver {
139
140
 
140
141
  private addConnectorTrace(
141
142
  label: NetLabelPlacement,
142
- candidate: CandidateLabel,
143
+ candidate: EvaluatedCandidate,
143
144
  labelIndex: number,
144
145
  ) {
145
- const tracePath = getConnectorTracePath(
146
- label.anchorPoint,
147
- candidate.anchorPoint,
148
- candidate.orientation,
149
- )
146
+ let tracePath: Point[]
147
+
148
+ if (candidate.phase === "lateral-shift") {
149
+ const orientDir = dir(candidate.orientation)
150
+ const kickedSource = {
151
+ x: label.anchorPoint.x - orientDir.x * LABEL_SEARCH_STEP,
152
+ y: label.anchorPoint.y - orientDir.y * LABEL_SEARCH_STEP,
153
+ }
154
+ tracePath = simplifyOrthogonalPath([
155
+ label.anchorPoint,
156
+ ...getConnectorTracePath(
157
+ kickedSource,
158
+ candidate.anchorPoint,
159
+ candidate.orientation,
160
+ ),
161
+ ])
162
+ } else {
163
+ tracePath = getConnectorTracePath(
164
+ label.anchorPoint,
165
+ candidate.anchorPoint,
166
+ candidate.orientation,
167
+ )
168
+ }
169
+
150
170
  if (tracePath.length < 2) return
151
171
 
152
172
  const mspPairId = `available-net-orientation-${labelIndex}-${label.netId ?? label.globalConnNetId}`
@@ -185,10 +205,19 @@ export class AvailableNetOrientationSolver extends BaseSolver {
185
205
  labelIndex,
186
206
  orientations,
187
207
  )
208
+ if (rotatedCandidate) return rotatedCandidate
209
+
210
+ const shiftedCandidate = this.findValidShiftedCandidate(
211
+ label,
212
+ orientations[0]!,
213
+ labelIndex,
214
+ )
215
+ if (shiftedCandidate) return shiftedCandidate
188
216
 
189
- return (
190
- rotatedCandidate ??
191
- this.findValidShiftedCandidate(label, orientations[0]!, labelIndex)
217
+ return this.findValidLateralShiftedCandidate(
218
+ label,
219
+ orientations[0]!,
220
+ labelIndex,
192
221
  )
193
222
  }
194
223
 
@@ -266,6 +295,63 @@ export class AvailableNetOrientationSolver extends BaseSolver {
266
295
  return null
267
296
  }
268
297
 
298
+ /**
299
+ * When all candidates fail for the current (unshifted) position, try
300
+ * shifting the label anchor laterally — x for y-orientations, y for
301
+ * x-orientations — and re-attempting the required orientation.
302
+ *
303
+ * Offsets are tried in alternating sign order:
304
+ * -1·step, +1·step, -2·step, +2·step, …
305
+ * so the nearest escape routes are tested first.
306
+ */
307
+ private findValidLateralShiftedCandidate(
308
+ label: NetLabelPlacement,
309
+ orientation: FacingDirection,
310
+ labelIndex: number,
311
+ ): EvaluatedCandidate | null {
312
+ const direction = dir(orientation)
313
+ const initialBaseAnchor = this.getSearchStartAnchor(label, orientation)
314
+
315
+ // Lateral axis: perpendicular to the orientation direction
316
+ const lateralDir: Point = {
317
+ x: isYOrientation(orientation) ? 1 : 0,
318
+ y: isXOrientation(orientation) ? 1 : 0,
319
+ }
320
+
321
+ const maxSteps = Math.ceil(this.maxSearchDistance / LABEL_SEARCH_STEP)
322
+
323
+ for (let step = 1; step <= maxSteps; step++) {
324
+ for (const sign of [-1, 1]) {
325
+ const lateralOffset = sign * step * LABEL_SEARCH_STEP
326
+ const baseAnchor = {
327
+ x: initialBaseAnchor.x + lateralDir.x * lateralOffset,
328
+ y: initialBaseAnchor.y + lateralDir.y * lateralOffset,
329
+ }
330
+
331
+ const maxSearchDistance = this.getLateralColumnMaxDistance(
332
+ label,
333
+ orientation,
334
+ baseAnchor,
335
+ )
336
+
337
+ const candidate = this.findValidCandidateInShiftColumn({
338
+ label,
339
+ labelIndex,
340
+ orientation,
341
+ direction,
342
+ baseAnchor,
343
+ maxSearchDistance,
344
+ outwardDistance: lateralOffset,
345
+ phase: "lateral-shift",
346
+ })
347
+
348
+ if (candidate) return candidate
349
+ }
350
+ }
351
+
352
+ return null
353
+ }
354
+
269
355
  private findValidCandidateInShiftColumn(params: {
270
356
  label: NetLabelPlacement
271
357
  labelIndex: number
@@ -274,6 +360,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
274
360
  baseAnchor: Point
275
361
  maxSearchDistance: number
276
362
  outwardDistance: number
363
+ phase?: CandidatePhase
277
364
  }) {
278
365
  const {
279
366
  label,
@@ -283,6 +370,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
283
370
  baseAnchor,
284
371
  maxSearchDistance,
285
372
  outwardDistance,
373
+ phase = "shift",
286
374
  } = params
287
375
 
288
376
  for (
@@ -299,7 +387,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
299
387
  candidate,
300
388
  label,
301
389
  labelIndex,
302
- "shift",
390
+ phase,
303
391
  distance,
304
392
  outwardDistance,
305
393
  )
@@ -412,6 +500,32 @@ export class AvailableNetOrientationSolver extends BaseSolver {
412
500
  return Math.min(this.maxSearchDistance, labelLength * 2)
413
501
  }
414
502
 
503
+ private getLateralColumnMaxDistance(
504
+ label: NetLabelPlacement,
505
+ orientation: FacingDirection,
506
+ baseAnchor: Point,
507
+ ) {
508
+ const chipId = label.pinIds
509
+ .map((pid) => this.pinMap[pid]?.chipId)
510
+ .find(Boolean)
511
+ const chip = chipId
512
+ ? this.chipObstacleSpatialIndex.chips.find((c) => c.chipId === chipId)
513
+ : null
514
+
515
+ if (chip) {
516
+ if (orientation === "y-")
517
+ return Math.max(0, baseAnchor.y - chip.bounds.minY)
518
+ if (orientation === "y+")
519
+ return Math.max(0, chip.bounds.maxY - baseAnchor.y)
520
+ if (orientation === "x-")
521
+ return Math.max(0, baseAnchor.x - chip.bounds.minX)
522
+ if (orientation === "x+")
523
+ return Math.max(0, chip.bounds.maxX - baseAnchor.x)
524
+ }
525
+
526
+ return this.getSearchDistanceLimit(label, orientation)
527
+ }
528
+
415
529
  private createCandidate(
416
530
  label: NetLabelPlacement,
417
531
  anchorPoint: Point,
@@ -474,6 +588,12 @@ export class AvailableNetOrientationSolver extends BaseSolver {
474
588
  return "trace-collision"
475
589
  }
476
590
 
591
+ for (const chip of this.chipObstacleSpatialIndex.chips) {
592
+ if (tracePathCrossesAnyBounds(connectorTrace, chip.bounds)) {
593
+ return "chip-collision"
594
+ }
595
+ }
596
+
477
597
  for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
478
598
  if (i === labelIndex) continue
479
599
  const otherLabel = this.outputNetLabelPlacements[i]!
@@ -168,7 +168,7 @@ export const getConnectorTracePath = (
168
168
  : [source, { x: source.x, y: target.y }, target],
169
169
  )
170
170
 
171
- const simplifyOrthogonalPath = (path: Point[]) => {
171
+ export const simplifyOrthogonalPath = (path: Point[]) => {
172
172
  const deduped = path.filter(
173
173
  (point, index) => index === 0 || !pointsEqual(point, path[index - 1]!),
174
174
  )
@@ -33,7 +33,7 @@ export type CandidateStatus =
33
33
  | "trace-collision"
34
34
  | "netlabel-collision"
35
35
 
36
- export type CandidatePhase = "rotate" | "shift"
36
+ export type CandidatePhase = "rotate" | "shift" | "lateral-shift"
37
37
 
38
38
  export type EvaluatedCandidate = CandidateLabel & {
39
39
  status: CandidateStatus
@@ -0,0 +1,441 @@
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 type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
5
+ import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
6
+ import type { InputProblem } from "lib/types/InputProblem"
7
+ import type { FacingDirection } from "lib/utils/dir"
8
+ import {
9
+ getDimsForOrientation,
10
+ getCenterFromAnchor,
11
+ getRectBounds,
12
+ } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
13
+ import { rectIntersectsAnyTrace } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions"
14
+ import { ChipObstacleSpatialIndex } from "lib/data-structures/ChipObstacleSpatialIndex"
15
+ import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
16
+ import { getColorFromString } from "lib/utils/getColorFromString"
17
+
18
+ type CandidateStatus =
19
+ | "ok"
20
+ | "chip-collision"
21
+ | "trace-collision"
22
+ | "label-collision"
23
+
24
+ const ANCHOR_TRACE_CLEARANCE = 1e-4
25
+ const SEGMENT_PARALLEL_EPS = 1e-6
26
+ const CANDIDATE_STEP = 0.1
27
+
28
+ const OUTWARD_DIR: Record<FacingDirection, { x: number; y: number }> = {
29
+ "x+": { x: 1, y: 0 },
30
+ "x-": { x: -1, y: 0 },
31
+ "y+": { x: 0, y: 1 },
32
+ "y-": { x: 0, y: -1 },
33
+ }
34
+
35
+ const CANDIDATE_STATUS_COLOR: Record<CandidateStatus, string> = {
36
+ ok: "green",
37
+ "label-collision": "orange",
38
+ "trace-collision": "darkorange",
39
+ "chip-collision": "red",
40
+ }
41
+
42
+ const CANDIDATE_STATUS_FILL: Record<CandidateStatus, string> = {
43
+ ok: "rgba(0, 200, 0, 0.25)",
44
+ "label-collision": "rgba(255, 160, 0, 0.2)",
45
+ "trace-collision": "rgba(200, 80, 0, 0.2)",
46
+ "chip-collision": "rgba(220, 0, 0, 0.15)",
47
+ }
48
+
49
+ type Candidate = {
50
+ orientation: FacingDirection
51
+ anchor: { x: number; y: number }
52
+ center: { x: number; y: number }
53
+ width: number
54
+ height: number
55
+ bounds: { minX: number; minY: number; maxX: number; maxY: number }
56
+ hostPairId?: MspConnectionPairId
57
+ hostSegIndex?: number
58
+ status: CandidateStatus | null
59
+ }
60
+
61
+ function boundsOverlap(
62
+ a: { minX: number; minY: number; maxX: number; maxY: number },
63
+ b: { minX: number; minY: number; maxX: number; maxY: number },
64
+ ): boolean {
65
+ return (
66
+ a.minX < b.maxX - 1e-9 &&
67
+ a.maxX > b.minX + 1e-9 &&
68
+ a.minY < b.maxY - 1e-9 &&
69
+ a.maxY > b.minY + 1e-9
70
+ )
71
+ }
72
+
73
+ function sampleAnchorsAlongSegment(
74
+ a: { x: number; y: number },
75
+ b: { x: number; y: number },
76
+ ): Array<{ x: number; y: number }> {
77
+ const dx = b.x - a.x
78
+ const dy = b.y - a.y
79
+ const len = Math.sqrt(dx * dx + dy * dy)
80
+ const steps = Math.max(1, Math.round(len / CANDIDATE_STEP))
81
+ const anchors: Array<{ x: number; y: number }> = []
82
+ for (let k = 0; k <= steps; k++) {
83
+ const t = k / steps
84
+ anchors.push({ x: a.x + t * dx, y: a.y + t * dy })
85
+ }
86
+ return anchors
87
+ }
88
+
89
+ export interface NetLabelNetLabelCollisionSolverParams {
90
+ inputProblem: InputProblem
91
+ traces: SolvedTracePath[]
92
+ netLabelPlacements: NetLabelPlacement[]
93
+ }
94
+
95
+ export class NetLabelNetLabelCollisionSolver extends BaseSolver {
96
+ inputProblem: InputProblem
97
+ traces: SolvedTracePath[]
98
+ netLabelPlacements: NetLabelPlacement[]
99
+
100
+ outputNetLabelPlacements: NetLabelPlacement[]
101
+
102
+ currentCollision: [NetLabelPlacement, NetLabelPlacement] | null = null
103
+ currentLabelToMove: NetLabelPlacement | null = null
104
+ candidateResults: Candidate[] = []
105
+
106
+ private chipIndex: ChipObstacleSpatialIndex
107
+ private traceMap: Record<MspConnectionPairId, SolvedTracePath>
108
+ private skippedCollisionKeys = new Set<string>()
109
+
110
+ private labelsToTry: NetLabelPlacement[] = []
111
+ private candidateQueue: Candidate[] = []
112
+ private candidateIndex = 0
113
+
114
+ constructor(params: NetLabelNetLabelCollisionSolverParams) {
115
+ super()
116
+ this.inputProblem = params.inputProblem
117
+ this.traces = params.traces
118
+ this.netLabelPlacements = params.netLabelPlacements
119
+ this.outputNetLabelPlacements = [...params.netLabelPlacements]
120
+ this.chipIndex =
121
+ params.inputProblem._chipObstacleSpatialIndex ??
122
+ new ChipObstacleSpatialIndex(params.inputProblem.chips)
123
+ this.traceMap = Object.fromEntries(
124
+ params.traces.map((t) => [t.mspPairId, t]),
125
+ ) as Record<MspConnectionPairId, SolvedTracePath>
126
+ }
127
+
128
+ override getConstructorParams(): ConstructorParameters<
129
+ typeof NetLabelNetLabelCollisionSolver
130
+ >[0] {
131
+ return {
132
+ inputProblem: this.inputProblem,
133
+ traces: this.traces,
134
+ netLabelPlacements: this.netLabelPlacements,
135
+ }
136
+ }
137
+
138
+ getOutput() {
139
+ return { netLabelPlacements: this.outputNetLabelPlacements }
140
+ }
141
+
142
+ private labelBounds(label: NetLabelPlacement) {
143
+ return getRectBounds(label.center, label.width, label.height)
144
+ }
145
+
146
+ private collisionKey(a: NetLabelPlacement, b: NetLabelPlacement) {
147
+ return [a.globalConnNetId, b.globalConnNetId].sort().join("::")
148
+ }
149
+
150
+ private findNextCollidingPair():
151
+ | [NetLabelPlacement, NetLabelPlacement]
152
+ | null {
153
+ const labels = this.outputNetLabelPlacements
154
+ for (let i = 0; i < labels.length; i++) {
155
+ for (let j = i + 1; j < labels.length; j++) {
156
+ const a = labels[i]!
157
+ const b = labels[j]!
158
+ if (a.globalConnNetId === b.globalConnNetId) continue
159
+ if (this.skippedCollisionKeys.has(this.collisionKey(a, b))) continue
160
+ if (boundsOverlap(this.labelBounds(a), this.labelBounds(b)))
161
+ return [a, b]
162
+ }
163
+ }
164
+ return null
165
+ }
166
+
167
+ private netLabelWidthOf(label: NetLabelPlacement): number | undefined {
168
+ if (label.orientation === "x+" || label.orientation === "x-")
169
+ return label.width
170
+ return label.height
171
+ }
172
+
173
+ private buildCandidatesForLabel(label: NetLabelPlacement): Candidate[] {
174
+ const netLabelWidth = this.netLabelWidthOf(label)
175
+ const candidates: Candidate[] = []
176
+
177
+ const buildCandidate = (
178
+ orientation: FacingDirection,
179
+ anchor: { x: number; y: number },
180
+ hostPairId?: MspConnectionPairId,
181
+ hostSegIndex?: number,
182
+ ): Candidate => {
183
+ const { width, height } = getDimsForOrientation({
184
+ orientation,
185
+ netLabelWidth,
186
+ })
187
+ const baseCenter = getCenterFromAnchor(anchor, orientation, width, height)
188
+ const outwardDir = OUTWARD_DIR[orientation]
189
+ const center = {
190
+ x: baseCenter.x + outwardDir.x * ANCHOR_TRACE_CLEARANCE,
191
+ y: baseCenter.y + outwardDir.y * ANCHOR_TRACE_CLEARANCE,
192
+ }
193
+ return {
194
+ orientation,
195
+ anchor,
196
+ center,
197
+ width,
198
+ height,
199
+ bounds: getRectBounds(center, width, height),
200
+ hostPairId,
201
+ hostSegIndex,
202
+ status: null,
203
+ }
204
+ }
205
+
206
+ const isPortOnly = label.mspConnectionPairIds.length === 0
207
+
208
+ if (isPortOnly) {
209
+ const allOrientations: FacingDirection[] = ["x+", "x-", "y+", "y-"]
210
+ const orderedOrientations = [
211
+ label.orientation,
212
+ ...allOrientations.filter((o) => o !== label.orientation),
213
+ ]
214
+ for (const orientation of orderedOrientations) {
215
+ candidates.push(buildCandidate(orientation, label.anchorPoint))
216
+ }
217
+ } else {
218
+ for (const mspPairId of label.mspConnectionPairIds) {
219
+ const trace = this.traceMap[mspPairId]
220
+ if (!trace) continue
221
+ const pts = trace.tracePath
222
+ for (let si = 0; si < pts.length - 1; si++) {
223
+ const segStart = pts[si]!
224
+ const segEnd = pts[si + 1]!
225
+ const isHorizontal =
226
+ Math.abs(segStart.y - segEnd.y) < SEGMENT_PARALLEL_EPS
227
+ const isVertical =
228
+ Math.abs(segStart.x - segEnd.x) < SEGMENT_PARALLEL_EPS
229
+ if (!isHorizontal && !isVertical) continue
230
+ let perpendicularOrientations: FacingDirection[]
231
+ if (isHorizontal) {
232
+ perpendicularOrientations = ["y+", "y-"]
233
+ } else {
234
+ perpendicularOrientations = ["x+", "x-"]
235
+ }
236
+ for (const anchor of sampleAnchorsAlongSegment(segStart, segEnd)) {
237
+ for (const orientation of perpendicularOrientations) {
238
+ candidates.push(
239
+ buildCandidate(
240
+ orientation,
241
+ anchor,
242
+ mspPairId as MspConnectionPairId,
243
+ si,
244
+ ),
245
+ )
246
+ }
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ return candidates
253
+ }
254
+
255
+ private checkCandidate(
256
+ candidate: Candidate,
257
+ movingLabelNetId: string,
258
+ obstacleLabels: NetLabelPlacement[],
259
+ ): CandidateStatus {
260
+ const { bounds, hostPairId, hostSegIndex } = candidate
261
+
262
+ if (this.chipIndex.getChipsInBounds(bounds).length > 0)
263
+ return "chip-collision"
264
+
265
+ if (
266
+ rectIntersectsAnyTrace(bounds, this.traceMap, hostPairId, hostSegIndex)
267
+ .hasIntersection
268
+ ) {
269
+ return "trace-collision"
270
+ }
271
+
272
+ for (const obstacle of obstacleLabels) {
273
+ if (obstacle.globalConnNetId === movingLabelNetId) continue
274
+ if (boundsOverlap(bounds, this.labelBounds(obstacle)))
275
+ return "label-collision"
276
+ }
277
+
278
+ return "ok"
279
+ }
280
+
281
+ private beginSearchForLabel(label: NetLabelPlacement) {
282
+ this.currentLabelToMove = label
283
+ this.candidateQueue = this.buildCandidatesForLabel(label)
284
+ this.candidateIndex = 0
285
+ this.candidateResults = []
286
+ }
287
+
288
+ private clearActiveSearch() {
289
+ this.currentCollision = null
290
+ this.currentLabelToMove = null
291
+ this.labelsToTry = []
292
+ this.candidateQueue = []
293
+ this.candidateIndex = 0
294
+ this.candidateResults = []
295
+ }
296
+
297
+ override _step() {
298
+ if (!this.currentCollision) {
299
+ const pair = this.findNextCollidingPair()
300
+ if (!pair) {
301
+ this.solved = true
302
+ return
303
+ }
304
+ this.currentCollision = pair
305
+ this.labelsToTry = [pair[1], pair[0]]
306
+ this.beginSearchForLabel(this.labelsToTry.shift()!)
307
+ return
308
+ }
309
+
310
+ if (this.candidateIndex >= this.candidateQueue.length) {
311
+ if (this.labelsToTry.length > 0) {
312
+ this.beginSearchForLabel(this.labelsToTry.shift()!)
313
+ } else {
314
+ this.skippedCollisionKeys.add(
315
+ this.collisionKey(this.currentCollision[0], this.currentCollision[1]),
316
+ )
317
+ this.clearActiveSearch()
318
+ }
319
+ return
320
+ }
321
+
322
+ const candidate = this.candidateQueue[this.candidateIndex++]!
323
+ const [labelA, labelB] = this.currentCollision
324
+ let fixedLabel: NetLabelPlacement
325
+ if (this.currentLabelToMove === labelB) {
326
+ fixedLabel = labelA
327
+ } else {
328
+ fixedLabel = labelB
329
+ }
330
+ const obstacleLabels = [
331
+ ...this.outputNetLabelPlacements.filter(
332
+ (l) => l !== labelA && l !== labelB,
333
+ ),
334
+ fixedLabel,
335
+ ]
336
+
337
+ const status = this.checkCandidate(
338
+ candidate,
339
+ this.currentLabelToMove!.globalConnNetId,
340
+ obstacleLabels,
341
+ )
342
+ candidate.status = status
343
+ this.candidateResults.push({ ...candidate })
344
+
345
+ if (status === "ok") {
346
+ const idx = this.outputNetLabelPlacements.indexOf(
347
+ this.currentLabelToMove!,
348
+ )
349
+ if (idx !== -1) {
350
+ this.outputNetLabelPlacements[idx] = {
351
+ ...this.currentLabelToMove!,
352
+ orientation: candidate.orientation,
353
+ anchorPoint: candidate.anchor,
354
+ width: candidate.width,
355
+ height: candidate.height,
356
+ center: candidate.center,
357
+ }
358
+ }
359
+ this.clearActiveSearch()
360
+ }
361
+ }
362
+
363
+ override visualize(): GraphicsObject {
364
+ const graphics = visualizeInputProblem(this.inputProblem)
365
+ if (!graphics.lines) graphics.lines = []
366
+ if (!graphics.rects) graphics.rects = []
367
+ if (!graphics.points) graphics.points = []
368
+
369
+ for (const trace of this.traces) {
370
+ graphics.lines.push({
371
+ points: trace.tracePath,
372
+ strokeColor: "purple",
373
+ } as any)
374
+ }
375
+
376
+ for (const label of this.outputNetLabelPlacements) {
377
+ const isInActiveCollision =
378
+ this.currentCollision != null &&
379
+ (label === this.currentCollision[0] ||
380
+ label === this.currentCollision[1])
381
+
382
+ let labelFill: string
383
+ let labelStroke: string
384
+ let labelText: string
385
+ let pointColor: string
386
+ if (isInActiveCollision) {
387
+ labelFill = "rgba(255, 0, 0, 0.2)"
388
+ labelStroke = "red"
389
+ labelText = `netId: ${label.netId}\nglobalConnNetId: ${label.globalConnNetId}\n⚠ COLLIDING`
390
+ pointColor = "red"
391
+ } else {
392
+ labelFill = getColorFromString(label.globalConnNetId, 0.35)
393
+ labelStroke = getColorFromString(label.globalConnNetId, 0.9)
394
+ labelText = `netId: ${label.netId}\nglobalConnNetId: ${label.globalConnNetId}`
395
+ pointColor = getColorFromString(label.globalConnNetId, 0.9)
396
+ }
397
+
398
+ graphics.rects.push({
399
+ center: label.center,
400
+ width: label.width,
401
+ height: label.height,
402
+ fill: labelFill,
403
+ strokeColor: labelStroke,
404
+ label: labelText,
405
+ } as any)
406
+ graphics.points.push({
407
+ x: label.anchorPoint.x,
408
+ y: label.anchorPoint.y,
409
+ color: pointColor,
410
+ label: `anchorPoint\norientation: ${label.orientation}`,
411
+ } as any)
412
+ }
413
+
414
+ const movingNetId = this.currentLabelToMove
415
+ ? this.currentLabelToMove.netId
416
+ : "?"
417
+ for (const c of this.candidateResults) {
418
+ const statusColor = CANDIDATE_STATUS_COLOR[c.status!]
419
+ const statusFill = CANDIDATE_STATUS_FILL[c.status!]
420
+ let strokeDash: string | undefined
421
+ if (c.status !== "ok") strokeDash = "4 2"
422
+ graphics.rects.push({
423
+ center: c.center,
424
+ width: c.width,
425
+ height: c.height,
426
+ fill: statusFill,
427
+ strokeColor: statusColor,
428
+ strokeDash,
429
+ label: `candidate: ${c.status}\norientation: ${c.orientation}\nmoving: ${movingNetId}`,
430
+ } as any)
431
+ graphics.points.push({
432
+ x: c.anchor.x,
433
+ y: c.anchor.y,
434
+ color: statusColor,
435
+ label: `candidate anchor\n${c.status}`,
436
+ } as any)
437
+ }
438
+
439
+ return graphics
440
+ }
441
+ }
@@ -26,6 +26,7 @@ import { AvailableNetOrientationSolver } from "../AvailableNetOrientationSolver/
26
26
  import { VccNetLabelCornerPlacementSolver } from "../VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver"
27
27
  import { TraceAnchoredNetLabelOverlapSolver } from "../TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver"
28
28
  import { NetLabelTraceCollisionSolver } from "../NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver"
29
+ import { NetLabelNetLabelCollisionSolver } from "../NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver"
29
30
 
30
31
  type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
31
32
  solverName: string
@@ -80,6 +81,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
80
81
  vccNetLabelCornerPlacementSolver?: VccNetLabelCornerPlacementSolver
81
82
  traceAnchoredNetLabelOverlapSolver?: TraceAnchoredNetLabelOverlapSolver
82
83
  netLabelTraceCollisionSolver?: NetLabelTraceCollisionSolver
84
+ netLabelNetLabelCollisionSolver?: NetLabelNetLabelCollisionSolver
83
85
 
84
86
  startTimeOfPhase: Record<string, number>
85
87
  endTimeOfPhase: Record<string, number>
@@ -300,6 +302,19 @@ export class SchematicTracePipelineSolver extends BaseSolver {
300
302
  },
301
303
  ],
302
304
  ),
305
+ definePipelineStep(
306
+ "netLabelNetLabelCollisionSolver",
307
+ NetLabelNetLabelCollisionSolver,
308
+ (instance) => [
309
+ {
310
+ inputProblem: instance.inputProblem,
311
+ traces: instance.netLabelTraceCollisionSolver!.getOutput().traces,
312
+ netLabelPlacements:
313
+ instance.netLabelTraceCollisionSolver!.getOutput()
314
+ .netLabelPlacements,
315
+ },
316
+ ],
317
+ ),
303
318
  ]
304
319
 
305
320
  constructor(inputProblem: InputProblem) {