@tscircuit/schematic-trace-solver 0.0.7 → 0.0.9

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 (19) hide show
  1. package/dist/index.d.ts +5 -5
  2. package/dist/index.js +488 -344
  3. package/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts +29 -0
  4. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver.ts +199 -465
  5. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver_visualize.ts +89 -0
  6. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/anchors.ts +11 -0
  7. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions.ts +45 -0
  8. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry.ts +48 -0
  9. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/host.ts +64 -0
  10. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/solvePortOnlyPin.ts +177 -0
  11. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver.ts +24 -3
  12. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts +38 -3
  13. package/package.json +1 -1
  14. package/site/SchematicTraceLinesSolver/SchematicTraceLinesSolver01.page.tsx +122 -0
  15. package/site/components/GenericSolverDebugger.tsx +16 -0
  16. package/site/examples/example01-basic.page.tsx +2 -2
  17. package/site/examples/example04-single-symbol.page.tsx +46 -0
  18. package/tests/solvers/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver_repro01.json +88 -0
  19. package/tests/solvers/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver_repro01.test.ts +16 -0
@@ -3,17 +3,27 @@ import type {
3
3
  NetLabelPlacement,
4
4
  OverlappingSameNetTraceGroup,
5
5
  } from "../NetLabelPlacementSolver"
6
- import type { InputChip, InputProblem } from "lib/types/InputProblem"
6
+ import type { InputProblem } from "lib/types/InputProblem"
7
7
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
8
8
  import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
9
9
  import type { FacingDirection } from "lib/utils/dir"
10
10
  import type { GraphicsObject } from "graphics-debug"
11
11
  import { ChipObstacleSpatialIndex } from "lib/data-structures/ChipObstacleSpatialIndex"
12
- import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
13
- import { getColorFromString } from "lib/utils/getColorFromString"
14
-
15
- export const NET_LABEL_HORIZONTAL_WIDTH = 0.4
16
- export const NET_LABEL_HORIZONTAL_HEIGHT = 0.2
12
+ import {
13
+ getDimsForOrientation,
14
+ getCenterFromAnchor,
15
+ getRectBounds,
16
+ } from "./geometry"
17
+ import { rectIntersectsAnyTrace } from "./collisions"
18
+ import { chooseHostTraceForGroup } from "./host"
19
+ import { anchorsForSegment } from "./anchors"
20
+ import { solveNetLabelPlacementForPortOnlyPin } from "./solvePortOnlyPin"
21
+ import { visualizeSingleNetLabelPlacementSolver } from "./SingleNetLabelPlacementSolver_visualize"
22
+
23
+ export {
24
+ NET_LABEL_HORIZONTAL_WIDTH,
25
+ NET_LABEL_HORIZONTAL_HEIGHT,
26
+ } from "./geometry"
17
27
  // NOTE: net labels, when in the y+/y- orientation, are rotated and therefore
18
28
  // the width/height are swapped
19
29
 
@@ -77,95 +87,6 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
77
87
  new ChipObstacleSpatialIndex(params.inputProblem.chips)
78
88
  }
79
89
 
