@tscircuit/fanout-solver 0.0.59 → 0.0.61

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.
@@ -5,7 +5,11 @@ import { refineAdaptivePlaneReservationCore } from "./refine-adaptive-plane-rese
5
5
  import { shouldUseAdaptiveDensePlaneRouting } from "./should-use-adaptive-dense-plane-routing"
6
6
  import { type GraphicsObject, mergeGraphics } from "graphics-debug"
7
7
  import { addViaLayerMetadataToSrj } from "./add-via-layer-metadata"
8
- import { getCornerBandSide, getExitEdgeForDirection } from "./boundary-exit"
8
+ import {
9
+ getCornerBandSide,
10
+ getDirectionForExitEdge,
11
+ getExitEdgeForDirection,
12
+ } from "./boundary-exit"
9
13
  import { buildOutputSimpleRouteJson } from "./build-output"
10
14
  import {
11
15
  type CompleteOriginalEndpointsResult,
@@ -19,6 +23,7 @@ import {
19
23
  import { matchBusPlanLengths } from "./match-bus-lengths"
20
24
  import {
21
25
  getComponentDogboneViaSiteCandidates,
26
+ getSingleDogboneViaSiteRepairs,
22
27
  matchComponentDogboneViaSites,
23
28
  } from "./match-component-dogbone-via-sites"
24
29
  import { connectionsShareElectricalNet } from "./net-identity"
@@ -463,6 +468,9 @@ function createInitialLayerAssignment(params: {
463
468
  escapeLayers: string[]
464
469
  escapeLayersByBusId: Readonly<Record<string, readonly string[]>>
465
470
  preferOrderedCoordinatedWindingLayers: boolean
471
+ traceWidth: number
472
+ viaDiameter: number
473
+ clearance: number
466
474
  }): Readonly<Record<string, string>> {
467
475
  const {
468
476
  buses,
@@ -509,10 +517,151 @@ function createInitialLayerAssignment(params: {
509
517
  preferOrderedCoordinatedWindingLayers &&
510
518
  busUsesCoordinatedWinding(bus)
511
519
  ) {
512
- // Coordinated winding treats allowedLayers as an ordered preference.
513
- // A global round-robin index can otherwise skip a bus's first choice
514
- // just because a previous bus had a different set of legal layers.
515
- assignment[bus.busId] = viaLayers[0]!
520
+ // Preserve caller preferences when boundary corridors are free. A
521
+ // centered or turning bus with a common target layer can occupy the
522
+ // same boundary band, so prefer another legal layer for a wide route.
523
+ const cornerSide = getCornerBandSide(bus.exitEdge, bus.preferredExit)
524
+ const getBoundaryCongestion = (layer: string): number => {
525
+ if (!bus.exitEdge || bus.connections.length < 8) return 0
526
+ const horizontalEdge =
527
+ bus.exitEdge === "left" || bus.exitEdge === "right"
528
+ const axis = horizontalEdge ? "y" : "x"
529
+ const minimum = horizontalEdge
530
+ ? bus.sharedBoundary.minY
531
+ : bus.sharedBoundary.minX
532
+ const maximum = horizontalEdge
533
+ ? bus.sharedBoundary.maxY
534
+ : bus.sharedBoundary.maxX
535
+ if (!cornerSide) {
536
+ const tracks = bus.connections.map((connection) => {
537
+ const target =
538
+ connection.exitTargetPoint ?? connection.targetPoint
539
+ return Math.max(minimum, Math.min(maximum, target[axis]))
540
+ })
541
+ const bandMinimum = Math.min(...tracks)
542
+ const bandMaximum = Math.max(...tracks)
543
+ const forwardAxis = horizontalEdge ? "x" : "y"
544
+ const sign =
545
+ bus.exitEdge === "right" || bus.exitEdge === "top" ? 1 : -1
546
+ const sourceNearEnd = Math.min(
547
+ ...bus.connections.map(
548
+ (connection) => sign * connection.sourcePoint[forwardAxis],
549
+ ),
550
+ )
551
+ const sourceMinimum = Math.min(
552
+ ...bus.connections.map(
553
+ (connection) => connection.sourcePoint[axis],
554
+ ),
555
+ )
556
+ const sourceMaximum = Math.max(
557
+ ...bus.connections.map(
558
+ (connection) => connection.sourcePoint[axis],
559
+ ),
560
+ )
561
+ return buses.reduce((count, other) => {
562
+ const otherCorner = getCornerBandSide(
563
+ other.exitEdge,
564
+ other.preferredExit,
565
+ )
566
+ if (
567
+ other === bus ||
568
+ other.termination.type !== "boundary" ||
569
+ other.componentId !== bus.componentId ||
570
+ other.exitEdge !== bus.exitEdge ||
571
+ !otherCorner ||
572
+ getCommonExplicitExitTargetLayer(other) !== layer
573
+ )
574
+ return count
575
+ const center =
576
+ minimum +
577
+ (maximum - minimum) * (otherCorner === "minimum" ? 0.25 : 0.75)
578
+ const halfWidth =
579
+ ((Math.max(
580
+ other.connections.length,
581
+ other.cornerBandConnectionCount ?? 0,
582
+ ) -
583
+ 1) *
584
+ Math.max(
585
+ params.traceWidth + params.clearance,
586
+ params.viaDiameter + params.clearance,
587
+ )) /
588
+ 2
589
+ const margin = params.traceWidth + params.clearance
590
+ const overlapsBoundaryBand =
591
+ bandMaximum + margin >= center - halfWidth &&
592
+ bandMinimum - margin <= center + halfWidth
593
+ // A turning bus behind this source field must also pass its lanes
594
+ // on the way to the edge, even when its final band is elsewhere.
595
+ const crossesSourceField =
596
+ Math.max(
597
+ ...other.connections.map(
598
+ (connection) => sign * connection.sourcePoint[forwardAxis],
599
+ ),
600
+ ) <
601
+ sourceNearEnd - 1e-9 &&
602
+ Math.min(
603
+ ...other.connections.map(
604
+ (connection) => connection.sourcePoint[axis],
605
+ ),
606
+ ) <=
607
+ sourceMaximum + margin &&
608
+ Math.max(
609
+ ...other.connections.map(
610
+ (connection) => connection.sourcePoint[axis],
611
+ ),
612
+ ) >=
613
+ sourceMinimum - margin
614
+ return (
615
+ count +
616
+ (overlapsBoundaryBand || crossesSourceField
617
+ ? other.connections.length
618
+ : 0)
619
+ )
620
+ }, 0)
621
+ }
622
+ const bandCenter =
623
+ minimum +
624
+ (maximum - minimum) * (cornerSide === "minimum" ? 0.25 : 0.75)
625
+ const pitch = Math.max(
626
+ params.traceWidth + params.clearance,
627
+ params.viaDiameter + params.clearance,
628
+ )
629
+ const bandHalfWidth =
630
+ ((Math.max(
631
+ bus.connections.length,
632
+ bus.cornerBandConnectionCount ?? 0,
633
+ ) -
634
+ 1) *
635
+ pitch) /
636
+ 2
637
+ return buses.reduce((count, other) => {
638
+ if (
639
+ other === bus ||
640
+ other.termination.type !== "boundary" ||
641
+ other.componentId !== bus.componentId ||
642
+ other.exitEdge !== bus.exitEdge ||
643
+ getCornerBandSide(other.exitEdge, other.preferredExit) ||
644
+ getCommonExplicitExitTargetLayer(other) !== layer
645
+ )
646
+ return count
647
+ return (
648
+ count +
649
+ other.connections.filter((connection) => {
650
+ const target =
651
+ connection.exitTargetPoint ?? connection.targetPoint
652
+ const track = Math.max(minimum, Math.min(maximum, target[axis]))
653
+ return (
654
+ Math.abs(track - bandCenter) <=
655
+ bandHalfWidth + params.traceWidth + params.clearance
656
+ )
657
+ }).length
658
+ )
659
+ }, 0)
660
+ }
661
+ assignment[bus.busId] = viaLayers.toSorted(
662
+ (first, second) =>
663
+ getBoundaryCongestion(first) - getBoundaryCongestion(second),
664
+ )[0]!
516
665
  continue
517
666
  }
518
667
  const componentDirections = directionsByComponent.get(bus.componentId)!
@@ -1042,6 +1191,9 @@ export class FanoutSolver extends BaseSolver {
1042
1191
  buses: this.preparedBuses,
1043
1192
  escapeLayers: this.config.escapeLayers,
1044
1193
  escapeLayersByBusId: this.escapeLayersByBusId,
1194
+ traceWidth: this.config.traceWidth,
1195
+ viaDiameter: this.config.viaDiameter,
1196
+ clearance: this.config.clearance,
1045
1197
  preferOrderedCoordinatedWindingLayers:
1046
1198
  this.config.densePlaneReservationBusIds.length > 0 ||
1047
1199
  this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0 ||
@@ -1482,6 +1634,64 @@ export class FanoutSolver extends BaseSolver {
1482
1634
  const wideBoundaryBuses = unsortedBoundaryBuses.filter(
1483
1635
  (bus) => bus.connections.length >= 8,
1484
1636
  )
1637
+ // A single-layer turning bus beside the end of a centered source field
1638
+ // has fewer escape choices than a corner bus farther behind it. Reserve
1639
+ // that turning channel before the farther bus fences its local via sites.
1640
+ const adjacentCenteredFieldByTurningBus = new Map<
1641
+ PreparedBus,
1642
+ PreparedBus
1643
+ >()
1644
+ if (usePadAlignedDenseRouting && !configuredDensePlaneRouting) {
1645
+ for (const bus of wideBoundaryBuses) {
1646
+ if (
1647
+ !getCornerBandSide(bus.exitEdge, bus.preferredExit) ||
1648
+ new Set(bus.routableEscapeLayers ?? bus.allowedLayers ?? []).size !==
1649
+ 1
1650
+ )
1651
+ continue
1652
+ const axis =
1653
+ bus.direction === "up" || bus.direction === "down" ? "y" : "x"
1654
+ const track = axis === "x" ? "y" : "x"
1655
+ const sign =
1656
+ bus.direction === "up" || bus.direction === "right" ? 1 : -1
1657
+ const forwardEnd = Math.max(
1658
+ ...bus.connections.map((c) => sign * c.sourcePoint[axis]),
1659
+ )
1660
+ const minTrack = Math.min(
1661
+ ...bus.connections.map((c) => c.sourcePoint[track]),
1662
+ )
1663
+ const maxTrack = Math.max(
1664
+ ...bus.connections.map((c) => c.sourcePoint[track]),
1665
+ )
1666
+ const pitch = axis === "x" ? bus.pitchX : bus.pitchY
1667
+ const field = wideBoundaryBuses.find((candidate) => {
1668
+ if (
1669
+ candidate === bus ||
1670
+ candidate.componentId !== bus.componentId ||
1671
+ candidate.exitEdge !== bus.exitEdge ||
1672
+ getCornerBandSide(candidate.exitEdge, candidate.preferredExit)
1673
+ )
1674
+ return false
1675
+ const nearEnd = Math.min(
1676
+ ...candidate.connections.map((c) => sign * c.sourcePoint[axis]),
1677
+ )
1678
+ const gap = nearEnd - forwardEnd
1679
+ return (
1680
+ gap >= -1e-9 &&
1681
+ gap <= pitch + 1e-9 &&
1682
+ Math.min(
1683
+ ...candidate.connections.map((c) => c.sourcePoint[track]),
1684
+ ) <=
1685
+ maxTrack + 1e-9 &&
1686
+ Math.max(
1687
+ ...candidate.connections.map((c) => c.sourcePoint[track]),
1688
+ ) >=
1689
+ minTrack - 1e-9
1690
+ )
1691
+ })
1692
+ if (field) adjacentCenteredFieldByTurningBus.set(bus, field)
1693
+ }
1694
+ }
1485
1695
  const hasThreeWideBoundaryBuses =
1486
1696
  useConfiguredDensePlaneRouting && wideBoundaryBuses.length === 3
1487
1697
  const getBoundaryTargetSpan = (bus: PreparedBus) => {
@@ -1982,14 +2192,35 @@ export class FanoutSolver extends BaseSolver {
1982
2192
  : []),
1983
2193
  ...singletonDeferralCandidates.filter((bus) => {
1984
2194
  const containingBus = getContainingWideSourceField(bus)
1985
- const sharesContainingBusLayer =
2195
+ const sourcePoint = bus.connections[0]!.sourcePoint
2196
+ const componentCenter = {
2197
+ x: (bus.componentBounds.minX + bus.componentBounds.maxX) / 2,
2198
+ y: (bus.componentBounds.minY + bus.componentBounds.maxY) / 2,
2199
+ }
2200
+ const boundaryDirection = bus.exitEdge
2201
+ ? getDirectionForExitEdge(bus.exitEdge)
2202
+ : bus.direction
2203
+ const inwardProjection =
2204
+ boundaryDirection === "right"
2205
+ ? componentCenter.x - sourcePoint.x
2206
+ : boundaryDirection === "left"
2207
+ ? sourcePoint.x - componentCenter.x
2208
+ : boundaryDirection === "up"
2209
+ ? componentCenter.y - sourcePoint.y
2210
+ : sourcePoint.y - componentCenter.y
2211
+ // Crossing the component can consume an embedded singleton's source
2212
+ // dogbone even when the target layers differ. Keep outward boundary
2213
+ // escapes provisional unless they share the wide bus's target layer.
2214
+ const reserveEmbeddedSourceEscape =
1986
2215
  usePadAlignedDenseRouting &&
1987
2216
  !useConfiguredDensePlaneRouting &&
1988
2217
  containingBus &&
1989
- params.busLayerAssignments[containingBus.busId] ===
1990
- params.busLayerAssignments[bus.busId]
2218
+ (params.busLayerAssignments[containingBus.busId] ===
2219
+ params.busLayerAssignments[bus.busId] ||
2220
+ inwardProjection > 1e-9)
1991
2221
  return (
1992
- !leadingWideSingletonBuses.includes(bus) && !sharesContainingBusLayer
2222
+ !leadingWideSingletonBuses.includes(bus) &&
2223
+ !reserveEmbeddedSourceEscape
1993
2224
  )
1994
2225
  }),
1995
2226
  ...(hasThreeWideBoundaryBuses
@@ -2164,6 +2395,167 @@ export class FanoutSolver extends BaseSolver {
2164
2395
  bus,
2165
2396
  ]),
2166
2397
  ]
2398
+ for (const [bus] of adjacentCenteredFieldByTurningBus) {
2399
+ const axis =
2400
+ bus.direction === "up" || bus.direction === "down" ? "y" : "x"
2401
+ const sign =
2402
+ bus.direction === "up" || bus.direction === "right" ? 1 : -1
2403
+ const backwardEnd = Math.min(
2404
+ ...bus.connections.map(
2405
+ (connection) => sign * connection.sourcePoint[axis],
2406
+ ),
2407
+ )
2408
+ const index = denseBoundaryBusesInRoutingOrder.indexOf(bus)
2409
+ const earlierCornerIndex = denseBoundaryBusesInRoutingOrder.findIndex(
2410
+ (candidate) =>
2411
+ candidate !== bus &&
2412
+ candidate.connections.length >= 8 &&
2413
+ candidate.componentId === bus.componentId &&
2414
+ candidate.direction === bus.direction &&
2415
+ Math.max(
2416
+ ...candidate.connections.map(
2417
+ (connection) => sign * connection.sourcePoint[axis],
2418
+ ),
2419
+ ) <=
2420
+ backwardEnd + 1e-9 &&
2421
+ candidate.exitEdge === bus.exitEdge &&
2422
+ getCornerBandSide(candidate.exitEdge, candidate.preferredExit) ===
2423
+ getCornerBandSide(bus.exitEdge, bus.preferredExit),
2424
+ )
2425
+ if (earlierCornerIndex >= 0 && earlierCornerIndex < index) {
2426
+ denseBoundaryBusesInRoutingOrder.splice(index, 1)
2427
+ denseBoundaryBusesInRoutingOrder.splice(earlierCornerIndex, 0, bus)
2428
+ }
2429
+ }
2430
+ const areAdjacentInvertedNarrowBuses = (
2431
+ first: PreparedBus,
2432
+ second: PreparedBus,
2433
+ ): boolean => {
2434
+ if (
2435
+ !usePadAlignedDenseRouting ||
2436
+ first.componentId !== second.componentId ||
2437
+ first.exitEdge !== second.exitEdge ||
2438
+ first.direction !== second.direction ||
2439
+ getCornerBandSide(first.exitEdge, first.preferredExit) !==
2440
+ getCornerBandSide(second.exitEdge, second.preferredExit) ||
2441
+ params.busLayerAssignments[first.busId] !==
2442
+ params.busLayerAssignments[second.busId] ||
2443
+ getContainingWideSourceField(first) !==
2444
+ getContainingWideSourceField(second) ||
2445
+ !getContainingWideSourceField(first)
2446
+ )
2447
+ return false
2448
+ const firstCenter = {
2449
+ x:
2450
+ first.connections.reduce(
2451
+ (sum, connection) => sum + connection.sourcePoint.x,
2452
+ 0,
2453
+ ) / first.connections.length,
2454
+ y:
2455
+ first.connections.reduce(
2456
+ (sum, connection) => sum + connection.sourcePoint.y,
2457
+ 0,
2458
+ ) / first.connections.length,
2459
+ }
2460
+ const secondCenter = {
2461
+ x:
2462
+ second.connections.reduce(
2463
+ (sum, connection) => sum + connection.sourcePoint.x,
2464
+ 0,
2465
+ ) / second.connections.length,
2466
+ y:
2467
+ second.connections.reduce(
2468
+ (sum, connection) => sum + connection.sourcePoint.y,
2469
+ 0,
2470
+ ) / second.connections.length,
2471
+ }
2472
+ if (
2473
+ Math.hypot(
2474
+ firstCenter.x - secondCenter.x,
2475
+ firstCenter.y - secondCenter.y,
2476
+ ) >
2477
+ 1.5 * Math.min(first.pitchX, first.pitchY)
2478
+ )
2479
+ return false
2480
+ const axis =
2481
+ first.exitEdge === "left" || first.exitEdge === "right" ? "y" : "x"
2482
+ const targetTrack = (bus: PreparedBus) =>
2483
+ bus.connections.reduce(
2484
+ (sum, connection) =>
2485
+ sum +
2486
+ (connection.exitTargetPoint ?? connection.targetPoint)[axis],
2487
+ 0,
2488
+ ) / bus.connections.length
2489
+ return (
2490
+ (firstCenter[axis] - secondCenter[axis]) *
2491
+ (targetTrack(first) - targetTrack(second)) <
2492
+ -1e-9
2493
+ )
2494
+ }
2495
+ // Preserve a centered pair's channel before its leading singleton. A
2496
+ // turning pair instead leaves its adjacent singleton room to escape
2497
+ // before searching for an outside-package via.
2498
+ const centeredPairPromotionGroups = new Set<string>()
2499
+ for (const singleton of leadingWideSingletonBuses) {
2500
+ if (getCornerBandSide(singleton.exitEdge, singleton.preferredExit))
2501
+ continue
2502
+ for (const pair of boundaryBuses) {
2503
+ if (
2504
+ pair.connections.length !== 2 ||
2505
+ !areAdjacentInvertedNarrowBuses(pair, singleton)
2506
+ )
2507
+ continue
2508
+ const pairIndex = denseBoundaryBusesInRoutingOrder.indexOf(pair)
2509
+ const singletonIndex =
2510
+ denseBoundaryBusesInRoutingOrder.indexOf(singleton)
2511
+ if (pairIndex > singletonIndex) {
2512
+ denseBoundaryBusesInRoutingOrder.splice(pairIndex, 1)
2513
+ denseBoundaryBusesInRoutingOrder.splice(singletonIndex, 0, pair)
2514
+ centeredPairPromotionGroups.add(
2515
+ `${pair.componentId}:${pair.exitEdge}`,
2516
+ )
2517
+ }
2518
+ }
2519
+ }
2520
+ for (const pair of boundaryBuses) {
2521
+ if (
2522
+ !centeredPairPromotionGroups.has(
2523
+ `${pair.componentId}:${pair.exitEdge}`,
2524
+ ) ||
2525
+ pair.connections.length !== 2 ||
2526
+ !getCornerBandSide(pair.exitEdge, pair.preferredExit)
2527
+ )
2528
+ continue
2529
+ for (const singleton of singletonBoundaryBuses) {
2530
+ if (!areAdjacentInvertedNarrowBuses(singleton, pair)) continue
2531
+ const singletonIndex =
2532
+ denseBoundaryBusesInRoutingOrder.indexOf(singleton)
2533
+ const pairIndex = denseBoundaryBusesInRoutingOrder.indexOf(pair)
2534
+ if (singletonIndex > pairIndex) {
2535
+ denseBoundaryBusesInRoutingOrder.splice(singletonIndex, 1)
2536
+ denseBoundaryBusesInRoutingOrder.splice(pairIndex, 0, singleton)
2537
+ }
2538
+ }
2539
+ }
2540
+ for (const field of adjacentCenteredFieldByTurningBus.values()) {
2541
+ for (const pair of boundaryBuses) {
2542
+ if (
2543
+ pair.connections.length !== 2 ||
2544
+ getContainingWideSourceField(pair) !== field
2545
+ )
2546
+ continue
2547
+ for (const singleton of singletonBoundaryBuses) {
2548
+ if (!areAdjacentInvertedNarrowBuses(singleton, pair)) continue
2549
+ const pairIndex = denseBoundaryBusesInRoutingOrder.indexOf(pair)
2550
+ const singletonIndex =
2551
+ denseBoundaryBusesInRoutingOrder.indexOf(singleton)
2552
+ if (singletonIndex > pairIndex) {
2553
+ denseBoundaryBusesInRoutingOrder.splice(singletonIndex, 1)
2554
+ denseBoundaryBusesInRoutingOrder.splice(pairIndex, 0, singleton)
2555
+ }
2556
+ }
2557
+ }
2558
+ }
2167
2559
  let fixedViaPointsByConnectionIndex: ReadonlyMap<
2168
2560
  number,
2169
2561
  { x: number; y: number }
@@ -2311,6 +2703,7 @@ export class FanoutSolver extends BaseSolver {
2311
2703
  useConfiguredDensePlaneRouting &&
2312
2704
  singleLayerBus !== bus &&
2313
2705
  !embeddedNarrowBusAlreadyRouted
2706
+ let usedRepairedViaSites = false
2314
2707
  let busPlans = (yield* routeAlternatives(
2315
2708
  preferSingleLayerWinding
2316
2709
  ? { ...routeParams, bus: singleLayerBus }
@@ -2417,6 +2810,197 @@ export class FanoutSolver extends BaseSolver {
2417
2810
  }
2418
2811
  }
2419
2812
  }
2813
+ // A first turning bus can be fenced by one diagonal site even though
2814
+ // every provisional dogbone is individually legal. Try moving one of
2815
+ // its own sites while retaining all other through-via reservations.
2816
+ // Keep this bounded repair ahead of the broader free-site search.
2817
+ if (
2818
+ !busPlans &&
2819
+ useAdaptiveDensePlaneRouting &&
2820
+ bus.connections.length >= 8 &&
2821
+ getCornerBandSide(bus.exitEdge, bus.preferredExit) &&
2822
+ new Set(bus.routableEscapeLayers ?? bus.allowedLayers ?? []).size ===
2823
+ 1 &&
2824
+ !matchedPlans.some((plan) =>
2825
+ wideBoundaryBuses.some(
2826
+ (candidate) => candidate.busId === plan.busId,
2827
+ ),
2828
+ )
2829
+ ) {
2830
+ const reservedVias = getReservedVias(bus)
2831
+ const siteRepairs = getSingleDogboneViaSiteRepairs(
2832
+ bus,
2833
+ {
2834
+ viaDiameter: this.config.viaDiameter,
2835
+ viaHoleDiameter: this.config.viaHoleDiameter,
2836
+ traceWidth: this.config.traceWidth,
2837
+ clearance: this.config.clearance,
2838
+ additionalObstacles: this.routingSrj.obstacles,
2839
+ blockingSegments: [
2840
+ ...matchedPlans.flatMap((plan) =>
2841
+ plan.segments.map((segment) => ({
2842
+ connectionIndex: plan.connectionIndex,
2843
+ segment,
2844
+ })),
2845
+ ),
2846
+ ...reservedVias.flatMap((reserved) =>
2847
+ reserved.sourceEscapeSegment
2848
+ ? [
2849
+ {
2850
+ connectionIndex: -1,
2851
+ segment: reserved.sourceEscapeSegment,
2852
+ },
2853
+ ]
2854
+ : [],
2855
+ ),
2856
+ ],
2857
+ blockingVias: [
2858
+ ...matchedPlans.flatMap((plan) =>
2859
+ plan.via
2860
+ ? [{ connectionIndex: plan.connectionIndex, ...plan.via }]
2861
+ : [],
2862
+ ),
2863
+ ...reservedVias.map((reserved) => ({
2864
+ connectionIndex: -1,
2865
+ ...reserved.via,
2866
+ })),
2867
+ ],
2868
+ },
2869
+ fixedViaPointsByConnectionIndex,
2870
+ )
2871
+ for (const replacementPoints of siteRepairs) {
2872
+ const replacementPlans = (yield* routeAlternatives(
2873
+ {
2874
+ ...routeParams,
2875
+ fixedViaPointsByConnectionIndex: replacementPoints,
2876
+ reservedVias,
2877
+ fixedViaFallbackRouteOrderAttempts: 1,
2878
+ },
2879
+ 1,
2880
+ ))[0]
2881
+ if (!replacementPlans) continue
2882
+ busPlans = replacementPlans
2883
+ fixedViaPointsByConnectionIndex = replacementPoints
2884
+ usedRepairedViaSites = true
2885
+ break
2886
+ }
2887
+ }
2888
+ // Fixed dogbones can close a turning bus's own escape channel. Retry
2889
+ // local sites while retaining every other connection's reservations.
2890
+ if (
2891
+ !busPlans &&
2892
+ useAdaptiveDensePlaneRouting &&
2893
+ bus.connections.length >= 8 &&
2894
+ getCornerBandSide(bus.exitEdge, bus.preferredExit)
2895
+ ) {
2896
+ busPlans = (yield* routeAlternatives(
2897
+ {
2898
+ ...routeParams,
2899
+ fixedViaPointsByConnectionIndex: undefined,
2900
+ reservedVias: getReservedVias(bus),
2901
+ },
2902
+ 1,
2903
+ ))[0]
2904
+ if (busPlans) {
2905
+ usedRepairedViaSites = true
2906
+ fixedViaPointsByConnectionIndex = new Map([
2907
+ ...fixedViaPointsByConnectionIndex,
2908
+ ...busPlans
2909
+ .filter((plan) => plan.via)
2910
+ .map(
2911
+ (plan) => [plan.connectionIndex, plan.via!.center] as const,
2912
+ ),
2913
+ ])
2914
+ }
2915
+ debugDense("local-sites", bus.busId, busPlans?.length ?? "failed")
2916
+ }
2917
+ // Retry a blocked wide bus with provisional plane sites as search
2918
+ // costs. A turning bus beside a centered field may also need that
2919
+ // neighboring field's uncommitted sites to move. Keep every committed
2920
+ // route and narrow reservation hard, then require a complete rematch.
2921
+ if (
2922
+ !busPlans &&
2923
+ bus.connections.length >= 8 &&
2924
+ (adjacentCenteredFieldByTurningBus.has(bus) ||
2925
+ boundaryBuses.some(
2926
+ (candidate) =>
2927
+ candidate.connections.length >= 8 &&
2928
+ matchedPlans.some((plan) => plan.busId === candidate.busId),
2929
+ ))
2930
+ ) {
2931
+ const committedNames = new Set(
2932
+ matchedPlans.map((plan) => plan.connectionName),
2933
+ )
2934
+ const provisionalReservationNames = new Set(
2935
+ this.preparedBuses
2936
+ .filter(
2937
+ (candidate) =>
2938
+ candidate.termination.type === "plane" ||
2939
+ candidate === adjacentCenteredFieldByTurningBus.get(bus),
2940
+ )
2941
+ .flatMap((candidate) =>
2942
+ candidate.connections.map(
2943
+ (connection) => connection.connection.name,
2944
+ ),
2945
+ )
2946
+ .filter((name) => !committedNames.has(name)),
2947
+ )
2948
+ const freePlans = (yield* routeAlternatives(
2949
+ {
2950
+ ...routeParams,
2951
+ fixedViaPointsByConnectionIndex: undefined,
2952
+ reservedVias: routeParams.reservedVias.filter(
2953
+ (reserved) =>
2954
+ !provisionalReservationNames.has(reserved.connectionName),
2955
+ ),
2956
+ softReservedVias: routeParams.reservedVias.filter((reserved) =>
2957
+ provisionalReservationNames.has(reserved.connectionName),
2958
+ ),
2959
+ },
2960
+ 1,
2961
+ ))[0]
2962
+ if (freePlans) {
2963
+ const allPlans = [...matchedPlans, ...freePlans]
2964
+ const rematchedPoints = matchComponentDogboneViaSites(
2965
+ this.preparedBuses,
2966
+ {
2967
+ viaDiameter: this.config.viaDiameter,
2968
+ viaHoleDiameter: this.config.viaHoleDiameter,
2969
+ traceWidth: this.config.traceWidth,
2970
+ clearance: this.config.clearance,
2971
+ maximumSearchStates: 3_000_000,
2972
+ preferredBoundaryPerpendicularSideByBusId,
2973
+ preferBoundaryOutwardByBusId,
2974
+ fixedViaPointsByConnectionIndex: new Map(
2975
+ allPlans
2976
+ .filter((plan) => plan.via)
2977
+ .map((plan) => [plan.connectionIndex, plan.via!.center]),
2978
+ ),
2979
+ preferredViaPointsByConnectionIndex:
2980
+ fixedViaPointsByConnectionIndex,
2981
+ blockingSegments: allPlans.flatMap((plan) =>
2982
+ plan.segments.map((segment) => ({
2983
+ connectionIndex: plan.connectionIndex,
2984
+ segment,
2985
+ })),
2986
+ ),
2987
+ additionalObstacles: this.routingSrj.obstacles,
2988
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
2989
+ canShareCopper,
2990
+ },
2991
+ )
2992
+ debugDense(
2993
+ "free-sites:rematched",
2994
+ bus.busId,
2995
+ rematchedPoints?.size ?? "failed",
2996
+ )
2997
+ if (rematchedPoints) {
2998
+ busPlans = freePlans
2999
+ fixedViaPointsByConnectionIndex = rematchedPoints
3000
+ usedRepairedViaSites = true
3001
+ }
3002
+ }
3003
+ }
2420
3004
  if (busPlans && bus.maxLengthSkew !== undefined) {
2421
3005
  const lengths = busPlans.map((plan) => plan.length)
2422
3006
  const rawSkew = Math.max(...lengths) - Math.min(...lengths)
@@ -2430,7 +3014,13 @@ export class FanoutSolver extends BaseSolver {
2430
3014
  // Only pay for additional A* variants when the first topology is so
2431
3015
  // skewed that compact meanders are unlikely to absorb the deficit.
2432
3016
  // This keeps already-near-matched buses on the single-attempt path.
2433
- if (needsRouteDiversity && !matchLengthsAfterPlanes) {
3017
+ // Keep repaired via sites: routeParams still carries the
3018
+ // earlier provisional sites and cannot safely replace its geometry.
3019
+ if (
3020
+ needsRouteDiversity &&
3021
+ !matchLengthsAfterPlanes &&
3022
+ !usedRepairedViaSites
3023
+ ) {
2434
3024
  busPlans = (yield* routeAlternatives(routeParams, 3)).toSorted(
2435
3025
  (first, second) => {
2436
3026
  const firstLengths = first.map((plan) => plan.length)
@@ -338,9 +338,33 @@ function replacementCopperIsSelfClear(params: {
338
338
  ) {
339
339
  continue
340
340
  }
341
+ const viaClearance = via.diameter / 2 + replacement.width / 2 + clearance
342
+ // An off-grid via can join this run through a short, connected stub.
343
+ // Preserve that existing connection while rejecting later approaches.
344
+ const connectsThroughShortStub = (direction: -1 | 1): boolean => {
345
+ let point = direction === -1 ? replacement.start : replacement.end
346
+ let pathDistance = 0
347
+ for (
348
+ let index = replacementIndex + direction;
349
+ index >= 0 && index < segments.length;
350
+ index += direction
351
+ ) {
352
+ const segment = segments[index]!
353
+ if (segment.layer !== replacement.layer) return false
354
+ const connectedEnd = direction === -1 ? segment.end : segment.start
355
+ if (!pointsMatch(point, connectedEnd)) return false
356
+ pathDistance += distance(segment.start, segment.end)
357
+ if (pathDistance > viaClearance + EPSILON) return false
358
+ point = direction === -1 ? segment.start : segment.end
359
+ if (pointsMatch(point, via.center)) return true
360
+ }
361
+ return false
362
+ }
341
363
  if (
342
364
  distancePointToSegment(via.center, replacement.start, replacement.end) <
343
- via.diameter / 2 + replacement.width / 2 + clearance - EPSILON
365
+ viaClearance - EPSILON &&
366
+ !connectsThroughShortStub(-1) &&
367
+ !connectsThroughShortStub(1)
344
368
  ) {
345
369
  return false
346
370
  }
@@ -782,3 +782,30 @@ export function getComponentDogboneViaSiteCandidates(
782
782
  ),
783
783
  )
784
784
  }
785
+
786
+ /** Try one adjacent site change without releasing any other fixed via. */
787
+ export function* getSingleDogboneViaSiteRepairs(
788
+ bus: PreparedBus,
789
+ rules: DogboneViaSiteGeometryRules,
790
+ fixedViaPointsByConnectionIndex: ReadonlyMap<number, Point2D>,
791
+ ): Generator<ReadonlyMap<number, Point2D>, void, unknown> {
792
+ const sites = getComponentDogboneViaSiteCandidates([bus], rules)
793
+ let remainingAttempts = 24
794
+ for (const connection of bus.connections.toReversed()) {
795
+ const original = fixedViaPointsByConnectionIndex.get(
796
+ connection.connectionIndex,
797
+ )
798
+ if (!original) continue
799
+ for (const candidate of sites) {
800
+ if (
801
+ candidate.connectionIndex !== connection.connectionIndex ||
802
+ distance(candidate.point, original) <= EPSILON
803
+ )
804
+ continue
805
+ if (remainingAttempts-- <= 0) return
806
+ const replacement = new Map(fixedViaPointsByConnectionIndex)
807
+ replacement.set(connection.connectionIndex, candidate.point)
808
+ yield replacement
809
+ }
810
+ }
811
+ }
package/lib/route-bus.ts CHANGED
@@ -62,6 +62,8 @@ export interface RouteBusParams {
62
62
  stopAfterFirstRejectedViaMinimalCandidate?: boolean
63
63
  fixedViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
64
64
  reservedVias?: readonly ViaMinimalWindingReservedVia[]
65
+ /** Provisional site preferences; successful callers must rematch future vias. */
66
+ softReservedVias?: readonly ViaMinimalWindingReservedVia[]
65
67
  viaMinimalOnly?: boolean
66
68
  /** Permit a singleton or pair to move provisional vias near the boundary. */
67
69
  allowBoundarySideViaFallback?: boolean
@@ -163,6 +165,7 @@ function getWindingTargetOrders(params: {
163
165
  }): {
164
166
  orders: PreparedConnection[][]
165
167
  legacyOrder: PreparedConnection[]
168
+ ordinaryOrderCount: number
166
169
  } {
167
170
  const { bus, boundaryDirection, layerNames, targetLayer } = params
168
171
  const getTargetLayer = (candidate: PreparedConnection): string =>
@@ -266,6 +269,41 @@ function getWindingTargetOrders(params: {
266
269
  // but do not let sub-nanometer noise between unrelated layer bands choose
267
270
  // the primary topology.
268
271
  candidateOrders.push(legacyOrderedConnections)
272
+ const ordinaryOrderCount = new Set(
273
+ candidateOrders.map((order) =>
274
+ order.map((candidate) => candidate.connectionIndex).join(","),
275
+ ),
276
+ ).size
277
+ // Preserve each original layer's lane order while exploring other legal
278
+ // interleavings. Keep this bounded for buses with many source layers.
279
+ if (
280
+ !getCornerSide(bus) &&
281
+ bus.connections.length <= 8 &&
282
+ orderedLayers.length > 1
283
+ ) {
284
+ const layerSequences = orderedLayers.map((layer) =>
285
+ connectionsByLayer.get(layer)!.toSorted(compareWithinLayer),
286
+ )
287
+ const offsets = layerSequences.map(() => 0)
288
+ const current: PreparedConnection[] = []
289
+ const append = (): void => {
290
+ if (candidateOrders.length >= 128) return
291
+ if (current.length === bus.connections.length) {
292
+ candidateOrders.push([...current])
293
+ return
294
+ }
295
+ for (let layer = 0; layer < layerSequences.length; layer++) {
296
+ const next = layerSequences[layer]![offsets[layer]!]
297
+ if (!next) continue
298
+ offsets[layer]++
299
+ current.push(next)
300
+ append()
301
+ current.pop()
302
+ offsets[layer]--
303
+ }
304
+ }
305
+ append()
306
+ }
269
307
  const seenOrders = new Set<string>()
270
308
  const orders = candidateOrders.filter((order) => {
271
309
  const key = order.map((candidate) => candidate.connectionIndex).join(",")
@@ -273,7 +311,7 @@ function getWindingTargetOrders(params: {
273
311
  seenOrders.add(key)
274
312
  return true
275
313
  })
276
- return { orders, legacyOrder: legacyOrderedConnections }
314
+ return { orders, legacyOrder: legacyOrderedConnections, ordinaryOrderCount }
277
315
  }
278
316
 
279
317
  function getWindingTargetRank(params: {
@@ -311,10 +349,92 @@ function getWindingCrossoverLayer(params: {
311
349
  )
312
350
  }
313
351
 
352
+ function getDistributedBoundaryTargetTracks(params: {
353
+ bus: PreparedBus
354
+ boundaryDirection: FanoutDirection
355
+ traceWidth: number
356
+ clearance: number
357
+ allowLayerInterleaving?: boolean
358
+ }): number[] | undefined {
359
+ const { bus, boundaryDirection, traceWidth, clearance } = params
360
+ if (getCornerSide(bus) || !busUsesCoordinatedWindingChannel(bus))
361
+ return undefined
362
+ const layers = new Set(
363
+ bus.connections.map(
364
+ (connection) =>
365
+ connection.exitTargetPoint?.layer ??
366
+ getPointLayer(connection.targetPoint),
367
+ ),
368
+ )
369
+ if (layers.size < 2) return undefined
370
+ const minimum = isHorizontal(boundaryDirection)
371
+ ? bus.sharedBoundary.minY
372
+ : bus.sharedBoundary.minX
373
+ const maximum = isHorizontal(boundaryDirection)
374
+ ? bus.sharedBoundary.maxY
375
+ : bus.sharedBoundary.maxX
376
+ const tracks = bus.connections
377
+ .map((connection) =>
378
+ Math.max(
379
+ minimum,
380
+ Math.min(
381
+ maximum,
382
+ getPerpendicularAxis(
383
+ connection.exitTargetPoint ?? connection.targetPoint,
384
+ boundaryDirection,
385
+ ),
386
+ ),
387
+ ),
388
+ )
389
+ .toSorted((a, b) => a - b)
390
+ const pitch = traceWidth + clearance
391
+ if (
392
+ !params.allowLayerInterleaving &&
393
+ tracks.every(
394
+ (track, index) =>
395
+ index === 0 || track - tracks[index - 1]! >= pitch - 1e-9,
396
+ )
397
+ )
398
+ return undefined
399
+ if (maximum - minimum < (tracks.length - 1) * pitch - 1e-9) return undefined
400
+ // Pool neighboring overlaps while preserving their mean requested location.
401
+ // Subtracting the pitch reduces the clearance constraint to monotonicity.
402
+ const blocks: { start: number; count: number; mean: number }[] = []
403
+ for (const [index, track] of tracks.entries()) {
404
+ blocks.push({ start: index, count: 1, mean: track - index * pitch })
405
+ while (blocks.length > 1 && blocks.at(-2)!.mean > blocks.at(-1)!.mean) {
406
+ const second = blocks.pop()!
407
+ const first = blocks.pop()!
408
+ blocks.push({
409
+ start: first.start,
410
+ count: first.count + second.count,
411
+ mean:
412
+ (first.mean * first.count + second.mean * second.count) /
413
+ (first.count + second.count),
414
+ })
415
+ }
416
+ }
417
+ for (const block of blocks) {
418
+ const mean = Math.max(
419
+ minimum,
420
+ Math.min(maximum - (tracks.length - 1) * pitch, block.mean),
421
+ )
422
+ for (let index = block.start; index < block.start + block.count; index++)
423
+ tracks[index] = mean + index * pitch
424
+ }
425
+ return tracks
426
+ }
427
+
314
428
  export function getBoundaryTargetTrack(params: {
315
429
  bus: PreparedBus
316
430
  connection: PreparedConnection
317
431
  boundaryDirection: FanoutDirection
432
+ traceWidth?: number
433
+ clearance?: number
434
+ layerNames?: readonly string[]
435
+ targetLayer?: string
436
+ windingOrderIndex?: number
437
+ allowLayerInterleaving?: boolean
318
438
  }): number {
319
439
  const requestedTrack = getPerpendicularAxis(
320
440
  params.connection.exitTargetPoint ?? params.connection.targetPoint,
@@ -326,6 +446,22 @@ export function getBoundaryTargetTrack(params: {
326
446
  const boundaryMaximum = isHorizontal(params.boundaryDirection)
327
447
  ? params.bus.sharedBoundary.maxY
328
448
  : params.bus.sharedBoundary.maxX
449
+ const distributedTracks =
450
+ params.traceWidth !== undefined && params.clearance !== undefined
451
+ ? getDistributedBoundaryTargetTracks({
452
+ ...params,
453
+ traceWidth: params.traceWidth,
454
+ clearance: params.clearance,
455
+ })
456
+ : undefined
457
+ if (distributedTracks && params.layerNames && params.targetLayer) {
458
+ const { rank } = getWindingTargetRank({
459
+ ...params,
460
+ layerNames: params.layerNames,
461
+ targetLayer: params.targetLayer,
462
+ })
463
+ return distributedTracks[rank]!
464
+ }
329
465
  return Math.max(boundaryMinimum, Math.min(boundaryMaximum, requestedTrack))
330
466
  }
331
467
 
@@ -1102,6 +1238,10 @@ function buildPlan(params: {
1102
1238
  bus,
1103
1239
  connection: preparedConnection,
1104
1240
  boundaryDirection,
1241
+ traceWidth,
1242
+ clearance,
1243
+ layerNames,
1244
+ targetLayer,
1105
1245
  })
1106
1246
  : track
1107
1247
  const connectionRank = getConnectionRank(bus, preparedConnection)
@@ -2459,6 +2599,7 @@ export function* routeBusAlternativesSteps(
2459
2599
  stopAfterFirstRejectedViaMinimalCandidate = false,
2460
2600
  fixedViaPointsByConnectionIndex,
2461
2601
  reservedVias = [],
2602
+ softReservedVias = [],
2462
2603
  viaMinimalOnly = false,
2463
2604
  allowBoundarySideViaFallback = false,
2464
2605
  preferCornerBoundaryVia = false,
@@ -2606,15 +2747,29 @@ export function* routeBusAlternativesSteps(
2606
2747
  windingOrderIndex?: number
2607
2748
  preferTargetDirectedLaneBias?: boolean
2608
2749
  localDogboneRepair?: boolean
2750
+ reserveTerminalExitPoints?: boolean
2751
+ cornerExitLaneOffset?: number
2752
+ allowLayerInterleaving?: boolean
2609
2753
  }
2610
2754
  const maximumThroughAllRouteOrderAttempts = 24
2611
- const windingTargetOrderCount = cornerSide
2612
- ? getWindingTargetOrders({
2755
+ const usesDistributedWindingTargets = Boolean(
2756
+ cornerSide ||
2757
+ getDistributedBoundaryTargetTracks({
2613
2758
  bus,
2614
2759
  boundaryDirection,
2615
- layerNames,
2616
- targetLayer,
2617
- }).orders.length
2760
+ traceWidth,
2761
+ clearance,
2762
+ }),
2763
+ )
2764
+ const windingTargetOrders = getWindingTargetOrders({
2765
+ bus,
2766
+ boundaryDirection,
2767
+ layerNames,
2768
+ targetLayer,
2769
+ })
2770
+ const windingTargetOrderCount = windingTargetOrders.orders.length
2771
+ const ordinaryWindingTargetOrderCount = usesDistributedWindingTargets
2772
+ ? windingTargetOrders.ordinaryOrderCount
2618
2773
  : 1
2619
2774
  const uniformDogboneTerminalPatterns: CoordinatedTerminalPattern[] =
2620
2775
  viaHandednesses.map((viaHandedness) => ({
@@ -2760,7 +2915,13 @@ export function* routeBusAlternativesSteps(
2760
2915
  const coordinatedViaPoints =
2761
2916
  fixedViaPointsByConnectionIndex ??
2762
2917
  (!allowBlindAndBuriedVias &&
2763
- bus.connections.length >= 8 &&
2918
+ (bus.connections.length >= 8 ||
2919
+ getDistributedBoundaryTargetTracks({
2920
+ bus,
2921
+ boundaryDirection,
2922
+ traceWidth,
2923
+ clearance,
2924
+ })) &&
2764
2925
  reservedVias.length === 0
2765
2926
  ? matchComponentDogboneViaSites([bus], {
2766
2927
  viaDiameter,
@@ -2789,7 +2950,7 @@ export function* routeBusAlternativesSteps(
2789
2950
  coordinatedViaPoints
2790
2951
  ? [
2791
2952
  ...Array.from(
2792
- { length: windingTargetOrderCount },
2953
+ { length: ordinaryWindingTargetOrderCount },
2793
2954
  (_, windingOrderIndex) => ({
2794
2955
  label: `component-matched-vias-winding-${windingOrderIndex}`,
2795
2956
  useViaInPad: false,
@@ -2813,6 +2974,26 @@ export function* routeBusAlternativesSteps(
2813
2974
  windingOrderIndex: 0,
2814
2975
  preferTargetDirectedLaneBias: true,
2815
2976
  },
2977
+ // Keep ordinary fixed-site attempts first. Forward retries reserve
2978
+ // future exits so an earlier lane cannot close their final gap.
2979
+ ...(!getCornerSide(bus) && windingTargetOrderCount > 1
2980
+ ? Array.from(
2981
+ { length: windingTargetOrderCount },
2982
+ (_, windingOrderIndex) =>
2983
+ [true, false].map((preferTargetDirectedLaneBias) => ({
2984
+ label: `expanded-winding-${windingOrderIndex}-${preferTargetDirectedLaneBias}`,
2985
+ useViaInPad: false,
2986
+ getViaHandedness: () => 0 as const,
2987
+ getViaPoint: (connection: PreparedConnection) =>
2988
+ coordinatedViaPoints.get(connection.connectionIndex)!,
2989
+ maximumRouteOrderAttempts: 1,
2990
+ windingOrderIndex,
2991
+ preferTargetDirectedLaneBias,
2992
+ reserveTerminalExitPoints: !preferTargetDirectedLaneBias,
2993
+ allowLayerInterleaving: true,
2994
+ })),
2995
+ ).flat()
2996
+ : []),
2816
2997
  ]
2817
2998
  : []
2818
2999
  const planeTerminationsAlreadyOccupyTheFanout = acceptedPlans.some(
@@ -2880,6 +3061,23 @@ export function* routeBusAlternativesSteps(
2880
3061
  }
2881
3062
  }
2882
3063
  }
3064
+ if (alignWindingGridToPads && cornerSide) {
3065
+ const layerLocalExitOffset = getCornerLaneOffsets(
3066
+ bus,
3067
+ acceptedPlans.filter((plan) => plan.targetLayer === targetLayer),
3068
+ ).exit
3069
+ if (layerLocalExitOffset !== cornerLaneOffsets.exit) {
3070
+ // Preserve successful shared-band routes first. If those patterns
3071
+ // fail, reuse the corner slots occupied only on other copper layers.
3072
+ terminalPatterns.push(
3073
+ ...terminalPatterns.map((pattern) => ({
3074
+ ...pattern,
3075
+ label: `${pattern.label}-layer-local-corner`,
3076
+ cornerExitLaneOffset: layerLocalExitOffset,
3077
+ })),
3078
+ )
3079
+ }
3080
+ }
2883
3081
  const seenTerminalSignatures = new Set<string>()
2884
3082
  for (const terminalPattern of terminalPatterns) {
2885
3083
  const terminals = bus.connections.map((preparedConnection) => {
@@ -2889,7 +3087,8 @@ export function* routeBusAlternativesSteps(
2889
3087
  ? getCornerTargetTrack({
2890
3088
  bus,
2891
3089
  connection: preparedConnection,
2892
- cornerExitLaneOffset: cornerLaneOffsets.exit,
3090
+ cornerExitLaneOffset:
3091
+ terminalPattern.cornerExitLaneOffset ?? cornerLaneOffsets.exit,
2893
3092
  traceWidth,
2894
3093
  viaDiameter,
2895
3094
  clearance,
@@ -2899,9 +3098,15 @@ export function* routeBusAlternativesSteps(
2899
3098
  cornerBandTargetTrackOffset,
2900
3099
  })
2901
3100
  : getBoundaryTargetTrack({
3101
+ allowLayerInterleaving: terminalPattern.allowLayerInterleaving,
2902
3102
  bus,
2903
3103
  connection: preparedConnection,
2904
3104
  boundaryDirection,
3105
+ traceWidth,
3106
+ clearance,
3107
+ layerNames,
3108
+ targetLayer,
3109
+ windingOrderIndex: terminalPattern.windingOrderIndex,
2905
3110
  })
2906
3111
  return {
2907
3112
  connection: preparedConnection,
@@ -2981,7 +3186,7 @@ export function* routeBusAlternativesSteps(
2981
3186
  )
2982
3187
  .join(
2983
3188
  "|",
2984
- )}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}:${gridStepDivisor}:${alignGridToPads}:${Boolean(terminalPattern.localDogboneRepair)}`
3189
+ )}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}:${gridStepDivisor}:${alignGridToPads}:${Boolean(terminalPattern.localDogboneRepair)}:${Boolean(terminalPattern.preferTargetDirectedLaneBias)}:${Boolean(terminalPattern.reserveTerminalExitPoints)}`
2985
3190
  if (seenTerminalSignatures.has(terminalSignature)) continue
2986
3191
  seenTerminalSignatures.add(terminalSignature)
2987
3192
  const windingSteps = routeViaMinimalWindingAlternativesSteps(
@@ -3003,6 +3208,8 @@ export function* routeBusAlternativesSteps(
3003
3208
  alignGridToPads,
3004
3209
  includeReverseTargetRotation: terminalPattern.localDogboneRepair,
3005
3210
  reservedVias,
3211
+ softReservedVias,
3212
+ reserveTerminalExitPoints: terminalPattern.reserveTerminalExitPoints,
3006
3213
  gridStepDivisor,
3007
3214
  preferTargetDirectedLaneBias:
3008
3215
  terminalPattern.preferTargetDirectedLaneBias,
@@ -3095,6 +3302,10 @@ export function* routeBusAlternativesSteps(
3095
3302
  bus,
3096
3303
  connection: preparedConnection,
3097
3304
  boundaryDirection,
3305
+ traceWidth,
3306
+ clearance,
3307
+ layerNames,
3308
+ targetLayer,
3098
3309
  }),
3099
3310
  )
3100
3311
  const finalExitPoints = finalTracks.map((track) =>
@@ -3182,47 +3393,64 @@ export function* routeBusAlternativesSteps(
3182
3393
  ),
3183
3394
  )
3184
3395
  : []
3185
- const singletonMatchedVia =
3186
- bus.connections.length === 1
3187
- ? fixedViaPointsByConnectionIndex.get(
3188
- bus.connections[0]!.connectionIndex,
3189
- )
3190
- : undefined
3191
- const packageEdgeViaCandidates = singletonMatchedVia
3192
- ? [
3193
- {
3194
- distance: sourceCenter.x - bus.componentBounds.minX,
3195
- point: {
3196
- x: bus.componentBounds.minX - 2 * insetStep,
3197
- y: singletonMatchedVia.y,
3198
- },
3199
- },
3200
- {
3201
- distance: bus.componentBounds.maxX - sourceCenter.x,
3202
- point: {
3203
- x: bus.componentBounds.maxX + 2 * insetStep,
3204
- y: singletonMatchedVia.y,
3205
- },
3206
- },
3207
- {
3208
- distance: sourceCenter.y - bus.componentBounds.minY,
3209
- point: {
3210
- x: singletonMatchedVia.x,
3211
- y: bus.componentBounds.minY - 2 * insetStep,
3212
- },
3213
- },
3214
- {
3215
- distance: bus.componentBounds.maxY - sourceCenter.y,
3216
- point: {
3217
- x: singletonMatchedVia.x,
3218
- y: bus.componentBounds.maxY + 2 * insetStep,
3219
- },
3220
- },
3221
- ]
3222
- .toSorted((first, second) => first.distance - second.distance)
3223
- .map(({ point }) => [point])
3224
- : []
3396
+ const matchedVias = bus.connections.map(
3397
+ (connection) =>
3398
+ fixedViaPointsByConnectionIndex.get(connection.connectionIndex)!,
3399
+ )
3400
+ const packageEdgeViaCandidates = [
3401
+ {
3402
+ distance: sourceCenter.x - bus.componentBounds.minX,
3403
+ axis: "x" as const,
3404
+ value: bus.componentBounds.minX - 2 * insetStep,
3405
+ },
3406
+ {
3407
+ distance: bus.componentBounds.maxX - sourceCenter.x,
3408
+ axis: "x" as const,
3409
+ value: bus.componentBounds.maxX + 2 * insetStep,
3410
+ },
3411
+ {
3412
+ distance: sourceCenter.y - bus.componentBounds.minY,
3413
+ axis: "y" as const,
3414
+ value: bus.componentBounds.minY - 2 * insetStep,
3415
+ },
3416
+ {
3417
+ distance: bus.componentBounds.maxY - sourceCenter.y,
3418
+ axis: "y" as const,
3419
+ value: bus.componentBounds.maxY + 2 * insetStep,
3420
+ },
3421
+ ]
3422
+ .toSorted((a, b) => a.distance - b.distance)
3423
+ .flatMap(({ axis, value }) => {
3424
+ const otherAxis = axis === "x" ? "y" : "x"
3425
+ const mean =
3426
+ matchedVias.reduce((sum, via) => sum + via[otherAxis], 0) /
3427
+ matchedVias.length
3428
+ const order = matchedVias
3429
+ .map((via, index) => ({ index, track: via[otherAxis] }))
3430
+ .toSorted((a, b) => a.track - b.track || a.index - b.index)
3431
+ const pitch = viaDiameter + clearance
3432
+ const points = matchedVias.map((via, index) => ({
3433
+ ...via,
3434
+ [axis]: value,
3435
+ [otherAxis]:
3436
+ order.length === 2 &&
3437
+ Math.abs(order[1]!.track - order[0]!.track) < pitch
3438
+ ? mean + (order.findIndex((v) => v.index === index) - 0.5) * pitch
3439
+ : via[otherAxis],
3440
+ }))
3441
+ return points.length === 2
3442
+ ? [points, [points[1]!, points[0]!]]
3443
+ : [points]
3444
+ })
3445
+ const preferPackageEdgeVias =
3446
+ bus.connections.length === 2 && Boolean(getCornerSide(bus))
3225
3447
  const viaCandidates = [
3448
+ ...(preferPackageEdgeVias ? packageEdgeViaCandidates : []).map(
3449
+ (points) => ({
3450
+ points,
3451
+ boundarySide: false,
3452
+ }),
3453
+ ),
3226
3454
  ...displacedViaCandidates.map((points) => ({
3227
3455
  points,
3228
3456
  boundarySide: false,
@@ -3231,13 +3459,15 @@ export function* routeBusAlternativesSteps(
3231
3459
  points,
3232
3460
  boundarySide: true,
3233
3461
  })),
3234
- // Preserve existing singleton escapes before trying a short source-layer
3235
- // route beyond the package. The target layer can then wind to the exit
3462
+ // Preserve existing centered and singleton escapes before trying a short
3463
+ // source-layer route beyond the package. The target layer can then wind to the exit
3236
3464
  // without a local via being fenced in by an already-routed wide bus.
3237
- ...packageEdgeViaCandidates.map((points) => ({
3238
- points,
3239
- boundarySide: false,
3240
- })),
3465
+ ...(!preferPackageEdgeVias ? packageEdgeViaCandidates : []).map(
3466
+ (points) => ({
3467
+ points,
3468
+ boundarySide: false,
3469
+ }),
3470
+ ),
3241
3471
  ]
3242
3472
  for (const { points: boundaryViaPoints, boundarySide } of viaCandidates) {
3243
3473
  const boundaryVias = bus.connections.map((connection, index) => ({
@@ -3301,6 +3531,7 @@ export function* routeBusAlternativesSteps(
3301
3531
  allowBlindAndBuriedVias,
3302
3532
  allowSameNetMerges,
3303
3533
  maximumRouteOrderAttempts: bus.connections.length === 1 ? 3 : 6,
3534
+ softReservedVias,
3304
3535
  reservedVias:
3305
3536
  bus.connections.length > 1
3306
3537
  ? [...reservedVias, ...boundaryVias]
@@ -3350,6 +3581,7 @@ export function* routeBusAlternativesSteps(
3350
3581
  allowSameNetMerges,
3351
3582
  maximumRouteOrderAttempts: 6,
3352
3583
  reservedVias,
3584
+ softReservedVias,
3353
3585
  gridStepDivisor: 2,
3354
3586
  alignGridToPads: true,
3355
3587
  },
@@ -62,6 +62,8 @@ export interface RouteViaMinimalWindingParams {
62
62
  allowSameNetMerges?: boolean
63
63
  maximumRouteOrderAttempts?: number
64
64
  reservedVias?: readonly ViaMinimalWindingReservedVia[]
65
+ /** Cost hints for provisional sites that the caller must rematch before commit. */
66
+ softReservedVias?: readonly ViaMinimalWindingReservedVia[]
65
67
  /** Use a finer uniform grid for narrow channels between reserved vias. */
66
68
  gridStepDivisor?: 1 | 2
67
69
  /** Bias bounded fixed-site searches toward the remote target band. */
@@ -74,6 +76,8 @@ export interface RouteViaMinimalWindingParams {
74
76
  alignGridToPads?: boolean
75
77
  /** Defer the outermost reversed target while routing the inner terminals. */
76
78
  includeReverseTargetRotation?: boolean
79
+ /** Keep earlier lanes clear of every remaining terminal exit. */
80
+ reserveTerminalExitPoints?: boolean
77
81
  }
78
82
 
79
83
  export interface RouteViaMinimalWindingProgress {
@@ -637,12 +641,14 @@ export function* routeViaMinimalWindingAlternativesSteps(
637
641
  allowSameNetMerges = false,
638
642
  maximumRouteOrderAttempts,
639
643
  reservedVias = [],
644
+ softReservedVias = [],
640
645
  gridStepDivisor = 1,
641
646
  preferTargetDirectedLaneBias = false,
642
647
  allowSourceLayerRouting = false,
643
648
  adaptiveRouteOrder = false,
644
649
  alignGridToPads = false,
645
650
  includeReverseTargetRotation = false,
651
+ reserveTerminalExitPoints = false,
646
652
  } = params
647
653
  if (
648
654
  maximumRouteOrderAttempts !== undefined &&
@@ -702,6 +708,40 @@ export function* routeViaMinimalWindingAlternativesSteps(
702
708
  point: { x: gridMinX + column * gridStep, y: gridMinY + row * gridStep },
703
709
  }
704
710
  })
711
+ // Provisional plane barrels guide search without being fixed obstacles.
712
+ // Touch only the small grid rectangles around each disk, not every node.
713
+ const softViaCosts = softReservedVias.length
714
+ ? new Float32Array(nodeCount)
715
+ : undefined
716
+ if (softViaCosts) {
717
+ for (const { via } of softReservedVias) {
718
+ if (!via.spanLayers.includes(targetLayer)) continue
719
+ const radius = via.diameter / 2 + traceWidth / 2 + clearance
720
+ const minimumColumn = Math.max(
721
+ 0,
722
+ Math.ceil((via.center.x - radius - gridMinX) / gridStep),
723
+ )
724
+ const maximumColumn = Math.min(
725
+ columnCount - 1,
726
+ Math.floor((via.center.x + radius - gridMinX) / gridStep),
727
+ )
728
+ const minimumRow = Math.max(
729
+ 0,
730
+ Math.ceil((via.center.y - radius - gridMinY) / gridStep),
731
+ )
732
+ const maximumRow = Math.min(
733
+ rowCount - 1,
734
+ Math.floor((via.center.y + radius - gridMinY) / gridStep),
735
+ )
736
+ for (let row = minimumRow; row <= maximumRow; row++) {
737
+ for (let column = minimumColumn; column <= maximumColumn; column++) {
738
+ const index = row * columnCount + column
739
+ if (distance(nodes[index]!.point, via.center) < radius)
740
+ softViaCosts[index] = softViaCosts[index]! + 25 * gridStep
741
+ }
742
+ }
743
+ }
744
+ }
705
745
  const sampledGridPoints = includeVisualization
706
746
  ? nodes
707
747
  .filter(
@@ -869,6 +909,14 @@ export function* routeViaMinimalWindingAlternativesSteps(
869
909
  return false
870
910
  }
871
911
  }
912
+ for (const other of reserveTerminalExitPoints ? terminals : []) {
913
+ if (sharesNet(connectionName, other.connection.connection.name)) continue
914
+ if (
915
+ distancePointToSegment(other.exitPoint, segment.start, segment.end) <
916
+ traceWidth + clearance - EPSILON
917
+ )
918
+ return false
919
+ }
872
920
  const segmentMinX = Math.min(segment.start.x, segment.end.x)
873
921
  const segmentMaxX = Math.max(segment.start.x, segment.end.x)
874
922
  const segmentMinY = Math.min(segment.start.y, segment.end.y)
@@ -1247,7 +1295,8 @@ export function* routeViaMinimalWindingAlternativesSteps(
1247
1295
  ? gridStep * Math.SQRT2
1248
1296
  : gridStep) +
1249
1297
  (addsTurn ? gridStep * 0.2 : 0) +
1250
- lanePenalty
1298
+ lanePenalty +
1299
+ (softViaCosts?.[nextNode] ?? 0)
1251
1300
  const nextState = nextNode * 9 + directionIndex
1252
1301
  if (nextDistance >= distances[nextState]! - EPSILON) continue
1253
1302
  const edgeIndex = current.node * 8 + directionIndex
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.59",
3
+ "version": "0.0.61",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",