@tscircuit/fanout-solver 0.0.63 → 0.0.65

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.
@@ -39,6 +39,13 @@ import {
39
39
  routeBusAlternatives,
40
40
  routeBusAlternativesSteps,
41
41
  } from "./route-bus"
42
+ import { routePeripheralSourceEscapesSteps } from "./route-peripheral-source-escapes"
43
+ import { routeStagedPerimeterBusSteps } from "./route-staged-perimeter-bus"
44
+ import { routeReservedSourceBusesSteps } from "./route-reserved-source-buses"
45
+ import { repairPeripheralBusLengthsSteps } from "./repair-peripheral-bus-lengths"
46
+ import { routeSplitPerimeterSourceEscapesSteps } from "./route-split-perimeter-source-escapes"
47
+ import { routeSplitPerimeterBusSteps } from "./route-split-perimeter-bus"
48
+ import { routeShallowSplitPerimeterBusSteps } from "./route-shallow-split-perimeter-bus"
42
49
  import { routeSingleLayerWithAdaptiveExitsSteps } from "./route-single-layer-adaptive-exits"
43
50
  import { routeSingleLayerWithPushAndShove } from "./route-single-layer-push-shove"
44
51
  import { getRuntimeProcess } from "./runtime-process"
@@ -84,6 +91,8 @@ interface ResolvedFanoutConfig {
84
91
 
85
92
  interface EvaluatedAssignment extends AssignmentAttempt {
86
93
  blockingBusIds: string[]
94
+ /** The bounded peripheral strategy may finish after complete validation. */
95
+ stopAfterCompleteValidation?: true
87
96
  }
88
97
 
89
98
  interface GroupedBeamState {
@@ -94,6 +103,7 @@ interface GroupedBeamState {
94
103
  interface MixedTerminationState {
95
104
  plans: FanoutRoutePlan[]
96
105
  failedBusIds: string[]
106
+ stopAfterCompleteValidation?: true
97
107
  }
98
108
 
99
109
  type RoutingStrategy = "default" | "group-by-layer" | "deep-first"
@@ -513,6 +523,43 @@ function createInitialLayerAssignment(params: {
513
523
  ) {
514
524
  assignment[bus.busId] = sourceLayer
515
525
  } else if (viaLayers.length > 0) {
526
+ // A source strictly inside a wide field can be enclosed by its routes.
527
+ // Boundary sources still have an outward channel, so preserve the
528
+ // ordinary layer preference for those buses.
529
+ const sourceXs = bus.connections.map(
530
+ (connection) => connection.sourcePoint.x,
531
+ )
532
+ const sourceYs = bus.connections.map(
533
+ (connection) => connection.sourcePoint.y,
534
+ )
535
+ const embeddedSingletonCount = (layer: string) =>
536
+ buses.filter((other) => {
537
+ const point = other.connections[0]?.sourcePoint
538
+ return (
539
+ other.termination.type === "boundary" &&
540
+ point !== undefined &&
541
+ point.x > Math.min(...sourceXs) + 1e-9 &&
542
+ point.x < Math.max(...sourceXs) - 1e-9 &&
543
+ point.y > Math.min(...sourceYs) + 1e-9 &&
544
+ point.y < Math.max(...sourceYs) - 1e-9 &&
545
+ getCommonExplicitExitTargetLayer(other) === layer &&
546
+ isDenseSingletonEmbeddedInMultiLayerWideBus({
547
+ singletonBus: other,
548
+ singletonTargetLayer: layer,
549
+ wideBuses: [bus],
550
+ })
551
+ )
552
+ }).length
553
+ const isolatedLayers = viaLayers.toSorted(
554
+ (a, b) => embeddedSingletonCount(a) - embeddedSingletonCount(b),
555
+ )
556
+ if (
557
+ embeddedSingletonCount(isolatedLayers[0]!) <
558
+ embeddedSingletonCount(viaLayers[0]!)
559
+ ) {
560
+ assignment[bus.busId] = isolatedLayers[0]!
561
+ continue
562
+ }
516
563
  if (
517
564
  preferOrderedCoordinatedWindingLayers &&
518
565
  busUsesCoordinatedWinding(bus)
@@ -1535,6 +1582,127 @@ export class FanoutSolver extends BaseSolver {
1535
1582
  })
1536
1583
  }
1537
1584
 
1585
+ /** Reserve source escapes before joining complete buses around the package. */
1586
+ private *routePeripheralMixedTerminationSteps(params: {
1587
+ busLayerAssignments: Readonly<Record<string, string>>
1588
+ busesInRoutingOrder: readonly PreparedBus[]
1589
+ }): Generator<FanoutWorkYield, MixedTerminationState | null, unknown> {
1590
+ if (this.config.allowBlindAndBuriedVias) return null
1591
+ const bus = params.busesInRoutingOrder.find(
1592
+ (candidate) =>
1593
+ candidate.termination.type === "boundary" &&
1594
+ (candidate.exitEdge === "right" || candidate.exitEdge === "left") &&
1595
+ candidate.connections.length >= 16 &&
1596
+ candidate.componentObstacles.length >= 200,
1597
+ )
1598
+ if (!bus) return null
1599
+ const targetLayer = params.busLayerAssignments[bus.busId]
1600
+ if (!targetLayer) return null
1601
+ const targetLayerByBusId = new Map(
1602
+ Object.entries(params.busLayerAssignments),
1603
+ )
1604
+ this.setInProgressPlans({
1605
+ phase: "route-peripheral-source-escapes",
1606
+ plans: [],
1607
+ busId: bus.busId,
1608
+ })
1609
+ const sourceSteps = (
1610
+ bus.exitEdge === "left"
1611
+ ? routeSplitPerimeterSourceEscapesSteps
1612
+ : routePeripheralSourceEscapesSteps
1613
+ )({
1614
+ ...this.config,
1615
+ srj: this.routingSrj,
1616
+ buses: this.preparedBuses,
1617
+ bus,
1618
+ targetLayer,
1619
+ targetLayerByBusId,
1620
+ })
1621
+ let sourceResult = sourceSteps.next()
1622
+ while (!sourceResult.done) {
1623
+ yield
1624
+ sourceResult = sourceSteps.next()
1625
+ }
1626
+ if (!sourceResult.value) return null
1627
+ let source = sourceResult.value
1628
+ const stageParams = {
1629
+ ...this.config,
1630
+ ...source,
1631
+ srj: this.routingSrj,
1632
+ bus,
1633
+ targetLayer,
1634
+ }
1635
+ const stageSteps =
1636
+ "lowerConnectionIndices" in source
1637
+ ? routeSplitPerimeterBusSteps({ ...stageParams, ...source })
1638
+ : routeStagedPerimeterBusSteps(stageParams)
1639
+ let stageResult = stageSteps.next()
1640
+ while (!stageResult.done) {
1641
+ yield
1642
+ stageResult = stageSteps.next()
1643
+ }
1644
+ let stagePlans = stageResult.value
1645
+ if (!stagePlans && "lowerConnectionIndices" in source) {
1646
+ const shallowSteps = routeShallowSplitPerimeterBusSteps({
1647
+ ...stageParams,
1648
+ ...source,
1649
+ buses: this.preparedBuses,
1650
+ })
1651
+ let shallowResult = shallowSteps.next()
1652
+ while (!shallowResult.done) {
1653
+ yield
1654
+ shallowResult = shallowSteps.next()
1655
+ }
1656
+ if (shallowResult.value) {
1657
+ source = shallowResult.value
1658
+ stagePlans = shallowResult.value.plans
1659
+ }
1660
+ }
1661
+ if (!stagePlans) return null
1662
+ this.setInProgressPlans({
1663
+ phase: "route-peripheral-bus-continuations",
1664
+ plans: stagePlans,
1665
+ busId: bus.busId,
1666
+ })
1667
+ const remainingSteps = routeReservedSourceBusesSteps({
1668
+ ...this.config,
1669
+ srj: this.routingSrj,
1670
+ buses: this.preparedBuses,
1671
+ sourceEscapes: source.sourceEscapes,
1672
+ sourceBoundary: source.sourceBoundary,
1673
+ initialPlans: stagePlans,
1674
+ targetLayerByBusId,
1675
+ })
1676
+ let remainingResult = remainingSteps.next()
1677
+ while (!remainingResult.done) {
1678
+ yield
1679
+ remainingResult = remainingSteps.next()
1680
+ }
1681
+ if (!remainingResult.value) return null
1682
+ const repairSteps = repairPeripheralBusLengthsSteps({
1683
+ ...this.config,
1684
+ srj: this.routingSrj,
1685
+ inputSrj: this.inputSrj,
1686
+ plans: remainingResult.value,
1687
+ preparedBuses: this.preparedBuses,
1688
+ sharedBoundary: this.getValidationBoundary(),
1689
+ })
1690
+ let repairResult = repairSteps.next()
1691
+ while (!repairResult.done) {
1692
+ yield
1693
+ repairResult = repairSteps.next()
1694
+ }
1695
+ if (!repairResult.value) return null
1696
+ const plans = repairResult.value
1697
+ const output = buildOutputSimpleRouteJson({
1698
+ inputSrj: this.inputSrj,
1699
+ plans,
1700
+ layerNames: this.config.layerNames,
1701
+ })
1702
+ if (!this.validateCompletePlans(plans, output).valid) return null
1703
+ return { plans, failedBusIds: [], stopAfterCompleteValidation: true }
1704
+ }
1705
+
1538
1706
  /**
1539
1707
  * Through-all source vias from a wide boundary bus can consume the only
1540
1708
  * legal dogbone channel for nearby plane pads. Conversely, routing hundreds
@@ -1551,6 +1719,11 @@ export class FanoutSolver extends BaseSolver {
1551
1719
  promotedPlaneReservationBusIds?: readonly string[]
1552
1720
  preferredBoundaryViaPoints?: ReadonlyMap<number, { x: number; y: number }>
1553
1721
  planeReservationRetryCount?: number
1722
+ boundaryRecovery?: {
1723
+ busId: string
1724
+ preferOutward: boolean
1725
+ perpendicularSide: -1 | 1
1726
+ }
1554
1727
  }): Generator<FanoutWorkYield, MixedTerminationState | null, unknown> {
1555
1728
  if (this.config.allowBlindAndBuriedVias) return null
1556
1729
  // An outside-package singleton escape can depend on the completed signal
@@ -1601,12 +1774,16 @@ export class FanoutSolver extends BaseSolver {
1601
1774
  )
1602
1775
  const useConfiguredDensePlaneRouting =
1603
1776
  configuredDensePlaneRouting || useAdaptiveDensePlaneRouting
1777
+ const useBoundaryRecovery = params.boundaryRecovery !== undefined
1778
+ const useJointPlaneRepair =
1779
+ useConfiguredDensePlaneRouting || useBoundaryRecovery
1604
1780
  // A completed boundary assignment remains worth repairing jointly after
1605
1781
  // plane reservations change; a greedy refill can discard that assignment.
1606
- const useAdaptiveJointPlaneSelection =
1607
- useAdaptiveDensePlaneRouting &&
1608
- ((params.planeReservationRetryCount ?? 0) === 0 ||
1609
- Boolean(params.preferredBoundaryViaPoints))
1782
+ const useJointPlaneSelection =
1783
+ useBoundaryRecovery ||
1784
+ (useAdaptiveDensePlaneRouting &&
1785
+ ((params.planeReservationRetryCount ?? 0) === 0 ||
1786
+ Boolean(params.preferredBoundaryViaPoints)))
1610
1787
  const matchLengthsAfterPlanes =
1611
1788
  useConfiguredDensePlaneRouting &&
1612
1789
  params.lengthMatchingStage !== "before-planes"
@@ -1852,7 +2029,9 @@ export class FanoutSolver extends BaseSolver {
1852
2029
  process.env.FANOUT_DEBUG_BOUNDARY_ORDER?.split(",") ??
1853
2030
  (process.env.FANOUT_DEBUG_FIRST_BOUNDARY_BUS
1854
2031
  ? [process.env.FANOUT_DEBUG_FIRST_BOUNDARY_BUS]
1855
- : [])
2032
+ : params.boundaryRecovery
2033
+ ? [params.boundaryRecovery.busId]
2034
+ : [])
1856
2035
  const boundaryBuses =
1857
2036
  debugBoundaryOrder.length > 0
1858
2037
  ? initiallySortedBoundaryBuses.toSorted((first, second) => {
@@ -1914,6 +2093,7 @@ export class FanoutSolver extends BaseSolver {
1914
2093
  ]
1915
2094
  }
1916
2095
  const unroutablePlaneBusIds = new Set<string>()
2096
+ let failedWideBoundaryBus: PreparedBus | undefined
1917
2097
  debugDense(
1918
2098
  "start",
1919
2099
  boundaryBuses.map((bus) => `${bus.busId}:${bus.connections.length}`),
@@ -2015,6 +2195,16 @@ export class FanoutSolver extends BaseSolver {
2015
2195
  if (entersNeighboringSourceField)
2016
2196
  preferBoundaryOutwardByBusId.set(bus.busId, false)
2017
2197
  }
2198
+ if (params.boundaryRecovery) {
2199
+ preferredBoundaryPerpendicularSideByBusId.set(
2200
+ params.boundaryRecovery.busId,
2201
+ params.boundaryRecovery.perpendicularSide,
2202
+ )
2203
+ preferBoundaryOutwardByBusId.set(
2204
+ params.boundaryRecovery.busId,
2205
+ params.boundaryRecovery.preferOutward,
2206
+ )
2207
+ }
2018
2208
  const debugFlippedBoundaryBus = process.env.FANOUT_DEBUG_FLIP_BOUNDARY_BUS
2019
2209
  if (debugFlippedBoundaryBus) {
2020
2210
  preferredBoundaryPerpendicularSideByBusId.set(debugFlippedBoundaryBus, -1)
@@ -2653,6 +2843,7 @@ export class FanoutSolver extends BaseSolver {
2653
2843
  allowBoundarySideViaFallback: bus.connections.length === 1,
2654
2844
  preferCornerBoundaryVia: useConfiguredDensePlaneRouting,
2655
2845
  adaptiveWindingRouteOrder,
2846
+ allowFixedViaReservedExitFallback: useBoundaryRecovery,
2656
2847
  // Retain pad-aligned channels even when plane sites are reserved
2657
2848
  // adaptively; the boundary grid can fence off a turning wide bus.
2658
2849
  alignWindingGridToPads:
@@ -3035,6 +3226,7 @@ export class FanoutSolver extends BaseSolver {
3035
3226
  }
3036
3227
  }
3037
3228
  if (!busPlans) {
3229
+ if (bus.connections.length >= 8) failedWideBoundaryBus ??= bus
3038
3230
  debugDense("route:failed", bus.busId)
3039
3231
  return false
3040
3232
  }
@@ -3365,6 +3557,7 @@ export class FanoutSolver extends BaseSolver {
3365
3557
  promotedAlternatePlaneBusIds: ReadonlySet<string> = new Set(),
3366
3558
  ): Map<number, { x: number; y: number }> | null => {
3367
3559
  feasibleAlternatePlanePlans = []
3560
+ matchedPlaneBusesInRoutingOrder = null
3368
3561
  const fixedBoundaryViaPoints = new Map(
3369
3562
  candidatePlans.flatMap((plan) =>
3370
3563
  plan.via
@@ -3431,11 +3624,16 @@ export class FanoutSolver extends BaseSolver {
3431
3624
  if (retainedViaPoints)
3432
3625
  return new Map([...fixedBoundaryViaPoints, ...retainedViaPoints])
3433
3626
  if (
3434
- useConfiguredDensePlaneRouting ||
3627
+ useJointPlaneRepair ||
3435
3628
  process.env.FANOUT_DEBUG_INCREMENTAL_PLANE_MATCH === "1"
3436
3629
  ) {
3437
3630
  let incrementalViaPoints = new Map(fixedBoundaryViaPoints)
3438
- const matchedPlaneBuses = [...activeBoundaryReservationPlaneBuses]
3631
+ // Reservations guided the boundary search, but their copper is
3632
+ // still uncommitted. Recovery must choose local and longer plane
3633
+ // escapes together instead of locking every provisional site.
3634
+ const matchedPlaneBuses = useBoundaryRecovery
3635
+ ? []
3636
+ : [...activeBoundaryReservationPlaneBuses]
3439
3637
  for (const planeBus of matchedPlaneBuses) {
3440
3638
  for (const connection of planeBus.connections) {
3441
3639
  const reservedPoint = fixedViaPointsByConnectionIndex.get(
@@ -3581,7 +3779,7 @@ export class FanoutSolver extends BaseSolver {
3581
3779
  independentlyUnmatchablePlaneBuses.map((bus) => bus.busId),
3582
3780
  )
3583
3781
  if (
3584
- !useConfiguredDensePlaneRouting &&
3782
+ !useJointPlaneRepair &&
3585
3783
  process.env.FANOUT_DEBUG_ROUTE_UNMATCHED_PLANES !== "1"
3586
3784
  ) {
3587
3785
  return null
@@ -3596,7 +3794,9 @@ export class FanoutSolver extends BaseSolver {
3596
3794
  ? 10_000
3597
3795
  : useConfiguredDensePlaneRouting
3598
3796
  ? 3_000_000
3599
- : 1_000),
3797
+ : useBoundaryRecovery
3798
+ ? 10_000
3799
+ : 1_000),
3600
3800
  )
3601
3801
  const maximumAlternatePlaneRoutes = Number(
3602
3802
  process.env.FANOUT_DEBUG_ALTERNATE_ROUTE_COUNT ??
@@ -3774,7 +3974,7 @@ export class FanoutSolver extends BaseSolver {
3774
3974
  }
3775
3975
  let alternatePlanePlans: FanoutRoutePlan[] | null
3776
3976
  if (
3777
- useConfiguredDensePlaneRouting ||
3977
+ useJointPlaneRepair ||
3778
3978
  process.env.FANOUT_DEBUG_EXACT_COVER_ALTERNATES === "1"
3779
3979
  ) {
3780
3980
  type IndependentPlaneRouteCandidate = {
@@ -3795,7 +3995,7 @@ export class FanoutSolver extends BaseSolver {
3795
3995
  })),
3796
3996
  }),
3797
3997
  )
3798
- if (useAdaptiveJointPlaneSelection) {
3998
+ if (useJointPlaneSelection) {
3799
3999
  const acceptedPlans = [
3800
4000
  ...candidatePlans,
3801
4001
  ...feasibleAlternatePlanePlans,
@@ -4106,7 +4306,7 @@ export class FanoutSolver extends BaseSolver {
4106
4306
  )
4107
4307
  if (!alternatePlanePlans) return null
4108
4308
  feasibleAlternatePlanePlans = alternatePlanePlans
4109
- if (useAdaptiveJointPlaneSelection) {
4309
+ if (useJointPlaneSelection) {
4110
4310
  matchedPlaneBusesInRoutingOrder = []
4111
4311
  return new Map([
4112
4312
  ...fixedBoundaryViaPoints,
@@ -4620,6 +4820,33 @@ export class FanoutSolver extends BaseSolver {
4620
4820
  denseRoutingStrategy: "boundary-aligned",
4621
4821
  })
4622
4822
  if (boundaryAlignedState) return boundaryAlignedState
4823
+ // A later wide bus can be fenced by provisional through-vias even
4824
+ // though its requested exit order is routable. After both existing
4825
+ // grids fail, give that bus first choice of the field and retry the
4826
+ // four dogbone orientations. The marker bounds recursion and enables
4827
+ // joint selection of the remaining uncommitted plane escapes.
4828
+ if (!useBoundaryRecovery && failedWideBoundaryBus) {
4829
+ const busId = failedWideBoundaryBus.busId
4830
+ const outward = preferBoundaryOutwardByBusId.get(busId) ?? true
4831
+ const side = preferredBoundaryPerpendicularSideByBusId.get(busId) ?? 1
4832
+ for (const [preferOutward, perpendicularSide] of [
4833
+ [!outward, -side],
4834
+ [!outward, side],
4835
+ [outward, -side],
4836
+ [outward, side],
4837
+ ] as const) {
4838
+ const recoveredState =
4839
+ yield* this.routeDenseThroughAllMixedTerminationSteps({
4840
+ ...params,
4841
+ boundaryRecovery: {
4842
+ busId,
4843
+ preferOutward,
4844
+ perpendicularSide: perpendicularSide as -1 | 1,
4845
+ },
4846
+ })
4847
+ if (recoveredState) return recoveredState
4848
+ }
4849
+ }
4623
4850
  }
4624
4851
  if (
4625
4852
  !usePadAlignedDenseRouting ||
@@ -5083,6 +5310,23 @@ export class FanoutSolver extends BaseSolver {
5083
5310
 
5084
5311
  let mixedTerminationState: MixedTerminationState | null = null
5085
5312
  if (!useSingleLayerPushAndShove && routingStrategy === "default") {
5313
+ const peripheralSolver = this.createWorkSolver(
5314
+ "PeripheralMixedTerminationSolver",
5315
+ this.routePeripheralMixedTerminationSteps({
5316
+ busLayerAssignments,
5317
+ busesInRoutingOrder,
5318
+ }),
5319
+ )
5320
+ mixedTerminationState = (yield {
5321
+ type: "subsolver",
5322
+ solver: peripheralSolver,
5323
+ }) as MixedTerminationState | null
5324
+ }
5325
+ if (
5326
+ !mixedTerminationState &&
5327
+ !useSingleLayerPushAndShove &&
5328
+ routingStrategy === "default"
5329
+ ) {
5086
5330
  const denseSolver = this.createWorkSolver(
5087
5331
  "DenseMixedTerminationSolver",
5088
5332
  this.routeDenseThroughAllMixedTerminationSteps({
@@ -5282,6 +5526,10 @@ export class FanoutSolver extends BaseSolver {
5282
5526
  return {
5283
5527
  summary,
5284
5528
  plans,
5529
+ ...(mixedTerminationState?.stopAfterCompleteValidation &&
5530
+ validation?.valid
5531
+ ? { stopAfterCompleteValidation: true as const }
5532
+ : {}),
5285
5533
  blockingBusIds: [...blockingBusCounts.entries()]
5286
5534
  .toSorted(([, firstCount], [, secondCount]) => secondCount - firstCount)
5287
5535
  .map(([busId]) => busId),
@@ -5302,7 +5550,8 @@ export class FanoutSolver extends BaseSolver {
5302
5550
  if (
5303
5551
  bestAttempt.summary.routedConnectionCount ===
5304
5552
  this.inputSrj.connections.length &&
5305
- this.getCoordinatedAdditionalViaCount(bestAttempt.plans) === 0
5553
+ (bestAttempt.stopAfterCompleteValidation ||
5554
+ this.getCoordinatedAdditionalViaCount(bestAttempt.plans) === 0)
5306
5555
  ) {
5307
5556
  return bestAttempt
5308
5557
  }
@@ -5319,7 +5568,8 @@ export class FanoutSolver extends BaseSolver {
5319
5568
  if (
5320
5569
  bestAttempt.summary.routedConnectionCount ===
5321
5570
  this.inputSrj.connections.length &&
5322
- this.getCoordinatedAdditionalViaCount(bestAttempt.plans) === 0
5571
+ (bestAttempt.stopAfterCompleteValidation ||
5572
+ this.getCoordinatedAdditionalViaCount(bestAttempt.plans) === 0)
5323
5573
  ) {
5324
5574
  return bestAttempt
5325
5575
  }
@@ -5873,10 +6123,11 @@ export class FanoutSolver extends BaseSolver {
5873
6123
  bestScore: this.bestAttempt.summary.score,
5874
6124
  }
5875
6125
  if (
5876
- this.groupedBeamEvaluated &&
5877
- attempt.summary.routedConnectionCount ===
5878
- this.inputSrj.connections.length &&
5879
- this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0
6126
+ (attempt.stopAfterCompleteValidation && this.hasCompleteBestAttempt()) ||
6127
+ (this.groupedBeamEvaluated &&
6128
+ attempt.summary.routedConnectionCount ===
6129
+ this.inputSrj.connections.length &&
6130
+ this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0)
5880
6131
  ) {
5881
6132
  this.completeBestAttemptEndpoints()
5882
6133
  this.solved = true
@@ -0,0 +1,102 @@
1
+ import { getCornerBandSide } from "./boundary-exit"
2
+ import type { RouteBusParams } from "./route-bus"
3
+
4
+ /** Midpoints of unoccupied edge intervals, clipped to the bus's declared band. */
5
+ export function getFreeBoundaryTracks(
6
+ params: Pick<
7
+ RouteBusParams,
8
+ | "bus"
9
+ | "targetLayer"
10
+ | "acceptedPlans"
11
+ | "reservedVias"
12
+ | "traceWidth"
13
+ | "clearance"
14
+ >,
15
+ ): number[] {
16
+ const { bus, targetLayer, traceWidth, clearance } = params
17
+ if (!bus.exitEdge) return []
18
+ const vertical = bus.exitEdge === "left" || bus.exitEdge === "right"
19
+ const along = vertical ? "y" : "x"
20
+ const across = vertical ? "x" : "y"
21
+ const boundary = bus.sharedBoundary
22
+ const edge =
23
+ bus.exitEdge === "left"
24
+ ? boundary.minX
25
+ : bus.exitEdge === "right"
26
+ ? boundary.maxX
27
+ : bus.exitEdge === "bottom"
28
+ ? boundary.minY
29
+ : boundary.maxY
30
+ const lower = vertical ? boundary.minY : boundary.minX
31
+ const upper = vertical ? boundary.maxY : boundary.maxX
32
+ const middle = (lower + upper) / 2
33
+ const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
34
+ const minimum = (side === "maximum" ? middle : lower) + traceWidth / 2
35
+ const maximum = (side === "minimum" ? middle : upper) - traceWidth / 2
36
+ const blocked: [number, number][] = []
37
+ for (const plan of params.acceptedPlans) {
38
+ for (const segment of [
39
+ ...plan.segments,
40
+ ...(plan.planeEndpointSegments ?? []),
41
+ ]) {
42
+ if (segment.layer !== targetLayer) continue
43
+ const radius = (segment.width + traceWidth) / 2 + clearance
44
+ const start = segment.start[across]
45
+ const delta = segment.end[across] - start
46
+ let from = 0
47
+ let to = 1
48
+ if (Math.abs(delta) < 1e-9) {
49
+ if (Math.abs(start - edge) > radius) continue
50
+ } else {
51
+ const first = (edge - radius - start) / delta
52
+ const last = (edge + radius - start) / delta
53
+ from = Math.max(0, Math.min(first, last))
54
+ to = Math.min(1, Math.max(first, last))
55
+ if (from > to) continue
56
+ }
57
+ const alongStart = segment.start[along]
58
+ const alongDelta = segment.end[along] - alongStart
59
+ const first = alongStart + from * alongDelta
60
+ const last = alongStart + to * alongDelta
61
+ blocked.push([
62
+ Math.min(first, last) - radius,
63
+ Math.max(first, last) + radius,
64
+ ])
65
+ }
66
+ }
67
+ const vias = [
68
+ ...params.acceptedPlans.flatMap((plan) => [
69
+ ...(plan.via ? [plan.via] : []),
70
+ ...(plan.additionalVias ?? []),
71
+ ...(plan.planeEndpointVia ? [plan.planeEndpointVia] : []),
72
+ ]),
73
+ ...(params.reservedVias ?? []).map((reserved) => reserved.via),
74
+ ]
75
+ for (const via of vias) {
76
+ if (!via.spanLayers.includes(targetLayer)) continue
77
+ const radius = via.diameter / 2 + traceWidth / 2 + clearance
78
+ const distance = Math.abs(via.center[across] - edge)
79
+ if (distance >= radius) continue
80
+ const extent = Math.sqrt(radius * radius - distance * distance)
81
+ blocked.push([via.center[along] - extent, via.center[along] + extent])
82
+ }
83
+ const gaps: [number, number][] = []
84
+ let cursor = minimum
85
+ for (const [from, to] of blocked.sort((a, b) => a[0] - b[0])) {
86
+ if (to <= cursor || from >= maximum) continue
87
+ if (from > cursor) gaps.push([cursor, Math.min(from, maximum)])
88
+ cursor = Math.max(cursor, to)
89
+ if (cursor >= maximum) break
90
+ }
91
+ if (cursor < maximum) gaps.push([cursor, maximum])
92
+ // Edge order gives deterministic, symmetric coverage without hard-coded tracks.
93
+ const tracks = gaps
94
+ .filter(([from, to]) => to - from > 1e-6)
95
+ .map(([from, to]) => (from + to) / 2)
96
+ return tracks.length <= 32
97
+ ? tracks
98
+ : Array.from(
99
+ { length: 32 },
100
+ (_, index) => tracks[Math.round((index * (tracks.length - 1)) / 31)]!,
101
+ )
102
+ }