@tscircuit/fanout-solver 0.0.60 → 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.
@@ -23,6 +23,7 @@ import {
23
23
  import { matchBusPlanLengths } from "./match-bus-lengths"
24
24
  import {
25
25
  getComponentDogboneViaSiteCandidates,
26
+ getSingleDogboneViaSiteRepairs,
26
27
  matchComponentDogboneViaSites,
27
28
  } from "./match-component-dogbone-via-sites"
28
29
  import { connectionsShareElectricalNet } from "./net-identity"
@@ -467,6 +468,9 @@ function createInitialLayerAssignment(params: {
467
468
  escapeLayers: string[]
468
469
  escapeLayersByBusId: Readonly<Record<string, readonly string[]>>
469
470
  preferOrderedCoordinatedWindingLayers: boolean
471
+ traceWidth: number
472
+ viaDiameter: number
473
+ clearance: number
470
474
  }): Readonly<Record<string, string>> {
471
475
  const {
472
476
  buses,
@@ -513,10 +517,151 @@ function createInitialLayerAssignment(params: {
513
517
  preferOrderedCoordinatedWindingLayers &&
514
518
  busUsesCoordinatedWinding(bus)
515
519
  ) {
516
- // Coordinated winding treats allowedLayers as an ordered preference.
517
- // A global round-robin index can otherwise skip a bus's first choice
518
- // just because a previous bus had a different set of legal layers.
519
- 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]!
520
665
  continue
521
666
  }
522
667
  const componentDirections = directionsByComponent.get(bus.componentId)!
@@ -1046,6 +1191,9 @@ export class FanoutSolver extends BaseSolver {
1046
1191
  buses: this.preparedBuses,
1047
1192
  escapeLayers: this.config.escapeLayers,
1048
1193
  escapeLayersByBusId: this.escapeLayersByBusId,
1194
+ traceWidth: this.config.traceWidth,
1195
+ viaDiameter: this.config.viaDiameter,
1196
+ clearance: this.config.clearance,
1049
1197
  preferOrderedCoordinatedWindingLayers:
1050
1198
  this.config.densePlaneReservationBusIds.length > 0 ||
1051
1199
  this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0 ||
@@ -1486,6 +1634,64 @@ export class FanoutSolver extends BaseSolver {
1486
1634
  const wideBoundaryBuses = unsortedBoundaryBuses.filter(
1487
1635
  (bus) => bus.connections.length >= 8,
1488
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
+ }
1489
1695
  const hasThreeWideBoundaryBuses =
1490
1696
  useConfiguredDensePlaneRouting && wideBoundaryBuses.length === 3
1491
1697
  const getBoundaryTargetSpan = (bus: PreparedBus) => {
@@ -2189,13 +2395,44 @@ export class FanoutSolver extends BaseSolver {
2189
2395
  bus,
2190
2396
  ]),
2191
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
+ }
2192
2430
  const areAdjacentInvertedNarrowBuses = (
2193
2431
  first: PreparedBus,
2194
2432
  second: PreparedBus,
2195
2433
  ): boolean => {
2196
2434
  if (
2197
2435
  !usePadAlignedDenseRouting ||
2198
- useConfiguredDensePlaneRouting ||
2199
2436
  first.componentId !== second.componentId ||
2200
2437
  first.exitEdge !== second.exitEdge ||
2201
2438
  first.direction !== second.direction ||
@@ -2259,7 +2496,7 @@ export class FanoutSolver extends BaseSolver {
2259
2496
  // turning pair instead leaves its adjacent singleton room to escape
2260
2497
  // before searching for an outside-package via.
2261
2498
  const centeredPairPromotionGroups = new Set<string>()
2262
- for (const singleton of multiLayerLeadingSingletonBuses) {
2499
+ for (const singleton of leadingWideSingletonBuses) {
2263
2500
  if (getCornerBandSide(singleton.exitEdge, singleton.preferredExit))
2264
2501
  continue
2265
2502
  for (const pair of boundaryBuses) {
@@ -2300,6 +2537,25 @@ export class FanoutSolver extends BaseSolver {
2300
2537
  }
2301
2538
  }
2302
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
+ }
2303
2559
  let fixedViaPointsByConnectionIndex: ReadonlyMap<
2304
2560
  number,
2305
2561
  { x: number; y: number }
@@ -2447,7 +2703,7 @@ export class FanoutSolver extends BaseSolver {
2447
2703
  useConfiguredDensePlaneRouting &&
2448
2704
  singleLayerBus !== bus &&
2449
2705
  !embeddedNarrowBusAlreadyRouted
2450
- let usedSoftPlaneRepair = false
2706
+ let usedRepairedViaSites = false
2451
2707
  let busPlans = (yield* routeAlternatives(
2452
2708
  preferSingleLayerWinding
2453
2709
  ? { ...routeParams, bus: singleLayerBus }
@@ -2554,24 +2810,134 @@ export class FanoutSolver extends BaseSolver {
2554
2810
  }
2555
2811
  }
2556
2812
  }
2557
- // Once wide-bus copper constrains the field, retry a blocked wide bus
2558
- // with provisional plane sites as search costs. Future boundary sites
2559
- // remain fixed, and a complete joint rematch must validate the repair.
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.
2560
2817
  if (
2561
2818
  !busPlans &&
2819
+ useAdaptiveDensePlaneRouting &&
2562
2820
  bus.connections.length >= 8 &&
2563
- boundaryBuses.some(
2564
- (candidate) =>
2565
- candidate.connections.length >= 8 &&
2566
- matchedPlans.some((plan) => plan.busId === candidate.busId),
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
+ ),
2567
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
+ ))
2568
2930
  ) {
2569
2931
  const committedNames = new Set(
2570
2932
  matchedPlans.map((plan) => plan.connectionName),
2571
2933
  )
2572
- const futurePlaneNames = new Set(
2934
+ const provisionalReservationNames = new Set(
2573
2935
  this.preparedBuses
2574
- .filter((candidate) => candidate.termination.type === "plane")
2936
+ .filter(
2937
+ (candidate) =>
2938
+ candidate.termination.type === "plane" ||
2939
+ candidate === adjacentCenteredFieldByTurningBus.get(bus),
2940
+ )
2575
2941
  .flatMap((candidate) =>
2576
2942
  candidate.connections.map(
2577
2943
  (connection) => connection.connection.name,
@@ -2584,10 +2950,11 @@ export class FanoutSolver extends BaseSolver {
2584
2950
  ...routeParams,
2585
2951
  fixedViaPointsByConnectionIndex: undefined,
2586
2952
  reservedVias: routeParams.reservedVias.filter(
2587
- (reserved) => !futurePlaneNames.has(reserved.connectionName),
2953
+ (reserved) =>
2954
+ !provisionalReservationNames.has(reserved.connectionName),
2588
2955
  ),
2589
2956
  softReservedVias: routeParams.reservedVias.filter((reserved) =>
2590
- futurePlaneNames.has(reserved.connectionName),
2957
+ provisionalReservationNames.has(reserved.connectionName),
2591
2958
  ),
2592
2959
  },
2593
2960
  1,
@@ -2630,7 +2997,7 @@ export class FanoutSolver extends BaseSolver {
2630
2997
  if (rematchedPoints) {
2631
2998
  busPlans = freePlans
2632
2999
  fixedViaPointsByConnectionIndex = rematchedPoints
2633
- usedSoftPlaneRepair = true
3000
+ usedRepairedViaSites = true
2634
3001
  }
2635
3002
  }
2636
3003
  }
@@ -2647,12 +3014,12 @@ export class FanoutSolver extends BaseSolver {
2647
3014
  // Only pay for additional A* variants when the first topology is so
2648
3015
  // skewed that compact meanders are unlikely to absorb the deficit.
2649
3016
  // This keeps already-near-matched buses on the single-attempt path.
2650
- // Keep the jointly rematched repair: routeParams still carries the
3017
+ // Keep repaired via sites: routeParams still carries the
2651
3018
  // earlier provisional sites and cannot safely replace its geometry.
2652
3019
  if (
2653
3020
  needsRouteDiversity &&
2654
3021
  !matchLengthsAfterPlanes &&
2655
- !usedSoftPlaneRepair
3022
+ !usedRepairedViaSites
2656
3023
  ) {
2657
3024
  busPlans = (yield* routeAlternatives(routeParams, 3)).toSorted(
2658
3025
  (first, second) => {
@@ -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
@@ -165,6 +165,7 @@ function getWindingTargetOrders(params: {
165
165
  }): {
166
166
  orders: PreparedConnection[][]
167
167
  legacyOrder: PreparedConnection[]
168
+ ordinaryOrderCount: number
168
169
  } {
169
170
  const { bus, boundaryDirection, layerNames, targetLayer } = params
170
171
  const getTargetLayer = (candidate: PreparedConnection): string =>
@@ -268,6 +269,41 @@ function getWindingTargetOrders(params: {
268
269
  // but do not let sub-nanometer noise between unrelated layer bands choose
269
270
  // the primary topology.
270
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
+ }
271
307
  const seenOrders = new Set<string>()
272
308
  const orders = candidateOrders.filter((order) => {
273
309
  const key = order.map((candidate) => candidate.connectionIndex).join(",")
@@ -275,7 +311,7 @@ function getWindingTargetOrders(params: {
275
311
  seenOrders.add(key)
276
312
  return true
277
313
  })
278
- return { orders, legacyOrder: legacyOrderedConnections }
314
+ return { orders, legacyOrder: legacyOrderedConnections, ordinaryOrderCount }
279
315
  }
280
316
 
281
317
  function getWindingTargetRank(params: {
@@ -318,6 +354,7 @@ function getDistributedBoundaryTargetTracks(params: {
318
354
  boundaryDirection: FanoutDirection
319
355
  traceWidth: number
320
356
  clearance: number
357
+ allowLayerInterleaving?: boolean
321
358
  }): number[] | undefined {
322
359
  const { bus, boundaryDirection, traceWidth, clearance } = params
323
360
  if (getCornerSide(bus) || !busUsesCoordinatedWindingChannel(bus))
@@ -352,6 +389,7 @@ function getDistributedBoundaryTargetTracks(params: {
352
389
  .toSorted((a, b) => a - b)
353
390
  const pitch = traceWidth + clearance
354
391
  if (
392
+ !params.allowLayerInterleaving &&
355
393
  tracks.every(
356
394
  (track, index) =>
357
395
  index === 0 || track - tracks[index - 1]! >= pitch - 1e-9,
@@ -396,6 +434,7 @@ export function getBoundaryTargetTrack(params: {
396
434
  layerNames?: readonly string[]
397
435
  targetLayer?: string
398
436
  windingOrderIndex?: number
437
+ allowLayerInterleaving?: boolean
399
438
  }): number {
400
439
  const requestedTrack = getPerpendicularAxis(
401
440
  params.connection.exitTargetPoint ?? params.connection.targetPoint,
@@ -2708,23 +2747,30 @@ export function* routeBusAlternativesSteps(
2708
2747
  windingOrderIndex?: number
2709
2748
  preferTargetDirectedLaneBias?: boolean
2710
2749
  localDogboneRepair?: boolean
2750
+ reserveTerminalExitPoints?: boolean
2751
+ cornerExitLaneOffset?: number
2752
+ allowLayerInterleaving?: boolean
2711
2753
  }
2712
2754
  const maximumThroughAllRouteOrderAttempts = 24
2713
- const windingTargetOrderCount =
2755
+ const usesDistributedWindingTargets = Boolean(
2714
2756
  cornerSide ||
2715
- getDistributedBoundaryTargetTracks({
2716
- bus,
2717
- boundaryDirection,
2718
- traceWidth,
2719
- clearance,
2720
- })
2721
- ? getWindingTargetOrders({
2722
- bus,
2723
- boundaryDirection,
2724
- layerNames,
2725
- targetLayer,
2726
- }).orders.length
2727
- : 1
2757
+ getDistributedBoundaryTargetTracks({
2758
+ bus,
2759
+ boundaryDirection,
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
2773
+ : 1
2728
2774
  const uniformDogboneTerminalPatterns: CoordinatedTerminalPattern[] =
2729
2775
  viaHandednesses.map((viaHandedness) => ({
2730
2776
  label: `uniform-${viaHandedness}`,
@@ -2904,7 +2950,7 @@ export function* routeBusAlternativesSteps(
2904
2950
  coordinatedViaPoints
2905
2951
  ? [
2906
2952
  ...Array.from(
2907
- { length: windingTargetOrderCount },
2953
+ { length: ordinaryWindingTargetOrderCount },
2908
2954
  (_, windingOrderIndex) => ({
2909
2955
  label: `component-matched-vias-winding-${windingOrderIndex}`,
2910
2956
  useViaInPad: false,
@@ -2928,6 +2974,26 @@ export function* routeBusAlternativesSteps(
2928
2974
  windingOrderIndex: 0,
2929
2975
  preferTargetDirectedLaneBias: true,
2930
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
+ : []),
2931
2997
  ]
2932
2998
  : []
2933
2999
  const planeTerminationsAlreadyOccupyTheFanout = acceptedPlans.some(
@@ -2995,6 +3061,23 @@ export function* routeBusAlternativesSteps(
2995
3061
  }
2996
3062
  }
2997
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
+ }
2998
3081
  const seenTerminalSignatures = new Set<string>()
2999
3082
  for (const terminalPattern of terminalPatterns) {
3000
3083
  const terminals = bus.connections.map((preparedConnection) => {
@@ -3004,7 +3087,8 @@ export function* routeBusAlternativesSteps(
3004
3087
  ? getCornerTargetTrack({
3005
3088
  bus,
3006
3089
  connection: preparedConnection,
3007
- cornerExitLaneOffset: cornerLaneOffsets.exit,
3090
+ cornerExitLaneOffset:
3091
+ terminalPattern.cornerExitLaneOffset ?? cornerLaneOffsets.exit,
3008
3092
  traceWidth,
3009
3093
  viaDiameter,
3010
3094
  clearance,
@@ -3014,6 +3098,7 @@ export function* routeBusAlternativesSteps(
3014
3098
  cornerBandTargetTrackOffset,
3015
3099
  })
3016
3100
  : getBoundaryTargetTrack({
3101
+ allowLayerInterleaving: terminalPattern.allowLayerInterleaving,
3017
3102
  bus,
3018
3103
  connection: preparedConnection,
3019
3104
  boundaryDirection,
@@ -3101,7 +3186,7 @@ export function* routeBusAlternativesSteps(
3101
3186
  )
3102
3187
  .join(
3103
3188
  "|",
3104
- )}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}:${gridStepDivisor}:${alignGridToPads}:${Boolean(terminalPattern.localDogboneRepair)}`
3189
+ )}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}:${gridStepDivisor}:${alignGridToPads}:${Boolean(terminalPattern.localDogboneRepair)}:${Boolean(terminalPattern.preferTargetDirectedLaneBias)}:${Boolean(terminalPattern.reserveTerminalExitPoints)}`
3105
3190
  if (seenTerminalSignatures.has(terminalSignature)) continue
3106
3191
  seenTerminalSignatures.add(terminalSignature)
3107
3192
  const windingSteps = routeViaMinimalWindingAlternativesSteps(
@@ -3124,6 +3209,7 @@ export function* routeBusAlternativesSteps(
3124
3209
  includeReverseTargetRotation: terminalPattern.localDogboneRepair,
3125
3210
  reservedVias,
3126
3211
  softReservedVias,
3212
+ reserveTerminalExitPoints: terminalPattern.reserveTerminalExitPoints,
3127
3213
  gridStepDivisor,
3128
3214
  preferTargetDirectedLaneBias:
3129
3215
  terminalPattern.preferTargetDirectedLaneBias,
@@ -76,6 +76,8 @@ export interface RouteViaMinimalWindingParams {
76
76
  alignGridToPads?: boolean
77
77
  /** Defer the outermost reversed target while routing the inner terminals. */
78
78
  includeReverseTargetRotation?: boolean
79
+ /** Keep earlier lanes clear of every remaining terminal exit. */
80
+ reserveTerminalExitPoints?: boolean
79
81
  }
80
82
 
81
83
  export interface RouteViaMinimalWindingProgress {
@@ -646,6 +648,7 @@ export function* routeViaMinimalWindingAlternativesSteps(
646
648
  adaptiveRouteOrder = false,
647
649
  alignGridToPads = false,
648
650
  includeReverseTargetRotation = false,
651
+ reserveTerminalExitPoints = false,
649
652
  } = params
650
653
  if (
651
654
  maximumRouteOrderAttempts !== undefined &&
@@ -906,6 +909,14 @@ export function* routeViaMinimalWindingAlternativesSteps(
906
909
  return false
907
910
  }
908
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
+ }
909
920
  const segmentMinX = Math.min(segment.start.x, segment.end.x)
910
921
  const segmentMaxX = Math.max(segment.start.x, segment.end.x)
911
922
  const segmentMinY = Math.min(segment.start.y, segment.end.y)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.60",
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",