@tscircuit/schematic-trace-solver 0.0.145 → 0.0.147

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 (20) hide show
  1. package/dist/index.d.ts +9 -2
  2. package/dist/index.js +482 -86
  3. package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +38 -30
  4. package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +30 -8
  5. package/lib/solvers/InlineNetLabelSolver/pushAnchoredNetLabelsAwayFromInlineLabels.ts +530 -0
  6. package/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts +6 -21
  7. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +7 -23
  8. package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/candidates.ts +7 -14
  9. package/lib/types/InputProblem.ts +8 -0
  10. package/lib/utils/getNetLabelWidthForConnection.ts +63 -0
  11. package/package.json +1 -1
  12. package/tests/assets/inline-net-label-anchored-label-clearance.json +56 -0
  13. package/tests/repros/__snapshots__/repro-ti-power-output-section.snap.svg +80 -0
  14. package/tests/repros/repro-ti-power-output-section.input.ts +341 -0
  15. package/tests/repros/repro-ti-power-output-section.test.ts +13 -0
  16. package/tests/solvers/InlineNetLabelSolver/__snapshots__/fallback-net-label-width.snap.svg +69 -0
  17. package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label-anchored-label-clearance.snap.svg +54 -0
  18. package/tests/solvers/InlineNetLabelSolver/fallback-net-label-width.test.ts +152 -0
  19. package/tests/solvers/InlineNetLabelSolver/inline-net-label-anchored-label-clearance.test.ts +28 -0
  20. package/tests/solvers/InlineNetLabelSolver/push-anchored-net-labels-away.test.ts +227 -0
