@tscircuit/schematic-trace-solver 0.0.144 → 0.0.146

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.
@@ -18,6 +18,7 @@ import {
18
18
  getAxisAlignedSegments,
19
19
  } from "./getAxisAlignedSegments"
20
20
  import { alignPortOnlyInlineNetLabelStubs } from "./alignPortOnlyInlineNetLabelStubs"
21
+ import { pushAnchoredNetLabelsAwayFromInlineLabels } from "./pushAnchoredNetLabelsAwayFromInlineLabels"
21
22
 
22
23
  export const DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18
23
24
 
@@ -111,6 +112,11 @@ export class InlineNetLabelSolver extends BaseSolver {
111
112
 
112
113
  private tracesByPinPairKey: Map<string, SolvedTracePath[]>
113
114
  private hasAlignedPortOnlyStubs = false
115
+ private postProcessedOutput?: {
116
+ traces: SolvedTracePath[]
117
+ netLabelPlacements: NetLabelPlacement[]
118
+ inlineNetLabelPlacements: InlineNetLabelPlacement[]
119
+ }
114
120
 
115
121
  constructor(input: InlineNetLabelSolverInput) {
116
122
  super()
@@ -191,6 +197,11 @@ export class InlineNetLabelSolver extends BaseSolver {
191
197
  return
192
198
  }
193
199
 
200
+ if (!this.postProcessedOutput) {
201
+ this.postProcessedOutput = this.buildPostProcessedOutput()
202
+ return
203
+ }
204
+
194
205
  this.solved = true
195
206
  this.stats.inlineNetLabelCount = this.inlineNetLabelPlacements.length
196
207
  }
@@ -584,20 +595,31 @@ export class InlineNetLabelSolver extends BaseSolver {
584
595
  )
585
596
  }
586
597
 
587
- getOutput() {
598
+ private buildPostProcessedOutput() {
588
599
  const superseded = this.getSupersededNetLabelKeys()
600
+ const retainedNetLabelPlacements = this.inputNetLabelPlacements.filter(
601
+ (placement) => !superseded.has(placement.globalConnNetId),
602
+ )
603
+ const outputTraces = this.getOutputTraces(superseded)
604
+ const pushed = pushAnchoredNetLabelsAwayFromInlineLabels({
605
+ inputProblem: this.inputProblem,
606
+ traces: outputTraces,
607
+ netLabelPlacements: retainedNetLabelPlacements,
608
+ inlineNetLabelPlacements: this.inlineNetLabelPlacements,
609
+ })
610
+ this.stats.pushedAnchoredNetLabelCount = pushed.movedLabelCount
589
611
  return {
590
- // AvailableNetOrientationSolver may have routed an elbow from a port to
591
- // the anchored label that this inline placement supersedes. Keep the
592
- // actual net trace, but discard that now-orphaned label connector.
593
- traces: this.getOutputTraces(superseded),
594
- netLabelPlacements: this.inputNetLabelPlacements.filter(
595
- (placement) => !superseded.has(placement.globalConnNetId),
596
- ),
612
+ traces: pushed.traces,
613
+ netLabelPlacements: pushed.netLabelPlacements,
597
614
  inlineNetLabelPlacements: this.inlineNetLabelPlacements,
598
615
  }
599
616
  }
600
617
 
618
+ getOutput() {
619
+ if (this.postProcessedOutput) return this.postProcessedOutput
620
+ return this.buildPostProcessedOutput()
621
+ }
622
+
601
623
  override visualize(): GraphicsObject {
602
624
  // Mirrors the previous pipeline stage's visualization so that a problem
603
625
  // with no inline labels renders identically, then layers the inline labels
@@ -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
+ }
@@ -14,7 +14,7 @@ import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualize
14
14
  import { doesPairCrossRestrictedCenterLines } from "./doesPairCrossRestrictedCenterLines"
15
15
  import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInputProblem"
16
16
  import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
17
- import { isLabeledPeripheralConnection } from "./isLabeledPeripheralConnection"
17
+ import { getLabeledConnectionRouteReason } from "./isLabeledPeripheralConnection"
18
18
 
19
19
  export type MspConnectionPairId = string
20
20
  export const DEFAULT_MAX_MSP_PAIR_DISTANCE = 1
@@ -26,6 +26,8 @@ export type MspConnectionPair = {
26
26
  dcConnNetId: string
27
27
  globalConnNetId: string
28
28
  userNetId?: string
29
+ /** The trace replaces fallback labels that could not fit between its pins. */
30
+ suppressNetLabel?: boolean
29
31
  pins: [InputPin & { chipId: string }, InputPin & { chipId: string }]
30
32
  }
31
33
 
@@ -121,14 +123,17 @@ export class MspConnectionPairSolver extends BaseSolver {
121
123
  if (this.directConnectionPinPairKeys.has(pinPairKey)) {
122
124
  pairDistance = distance(p1, p2)
123
125
  }
124
- // Labeled one-pin peripherals need a real trace even when they are far
125
- // apart; skipping the MSP pair would leave only the fallback path.
126
- const isLabeledPeripheral = isLabeledPeripheralConnection({
126
+ // Labeled one-pin peripherals and opposed pins whose fallback labels
127
+ // cannot fit need a real trace even when they are far apart.
128
+ const labeledConnectionRouteReason = getLabeledConnectionRouteReason({
127
129
  inputProblem: this.inputProblem,
128
130
  chipMap: this.chipMap,
129
131
  pins: [p1, p2],
130
132
  })
131
- if (pairDistance > this.maxMspPairDistance && !isLabeledPeripheral) {
133
+ if (
134
+ pairDistance > this.maxMspPairDistance &&
135
+ !labeledConnectionRouteReason
136
+ ) {
132
137
  // Too far apart; skip creating an MSP pair for this net
133
138
  return
134
139
  }
@@ -155,12 +160,16 @@ export class MspConnectionPairSolver extends BaseSolver {
155
160
  const globalConnNetId = this.globalConnMap.getNetConnectedToId(pin1!)!
156
161
  const userNetId =
157
162
  this.userNetIdByPinId[pin1!] ?? this.userNetIdByPinId[pin2!]
163
+ const suppressNetLabel =
164
+ pairDistance > this.maxMspPairDistance &&
165
+ labeledConnectionRouteReason === "overlapping-fallback-labels"
158
166
 
159
167
  this.mspConnectionPairs.push({
160
168
  mspPairId: `${pin1}-${pin2}`,
161
169
  dcConnNetId: dcNetId,
162
170
  globalConnNetId,
163
171
  userNetId,
172
+ suppressNetLabel,
164
173
  pins: [p1, p2],
165
174
  })
166
175