@tscircuit/schematic-trace-solver 0.0.158 → 0.0.160

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 (27) hide show
  1. package/dist/index.d.ts +41 -9
  2. package/dist/index.js +762 -110
  3. package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +536 -94
  4. package/lib/solvers/InlineNetLabelSolver/pushAnchoredNetLabelsAwayFromInlineLabels.ts +9 -7
  5. package/lib/solvers/InlineNetLabelSolver/pushInlineTerminalLabelsAwayFromAnchoredLabels.ts +295 -0
  6. package/lib/solvers/LongDistancePairSolver/LongDistancePairSolver.ts +15 -8
  7. package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +5 -0
  8. package/lib/solvers/MspConnectionPairSolver/getGroundConnectionPolicy.ts +83 -0
  9. package/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts +7 -35
  10. package/lib/solvers/SameNetJunctionAlignmentSolver/SameNetJunctionAlignmentSolver.ts +6 -1
  11. package/lib/solvers/SameNetJunctionAlignmentSolver/placeGroundRailLabelsAtOuterEnd.ts +122 -0
  12. package/lib/solvers/SchematicTraceLinesSolver/getTraceConnectedPinComponents.ts +60 -0
  13. package/lib/types/InputProblem.ts +5 -4
  14. package/package.json +1 -1
  15. package/tests/bug-reports/bug-report-20260826T072956Z/__snapshots__/bug-report-20260826T072956Z.snap.svg +600 -783
  16. package/tests/bug-reports/bug-report-20260826T072956Z/bug-report-20260826T072956Z.test.ts +66 -1
  17. package/tests/fixtures/parallel-ground-rail.ts +53 -0
  18. package/tests/repros/__snapshots__/repro-mspm0l1306-capacitor-symmetry.snap.svg +34 -16
  19. package/tests/repros/repro-mspm0l1306-capacitor-symmetry.test.ts +40 -7
  20. package/tests/repros/repro-usb-power-vbus-label-detour.test.ts +14 -0
  21. package/tests/solvers/InlineNetLabelSolver/multi-pin-connected-components.test.ts +252 -0
  22. package/tests/solvers/InlineNetLabelSolver/opposite-side-placement.test.ts +71 -0
  23. package/tests/solvers/InlineNetLabelSolver/push-anchored-net-labels-away.test.ts +8 -2
  24. package/tests/solvers/InlineNetLabelSolver/push-inline-terminal-labels-away.test.ts +91 -0
  25. package/tests/solvers/InlineNetLabelSolver/two-pin-terminal-stubs-atomic.test.ts +7 -1
  26. package/tests/solvers/MspConnectionPairSolver/local-ground-branches.test.ts +105 -0
  27. package/tests/solvers/SameNetJunctionAlignmentSolver/ground-rail-label.test.ts +188 -0
@@ -1,7 +1,10 @@
1
1
  import type { Bounds, Point } from "@tscircuit/math-utils"
2
2
  import type { GraphicsObject, Rect, Text } from "graphics-debug"
3
+ import type { ConnectivityMap } from "connectivity-map"
3
4
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
5
+ import { getConnectivityMapsFromInputProblem } from "lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem"
4
6
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
7
+ import { getTraceConnectedPinComponents } from "lib/solvers/SchematicTraceLinesSolver/getTraceConnectedPinComponents"
5
8
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
6
9
  import { getPinDirection } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getPinDirection"
7
10
  import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
@@ -19,6 +22,7 @@ import {
19
22
  getAxisAlignedSegments,
20
23
  } from "./getAxisAlignedSegments"
21
24
  import { pushAnchoredNetLabelsAwayFromInlineLabels } from "./pushAnchoredNetLabelsAwayFromInlineLabels"
25
+ import { pushInlineTerminalLabelsAwayFromAnchoredLabels } from "./pushInlineTerminalLabelsAwayFromAnchoredLabels"
22
26
 
23
27
  export const DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18
24
28
 