@@ -0,0 +1,530 @@
1
+ import type { Bounds, Point } from "@tscircuit/math-utils"
2
+ import {
3
+ getPinMap,
4
+ getTracePins,
5
+ } from "lib/solvers/AvailableNetOrientationSolver/traces"
6
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
7
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
8
+ import type { InputProblem } from "lib/types/InputProblem"
9
+ import { dir, type FacingDirection } from "lib/utils/dir"
10
+ import { boundsOverlap, getTextBoxBounds } from "lib/utils/textBoxBounds"
11
+ import type { InlineNetLabelPlacement } from "./InlineNetLabelSolver"
12
+
13
+ const LABEL_CLEARANCE = 0.05
14
+ const POINT_EPSILON = 1e-6
15
+ const CONTIGUOUS_LABEL_GAP = 0.01
16
+ const MAX_OUTWARD_DISTANCE = 5
17
+
18
+ const getBounds = (placement: {
19
+ center: Point
20
+ width: number
21
+ height: number
22
+ axis?: "x" | "y"
23
+ }): Bounds => {
24
+ const renderedWidth =
25
+ placement.axis === "y" ? placement.height : placement.width
26
+ const renderedHeight =
27
+ placement.axis === "y" ? placement.width : placement.height
28
+ return {
29
+ minX: placement.center.x - renderedWidth / 2,
30
+ maxX: placement.center.x + renderedWidth / 2,
31
+ minY: placement.center.y - renderedHeight / 2,
32
+ maxY: placement.center.y + renderedHeight / 2,
33
+ }
34
+ }
35
+
36
+ const pointsEqual = (a: Point, b: Point) =>
37
+ Math.abs(a.x - b.x) <= POINT_EPSILON && Math.abs(a.y - b.y) <= POINT_EPSILON
38
+
39
+ const pathIntersectsBounds = (path: Point[], bounds: Bounds) => {
40
+ for (let index = 0; index < path.length - 1; index++) {
41
+ const start = path[index]!
42
+ const end = path[index + 1]!
43
+ const segmentBounds: Bounds = {
44
+ minX: Math.min(start.x, end.x),
45
+ maxX: Math.max(start.x, end.x),
46
+ minY: Math.min(start.y, end.y),
47
+ maxY: Math.max(start.y, end.y),
48
+ }
49
+ if (boundsOverlap(segmentBounds, bounds)) return true
50
+ }
51
+ return false
52
+ }
53
+
54
+ const isPointOnPath = (point: Point, path: Point[]) => {
55
+ for (let index = 0; index < path.length - 1; index++) {
56
+ const start = path[index]!
57
+ const end = path[index + 1]!
58
+ const minX = Math.min(start.x, end.x) - POINT_EPSILON
59
+ const maxX = Math.max(start.x, end.x) + POINT_EPSILON
60
+ const minY = Math.min(start.y, end.y) - POINT_EPSILON
61
+ const maxY = Math.max(start.y, end.y) + POINT_EPSILON
62
+ const isHorizontal = Math.abs(start.y - end.y) <= POINT_EPSILON
63
+ const isVertical = Math.abs(start.x - end.x) <= POINT_EPSILON
64
+ if (
65
+ ((isHorizontal && Math.abs(point.y - start.y) <= POINT_EPSILON) ||
66
+ (isVertical && Math.abs(point.x - start.x) <= POINT_EPSILON)) &&
67
+ point.x >= minX &&
68
+ point.x <= maxX &&
69
+ point.y >= minY &&
70
+ point.y <= maxY
71
+ ) {
72
+ return true
73
+ }
74
+ }
75
+ return false
76
+ }
77
+
78
+ const getRequiredOutwardDistance = (
79
+ label: NetLabelPlacement,
80
+ inlineBounds: Bounds[],
81
+ ) => {
82
+ const labelBounds = getBounds(label)
83
+
84
+ if (label.orientation === "x-" || label.orientation === "x+") {
85
+ const nearby = inlineBounds.filter(
86
+ (bounds) =>
87
+ labelBounds.minY < bounds.maxY && labelBounds.maxY > bounds.minY,
88
+ )
89
+ if (nearby.length === 0) return 0
90
+ if (label.orientation === "x-") {
91
+ const targetMaxX = Math.min(...nearby.map((bounds) => bounds.minX))
92
+ return Math.max(0, labelBounds.maxX - targetMaxX + LABEL_CLEARANCE)
93
+ }
94
+ const targetMinX = Math.max(...nearby.map((bounds) => bounds.maxX))
95
+ return Math.max(0, targetMinX - labelBounds.minX + LABEL_CLEARANCE)
96
+ }
97
+
98
+ const nearby = inlineBounds.filter(
99
+ (bounds) =>
100
+ labelBounds.minX < bounds.maxX && labelBounds.maxX > bounds.minX,
101
+ )
102
+ if (nearby.length === 0) return 0
103
+ if (label.orientation === "y-") {
104
+ const targetMaxY = Math.min(...nearby.map((bounds) => bounds.minY))
105
+ return Math.max(0, labelBounds.maxY - targetMaxY + LABEL_CLEARANCE)
106
+ }
107
+ const targetMinY = Math.max(...nearby.map((bounds) => bounds.maxY))
108
+ return Math.max(0, targetMinY - labelBounds.minY + LABEL_CLEARANCE)
109
+ }
110
+
111
+ const moveLabel = (
112
+ label: NetLabelPlacement,
113
+ orientation: FacingDirection,
114
+ distance: number,
115
+ ): NetLabelPlacement => {
116
+ const direction = dir(orientation)
117
+ return {
118
+ ...label,
119
+ anchorPoint: {
120
+ x: label.anchorPoint.x + direction.x * distance,
121
+ y: label.anchorPoint.y + direction.y * distance,
122
+ },
123
+ center: {
124
+ x: label.center.x + direction.x * distance,
125
+ y: label.center.y + direction.y * distance,
126
+ },
127
+ }
128
+ }
129
+
130
+ const getDistanceToShoveBoundsPast = (
131
+ obstacleBounds: Bounds,
132
+ movingBounds: Bounds,
133
+ orientation: FacingDirection,
134
+ ) => {
135
+ switch (orientation) {
136
+ case "x-":
137
+ return obstacleBounds.maxX - movingBounds.minX + LABEL_CLEARANCE
138
+ case "x+":
139
+ return movingBounds.maxX - obstacleBounds.minX + LABEL_CLEARANCE
140
+ case "y-":
141
+ return obstacleBounds.maxY - movingBounds.minY + LABEL_CLEARANCE
142
+ case "y+":
143
+ return movingBounds.maxY - obstacleBounds.minY + LABEL_CLEARANCE
144
+ }
145
+ }
146
+
147
+ const boundsGapOnPerpendicularAxis = (
148
+ a: Bounds,
149
+ b: Bounds,
150
+ orientation: FacingDirection,
151
+ ) => {
152
+ if (orientation === "x-" || orientation === "x+") {
153
+ return Math.max(0, a.minY - b.maxY, b.minY - a.maxY)
154
+ }
155
+ return Math.max(0, a.minX - b.maxX, b.minX - a.maxX)
156
+ }
157
+
158
+ const sharesOwnerChip = (
159
+ label: NetLabelPlacement,
160
+ ownerChipIds: Set<string>,
161
+ chipIdByPinId: Map<string, string>,
162
+ ) =>
163
+ label.pinIds.some((pinId) => ownerChipIds.has(chipIdByPinId.get(pinId) ?? ""))
164
+
165
+ const isGeneratedLabelConnector = (trace: SolvedTracePath) =>
166
+ trace.mspPairId.startsWith("available-net-orientation-") ||
167
+ trace.mspPairId.startsWith("inline-net-label-clearance-")
168
+
169
+ const findConnectorTraceIndex = (
170
+ label: NetLabelPlacement,
171
+ traces: SolvedTracePath[],
172
+ ) =>
173
+ traces.findIndex((trace) => {
174
+ if (trace.globalConnNetId !== label.globalConnNetId) return false
175
+ if (!isGeneratedLabelConnector(trace)) return false
176
+ const first = trace.tracePath[0]
177
+ const last = trace.tracePath.at(-1)
178
+ return Boolean(
179
+ (first && pointsEqual(first, label.anchorPoint)) ||
180
+ (last && pointsEqual(last, label.anchorPoint)),
181
+ )
182
+ })
183
+
184
+ const canAddConnectorAtAnchor = (
185
+ label: NetLabelPlacement,
186
+ traces: SolvedTracePath[],
187
+ pinMap: ReturnType<typeof getPinMap>,
188
+ ) => {
189
+ if (
190
+ label.pinIds.some((pinId) => {
191
+ const pin = pinMap[pinId]
192
+ return pin && pointsEqual(pin, label.anchorPoint)
193
+ })
194
+ ) {
195
+ return true
196
+ }
197
+ return traces.some(
198
+ (trace) =>
199
+ trace.globalConnNetId === label.globalConnNetId &&
200
+ isPointOnPath(label.anchorPoint, trace.tracePath),
201
+ )
202
+ }
203
+
204
+ const moveConnectorEndpoint = (
205
+ trace: SolvedTracePath,
206
+ oldAnchor: Point,
207
+ newAnchor: Point,
208
+ ): SolvedTracePath => {
209
+ const tracePath = trace.tracePath.map((point) => ({ ...point }))
210
+ if (pointsEqual(tracePath[0]!, oldAnchor)) tracePath[0] = newAnchor
211
+ if (pointsEqual(tracePath.at(-1)!, oldAnchor)) {
212
+ tracePath[tracePath.length - 1] = newAnchor
213
+ }
214
+ return { ...trace, tracePath }
215
+ }
216
+
217
+ const createConnectorTrace = ({
218
+ label,
219
+ labelIndex,
220
+ newAnchor,
221
+ pinMap,
222
+ }: {
223
+ label: NetLabelPlacement
224
+ labelIndex: number
225
+ newAnchor: Point
226
+ pinMap: ReturnType<typeof getPinMap>
227
+ }): SolvedTracePath => {
228
+ const mspPairId = `inline-net-label-clearance-${labelIndex}-${label.netId ?? label.globalConnNetId}`
229
+ return {
230
+ mspPairId,
231
+ dcConnNetId: label.dcConnNetId ?? label.globalConnNetId,
232
+ globalConnNetId: label.globalConnNetId,
233
+ userNetId: label.netId,
234
+ pins: getTracePins(label, pinMap),
235
+ tracePath: [label.anchorPoint, newAnchor],
236
+ mspConnectionPairIds: [mspPairId],
237
+ pinIds: label.pinIds,
238
+ }
239
+ }
240
+
241
+ const getContiguousLabelGroup = ({
242
+ triggerIndex,
243
+ labels,
244
+ chipIdByPinId,
245
+ }: {
246
+ triggerIndex: number
247
+ labels: NetLabelPlacement[]
248
+ chipIdByPinId: Map<string, string>
249
+ }) => {
250
+ const trigger = labels[triggerIndex]!
251
+ const ownerChipIds = new Set(
252
+ trigger.pinIds.flatMap((pinId) => {
253
+ const chipId = chipIdByPinId.get(pinId)
254
+ return chipId ? [chipId] : []
255
+ }),
256
+ )
257
+ const candidates = labels
258
+ .map((label, labelIndex) => ({ label, labelIndex }))
259
+ .filter(
260
+ ({ label }) =>
261
+ label.orientation === trigger.orientation &&
262
+ label.mspConnectionPairIds.length === 0 &&
263
+ sharesOwnerChip(label, ownerChipIds, chipIdByPinId),
264
+ )
265
+ const group = new Set([triggerIndex])
266
+ let changed = true
267
+ while (changed) {
268
+ changed = false
269
+ for (const { label, labelIndex } of candidates) {
270
+ if (group.has(labelIndex)) continue
271
+ if (
272
+ [...group].some(
273
+ (memberIndex) =>
274
+ boundsGapOnPerpendicularAxis(
275
+ getBounds(labels[memberIndex]!),
276
+ getBounds(label),
277
+ trigger.orientation,
278
+ ) <= CONTIGUOUS_LABEL_GAP,
279
+ )
280
+ ) {
281
+ group.add(labelIndex)
282
+ changed = true
283
+ }
284
+ }
285
+ }
286
+ return { group, ownerChipIds }
287
+ }
288
+
289
+ /**
290
+ * Pushes conventional endpoint labels past nearby inline label text.
291
+ *
292
+ * Contiguous conventional labels on the same component side move as a group,
293
+ * keeping their connector tips aligned. When that new column encounters
294
+ * another label belonging to the same component, the obstacle is shoved one
295
+ * column farther outward and receives its own short connector. The entire
296
+ * proposal is rejected if a chip, component text, inline label, fixed label,
297
+ * or unrelated trace would still be hit.
298
+ */
299
+ export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
300
+ inputProblem,
301
+ traces,
302
+ netLabelPlacements,
303
+ inlineNetLabelPlacements,
304
+ }: {
305
+ inputProblem: InputProblem
306
+ traces: SolvedTracePath[]
307
+ netLabelPlacements: NetLabelPlacement[]
308
+ inlineNetLabelPlacements: InlineNetLabelPlacement[]
309
+ }): {
310
+ traces: SolvedTracePath[]
311
+ netLabelPlacements: NetLabelPlacement[]
312
+ movedLabelCount: number
313
+ } => {
314
+ const outputTraces = traces.map((trace) => ({
315
+ ...trace,
316
+ tracePath: trace.tracePath.map((point) => ({ ...point })),
317
+ }))
318
+ const outputLabels = netLabelPlacements.map((label) => ({ ...label }))
319
+ const inlineBounds = inlineNetLabelPlacements.map(getBounds)
320
+ const pinMap = getPinMap(inputProblem)
321
+ const chipIdByPinId = new Map<string, string>()
322
+ for (const chip of inputProblem.chips) {
323
+ for (const pin of chip.pins) chipIdByPinId.set(pin.pinId, chip.chipId)
324
+ }
325
+ const movedLabelIndices = new Set<number>()
326
+
327
+ for (
328
+ let triggerIndex = 0;
329
+ triggerIndex < outputLabels.length;
330
+ triggerIndex++
331
+ ) {
332
+ const trigger = outputLabels[triggerIndex]!
333
+ const distance = getRequiredOutwardDistance(trigger, inlineBounds)
334
+ if (distance <= POINT_EPSILON || distance > MAX_OUTWARD_DISTANCE) continue
335
+
336
+ const { group, ownerChipIds } = getContiguousLabelGroup({
337
+ triggerIndex,
338
+ labels: outputLabels,
339
+ chipIdByPinId,
340
+ })
341
+ const distances = new Map<number, number>(
342
+ [...group].map((labelIndex) => [labelIndex, distance]),
343
+ )
344
+
345
+ let failed = false
346
+ for (let iteration = 0; iteration < outputLabels.length; iteration++) {
347
+ let adjustedObstacle = false
348
+ for (const [movingIndex, movingDistance] of distances) {
349
+ const movingBounds = getBounds(
350
+ moveLabel(
351
+ outputLabels[movingIndex]!,
352
+ trigger.orientation,
353
+ movingDistance,
354
+ ),
355
+ )
356
+ for (
357
+ let obstacleIndex = 0;
358
+ obstacleIndex < outputLabels.length;
359
+ obstacleIndex++
360
+ ) {
361
+ if (group.has(obstacleIndex)) continue
362
+ const obstacle = outputLabels[obstacleIndex]!
363
+ const existingObstacleDistance = distances.get(obstacleIndex) ?? 0
364
+ const obstacleBounds = getBounds(
365
+ moveLabel(obstacle, trigger.orientation, existingObstacleDistance),
366
+ )
367
+ if (!boundsOverlap(movingBounds, obstacleBounds)) continue
368
+ if (
369
+ !sharesOwnerChip(obstacle, ownerChipIds, chipIdByPinId) ||
370
+ (findConnectorTraceIndex(obstacle, outputTraces) === -1 &&
371
+ !canAddConnectorAtAnchor(obstacle, outputTraces, pinMap))
372
+ ) {
373
+ failed = true
374
+ break
375
+ }
376
+ const shoveDistance = getDistanceToShoveBoundsPast(
377
+ getBounds(obstacle),
378
+ movingBounds,
379
+ trigger.orientation,
380
+ )
381
+ if (
382
+ shoveDistance > MAX_OUTWARD_DISTANCE ||
383
+ shoveDistance <= existingObstacleDistance + POINT_EPSILON
384
+ ) {
385
+ failed = true
386
+ break
387
+ }
388
+ distances.set(obstacleIndex, shoveDistance)
389
+ adjustedObstacle = true
390
+ }
391
+ if (failed) break
392
+ }
393
+ if (failed || !adjustedObstacle) break
394
+ }
395
+ if (failed) continue
396
+
397
+ const proposals = new Map<number, NetLabelPlacement>()
398
+ for (const [labelIndex, labelDistance] of distances) {
399
+ proposals.set(
400
+ labelIndex,
401
+ moveLabel(
402
+ outputLabels[labelIndex]!,
403
+ trigger.orientation,
404
+ labelDistance,
405
+ ),
406
+ )
407
+ }
408
+
409
+ const finalLabelAt = (labelIndex: number) =>
410
+ proposals.get(labelIndex) ?? outputLabels[labelIndex]!
411
+ for (const [labelIndex, movedLabel] of proposals) {
412
+ const movedBounds = getBounds(movedLabel)
413
+ if (inlineBounds.some((bounds) => boundsOverlap(movedBounds, bounds))) {
414
+ failed = true
415
+ break
416
+ }
417
+ if (
418
+ inputProblem.chips.some((chip) =>
419
+ boundsOverlap(movedBounds, {
420
+ minX: chip.center.x - chip.width / 2,
421
+ maxX: chip.center.x + chip.width / 2,
422
+ minY: chip.center.y - chip.height / 2,
423
+ maxY: chip.center.y + chip.height / 2,
424
+ }),
425
+ ) ||
426
+ (inputProblem.textBoxes ?? []).some((textBox) =>
427
+ boundsOverlap(movedBounds, getTextBoxBounds(textBox)),
428
+ )
429
+ ) {
430
+ failed = true
431
+ break
432
+ }
433
+ if (
434
+ outputLabels.some(
435
+ (_, otherIndex) =>
436
+ otherIndex !== labelIndex &&
437
+ boundsOverlap(movedBounds, getBounds(finalLabelAt(otherIndex))),
438
+ )
439
+ ) {
440
+ failed = true
441
+ break
442
+ }
443
+ if (
444
+ outputTraces.some(
445
+ (trace) =>
446
+ trace.globalConnNetId !== movedLabel.globalConnNetId &&
447
+ pathIntersectsBounds(trace.tracePath, movedBounds),
448
+ )
449
+ ) {
450
+ failed = true
451
+ break
452
+ }
453
+ }
454
+ if (failed) continue
455
+
456
+ const connectorUpdates: Array<{
457
+ labelIndex: number
458
+ connectorIndex: number
459
+ trace: SolvedTracePath
460
+ }> = []
461
+ for (const [labelIndex, movedLabel] of proposals) {
462
+ const label = outputLabels[labelIndex]!
463
+ const connectorIndex = findConnectorTraceIndex(label, outputTraces)
464
+ if (
465
+ connectorIndex === -1 &&
466
+ !canAddConnectorAtAnchor(label, outputTraces, pinMap)
467
+ ) {
468
+ failed = true
469
+ break
470
+ }
471
+ const connector =
472
+ connectorIndex === -1
473
+ ? createConnectorTrace({
474
+ label,
475
+ labelIndex,
476
+ newAnchor: movedLabel.anchorPoint,
477
+ pinMap,
478
+ })
479
+ : moveConnectorEndpoint(
480
+ outputTraces[connectorIndex]!,
481
+ label.anchorPoint,
482
+ movedLabel.anchorPoint,
483
+ )
484
+ const connectorObstructed =
485
+ inlineBounds.some((bounds) =>
486
+ pathIntersectsBounds(connector.tracePath, bounds),
487
+ ) ||
488
+ inputProblem.chips.some((chip) =>
489
+ pathIntersectsBounds(connector.tracePath, {
490
+ minX: chip.center.x - chip.width / 2,
491
+ maxX: chip.center.x + chip.width / 2,
492
+ minY: chip.center.y - chip.height / 2,
493
+ maxY: chip.center.y + chip.height / 2,
494
+ }),
495
+ ) ||
496
+ (inputProblem.textBoxes ?? []).some((textBox) =>
497
+ pathIntersectsBounds(connector.tracePath, getTextBoxBounds(textBox)),
498
+ ) ||
499
+ outputLabels.some(
500
+ (_, otherIndex) =>
501
+ otherIndex !== labelIndex &&
502
+ pathIntersectsBounds(
503
+ connector.tracePath,
504
+ getBounds(finalLabelAt(otherIndex)),
505
+ ),
506
+ )
507
+ if (connectorObstructed) {
508
+ failed = true
509
+ break
510
+ }
511
+ connectorUpdates.push({ labelIndex, connectorIndex, trace: connector })
512
+ }
513
+ if (failed) continue
514
+
515
+ for (const [labelIndex, movedLabel] of proposals) {
516
+ outputLabels[labelIndex] = movedLabel
517
+ movedLabelIndices.add(labelIndex)
518
+ }
519
+ for (const update of connectorUpdates) {
520
+ if (update.connectorIndex === -1) outputTraces.push(update.trace)
521
+ else outputTraces[update.connectorIndex] = update.trace
522
+ }
523
+ }
524
+
525
+ return {
526
+ traces: outputTraces,
527
+ netLabelPlacements: outputLabels,
528
+ movedLabelCount: movedLabelIndices.size,
529
+ }
530
+ }
@@ -9,6 +9,7 @@ import type { GraphicsObject } from "graphics-debug"
9
9
  import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