80
- private getDimsForOrientation(orientation: FacingDirection) {
81
- if (orientation === "y+" || orientation === "y-") {
82
- return {
83
- width: NET_LABEL_HORIZONTAL_HEIGHT,
84
- height: NET_LABEL_HORIZONTAL_WIDTH,
85
- }
86
- }
87
- return {
88
- width: NET_LABEL_HORIZONTAL_WIDTH,
89
- height: NET_LABEL_HORIZONTAL_HEIGHT,
90
- }
91
- }
92
-
93
- private getCenterFromAnchor(
94
- anchor: { x: number; y: number },
95
- orientation: FacingDirection,
96
- width: number,
97
- height: number,
98
- ) {
99
- switch (orientation) {
100
- case "x+":
101
- return { x: anchor.x + width / 2, y: anchor.y }
102
- case "x-":
103
- return { x: anchor.x - width / 2, y: anchor.y }
104
- case "y+":
105
- return { x: anchor.x, y: anchor.y + height / 2 }
106
- case "y-":
107
- return { x: anchor.x, y: anchor.y - height / 2 }
108
- }
109
- }
110
-
111
- private getRectBounds(
112
- center: { x: number; y: number },
113
- w: number,
114
- h: number,
115
- ) {
116
- return {
117
- minX: center.x - w / 2,
118
- minY: center.y - h / 2,
119
- maxX: center.x + w / 2,
120
- maxY: center.y + h / 2,
121
- }
122
- }
123
-
124
- private segmentIntersectsRect(
125
- p1: { x: number; y: number },
126
- p2: { x: number; y: number },
127
- rect: { minX: number; minY: number; maxX: number; maxY: number },
128
- EPS = 1e-9,
129
- ): boolean {
130
- const isVert = Math.abs(p1.x - p2.x) < EPS
131
- const isHorz = Math.abs(p1.y - p2.y) < EPS
132
- if (!isVert && !isHorz) return false
133
-
134
- if (isVert) {
135
- const x = p1.x
136
- if (x < rect.minX - EPS || x > rect.maxX + EPS) return false
137
- const segMinY = Math.min(p1.y, p2.y)
138
- const segMaxY = Math.max(p1.y, p2.y)
139
- const overlap =
140
- Math.min(segMaxY, rect.maxY) - Math.max(segMinY, rect.minY)
141
- return overlap > EPS
142
- } else {
143
- const y = p1.y
144
- if (y < rect.minY - EPS || y > rect.maxY + EPS) return false
145
- const segMinX = Math.min(p1.x, p2.x)
146
- const segMaxX = Math.max(p1.x, p2.x)
147
- const overlap =
148
- Math.min(segMaxX, rect.maxX) - Math.max(segMinX, rect.minX)
149
- return overlap > EPS
150
- }
151
- }
152
-
153
- private rectIntersectsAnyTrace(
154
- bounds: { minX: number; minY: number; maxX: number; maxY: number },
155
- hostPathId: MspConnectionPairId,
156
- hostSegIndex: number,
157
- ): boolean {
158
- for (const [pairId, solved] of Object.entries(this.inputTraceMap)) {
159
- const pts = solved.tracePath
160
- for (let i = 0; i < pts.length - 1; i++) {
161
- if (pairId === hostPathId && i === hostSegIndex) continue
162
- if (this.segmentIntersectsRect(pts[i]!, pts[i + 1]!, bounds))
163
- return true
164
- }
165
- }
166
- return false
167
- }
168
-
169
90
  override _step() {
170
91
  if (this.netLabelPlacement) {
171
92
  this.solved = true
@@ -174,162 +95,33 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
174
95
 
175
96
  // Handle port-only island (no traces) by placing a label at the port
176
97
  if (this.overlappingSameNetTraceGroup.portOnlyPinId) {
177
- const pinId = this.overlappingSameNetTraceGroup.portOnlyPinId
178
- // Find pin coordinates
179
- let pin: { x: number; y: number } | null = null
180
- for (const chip of this.inputProblem.chips) {
181
- const p = chip.pins.find((pp) => pp.pinId === pinId)
182
- if (p) {
183
- pin = { x: p.x, y: p.y }
184
- break
185
- }
186
- }
187
- if (!pin) {
188
- this.failed = true
189
- this.error = `Port-only pin not found: ${pinId}`
190
- return
191
- }
192
-
193
- const orientations =
194
- this.availableOrientations.length > 0
195
- ? this.availableOrientations
196
- : (["x+", "x-", "y+", "y-"] as FacingDirection[])
197
-
198
- const anchor = { x: pin.x, y: pin.y }
199
- const outwardOf = (o: FacingDirection) =>
200
- o === "x+"
201
- ? { x: 1, y: 0 }
202
- : o === "x-"
203
- ? { x: -1, y: 0 }
204
- : o === "y+"
205
- ? { x: 0, y: 1 }
206
- : { x: 0, y: -1 }
207
-
208
- for (const orientation of orientations) {
209
- const { width, height } = this.getDimsForOrientation(orientation)
210
- // Place label fully outside the chip: shift center slightly outward
211
- const baseCenter = this.getCenterFromAnchor(
212
- anchor,
213
- orientation,
214
- width,
215
- height,
216
- )
217
- const outward = outwardOf(orientation)
218
- const offset = 1e-3
219
- const center = {
220
- x: baseCenter.x + outward.x * offset,
221
- y: baseCenter.y + outward.y * offset,
222
- }
223
- const bounds = this.getRectBounds(center, width, height)
224
-
225
- // Chip collision check
226
- const chips = this.chipObstacleSpatialIndex.getChipsInBounds(bounds)
227
- if (chips.length > 0) {
228
- this.testedCandidates.push({
229
- center,
230
- width,
231
- height,
232
- bounds,
233
- anchor,
234
- orientation,
235
- status: "chip-collision",
236
- hostSegIndex: -1,
237
- })
238
- continue
239
- }
240
-
241
- // Trace collision check
242
- if (
243
- this.rectIntersectsAnyTrace(bounds, "" as MspConnectionPairId, -1)
244
- ) {
245
- this.testedCandidates.push({
246
- center,
247
- width,
248
- height,
249
- bounds,
250
- anchor,
251
- orientation,
252
- status: "trace-collision",
253
- hostSegIndex: -1,
254
- })
255
- continue
256
- }
257
-
258
- // Found a valid placement
259
- this.testedCandidates.push({
260
- center,
261
- width,
262
- height,
263
- bounds,
264
- anchor,
265
- orientation,
266
- status: "ok",
267
- hostSegIndex: -1,
268
- })
269
-
270
- this.netLabelPlacement = {
271
- globalConnNetId: this.overlappingSameNetTraceGroup.globalConnNetId,
272
- dcConnNetId: undefined,
273
- orientation,
274
- anchorPoint: anchor,
275
- width,
276
- height,
277
- center,
278
- }
98
+ const res = solveNetLabelPlacementForPortOnlyPin({
99
+ inputProblem: this.inputProblem,
100
+ inputTraceMap: this.inputTraceMap,
101
+ chipObstacleSpatialIndex: this.chipObstacleSpatialIndex,
102
+ overlappingSameNetTraceGroup: this.overlappingSameNetTraceGroup,
103
+ availableOrientations: this.availableOrientations,
104
+ })
105
+ this.testedCandidates.push(...res.testedCandidates)
106
+ if (res.placement) {
107
+ this.netLabelPlacement = res.placement
279
108
  this.solved = true
280
109
  return
281
110
  }
282
-
283
111
  this.failed = true
284
- this.error = "Could not place net label at port without collisions"
112
+ this.error =
113
+ res.error ?? "Could not place net label at port without collisions"
285
114
  return
286
115
  }
287
116
 
288
117
  // Prefer starting from the trace connected to the "largest" chip (most pins)
289
118
  const groupId = this.overlappingSameNetTraceGroup.globalConnNetId
290
- const chipsById: Record<string, InputChip> = Object.fromEntries(
291
- this.inputProblem.chips.map((c) => [c.chipId, c]),
292
- )
293
- const groupTraces = Object.values(this.inputTraceMap).filter(
294
- (t) => t.globalConnNetId === groupId,
295
- )
296
- const chipIdsInGroup = new Set<string>()
297
- for (const t of groupTraces) {
298
- chipIdsInGroup.add(t.pins[0].chipId)
299
- chipIdsInGroup.add(t.pins[1].chipId)
300
- }
301
- let largestChipId: string | null = null
302
- let largestPinCount = -1
303
- for (const id of chipIdsInGroup) {
304
- const chip = chipsById[id]
305
- const count = chip?.pins?.length ?? 0
306
- if (count > largestPinCount) {
307
- largestPinCount = count
308
- largestChipId = id
309
- }
310
- }
311
- const lengthOf = (path: SolvedTracePath) => {
312
- let sum = 0
313
- const pts = path.tracePath
314
- for (let i = 0; i < pts.length - 1; i++) {
315
- sum +=
316
- Math.abs(pts[i + 1]!.x - pts[i]!.x) +
317
- Math.abs(pts[i + 1]!.y - pts[i]!.y)
318
- }
319
- return sum
320
- }
321
- const hostCandidates =
322
- largestChipId == null
323
- ? []
324
- : groupTraces.filter(
325
- (t) =>
326
- t.pins[0].chipId === largestChipId ||
327
- t.pins[1].chipId === largestChipId,
328
- )
329
- let host =
330
- hostCandidates.length > 0
331
- ? hostCandidates.reduce((a, b) => (lengthOf(a) >= lengthOf(b) ? a : b))
332
- : this.overlappingSameNetTraceGroup.overlappingTraces
119
+ let host = chooseHostTraceForGroup({
120
+ inputProblem: this.inputProblem,
121
+ inputTraceMap: this.inputTraceMap,
122
+ globalConnNetId: groupId,
123
+ fallbackTrace: this.overlappingSameNetTraceGroup.overlappingTraces,
124
+ })
333
125
 
334
126
  if (!host) {
335
127
  this.failed = true
@@ -338,100 +130,138 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
338
130
  }
339
131
 
340
132
  // Ensure we traverse the host path starting at the segment attached to the largest chip's pin
341
- let pts = host.tracePath.slice()
342
- if (largestChipId && host) {
343
- const largePin =
344
- host.pins[0].chipId === largestChipId ? host.pins[0] : host.pins[1]
345
- const d0 =
346
- Math.abs(pts[0].x - largePin.x) + Math.abs(pts[0].y - largePin.y)
347
- const dL =
348
- Math.abs(pts[pts.length - 1].x - largePin.x) +
349
- Math.abs(pts[pts.length - 1].y - largePin.y)
350
- if (d0 > dL) {
351
- pts = pts.slice().reverse()
352
- }
353
- }
133
+ const tracesToScanBase = Object.values(this.inputTraceMap).filter(
134
+ (t) => t.globalConnNetId === groupId,
135
+ )
136
+ const tracesToScan =
137
+ this.availableOrientations.length === 1
138
+ ? [
139
+ host,
140
+ ...tracesToScanBase.filter((t) => t.mspPairId !== host!.mspPairId),
141
+ ]
142
+ : [host]
354
143
 
355
144
  const orientations =
356
145
  this.availableOrientations.length > 0
357
146
  ? this.availableOrientations
358
147
  : (["x+", "x-", "y+", "y-"] as FacingDirection[])
359
148
 
360
- const EPS = 1e-6
361
- const anchorsForSegment = (
362
- a: { x: number; y: number },
363
- b: { x: number; y: number },
149
+ const singleOrientationMode = this.availableOrientations.length === 1
150
+
151
+ // For axis-aligned comparisons (furthest-point selection)
152
+ const scoreFor = (
153
+ orientation: FacingDirection,
154
+ anchor: { x: number; y: number },
364
155
  ) => {
365
- // Start, midpoint, end
366
- return [
367
- { x: a.x, y: a.y },
368
- { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 },
369
- { x: b.x, y: b.y },
370
- ]
156
+ switch (orientation) {
157
+ case "y+":
158
+ return anchor.y
159
+ case "y-":
160
+ return -anchor.y
161
+ case "x+":
162
+ return anchor.x
163
+ case "x-":
164
+ return -anchor.x
165
+ }
371
166
  }
167
+ let bestCandidate: {
168
+ anchor: { x: number; y: number }
169
+ orientation: FacingDirection
170
+ width: number
171
+ height: number
172
+ center: { x: number; y: number }
173
+ hostSegIndex: number
174
+ dcConnNetId: string
175
+ } | null = null
176
+ let bestScore = -Infinity
372
177
 
373
- for (let si = 0; si < pts.length - 1; si++) {
374
- const a = pts[si]!
375
- const b = pts[si + 1]!
376
- const isH = Math.abs(a.y - b.y) < EPS
377
- const isV = Math.abs(a.x - b.x) < EPS
378
- if (!isH && !isV) continue
379
-
380
- // Only consider orientations perpendicular to the segment to avoid
381
- // self-overlap with the host segment.
382
- const segmentAllowed: FacingDirection[] = isH
383
- ? (["y+", "y-"] as FacingDirection[])
384
- : (["x+", "x-"] as FacingDirection[])
385
- const candidateOrients = orientations.filter((o) =>
386
- segmentAllowed.includes(o),
387
- )
388
- if (candidateOrients.length === 0) continue
389
-
390
- const anchors = anchorsForSegment(a, b)
391
- for (const anchor of anchors) {
392
- for (const orientation of candidateOrients) {
393
- const { width, height } = this.getDimsForOrientation(orientation)
394
- const center = this.getCenterFromAnchor(
395
- anchor,
396
- orientation,
397
- width,
398
- height,
399
- )
178
+ const EPS = 1e-6
400
179
 
401
- // Small outward offset to avoid counting the touching trace as a collision
402
- const outward =
403
- orientation === "x+"
404
- ? { x: 1, y: 0 }
405
- : orientation === "x-"
406
- ? { x: -1, y: 0 }
407
- : orientation === "y+"
408
- ? { x: 0, y: 1 }
409
- : { x: 0, y: -1 }
410
- const offset = 1e-4
411
- const testCenter = {
412
- x: center.x + outward.x * offset,
413
- y: center.y + outward.y * offset,
414
- }
415
- const bounds = this.getRectBounds(testCenter, width, height)
180
+ for (const curr of tracesToScan) {
181
+ const pts = curr.tracePath.slice()
182
+ for (let si = 0; si < pts.length - 1; si++) {
183
+ const a = pts[si]!
184
+ const b = pts[si + 1]!
185
+ const isH = Math.abs(a.y - b.y) < EPS
186
+ const isV = Math.abs(a.x - b.x) < EPS
187
+ if (!isH && !isV) continue
188
+
189
+ // Only consider orientations perpendicular to the segment to avoid
190
+ // self-overlap with the host segment.
191
+ const segmentAllowed: FacingDirection[] = isH
192
+ ? (["y+", "y-"] as FacingDirection[])
193
+ : (["x+", "x-"] as FacingDirection[])
194
+ const candidateOrients = orientations.filter((o) =>
195
+ segmentAllowed.includes(o),
196
+ )
197
+ if (candidateOrients.length === 0) continue
416
198
 
417
- // Chip collision check
418
- const chips = this.chipObstacleSpatialIndex.getChipsInBounds(bounds)
419
- if (chips.length > 0) {
420
- this.testedCandidates.push({
421
- center: testCenter,
422
- width,
423
- height,
424
- bounds,
199
+ const anchors = anchorsForSegment(a, b)
200
+ for (const anchor of anchors) {
201
+ for (const orientation of candidateOrients) {
202
+ const { width, height } = getDimsForOrientation(orientation)
203
+ const center = getCenterFromAnchor(
425
204
  anchor,
426
205
  orientation,
427
- status: "chip-collision",
428
- hostSegIndex: si,
429
- })
430
- continue
431
- }
432
-
433
- // Trace collision check (ignore the host segment)
434
- if (this.rectIntersectsAnyTrace(bounds, host!.mspPairId, si)) {
206
+ width,
207
+ height,
208
+ )
209
+
210
+ // Small outward offset to avoid counting the touching trace as a collision
211
+ const outward =
212
+ orientation === "x+"
213
+ ? { x: 1, y: 0 }
214
+ : orientation === "x-"
215
+ ? { x: -1, y: 0 }
216
+ : orientation === "y+"
217
+ ? { x: 0, y: 1 }
218
+ : { x: 0, y: -1 }
219
+ const offset = 1e-4
220
+ const testCenter = {
221
+ x: center.x + outward.x * offset,
222
+ y: center.y + outward.y * offset,
223
+ }
224
+ const bounds = getRectBounds(testCenter, width, height)
225
+
226
+ // Chip collision check
227
+ const chips = this.chipObstacleSpatialIndex.getChipsInBounds(bounds)
228
+ if (chips.length > 0) {
229
+ this.testedCandidates.push({
230
+ center: testCenter,
231
+ width,
232
+ height,
233
+ bounds,
234
+ anchor,
235
+ orientation,
236
+ status: "chip-collision",
237
+ hostSegIndex: si,
238
+ })
239
+ continue
240
+ }
241
+
242
+ // Trace collision check (ignore the host segment)
243
+ if (
244
+ rectIntersectsAnyTrace(
245
+ bounds,
246
+ this.inputTraceMap,
247
+ curr.mspPairId,
248
+ si,
249
+ )
250
+ ) {
251
+ this.testedCandidates.push({
252
+ center: testCenter,
253
+ width,
254
+ height,
255
+ bounds,
256
+ anchor,
257
+ orientation,
258
+ status: "trace-collision",
259
+ hostSegIndex: si,
260
+ })
261
+ continue
262
+ }
263
+
264
+ // Found a valid placement
435
265
  this.testedCandidates.push({
436
266
  center: testCenter,
437
267
  width,
@@ -439,160 +269,64 @@ export class SingleNetLabelPlacementSolver extends BaseSolver {
439
269
  bounds,
440
270
  anchor,
441
271
  orientation,
442
- status: "trace-collision",
272
+ status: "ok",
443
273
  hostSegIndex: si,
444
274
  })
445
- continue
446
- }
447
-
448
- // Found a valid placement
449
- this.testedCandidates.push({
450
- center: testCenter,
451
- width,
452
- height,
453
- bounds,
454
- anchor,
455
- orientation,
456
- status: "ok",
457
- hostSegIndex: si,
458
- })
459
275
 
460
- this.netLabelPlacement = {
461
- globalConnNetId: this.overlappingSameNetTraceGroup.globalConnNetId,
462
- dcConnNetId: host!.dcConnNetId,
463
- orientation,
464
- anchorPoint: anchor,
465
- width,
466
- height,
467
- center,
276
+ if (singleOrientationMode) {
277
+ const s = scoreFor(orientation, anchor)
278
+ if (s > bestScore + 1e-9) {
279
+ bestScore = s
280
+ bestCandidate = {
281
+ anchor,
282
+ orientation,
283
+ width,
284
+ height,
285
+ center,
286
+ hostSegIndex: si,
287
+ dcConnNetId: curr.dcConnNetId,
288
+ }
289
+ }
290
+ // Continue traversing to prioritize the furthest valid point
291
+ continue
292
+ }
293
+
294
+ this.netLabelPlacement = {
295
+ globalConnNetId:
296
+ this.overlappingSameNetTraceGroup.globalConnNetId,
297
+ dcConnNetId: curr.dcConnNetId,
298
+ orientation,
299
+ anchorPoint: anchor,
300
+ width,
301
+ height,
302
+ center,
303
+ }
304
+ this.solved = true
305
+ return
468
306
  }
469
- this.solved = true
470
- return
471
307
  }
472
308
  }
473
309
  }
474
310
 
311
+ if (singleOrientationMode && bestCandidate) {
312
+ this.netLabelPlacement = {
313
+ globalConnNetId: this.overlappingSameNetTraceGroup.globalConnNetId,
314
+ dcConnNetId: bestCandidate.dcConnNetId,
315
+ orientation: bestCandidate.orientation,
316
+ anchorPoint: bestCandidate.anchor,
317
+ width: bestCandidate.width,
318
+ height: bestCandidate.height,
319
+ center: bestCandidate.center,
320
+ }
321
+ this.solved = true
322
+ return
323
+ }
324
+
475
325
  this.failed = true
476
326
  this.error = "Could not place net label without collisions"
477
327
  }
478
328
 
479
329
  override visualize(): GraphicsObject {
480
- const graphics = visualizeInputProblem(this.inputProblem)
481
-
482
- // Visualize the entire trace group for this net id
483
- const groupId = this.overlappingSameNetTraceGroup.globalConnNetId
484
- // Choose host as in _step: the trace that touches the largest chip in the group
485
- const chipsById: Record<string, InputChip> = Object.fromEntries(
486
- this.inputProblem.chips.map((c) => [c.chipId, c]),
487
- )
488
- const groupTraces = Object.values(this.inputTraceMap).filter(
489
- (t) => t.globalConnNetId === groupId,
490
- )
491
- const chipIdsInGroup = new Set<string>()
492
- for (const t of groupTraces) {
493
- chipIdsInGroup.add(t.pins[0].chipId)
494
- chipIdsInGroup.add(t.pins[1].chipId)
495
- }
496
- let largestChipId: string | null = null
497
- let largestPinCount = -1
498
- for (const id of chipIdsInGroup) {
499
- const chip = chipsById[id]
500
- const count = chip?.pins?.length ?? 0
501
- if (count > largestPinCount) {
502
- largestPinCount = count
503
- largestChipId = id
504
- }
505
- }
506
- const lengthOf = (path: SolvedTracePath) => {
507
- let sum = 0
508
- const pts = path.tracePath
509
- for (let i = 0; i < pts.length - 1; i++) {
510
- sum +=
511
- Math.abs(pts[i + 1]!.x - pts[i]!.x) +
512
- Math.abs(pts[i + 1]!.y - pts[i]!.y)
513
- }
514
- return sum
515
- }
516
- const hostCandidates =
517
- largestChipId == null
518
- ? []
519
- : groupTraces.filter(
520
- (t) =>
521
- t.pins[0].chipId === largestChipId ||
522
- t.pins[1].chipId === largestChipId,
523
- )
524
- const host =
525
- hostCandidates.length > 0
526
- ? hostCandidates.reduce((a, b) => (lengthOf(a) >= lengthOf(b) ? a : b))
527
- : this.overlappingSameNetTraceGroup.overlappingTraces
528
- const groupStroke = getColorFromString(groupId, 0.9)
529
- const groupFill = getColorFromString(groupId, 0.5)
530
-
531
- for (const trace of Object.values(this.inputTraceMap)) {
532
- if (trace.globalConnNetId !== groupId) continue
533
- const isHost = host ? trace.mspPairId === host.mspPairId : false
534
- graphics.lines!.push({
535
- points: trace.tracePath,
536
- strokeColor: isHost ? groupStroke : groupFill,
537
- strokeWidth: isHost ? 0.006 : 0.003,
538
- strokeDash: isHost ? undefined : "4 2",
539
- } as any)
540
- }
541
-
542
- // Visualize all tested candidate rectangles with reason coloring
543
- for (const c of this.testedCandidates) {
544
- const fill =
545
- c.status === "ok"
546
- ? "rgba(0, 180, 0, 0.25)"
547
- : c.status === "chip-collision"
548
- ? "rgba(220, 0, 0, 0.25)"
549
- : c.status === "trace-collision"
550
- ? "rgba(220, 140, 0, 0.25)"
551
- : "rgba(120, 120, 120, 0.15)"
552
- const stroke =
553
- c.status === "ok"
554
- ? "green"
555
- : c.status === "chip-collision"
556
- ? "red"
557
- : c.status === "trace-collision"
558
- ? "orange"
559
- : "gray"
560
-
561
- graphics.rects!.push({
562
- center: {
563
- x: (c.bounds.minX + c.bounds.maxX) / 2,
564
- y: (c.bounds.minY + c.bounds.maxY) / 2,
565
- },
566
- width: c.width,
567
- height: c.height,
568
- fill,
569
- strokeColor: stroke,
570
- } as any)
571
-
572
- graphics.points!.push({
573
- x: c.anchor.x,
574
- y: c.anchor.y,
575
- color: stroke,
576
- } as any)
577
- }
578
-
579
- // Visualize the final accepted label (if any)
580
- if (this.netLabelPlacement) {
581
- const p = this.netLabelPlacement
582
- graphics.rects!.push({
583
- center: p.center,
584
- width: p.width,
585
- height: p.height,
586
- fill: "rgba(0, 128, 255, 0.35)",
587
- strokeColor: "blue",
588
- } as any)
589
- graphics.points!.push({
590
- x: p.anchorPoint.x,
591
- y: p.anchorPoint.y,
592
- color: "blue",
593
- } as any)
594
- }
595
-
596
- return graphics
330
+ return visualizeSingleNetLabelPlacementSolver(this)
597
331
  }
598
332
  }