@@ -101,13 +105,68 @@ export interface InlineNetLabelOutput {
101
105
 
102
106
  type InlineEligibleConnection = InputDirectConnection | InputNetConnection
103
107
 
108
+ type QueuedInlineConnection =
109
+ | { kind: "direct_connection"; connection: InputDirectConnection }
110
+ | { kind: "net_connection"; connection: InputNetConnection }
111
+
112
+ interface NetConnectionInlineConversion {
113
+ globalConnNetId: string
114
+ placements: InlineNetLabelPlacement[]
115
+ supersededTraceIds: Set<string>
116
+ }
117
+
118
+ interface NetConnectionComponent {
119
+ pinIds: PinId[]
120
+ labeledPinIds: PinId[]
121
+ traces: SolvedTracePath[]
122
+ }
123
+
124
+ const AVAILABLE_NET_ORIENTATION_TRACE_PREFIX = "available-net-orientation-"
125
+
126
+ const isGeneratedAnchoredLabelConnector = (trace: SolvedTracePath) =>
127
+ trace.mspPairId.startsWith(AVAILABLE_NET_ORIENTATION_TRACE_PREFIX)
128
+
104
129
  const getPinPairKey = (pinIds: readonly string[]) =>
105
130
  [...pinIds].sort().join("::")
106
131
 
132
+ const getTraceLength = (trace: SolvedTracePath) => {
133
+ let length = 0
134
+ for (let index = 0; index < trace.tracePath.length - 1; index++) {
135
+ const start = trace.tracePath[index]!
136
+ const end = trace.tracePath[index + 1]!
137
+ length += Math.abs(end.x - start.x) + Math.abs(end.y - start.y)
138
+ }
139
+ return length
140
+ }
141
+
142
+ const setsEqual = <T>(first: Set<T>, second: Set<T>) =>
143
+ first.size === second.size && [...first].every((value) => second.has(value))
144
+
145
+ const getInlinePlacementBounds = (
146
+ placement: InlineNetLabelPlacement,
147
+ ): Bounds => {
148
+ const renderedWidth =
149
+ placement.axis === "y" ? placement.height : placement.width
150
+ const renderedHeight =
151
+ placement.axis === "y" ? placement.width : placement.height
152
+ return {
153
+ minX: placement.center.x - renderedWidth / 2,
154
+ maxX: placement.center.x + renderedWidth / 2,
155
+ minY: placement.center.y - renderedHeight / 2,
156
+ maxY: placement.center.y + renderedHeight / 2,
157
+ }
158
+ }
159
+
160
+ const getAnchoredPlacementBounds = (placement: NetLabelPlacement): Bounds => ({
161
+ minX: placement.center.x - placement.width / 2,
162
+ maxX: placement.center.x + placement.width / 2,
163
+ minY: placement.center.y - placement.height / 2,
164
+ maxY: placement.center.y + placement.height / 2,
165
+ })
166
+
107
167
  /**
108
168
  * Places "inline net labels" - net names drawn alongside the trace they belong
109
- * to - for one- or two-pin connections that opted in via
110
- * `allowInlineNetLabel`.
169
+ * to - for connections that opted in via `allowInlineNetLabel`.
111
170
  *
112
171
  * Any regular (anchored) net label placement for the same net is dropped, so a
113
172
  * net is never labeled twice.
@@ -119,10 +178,12 @@ export class InlineNetLabelSolver extends BaseSolver {
119
178
 
120
179
  inlineNetLabelPlacements: InlineNetLabelPlacement[] = []
121
180
 
122
- /** Eligible one- or two-pin connections still waiting to be processed. */
123
- queuedConnections: InlineEligibleConnection[]
181
+ /** Eligible connections still waiting to be processed. */
182
+ queuedConnections: QueuedInlineConnection[]
124
183
 
125
184
  private tracesByPinPairKey: Map<string, SolvedTracePath[]>
185
+ private globalConnMap: ConnectivityMap
186
+ private supersededTraceIdsByGlobalConnNetId = new Map<string, Set<string>>()
126
187
  private hasAlignedPortOnlyStubs = false
127
188
  private postProcessedOutput?: {
128
189
  traces: SolvedTracePath[]
@@ -135,22 +196,35 @@ export class InlineNetLabelSolver extends BaseSolver {
135
196
  this.inputProblem = input.inputProblem
136
197
  this.traces = input.traces
137
198
  this.inputNetLabelPlacements = input.netLabelPlacements
199
+ this.globalConnMap = getConnectivityMapsFromInputProblem(
200
+ this.inputProblem,
201
+ ).netConnMap
138
202
 
139
203
  this.queuedConnections = [
140
- ...this.inputProblem.directConnections.filter(
141
- (connection) => connection.allowInlineNetLabel && connection.netId,
142
- ),
143
- ...this.inputProblem.netConnections.filter(
144
- (connection) =>
145
- connection.allowInlineNetLabel &&
146
- connection.pinIds.length >= 1 &&
147
- connection.pinIds.length <= 2 &&
148
- connection.netId,
149
- ),
204
+ ...this.inputProblem.directConnections
205
+ .filter(
206
+ (connection) => connection.allowInlineNetLabel && connection.netId,
207
+ )
208
+ .map((connection) => ({
209
+ kind: "direct_connection" as const,
210
+ connection,
211
+ })),
212
+ ...this.inputProblem.netConnections
213
+ .filter(
214
+ (connection) =>
215
+ connection.allowInlineNetLabel &&
216
+ connection.pinIds.length >= 1 &&
217
+ connection.netId,
218
+ )
219
+ .map((connection) => ({
220
+ kind: "net_connection" as const,
221
+ connection,
222
+ })),
150
223
  ]
151
224
 
152
225
  this.tracesByPinPairKey = new Map()
153
226
  for (const trace of this.traces) {
227
+ if (isGeneratedAnchoredLabelConnector(trace)) continue
154
228
  const key = getPinPairKey(trace.pins.map((p) => p.pinId))
155
229
  const existing = this.tracesByPinPairKey.get(key)
156
230
  if (existing) {
@@ -172,8 +246,53 @@ export class InlineNetLabelSolver extends BaseSolver {
172
246
  }
173
247
 
174
248
  override _step() {
175
- const connection = this.queuedConnections.shift()
176
- if (connection) {
249
+ const queuedConnection = this.queuedConnections.shift()
250
+ if (queuedConnection) {
251
+ if (
252
+ queuedConnection.kind === "net_connection" &&
253
+ queuedConnection.connection.pinIds.length > 2
254
+ ) {
255
+ const netConnections = [
256
+ queuedConnection.connection,
257
+ ...this.queuedConnections
258
+ .filter(
259
+ (
260
+ queued,
261
+ ): queued is Extract<
262
+ QueuedInlineConnection,
263
+ { kind: "net_connection" }
264
+ > =>
265
+ queued.kind === "net_connection" &&
266
+ queued.connection.pinIds.length > 2,
267
+ )
268
+ .map((queued) => queued.connection),
269
+ ]
270
+ this.queuedConnections = this.queuedConnections.filter(
271
+ (queued) =>
272
+ queued.kind !== "net_connection" ||
273
+ queued.connection.pinIds.length <= 2,
274
+ )
275
+ for (const conversion of this.computeNetConnectionInlineConversions(
276
+ netConnections,
277
+ )) {
278
+ this.inlineNetLabelPlacements.push(...conversion.placements)
279
+ const supersededTraceIds =
280
+ this.supersededTraceIdsByGlobalConnNetId.get(
281
+ conversion.globalConnNetId,
282
+ ) ?? new Set<string>()
283
+ for (const traceId of conversion.supersededTraceIds) {
284
+ supersededTraceIds.add(traceId)
285
+ }
286
+ this.supersededTraceIdsByGlobalConnNetId.set(
287
+ conversion.globalConnNetId,
288
+ supersededTraceIds,
289
+ )
290
+ }
291
+ return
292
+ }
293
+
294
+ const { connection } = queuedConnection
295
+
177
296
  const routedTraces =
178
297
  connection.pinIds.length === 2
179
298
  ? (this.tracesByPinPairKey.get(getPinPairKey(connection.pinIds)) ??
@@ -181,7 +300,11 @@ export class InlineNetLabelSolver extends BaseSolver {
181
300
  : []
182
301
 
183
302
  if (routedTraces.length > 0) {
184
- const placement = this.computeInlinePlacement(connection)
303
+ const placement = this.computeInlinePlacement({
304
+ connection,
305
+ trace: routedTraces[0]!,
306
+ pinIds: connection.pinIds,
307
+ })
185
308
  if (placement) this.inlineNetLabelPlacements.push(placement)
186
309
  } else {
187
310
  // A one-pin net has one conventional endpoint label, while a skipped
@@ -218,6 +341,215 @@ export class InlineNetLabelSolver extends BaseSolver {
218
341
  this.stats.inlineNetLabelCount = this.inlineNetLabelPlacements.length
219
342
  }
220
343
 
344
+ /**
345
+ * Plans all opted-in multi-pin nets together. Short routed components can
346
+ * mutually block the outward endpoint stubs that replace them, so tentative
347
+ * replacements are ignored as obstacles only while their owning net still
348
+ * converts successfully. Failed conversions retain both their traces and
349
+ * their conventional labels, and the remaining conversions are retried.
350
+ */
351
+ private computeNetConnectionInlineConversions(
352
+ connections: InputNetConnection[],
353
+ ): NetConnectionInlineConversion[] {
354
+ const prospectiveTraceIdsByConnection = new Map<
355
+ InputNetConnection,
356
+ Set<string>
357
+ >()
358
+ for (const connection of connections) {
359
+ const prospectiveTraceIds = new Set<string>()
360
+ for (const component of this.getNetConnectionComponents(connection)) {
361
+ if (component.traces.length === 0) continue
362
+ const hasInlinePlacement = component.traces.some((trace) =>
363
+ Boolean(
364
+ this.computeInlinePlacement({
365
+ connection,
366
+ trace,
367
+ pinIds: component.pinIds,
368
+ }),
369
+ ),
370
+ )
371
+ if (!hasInlinePlacement) {
372
+ for (const trace of component.traces) {
373
+ prospectiveTraceIds.add(trace.mspPairId)
374
+ }
375
+ }
376
+ }
377
+ prospectiveTraceIdsByConnection.set(connection, prospectiveTraceIds)
378
+ }
379
+
380
+ let ignoredTraceIds = new Set(
381
+ [...prospectiveTraceIdsByConnection.values()].flatMap((ids) => [...ids]),
382
+ )
383
+
384
+ while (true) {
385
+ const conversions = connections.map((connection) => ({
386
+ connection,
387
+ conversion: this.computeNetConnectionInlinePlacements(
388
+ connection,
389
+ ignoredTraceIds,
390
+ prospectiveTraceIdsByConnection.get(connection) ?? new Set(),
391
+ ),
392
+ }))
393
+ const nextIgnoredTraceIds = new Set(
394
+ conversions.flatMap(({ conversion }) =>
395
+ conversion ? [...conversion.supersededTraceIds] : [],
396
+ ),
397
+ )
398
+ if (setsEqual(ignoredTraceIds, nextIgnoredTraceIds)) {
399
+ return conversions.flatMap(({ conversion }) =>
400
+ conversion ? [conversion] : [],
401
+ )
402
+ }
403
+ ignoredTraceIds = nextIgnoredTraceIds
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Splits an input net into the connected components created by its solved
409
+ * traces. This intentionally does not depend on anchored-label placement:
410
+ * a group whose anchored label failed still has the same routing topology.
411
+ */
412
+ private getNetConnectionComponents(
413
+ connection: InputNetConnection,
414
+ ): NetConnectionComponent[] {
415
+ const firstPinId = connection.pinIds[0]
416
+ if (!firstPinId) return []
417
+ const canonicalGlobalConnNetId =
418
+ this.globalConnMap.getNetConnectedToId(firstPinId)
419
+ if (!canonicalGlobalConnNetId) return []
420
+
421
+ const inputPinIds = new Set(
422
+ this.inputProblem.chips.flatMap((chip) =>
423
+ chip.pins.map((pin) => pin.pinId),
424
+ ),
425
+ )
426
+ const pinIdsInGlobalNet = (
427
+ this.globalConnMap.getIdsConnectedToNet(
428
+ canonicalGlobalConnNetId,
429
+ ) as string[]
430
+ ).filter((id): id is PinId => inputPinIds.has(id))
431
+ const labeledPinIds = new Set(connection.pinIds)
432
+
433
+ return getTraceConnectedPinComponents({
434
+ pinIds: pinIdsInGlobalNet,
435
+ // Available-net-orientation traces only connect a pin to its generated
436
+ // anchored label. They are removed when that label becomes inline, so
437
+ // treating them as routed circuitry would leave text on a deleted wire.
438
+ traces: this.traces.filter(
439
+ (trace) => !isGeneratedAnchoredLabelConnector(trace),
440
+ ),
441
+ })
442
+ .map((component) => ({
443
+ pinIds: component.pinIds,
444
+ labeledPinIds: component.pinIds.filter((pinId) =>
445
+ labeledPinIds.has(pinId),
446
+ ),
447
+ traces: component.traces.sort(
448
+ (a, b) => getTraceLength(b) - getTraceLength(a),
449
+ ),
450
+ }))
451
+ .filter((component) => component.labeledPinIds.length > 0)
452
+ }
453
+
454
+ private getGlobalConnNetId(
455
+ connection: InputNetConnection,
456
+ ): string | undefined {
457
+ const firstPinId = connection.pinIds[0]
458
+ if (!firstPinId) return undefined
459
+ const connectionPinIds = new Set(connection.pinIds)
460
+ return (
461
+ this.traces.find((trace) =>
462
+ trace.pins.some((pin) => connectionPinIds.has(pin.pinId)),
463
+ )?.globalConnNetId ??
464
+ this.inputNetLabelPlacements.find(
465
+ (placement) => placement.netId === connection.netId,
466
+ )?.globalConnNetId ??
467
+ this.globalConnMap.getNetConnectedToId(firstPinId) ??
468
+ undefined
469
+ )
470
+ }
471
+
472
+ /**
473
+ * Converts every conventional label representation for an opted-in named
474
+ * net. Routed connected components become labels on their representative
475
+ * trace; disconnected pins become outward terminal stubs. The conversion is
476
+ * atomic so one blocked candidate cannot leave mixed label styles on a net.
477
+ */
478
+ private computeNetConnectionInlinePlacements(
479
+ connection: InputNetConnection,
480
+ ignoredTraceIds: Set<string>,
481
+ forcedTerminalTraceIds: Set<string>,
482
+ ): NetConnectionInlineConversion | null {
483
+ const components = this.getNetConnectionComponents(connection)
484
+ const globalConnNetId = this.getGlobalConnNetId(connection)
485
+ if (!globalConnNetId) return null
486
+
487
+ const inlinePlacements: InlineNetLabelPlacement[] = []
488
+ const supersededTraceIds = new Set<string>()
489
+ for (const component of components) {
490
+ const shouldUseTerminalStubs = component.traces.some((trace) =>
491
+ forcedTerminalTraceIds.has(trace.mspPairId),
492
+ )
493
+
494
+ let inlinePlacement: InlineNetLabelPlacement | null = null
495
+ if (!shouldUseTerminalStubs) {
496
+ for (const trace of component.traces) {
497
+ inlinePlacement = this.computeInlinePlacement({
498
+ connection,
499
+ trace,
500
+ pinIds: component.pinIds,
501
+ ignoredTraceIds,
502
+ })
503
+ if (inlinePlacement) break
504
+ }
505
+ }
506
+ if (inlinePlacement) {
507
+ inlinePlacements.push(inlinePlacement)
508
+ continue
509
+ }
510
+ if (component.traces.length > 0 && !shouldUseTerminalStubs) {
511
+ // This route originally had a valid inline placement, but a trace from
512
+ // a conversion that had to be rolled back now blocks it. Roll back
513
+ // this whole net as well instead of introducing a new replacement and
514
+ // allowing the batch plan to oscillate.
515
+ return null
516
+ }
517
+ if (component.labeledPinIds.length !== component.pinIds.length) {
518
+ // Removing this component's traces would strand an intermediate pin
519
+ // that was connected through the global net but was not an endpoint of
520
+ // this named connection. Retain the original routed representation.
521
+ return null
522
+ }
523
+
524
+ // A routed component may be too short to carry its text. In that case,
525
+ // replace the component atomically with equivalent named terminal stubs
526
+ // at every endpoint. This preserves connectivity through the net name
527
+ // without retaining a short, redundant wire beside the new labels.
528
+ const terminalPlacements = component.pinIds.map((pinId) =>
529
+ this.computeTerminalInlinePlacement(
530
+ connection,
531
+ pinId,
532
+ globalConnNetId,
533
+ ignoredTraceIds,
534
+ ),
535
+ )
536
+ if (terminalPlacements.some((placement) => placement === null)) {
537
+ return null
538
+ }
539
+ inlinePlacements.push(
540
+ ...terminalPlacements.filter(
541
+ (placement): placement is InlineNetLabelPlacement =>
542
+ placement !== null,
543
+ ),
544
+ )
545
+ for (const trace of component.traces) {
546
+ supersededTraceIds.add(trace.mspPairId)
547
+ }
548
+ }
549
+
550
+ return { globalConnNetId, placements: inlinePlacements, supersededTraceIds }
551
+ }
552
+
221
553
  /**
222
554
  * Converts one conventional endpoint placement into an inline label on a
223
555
  * generated outward stub. The stub follows the pin's true facing direction;
@@ -227,6 +559,8 @@ export class InlineNetLabelSolver extends BaseSolver {
227
559
  private computeTerminalInlinePlacement(
228
560
  connection: InlineEligibleConnection,
229
561
  pinId: PinId,
562
+ knownGlobalConnNetId?: string,
563
+ ignoredTraceIds = new Set<string>(),
230
564
  ): InlineNetLabelPlacement | null {
231
565
  if (!connection.netId) return null
232
566
 
@@ -236,7 +570,9 @@ export class InlineNetLabelSolver extends BaseSolver {
236
570
  placement.pinIds.length === 1 &&
237
571
  placement.pinIds[0] === pinId,
238
572
  )
239
- if (!anchoredPlacement) return null
573
+ const globalConnNetId =
574
+ knownGlobalConnNetId ?? anchoredPlacement?.globalConnNetId
575
+ if (!globalConnNetId) return null
240
576
 
241
577
  const inputChip = this.inputProblem.chips.find((chip) =>
242
578
  chip.pins.some((pin) => pin.pinId === pinId),
@@ -254,13 +590,14 @@ export class InlineNetLabelSolver extends BaseSolver {
254
590
  // Leave a small wire tail at both ends of the text so it unmistakably
255
591
  // reads as a label on a trace rather than free-standing text.
256
592
  const stubLength = Math.max(width + 0.2, 0.6)
593
+ if (!inputPin && !anchoredPlacement) return null
257
594
  const start = inputPin
258
595
  ? { x: inputPin.x, y: inputPin.y }
259
- : anchoredPlacement.anchorPoint
596
+ : anchoredPlacement!.anchorPoint
260
597
  const direction =
261
598
  inputPin && inputChip
262
599
  ? (inputPin._facingDirection ?? getPinDirection(inputPin, inputChip))
263
- : anchoredPlacement.orientation
600
+ : anchoredPlacement!.orientation
264
601
  const intendedEnd: Point =
265
602
  direction === "x+"
266
603
  ? { x: start.x + stubLength, y: start.y }
@@ -275,73 +612,86 @@ export class InlineNetLabelSolver extends BaseSolver {
275
612
  intendedEnd,
276
613
  minimumLength: width + 2 * INLINE_NET_LABEL_TRACE_MARGIN,
277
614
  traces: this.traces,
278
- ownGlobalConnNetId: anchoredPlacement.globalConnNetId,
615
+ ownGlobalConnNetId: globalConnNetId,
616
+ ignoredTraceIds,
279
617
  })
280
618
  if (!end) return null
281
619
 
282
620
  const axis: InlineNetLabelPlacement["axis"] =
283
621
  direction === "x+" || direction === "x-" ? "x" : "y"
284
- const side: InlineNetLabelPlacement["side"] = axis === "x" ? "y+" : "x-"
285
622
  const anchorPoint = {
286
623
  x: (start.x + end.x) / 2,
287
624
  y: (start.y + end.y) / 2,
288
625
  }
289
626
  const offset = height / 2 + INLINE_NET_LABEL_TRACE_MARGIN
290
- const center: Point =
291
- side === "y+"
292
- ? { x: anchorPoint.x, y: anchorPoint.y + offset }
293
- : { x: anchorPoint.x - offset, y: anchorPoint.y }
294
-
295
- const halfAlong = width / 2
296
- const halfAcross = height / 2
297
- const bounds: Bounds =
298
- axis === "x"
299
- ? {
300
- minX: center.x - halfAlong,
301
- maxX: center.x + halfAlong,
302
- minY: center.y - halfAcross,
303
- maxY: center.y + halfAcross,
304
- }
305
- : {
306
- minX: center.x - halfAcross,
307
- maxX: center.x + halfAcross,
308
- minY: center.y - halfAlong,
309
- maxY: center.y + halfAlong,
310
- }
627
+ const sides: InlineNetLabelPlacement["side"][] =
628
+ axis === "x" ? ["y+", "y-"] : ["x-", "x+"]
311
629
 
312
- if (
313
- this.isObstructed(bounds, {
314
- ownGlobalConnNetId: anchoredPlacement.globalConnNetId,
315
- })
316
- ) {
317
- return null
318
- }
630
+ for (const side of sides) {
631
+ const center: Point =
632
+ side === "y+"
633
+ ? { x: anchorPoint.x, y: anchorPoint.y + offset }
634
+ : side === "y-"
635
+ ? { x: anchorPoint.x, y: anchorPoint.y - offset }
636
+ : side === "x-"
637
+ ? { x: anchorPoint.x - offset, y: anchorPoint.y }
638
+ : { x: anchorPoint.x + offset, y: anchorPoint.y }
639
+
640
+ const halfAlong = width / 2
641
+ const halfAcross = height / 2
642
+ const bounds: Bounds =
643
+ axis === "x"
644
+ ? {
645
+ minX: center.x - halfAlong,
646
+ maxX: center.x + halfAlong,
647
+ minY: center.y - halfAcross,
648
+ maxY: center.y + halfAcross,
649
+ }
650
+ : {
651
+ minX: center.x - halfAcross,
652
+ maxX: center.x + halfAcross,
653
+ minY: center.y - halfAlong,
654
+ maxY: center.y + halfAlong,
655
+ }
319
656
 
320
- return {
321
- globalConnNetId: anchoredPlacement.globalConnNetId,
322
- netId: connection.netId,
323
- netLabelText,
324
- pinIds: [pinId],
325
- stubTracePath: [start, end],
326
- axis,
327
- anchorPoint,
328
- center,
329
- width,
330
- height,
331
- side,
657
+ if (
658
+ this.isObstructed(bounds, {
659
+ ownGlobalConnNetId: globalConnNetId,
660
+ ignoredTraceIds,
661
+ })
662
+ ) {
663
+ continue
664
+ }
665
+
666
+ return {
667
+ globalConnNetId,
668
+ netId: connection.netId,
669
+ netLabelText,
670
+ pinIds: [pinId],
671
+ stubTracePath: [start, end],
672
+ axis,
673
+ anchorPoint,
674
+ center,
675
+ width,
676
+ height,
677
+ side,
678
+ }
332
679
  }
333
- }
334
680
 
335
- private computeInlinePlacement(
336
- connection: InlineEligibleConnection,
337
- ): InlineNetLabelPlacement | null {
338
- // Only connections the router actually drew a trace for can carry an inline
339
- // label - there's nothing to run parallel to otherwise.
340
- const traces =
341
- this.tracesByPinPairKey.get(getPinPairKey(connection.pinIds)) ?? []
342
- if (traces.length === 0) return null
681
+ return null
682
+ }
343
683
 
344
- const trace = traces[0]!
684
+ private computeInlinePlacement({
685
+ connection,
686
+ trace,
687
+ pinIds,
688
+ ignoredTraceIds = new Set<string>(),
689
+ }: {
690
+ connection: InlineEligibleConnection
691
+ trace: SolvedTracePath
692
+ pinIds: PinId[]
693
+ ignoredTraceIds?: Set<string>
694
+ }): InlineNetLabelPlacement | null {
345
695
  const segments = getAxisAlignedSegments(trace.tracePath)
346
696
  if (segments.length === 0) return null
347
697
 
@@ -402,6 +752,7 @@ export class InlineNetLabelSolver extends BaseSolver {
402
752
  this.isObstructed(bounds, {
403
753
  ownTrace: trace,
404
754
  ownGlobalConnNetId: trace.globalConnNetId,
755
+ ignoredTraceIds,
405
756
  })
406
757
  )
407
758
  continue
@@ -411,7 +762,7 @@ export class InlineNetLabelSolver extends BaseSolver {
411
762
  netId: connection.netId,
412
763
  netLabelText,
413
764
  mspPairId: trace.mspPairId,
414
- pinIds: [...connection.pinIds],
765
+ pinIds: [...pinIds],
415
766
  axis: segment.axis,
416
767
  anchorPoint,
417
768
  center,
@@ -434,6 +785,8 @@ export class InlineNetLabelSolver extends BaseSolver {
434
785
  width,
435
786
  height,
436
787
  offset,
788
+ pinIds,
789
+ ignoredTraceIds,
437
790
  })
438
791
  if (spanPlacement) return spanPlacement
439
792
 
@@ -455,6 +808,8 @@ export class InlineNetLabelSolver extends BaseSolver {
455
808
  width,
456
809
  height,
457
810
  offset,
811
+ pinIds,
812
+ ignoredTraceIds,
458
813
  }: {
459
814
  trace: SolvedTracePath
460
815
  connection: InlineEligibleConnection
@@ -462,6 +817,8 @@ export class InlineNetLabelSolver extends BaseSolver {
462
817
  width: number
463
818
  height: number
464
819
  offset: number
820
+ pinIds: PinId[]
821
+ ignoredTraceIds: Set<string>
465
822
  }): InlineNetLabelPlacement | null {
466
823
  const path = trace.tracePath
467
824
  if (path.length < 2) return null
@@ -537,6 +894,7 @@ export class InlineNetLabelSolver extends BaseSolver {
537
894
  this.isObstructed(bounds, {
538
895
  ownTrace: trace,
539
896
  ownGlobalConnNetId: trace.globalConnNetId,
897
+ ignoredTraceIds,
540
898
  })
541
899
  )
542
900
  continue
@@ -551,7 +909,7 @@ export class InlineNetLabelSolver extends BaseSolver {
551
909
  netId: connection.netId,
552
910
  netLabelText,
553
911
  mspPairId: trace.mspPairId,
554
- pinIds: [...connection.pinIds],
912
+ pinIds: [...pinIds],
555
913
  axis,
556
914
  anchorPoint,
557
915
  center,
@@ -574,9 +932,11 @@ export class InlineNetLabelSolver extends BaseSolver {
574
932
  {
575
933
  ownTrace,
576
934
  ownGlobalConnNetId,
935
+ ignoredTraceIds = new Set<string>(),
577
936
  }: {
578
937
  ownTrace?: SolvedTracePath
579
938
  ownGlobalConnNetId: string
939
+ ignoredTraceIds?: Set<string>
580
940
  },
581
941
  ): boolean {
582
942
  for (const chip of this.inputProblem.chips) {
@@ -594,6 +954,7 @@ export class InlineNetLabelSolver extends BaseSolver {
594
954
  }
595
955
 
596
956
  for (const trace of this.traces) {
957
+ if (ignoredTraceIds.has(trace.mspPairId)) continue
597
958
  if (ownTrace && trace.mspPairId === ownTrace.mspPairId) continue
598
959
  if (trace.globalConnNetId === ownGlobalConnNetId) continue
599
960
  if (doesPathIntersectBounds(trace.tracePath, bounds)) return true
@@ -606,41 +967,119 @@ export class InlineNetLabelSolver extends BaseSolver {
606
967
  * Net label placements superseded by an inline label. A net gets one label or
607
968
  * the other, never both.
608
969
  */
609
- private getSupersededNetLabelKeys(): Set<string> {
970
+ private getSupersededNetLabelKeys(
971
+ placements = this.inlineNetLabelPlacements,
972
+ ): Set<string> {
610
973
  const keys = new Set<string>()
611
- for (const placement of this.inlineNetLabelPlacements) {
974
+ for (const placement of placements) {
612
975
  keys.add(placement.globalConnNetId)
613
976
  }
614
977
  return keys
615
978
  }
616
979
 
617
- private getOutputTraces(supersededNetLabelKeys: Set<string>) {
980
+ private getOutputTraces(
981
+ supersededNetLabelKeys: Set<string>,
982
+ supersededTraceIds: Set<string>,
983
+ ) {
618
984
  return this.traces.filter(
619
985
  (trace) =>
986
+ !supersededTraceIds.has(trace.mspPairId) &&
620
987
  !(
621
988
  supersededNetLabelKeys.has(trace.globalConnNetId) &&
622
- trace.mspPairId.startsWith("available-net-orientation-")
989
+ isGeneratedAnchoredLabelConnector(trace)
623
990
  ),
624
991
  )
625
992
  }
626
993
 
994
+ private getInlineGlobalsOverlappingAnchoredLabels({
995
+ inlineNetLabelPlacements,
996
+ anchoredNetLabelPlacements,
997
+ }: {
998
+ inlineNetLabelPlacements: InlineNetLabelPlacement[]
999
+ anchoredNetLabelPlacements: NetLabelPlacement[]
1000
+ }): Set<string> {
1001
+ const blockedGlobalConnNetIds = new Set<string>()
1002
+ for (const inlinePlacement of inlineNetLabelPlacements) {
1003
+ const inlineBounds = getInlinePlacementBounds(inlinePlacement)
1004
+ for (const anchoredPlacement of anchoredNetLabelPlacements) {
1005
+ if (
1006
+ anchoredPlacement.globalConnNetId === inlinePlacement.globalConnNetId
1007
+ ) {
1008
+ continue
1009
+ }
1010
+ const anchoredBounds = getAnchoredPlacementBounds(anchoredPlacement)
1011
+ if (
1012
+ boundsOverlap(inlineBounds, anchoredBounds) ||
1013
+ (inlinePlacement.stubTracePath &&
1014
+ doesPathIntersectBounds(
1015
+ inlinePlacement.stubTracePath,
1016
+ anchoredBounds,
1017
+ ))
1018
+ ) {
1019
+ blockedGlobalConnNetIds.add(inlinePlacement.globalConnNetId)
1020
+ break
1021
+ }
1022
+ }
1023
+ }
1024
+ return blockedGlobalConnNetIds
1025
+ }
1026
+
627
1027
  private buildPostProcessedOutput() {
628
- const superseded = this.getSupersededNetLabelKeys()
629
- const retainedNetLabelPlacements = this.inputNetLabelPlacements.filter(
630
- (placement) => !superseded.has(placement.globalConnNetId),
631
- )
632
- const outputTraces = this.getOutputTraces(superseded)
633
- const pushed = pushAnchoredNetLabelsAwayFromInlineLabels({
634
- inputProblem: this.inputProblem,
635
- traces: outputTraces,
636
- netLabelPlacements: retainedNetLabelPlacements,
637
- inlineNetLabelPlacements: this.inlineNetLabelPlacements,
638
- })
639
- this.stats.pushedAnchoredNetLabelCount = pushed.movedLabelCount
640
- return {
641
- traces: pushed.traces,
642
- netLabelPlacements: pushed.netLabelPlacements,
643
- inlineNetLabelPlacements: this.inlineNetLabelPlacements,
1028
+ let activeInlinePlacements = this.inlineNetLabelPlacements
1029
+ while (true) {
1030
+ const supersededNetLabelKeys = this.getSupersededNetLabelKeys(
1031
+ activeInlinePlacements,
1032
+ )
1033
+ const supersededTraceIds = new Set(
1034
+ [...supersededNetLabelKeys].flatMap((globalConnNetId) => [
1035
+ ...(this.supersededTraceIdsByGlobalConnNetId.get(globalConnNetId) ??
1036
+ []),
1037
+ ]),
1038
+ )
1039
+ const retainedNetLabelPlacements = this.inputNetLabelPlacements.filter(
1040
+ (placement) => !supersededNetLabelKeys.has(placement.globalConnNetId),
1041
+ )
1042
+ const outputTraces = this.getOutputTraces(
1043
+ supersededNetLabelKeys,
1044
+ supersededTraceIds,
1045
+ )
1046
+ const pushed = pushAnchoredNetLabelsAwayFromInlineLabels({
1047
+ inputProblem: this.inputProblem,
1048
+ traces: outputTraces,
1049
+ netLabelPlacements: retainedNetLabelPlacements,
1050
+ inlineNetLabelPlacements: activeInlinePlacements,
1051
+ })
1052
+ const blockedGlobalConnNetIds =
1053
+ this.getInlineGlobalsOverlappingAnchoredLabels({
1054
+ inlineNetLabelPlacements: activeInlinePlacements,
1055
+ anchoredNetLabelPlacements: pushed.netLabelPlacements,
1056
+ })
1057
+ if (blockedGlobalConnNetIds.size > 0) {
1058
+ const shiftedInlineTerminalLabels =
1059
+ pushInlineTerminalLabelsAwayFromAnchoredLabels({
1060
+ inputProblem: this.inputProblem,
1061
+ traces: pushed.traces,
1062
+ anchoredNetLabelPlacements: pushed.netLabelPlacements,
1063
+ inlineNetLabelPlacements: activeInlinePlacements,
1064
+ })
1065
+ if (shiftedInlineTerminalLabels.movedGroupCount > 0) {
1066
+ activeInlinePlacements =
1067
+ shiftedInlineTerminalLabels.inlineNetLabelPlacements
1068
+ continue
1069
+ }
1070
+ }
1071
+ if (blockedGlobalConnNetIds.size === 0) {
1072
+ this.inlineNetLabelPlacements = activeInlinePlacements
1073
+ this.stats.pushedAnchoredNetLabelCount = pushed.movedLabelCount
1074
+ return {
1075
+ traces: pushed.traces,
1076
+ netLabelPlacements: pushed.netLabelPlacements,
1077
+ inlineNetLabelPlacements: activeInlinePlacements,
1078
+ }
1079
+ }
1080
+ activeInlinePlacements = activeInlinePlacements.filter(
1081
+ (placement) => !blockedGlobalConnNetIds.has(placement.globalConnNetId),
1082
+ )
644
1083
  }
645
1084
  }
646
1085
 
@@ -841,12 +1280,14 @@ const getCollisionLimitedTerminalStubEnd = ({
841
1280
  minimumLength,
842
1281
  traces,
843
1282
  ownGlobalConnNetId,
1283
+ ignoredTraceIds,
844
1284
  }: {
845
1285
  start: Point
846
1286
  intendedEnd: Point
847
1287
  minimumLength: number
848
1288
  traces: SolvedTracePath[]
849
1289
  ownGlobalConnNetId: string
1290
+ ignoredTraceIds: Set<string>
850
1291
  }): Point | null => {
851
1292
  const isHorizontal =
852
1293
  Math.abs(intendedEnd.x - start.x) >= Math.abs(intendedEnd.y - start.y)
@@ -857,6 +1298,7 @@ const getCollisionLimitedTerminalStubEnd = ({
857
1298
  let availableLength = intendedLength
858
1299
 
859
1300
  for (const trace of traces) {
1301
+ if (ignoredTraceIds.has(trace.mspPairId)) continue
860
1302
  if (trace.globalConnNetId === ownGlobalConnNetId) continue
861
1303
 
862
1304
  for (