10
10
  import { getColorFromString } from "lib/utils/getColorFromString"
11
11
  import { getConnectivityMapsFromInputProblem } from "../MspConnectionPairSolver/getConnectivityMapFromInputProblem"
12
+ import { getNetLabelWidthForConnection } from "lib/utils/getNetLabelWidthForConnection"
12
13
 
13
14
  /**
14
15
  * A group of traces that have at least one overlapping segment and
@@ -261,31 +262,15 @@ export class NetLabelPlacementSolver extends BaseSolver {
261
262
  private getNetLabelWidthForGroup(
262
263
  group: OverlappingSameNetTraceGroup,
263
264
  ): number | undefined {
264
- if (group.netId) {
265
- const ncWidth = this.inputProblem.netConnections.find(
266
- (nc) => nc.netId === group.netId,
267
- )?.netLabelWidth
268
- if (ncWidth !== undefined) return ncWidth
269
-
270
- const dcWidthByNetId = this.inputProblem.directConnections.find(
271
- (dc) => dc.netId === group.netId,
272
- )?.netLabelWidth
273
- if (dcWidthByNetId !== undefined) return dcWidthByNetId
274
- }
275
-
276
265
  const pinIds = group.overlappingTraces?.pins.map((p) => p.pinId) ?? []
277
266
  if (group.portOnlyPinId) {
278
267
  pinIds.push(group.portOnlyPinId)
279
268
  }
280
-
281
- const dcWidthByPinId = this.inputProblem.directConnections.find((dc) =>
282
- dc.pinIds.some((pid) => pinIds.includes(pid)),
283
- )?.netLabelWidth
284
- if (dcWidthByPinId !== undefined) return dcWidthByPinId
285
-
286
- return this.inputProblem.netConnections.find((nc) =>
287
- nc.pinIds.some((pid) => pinIds.includes(pid)),
288
- )?.netLabelWidth
269
+ return getNetLabelWidthForConnection({
270
+ inputProblem: this.inputProblem,
271
+ netId: group.netId,
272
+ pinIds,
273
+ })
289
274
  }
290
275
 
291
276
  private getNetLabelHeightForGroup(
@@ -7,6 +7,7 @@ import { getDimsForOrientation } from "lib/solvers/NetLabelPlacementSolver/Singl
7
7
  import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
8
8
  import type { InputChip, InputProblem } from "lib/types/InputProblem"
9
9
  import type { FacingDirection } from "lib/utils/dir"
10
+ import { getNetLabelWidthForConnection } from "lib/utils/getNetLabelWidthForConnection"
10
11
  import { getTextBoxBounds, type RectPadding } from "lib/utils/textBoxBounds"
11
12
  import { getPinDirection } from "../SchematicTraceSingleLineSolver/getPinDirection"
12
13
  import { calculateDirectShortPath } from "./calculateDirectShortPath"
@@ -201,7 +202,12 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
201
202
  const orientations =
202
203
  this.inputProblem.availableNetLabelOrientations[netId] ??
203
204
  (["x+", "x-", "y+", "y-"] as FacingDirection[])
204
- const netLabelWidth = this.getNetLabelWidthForConnectionPair(netId)
205
+ const netLabelWidth = getNetLabelWidthForConnection({
206
+ inputProblem: this.inputProblem,
207
+ netId,
208
+ pinIds: this.pins.map((pin) => pin.pinId),
209
+ includeFallbackNetLabelWidth: false,
210
+ })
205
211
  const netLabelHeight = this.getNetLabelHeightForConnectionPair(netId)
206
212
  const padding: Required<RectPadding> = {
207
213
  minX: 0,
@@ -239,28 +245,6 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
239
245
  return padding
240
246
  }
241
247
 
242
- private getNetLabelWidthForConnectionPair(netId: string) {
243
- const ncWidth = this.inputProblem.netConnections.find(
244
- (nc) => nc.netId === netId,
245
- )?.netLabelWidth
246
- if (ncWidth !== undefined) return ncWidth
247
-
248
- const dcWidthByNetId = this.inputProblem.directConnections.find(
249
- (dc) => dc.netId === netId,
250
- )?.netLabelWidth
251
- if (dcWidthByNetId !== undefined) return dcWidthByNetId
252
-
253
- const pinIds = this.pins.map((p) => p.pinId)
254
- const dcWidthByPinId = this.inputProblem.directConnections.find((dc) =>
255
- dc.pinIds.some((pid) => pinIds.includes(pid)),
256
- )?.netLabelWidth
257
- if (dcWidthByPinId !== undefined) return dcWidthByPinId
258
-
259
- return this.inputProblem.netConnections.find((nc) =>
260
- nc.pinIds.some((pid) => pinIds.includes(pid)),
261
- )?.netLabelWidth
262
- }
263
-
264
248
  private getNetLabelHeightForConnectionPair(netId: string) {
265
249
  const ncHeight = this.inputProblem.netConnections.find(
266
250
  (nc) => nc.netId === netId,
@@ -8,6 +8,7 @@ import type { InputProblem } from "lib/types/InputProblem"
8
8
  import { dedupeOrientations } from "lib/utils/dedupeOrientations"
9
9
  import type { FacingDirection } from "lib/utils/dir"
10
10
  import { getOrientationConstraint } from "lib/utils/getOrientationConstraint"
11
+ import { getNetLabelWidthForConnection } from "lib/utils/getNetLabelWidthForConnection"
11
12
  import {
12
13
  EPS,
13
14
  getManhattanDistance,
@@ -310,20 +311,12 @@ const getNetLabelWidth = (
310
311
  inputProblem: InputProblem,
311
312
  label: NetLabelPlacement,
312
313
  ) => {
313
- const ncWidthByNetId = inputProblem.netConnections.find(
314
- (connection) => connection.netId === label.netId,
315
- )?.netLabelWidth
316
- if (ncWidthByNetId !== undefined) return ncWidthByNetId
317
-
318
- const dcWidth = inputProblem.directConnections.find((dc) =>
319
- dc.pinIds.some((pid) => label.pinIds.includes(pid)),
320
- )?.netLabelWidth
321
- if (dcWidth !== undefined) return dcWidth
322
-
323
- const ncWidthByPinId = inputProblem.netConnections.find((nc) =>
324
- nc.pinIds.some((pid) => label.pinIds.includes(pid)),
325
- )?.netLabelWidth
326
- if (ncWidthByPinId !== undefined) return ncWidthByPinId
314
+ const configuredWidth = getNetLabelWidthForConnection({
315
+ inputProblem,
316
+ netId: label.netId,
317
+ pinIds: label.pinIds,
318
+ })
319
+ if (configuredWidth !== undefined) return configuredWidth
327
320
 
328
321
  if (label.orientation === "y+" || label.orientation === "y-") {
329
322
  return label.height
@@ -35,6 +35,13 @@ export interface InputDirectConnection {
35
35
  netId?: string
36
36
  netLabelWidth?: number
37
37
 
38
+ /**
39
+ * Width of the conventional anchored label to use only when an inline label
40
+ * cannot be placed. Unlike `netLabelWidth`, this does not affect whether or
41
+ * how the point-to-point connection is routed.
42
+ */
43
+ fallbackNetLabelWidth?: number
44
+
38
45
  /**
39
46
  * When true, this point-to-point connection may be labeled with an "inline
40
47
  * net label": the net name is drawn parallel to (and offset from) the routed
@@ -66,6 +73,7 @@ export interface InputNetConnection {
66
73
  netId: string
67
74
  pinIds: Array<PinId>
68
75
  netLabelWidth?: number
76
+ fallbackNetLabelWidth?: number
69
77
  netLabelHeight?: number
70
78
 
71
79
  /**