@tscircuit/fanout-solver 0.0.50 → 0.0.51

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.
@@ -1710,6 +1710,46 @@ export class FanoutSolver extends BaseSolver {
1710
1710
  : getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
1711
1711
  ]),
1712
1712
  )
1713
+ // An outward half-pitch dogbone can enter a neighboring wide bus's pad
1714
+ // field when a corner bus first escapes toward the package center. Keep
1715
+ // those through-barrels on the opposite side before any earlier bus
1716
+ // commits copper; rematching after that point can be too late.
1717
+ for (const bus of wideBoundaryBuses) {
1718
+ if (useConfiguredDensePlaneRouting) continue
1719
+ if (!getCornerBandSide(bus.exitEdge, bus.preferredExit)) continue
1720
+ const entersNeighboringSourceField = wideBoundaryBuses.some((other) => {
1721
+ if (other === bus || other.componentId !== bus.componentId) return false
1722
+ const xs = other.connections.map(
1723
+ (connection) => connection.sourcePoint.x,
1724
+ )
1725
+ const ys = other.connections.map(
1726
+ (connection) => connection.sourcePoint.y,
1727
+ )
1728
+ const minX = Math.min(...xs)
1729
+ const maxX = Math.max(...xs)
1730
+ const minY = Math.min(...ys)
1731
+ const maxY = Math.max(...ys)
1732
+ return bus.connections.some(({ sourcePoint }) => {
1733
+ const x =
1734
+ sourcePoint.x +
1735
+ (bus.direction === "left"
1736
+ ? -bus.pitchX / 2
1737
+ : bus.direction === "right"
1738
+ ? bus.pitchX / 2
1739
+ : 0)
1740
+ const y =
1741
+ sourcePoint.y +
1742
+ (bus.direction === "down"
1743
+ ? -bus.pitchY / 2
1744
+ : bus.direction === "up"
1745
+ ? bus.pitchY / 2
1746
+ : 0)
1747
+ return x >= minX && x <= maxX && y >= minY && y <= maxY
1748
+ })
1749
+ })
1750
+ if (entersNeighboringSourceField)
1751
+ preferBoundaryOutwardByBusId.set(bus.busId, false)
1752
+ }
1713
1753
  const debugFlippedBoundaryBus = process.env.FANOUT_DEBUG_FLIP_BOUNDARY_BUS
1714
1754
  if (debugFlippedBoundaryBus) {
1715
1755
  preferredBoundaryPerpendicularSideByBusId.set(debugFlippedBoundaryBus, -1)
@@ -1859,9 +1899,17 @@ export class FanoutSolver extends BaseSolver {
1859
1899
  ...(process.env.FANOUT_DEBUG_PROVISIONAL_NARROW === "1"
1860
1900
  ? boundaryBuses.filter((bus) => bus.connections.length < 8)
1861
1901
  : []),
1862
- ...singletonDeferralCandidates.filter(
1863
- (bus) => !leadingWideSingletonBuses.includes(bus),
1864
- ),
1902
+ ...singletonDeferralCandidates.filter((bus) => {
1903
+ const containingBus = getContainingWideSourceField(bus)
1904
+ const sharesContainingBusLayer =
1905
+ !useConfiguredDensePlaneRouting &&
1906
+ containingBus &&
1907
+ params.busLayerAssignments[containingBus.busId] ===
1908
+ params.busLayerAssignments[bus.busId]
1909
+ return (
1910
+ !leadingWideSingletonBuses.includes(bus) && !sharesContainingBusLayer
1911
+ )
1912
+ }),
1865
1913
  ...(hasThreeWideBoundaryBuses
1866
1914
  ? boundaryBuses.filter(
1867
1915
  (bus) =>
@@ -2088,6 +2136,13 @@ export class FanoutSolver extends BaseSolver {
2088
2136
  if (!targetLayer) {
2089
2137
  return false
2090
2138
  }
2139
+ const adaptiveWindingRouteOrder =
2140
+ !useConfiguredDensePlaneRouting &&
2141
+ !getCornerBandSide(bus.exitEdge, bus.preferredExit) &&
2142
+ bus.connections.length > 2 &&
2143
+ wideBoundaryBuses.some((candidate) =>
2144
+ getCornerBandSide(candidate.exitEdge, candidate.preferredExit),
2145
+ )
2091
2146
  const routeParams = {
2092
2147
  srj: this.routingSrj,
2093
2148
  bus,
@@ -2105,9 +2160,14 @@ export class FanoutSolver extends BaseSolver {
2105
2160
  fixedViaPointsByConnectionIndex,
2106
2161
  reservedVias: getReservedVias(bus),
2107
2162
  viaMinimalOnly: process.env.FANOUT_DEBUG_ALLOW_EXTRA_VIAS !== "1",
2108
- fixedViaFallbackRouteOrderAttempts: useConfiguredDensePlaneRouting
2109
- ? 6
2110
- : 24,
2163
+ allowBoundarySideViaFallback: !useConfiguredDensePlaneRouting,
2164
+ adaptiveWindingRouteOrder,
2165
+ alignWindingGridToPads: !useConfiguredDensePlaneRouting,
2166
+ fixedViaFallbackRouteOrderAttempts: adaptiveWindingRouteOrder
2167
+ ? 60
2168
+ : useConfiguredDensePlaneRouting
2169
+ ? 6
2170
+ : 24,
2111
2171
  cornerBandTargetTrackOffset: getCornerBandTargetTrackOffset(bus),
2112
2172
  } as const
2113
2173
  const routeAlternatives = function* (
@@ -2158,6 +2218,88 @@ export class FanoutSolver extends BaseSolver {
2158
2218
  if (!busPlans && preferSingleLayerWinding) {
2159
2219
  busPlans = (yield* routeAlternatives(routeParams, 1))[0]
2160
2220
  }
2221
+ if (!busPlans && !useConfiguredDensePlaneRouting) {
2222
+ const originalPoints = fixedViaPointsByConnectionIndex
2223
+ const originalOutward =
2224
+ preferBoundaryOutwardByBusId.get(bus.busId) ?? true
2225
+ const originalSide =
2226
+ preferredBoundaryPerpendicularSideByBusId.get(bus.busId) ?? 1
2227
+ for (const [outward, side] of [
2228
+ [!originalOutward, originalSide],
2229
+ [originalOutward, -originalSide],
2230
+ [!originalOutward, -originalSide],
2231
+ ] as const) {
2232
+ const rematchedPoints = matchComponentDogboneViaSites(
2233
+ [
2234
+ ...new Set([
2235
+ ...activeBoundaryReservationPlaneBuses,
2236
+ ...initiallyMatchedBoundaryBuses,
2237
+ ...this.preparedBuses.filter((candidate) =>
2238
+ matchedPlans.some((plan) => plan.busId === candidate.busId),
2239
+ ),
2240
+ bus,
2241
+ ]),
2242
+ ],
2243
+ {
2244
+ viaDiameter: this.config.viaDiameter,
2245
+ viaHoleDiameter: this.config.viaHoleDiameter,
2246
+ traceWidth: this.config.traceWidth,
2247
+ clearance: this.config.clearance,
2248
+ maximumSearchStates: 100_000,
2249
+ preferredBoundaryPerpendicularSideByBusId: new Map([
2250
+ ...preferredBoundaryPerpendicularSideByBusId,
2251
+ [bus.busId, side as -1 | 1],
2252
+ ]),
2253
+ preferBoundaryOutwardByBusId: new Map([
2254
+ ...preferBoundaryOutwardByBusId,
2255
+ [bus.busId, outward],
2256
+ ]),
2257
+ fixedViaPointsByConnectionIndex: new Map(
2258
+ matchedPlans
2259
+ .filter((plan) => plan.via)
2260
+ .map((plan) => [plan.connectionIndex, plan.via!.center]),
2261
+ ),
2262
+ preferredViaPointsByConnectionIndex: new Map(
2263
+ [...originalPoints].filter(
2264
+ ([index]) =>
2265
+ !bus.connections.some(
2266
+ (connection) => connection.connectionIndex === index,
2267
+ ),
2268
+ ),
2269
+ ),
2270
+ blockingSegments: matchedPlans.flatMap((plan) =>
2271
+ plan.segments.map((segment) => ({
2272
+ connectionIndex: plan.connectionIndex,
2273
+ segment,
2274
+ })),
2275
+ ),
2276
+ additionalObstacles: this.routingSrj.obstacles,
2277
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
2278
+ canShareCopper,
2279
+ },
2280
+ )
2281
+ debugDense(
2282
+ "retry-sites",
2283
+ bus.busId,
2284
+ outward,
2285
+ side,
2286
+ rematchedPoints?.size ?? "failed",
2287
+ )
2288
+ if (!rematchedPoints) continue
2289
+ fixedViaPointsByConnectionIndex = rematchedPoints
2290
+ busPlans = (yield* routeAlternatives(
2291
+ {
2292
+ ...routeParams,
2293
+ fixedViaPointsByConnectionIndex: rematchedPoints,
2294
+ reservedVias: getReservedVias(bus),
2295
+ fixedViaFallbackRouteOrderAttempts: 3,
2296
+ },
2297
+ 1,
2298
+ ))[0]
2299
+ if (busPlans) break
2300
+ }
2301
+ if (!busPlans) fixedViaPointsByConnectionIndex = originalPoints
2302
+ }
2161
2303
  if (busPlans && bus.maxLengthSkew !== undefined) {
2162
2304
  const lengths = busPlans.map((plan) => plan.length)
2163
2305
  const rawSkew = Math.max(...lengths) - Math.min(...lengths)
@@ -2503,6 +2645,50 @@ export class FanoutSolver extends BaseSolver {
2503
2645
  segment,
2504
2646
  })),
2505
2647
  )
2648
+ const boundaryBusesToMatch = useConfiguredDensePlaneRouting
2649
+ ? boundaryBuses
2650
+ : []
2651
+ const blockingVias = useConfiguredDensePlaneRouting
2652
+ ? []
2653
+ : candidatePlans.flatMap((plan) =>
2654
+ [
2655
+ plan.via,
2656
+ ...(plan.additionalVias ?? []),
2657
+ plan.planeEndpointVia,
2658
+ ]
2659
+ .filter((via) => via !== undefined)
2660
+ .map((via) => ({
2661
+ connectionIndex: plan.connectionIndex,
2662
+ center: via!.center,
2663
+ diameter: via!.diameter,
2664
+ spanLayers: via!.spanLayers,
2665
+ })),
2666
+ )
2667
+ // Completed boundary paths can reach their via with several source-
2668
+ // layer segments. Treat their actual copper as fixed obstacles instead
2669
+ // of reinterpreting each as a straight pad-to-via dogbone.
2670
+ const retainedViaPoints = useConfiguredDensePlaneRouting
2671
+ ? null
2672
+ : matchComponentDogboneViaSites(planeBuses, {
2673
+ viaDiameter: this.config.viaDiameter,
2674
+ viaHoleDiameter: this.config.viaHoleDiameter,
2675
+ traceWidth: this.config.traceWidth,
2676
+ clearance: this.config.clearance,
2677
+ maximumSearchStates: 1,
2678
+ preferredBoundaryPerpendicularSideByBusId,
2679
+ preferBoundaryOutwardByBusId,
2680
+ fixedViaPointsByConnectionIndex: new Map([
2681
+ ...fixedViaPointsByConnectionIndex,
2682
+ ...fixedBoundaryViaPoints,
2683
+ ]),
2684
+ blockingSegments,
2685
+ blockingVias,
2686
+ additionalObstacles: denseAdditionalObstacles,
2687
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
2688
+ canShareCopper,
2689
+ })
2690
+ if (retainedViaPoints)
2691
+ return new Map([...fixedBoundaryViaPoints, ...retainedViaPoints])
2506
2692
  if (
2507
2693
  useConfiguredDensePlaneRouting ||
2508
2694
  process.env.FANOUT_DEBUG_INCREMENTAL_PLANE_MATCH === "1"
@@ -2551,7 +2737,7 @@ export class FanoutSolver extends BaseSolver {
2551
2737
  (planeBus) =>
2552
2738
  !matchedPlaneBuses.includes(planeBus) &&
2553
2739
  !matchComponentDogboneViaSites(
2554
- [...matchedPlaneBuses, planeBus, ...boundaryBuses],
2740
+ [...matchedPlaneBuses, planeBus, ...boundaryBusesToMatch],
2555
2741
  {
2556
2742
  viaDiameter: this.config.viaDiameter,
2557
2743
  viaHoleDiameter: this.config.viaHoleDiameter,
@@ -2562,6 +2748,7 @@ export class FanoutSolver extends BaseSolver {
2562
2748
  preferBoundaryOutwardByBusId,
2563
2749
  fixedViaPointsByConnectionIndex: incrementalViaPoints,
2564
2750
  blockingSegments,
2751
+ blockingVias,
2565
2752
  additionalObstacles: denseAdditionalObstacles,
2566
2753
  preferPlaneCheckerboardSites:
2567
2754
  useConfiguredDensePlaneRouting,
@@ -3212,7 +3399,7 @@ export class FanoutSolver extends BaseSolver {
3212
3399
  )
3213
3400
  }
3214
3401
  const nextViaPoints = matchComponentDogboneViaSites(
3215
- [...matchedPlaneBuses, planeBus, ...boundaryBuses],
3402
+ [...matchedPlaneBuses, planeBus, ...boundaryBusesToMatch],
3216
3403
  {
3217
3404
  viaDiameter: this.config.viaDiameter,
3218
3405
  viaHoleDiameter: this.config.viaHoleDiameter,
@@ -3223,7 +3410,7 @@ export class FanoutSolver extends BaseSolver {
3223
3410
  preferBoundaryOutwardByBusId,
3224
3411
  fixedViaPointsByConnectionIndex: incrementalViaPoints,
3225
3412
  blockingSegments: allBlockingSegments,
3226
- blockingVias: alternateBlockingVias,
3413
+ blockingVias: [...blockingVias, ...alternateBlockingVias],
3227
3414
  additionalObstacles: denseAdditionalObstacles,
3228
3415
  preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
3229
3416
  canShareCopper,
@@ -3256,8 +3443,8 @@ export class FanoutSolver extends BaseSolver {
3256
3443
  )
3257
3444
  return incrementalViaPoints
3258
3445
  }
3259
- return matchComponentDogboneViaSites(
3260
- [...planeBuses, ...boundaryBuses],
3446
+ const planeViaPoints = matchComponentDogboneViaSites(
3447
+ [...planeBuses, ...boundaryBusesToMatch],
3261
3448
  {
3262
3449
  viaDiameter: this.config.viaDiameter,
3263
3450
  viaHoleDiameter: this.config.viaHoleDiameter,
@@ -3268,11 +3455,15 @@ export class FanoutSolver extends BaseSolver {
3268
3455
  preferBoundaryOutwardByBusId,
3269
3456
  fixedViaPointsByConnectionIndex: fixedBoundaryViaPoints,
3270
3457
  blockingSegments,
3458
+ blockingVias,
3271
3459
  additionalObstacles: denseAdditionalObstacles,
3272
3460
  preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
3273
3461
  canShareCopper,
3274
3462
  },
3275
3463
  )
3464
+ return planeViaPoints
3465
+ ? new Map([...fixedBoundaryViaPoints, ...planeViaPoints])
3466
+ : null
3276
3467
  }
3277
3468
  debugDense("length-match:start", matchedPlans.length)
3278
3469
  const matchedLengthResult = matchBusPlanLengths({
@@ -35,6 +35,8 @@ export interface DogboneViaSiteGeometryRules {
35
35
  preferBoundaryOutwardByBusId?: ReadonlyMap<string, boolean>
36
36
  /** Existing assignments that must be preserved while matching other pads. */
37
37
  fixedViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
38
+ /** Prefer these provisional sites, but allow rematching when they conflict. */
39
+ preferredViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
38
40
  /** Routed copper that every newly assigned through-via/dogbone must clear. */
39
41
  blockingSegments?: readonly {
40
42
  connectionIndex: number
@@ -506,6 +508,9 @@ function getConnectionCandidates(params: {
506
508
  connection.terminationType === "boundary"
507
509
  ? (rules.preferBoundaryOutwardByBusId?.get(connection.busId) ?? true)
508
510
  : true
511
+ const preferredViaPoint = rules.preferredViaPointsByConnectionIndex?.get(
512
+ preparedConnection.connectionIndex,
513
+ )
509
514
  const getPerpendicularPreferenceRank = (
510
515
  candidate: ViaSiteCandidate,
511
516
  ): number => {
@@ -522,6 +527,10 @@ function getConnectionCandidates(params: {
522
527
  }
523
528
  return candidates.toSorted(
524
529
  (first, second) =>
530
+ (preferredViaPoint
531
+ ? Number(distance(first.point, preferredViaPoint) > EPSILON) -
532
+ Number(distance(second.point, preferredViaPoint) > EPSILON)
533
+ : 0) ||
525
534
  (preferOutward
526
535
  ? first.outwardRank - second.outwardRank
527
536
  : second.outwardRank - first.outwardRank) ||
package/lib/route-bus.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  } from "./geometry"
20
20
  import { getAllRoutedTraceCopper } from "./get-routed-trace-copper"
21
21
  import { getViaSpanLayers } from "./layer-names"
22
+ import { matchComponentDogboneViaSites } from "./match-component-dogbone-via-sites"
22
23
  import {
23
24
  connectionsShareElectricalNet,
24
25
  obstacleSharesElectricalNet,
@@ -61,6 +62,12 @@ export interface RouteBusParams {
61
62
  fixedViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
62
63
  reservedVias?: readonly ViaMinimalWindingReservedVia[]
63
64
  viaMinimalOnly?: boolean
65
+ /** Permit a singleton to move its provisional via near the boundary. */
66
+ allowBoundarySideViaFallback?: boolean
67
+ /** Retry blocked winding terminals ahead of already-routed terminals. */
68
+ adaptiveWindingRouteOrder?: boolean
69
+ /** Preserve pad-lattice channels in the automatic dense routing path. */
70
+ alignWindingGridToPads?: boolean
64
71
  /** Bounds the final fixed-via winding fallback after ordered attempts. */
65
72
  fixedViaFallbackRouteOrderAttempts?: number
66
73
  /** Skip this many otherwise-clear plane escapes when enumerating alternatives. */
@@ -2444,6 +2451,9 @@ export function* routeBusAlternativesSteps(
2444
2451
  fixedViaPointsByConnectionIndex,
2445
2452
  reservedVias = [],
2446
2453
  viaMinimalOnly = false,
2454
+ allowBoundarySideViaFallback = false,
2455
+ adaptiveWindingRouteOrder = false,
2456
+ alignWindingGridToPads = false,
2447
2457
  fixedViaFallbackRouteOrderAttempts = 24,
2448
2458
  cornerBandTargetTrackOffset,
2449
2459
  } = params
@@ -2723,8 +2733,36 @@ export function* routeBusAlternativesSteps(
2723
2733
  useViaInPad: true,
2724
2734
  getViaHandedness: () => 0 as const,
2725
2735
  }
2736
+ const coordinatedViaPoints =
2737
+ fixedViaPointsByConnectionIndex ??
2738
+ (!allowBlindAndBuriedVias &&
2739
+ bus.connections.length >= 8 &&
2740
+ reservedVias.length === 0
2741
+ ? matchComponentDogboneViaSites([bus], {
2742
+ viaDiameter,
2743
+ viaHoleDiameter,
2744
+ traceWidth,
2745
+ clearance,
2746
+ additionalObstacles: srj.obstacles,
2747
+ blockingSegments: acceptedPlans.flatMap((plan) =>
2748
+ getPlanSegments(plan).map((segment) => ({
2749
+ connectionIndex: plan.connectionIndex,
2750
+ segment,
2751
+ })),
2752
+ ),
2753
+ blockingVias: acceptedPlans.flatMap((plan) =>
2754
+ getPlanVias(plan).map((via) => ({
2755
+ connectionIndex: plan.connectionIndex,
2756
+ ...via.center,
2757
+ center: via.center,
2758
+ diameter: via.diameter,
2759
+ spanLayers: via.spanLayers,
2760
+ })),
2761
+ ),
2762
+ })
2763
+ : null)
2726
2764
  const fixedViaTerminalPatterns: CoordinatedTerminalPattern[] =
2727
- fixedViaPointsByConnectionIndex
2765
+ coordinatedViaPoints
2728
2766
  ? [
2729
2767
  ...Array.from(
2730
2768
  { length: windingTargetOrderCount },
@@ -2733,9 +2771,7 @@ export function* routeBusAlternativesSteps(
2733
2771
  useViaInPad: false,
2734
2772
  getViaHandedness: () => 0 as const,
2735
2773
  getViaPoint: (connection: PreparedConnection) =>
2736
- fixedViaPointsByConnectionIndex.get(
2737
- connection.connectionIndex,
2738
- )!,
2774
+ coordinatedViaPoints.get(connection.connectionIndex)!,
2739
2775
  maximumRouteOrderAttempts: 1,
2740
2776
  windingOrderIndex,
2741
2777
  preferTargetDirectedLaneBias: true,
@@ -2746,9 +2782,7 @@ export function* routeBusAlternativesSteps(
2746
2782
  useViaInPad: false,
2747
2783
  getViaHandedness: () => 0,
2748
2784
  getViaPoint: (connection) =>
2749
- fixedViaPointsByConnectionIndex.get(
2750
- connection.connectionIndex,
2751
- )!,
2785
+ coordinatedViaPoints.get(connection.connectionIndex)!,
2752
2786
  // Preserve the existing bounded search after every inexpensive
2753
2787
  // layer-interleave candidate has had one deterministic attempt.
2754
2788
  maximumRouteOrderAttempts: fixedViaFallbackRouteOrderAttempts,
@@ -2846,9 +2880,13 @@ export function* routeBusAlternativesSteps(
2846
2880
  allowBlindAndBuriedVias,
2847
2881
  allowSameNetMerges,
2848
2882
  maximumRouteOrderAttempts: terminalPattern.maximumRouteOrderAttempts,
2883
+ adaptiveRouteOrder: adaptiveWindingRouteOrder,
2884
+ alignGridToPads:
2885
+ alignWindingGridToPads ||
2886
+ Boolean(coordinatedViaPoints && !fixedViaPointsByConnectionIndex),
2849
2887
  reservedVias,
2850
2888
  gridStepDivisor:
2851
- fixedViaPointsByConnectionIndex &&
2889
+ coordinatedViaPoints &&
2852
2890
  Math.min(bus.pitchX, bus.pitchY) -
2853
2891
  2 * (viaDiameter / 2 + traceWidth / 2 + clearance) <
2854
2892
  traceWidth + clearance
@@ -2907,6 +2945,204 @@ export function* routeBusAlternativesSteps(
2907
2945
  }
2908
2946
  }
2909
2947
 
2948
+ // A through-via does not have to sit next to the source pad. A singleton
2949
+ // embedded in another bus's source field can have every local dogbone site
2950
+ // occupied while still having a clear top-layer escape. In that case, route
2951
+ // to one via near the boundary and make the short final hop on the requested
2952
+ // escape layer. This retains the one-via invariant and avoids weakening any
2953
+ // clearance rule.
2954
+ if (
2955
+ alternatives.length === 0 &&
2956
+ allowBoundarySideViaFallback &&
2957
+ fixedViaPointsByConnectionIndex &&
2958
+ bus.connections.length === 1 &&
2959
+ bus.exitEdge &&
2960
+ bus.connections[0]!.sourceLayer !== targetLayer
2961
+ ) {
2962
+ const preparedConnection = bus.connections[0]!
2963
+ const boundaryDirection = getDirectionForExitEdge(bus.exitEdge)
2964
+ const boundaryExitAxis = getExitAxis(bus, boundaryDirection)
2965
+ const finalTrack = getBoundaryTargetTrack({
2966
+ bus,
2967
+ connection: preparedConnection,
2968
+ boundaryDirection,
2969
+ })
2970
+ const finalExitPoint = makePoint(
2971
+ boundaryExitAxis,
2972
+ finalTrack,
2973
+ boundaryDirection,
2974
+ )
2975
+ const sourceLayerSrj: SimpleRouteJson = {
2976
+ ...srj,
2977
+ obstacles: srj.obstacles.map((obstacle) =>
2978
+ obstacle === preparedConnection.sourceObstacle ||
2979
+ (distance(obstacle.center, preparedConnection.sourcePoint) <= 1e-7 &&
2980
+ obstacle.layers.includes(preparedConnection.sourceLayer))
2981
+ ? {
2982
+ ...obstacle,
2983
+ connectedTo: [
2984
+ ...obstacle.connectedTo,
2985
+ preparedConnection.connection.name,
2986
+ ],
2987
+ }
2988
+ : obstacle,
2989
+ ),
2990
+ }
2991
+ const insetStep = Math.max(
2992
+ viaDiameter / 2 + clearance,
2993
+ Math.min(bus.pitchX, bus.pitchY) / 2,
2994
+ )
2995
+ for (const multiple of [1, 2, 3, 4, 5]) {
2996
+ const inset = multiple * insetStep
2997
+ const boundaryViaPoint = makePoint(
2998
+ boundaryExitAxis - directionSign(boundaryDirection) * inset,
2999
+ finalTrack,
3000
+ boundaryDirection,
3001
+ )
3002
+ const sourceLayerSteps = routeViaMinimalWindingAlternativesSteps(
3003
+ {
3004
+ srj: sourceLayerSrj,
3005
+ bus,
3006
+ targetLayer: preparedConnection.sourceLayer,
3007
+ terminals: [
3008
+ {
3009
+ connection: preparedConnection,
3010
+ viaPoint: preparedConnection.sourcePoint,
3011
+ exitPoint: boundaryViaPoint,
3012
+ },
3013
+ ],
3014
+ acceptedPlans,
3015
+ layerNames,
3016
+ traceWidth,
3017
+ viaDiameter,
3018
+ viaHoleDiameter,
3019
+ clearance,
3020
+ allowBlindAndBuriedVias,
3021
+ allowSameNetMerges,
3022
+ maximumRouteOrderAttempts: 3,
3023
+ reservedVias,
3024
+ gridStepDivisor: 2,
3025
+ allowSourceLayerRouting: true,
3026
+ alignGridToPads: true,
3027
+ },
3028
+ 1,
3029
+ includeVisualization,
3030
+ )
3031
+ let sourceLayerResult = sourceLayerSteps.next()
3032
+ while (!sourceLayerResult.done) {
3033
+ yield {
3034
+ phase: "via-minimal-winding",
3035
+ busId: bus.busId,
3036
+ targetLayer: preparedConnection.sourceLayer,
3037
+ winding: sourceLayerResult.value,
3038
+ }
3039
+ sourceLayerResult = sourceLayerSteps.next()
3040
+ }
3041
+ const sourceLayerPlan = sourceLayerResult.value[0]?.[0]
3042
+ if (!sourceLayerPlan) continue
3043
+ const targetSegment: RoutedSegment = {
3044
+ start: boundaryViaPoint,
3045
+ end: finalExitPoint,
3046
+ width: traceWidth,
3047
+ layer: targetLayer,
3048
+ }
3049
+ const via = {
3050
+ center: boundaryViaPoint,
3051
+ diameter: viaDiameter,
3052
+ holeDiameter: viaHoleDiameter,
3053
+ fromLayer: preparedConnection.sourceLayer,
3054
+ toLayer: targetLayer,
3055
+ spanLayers: getViaSpanLayers({
3056
+ fromLayer: preparedConnection.sourceLayer,
3057
+ toLayer: targetLayer,
3058
+ layerNames,
3059
+ allowBlindAndBuriedVias,
3060
+ }),
3061
+ }
3062
+ const sourceRoute = sourceLayerPlan.trace.route.filter(
3063
+ (item) => item.route_type === "wire",
3064
+ )
3065
+ const plans: FanoutRoutePlan[] = [
3066
+ {
3067
+ ...sourceLayerPlan,
3068
+ targetLayer,
3069
+ exitPoint: finalExitPoint,
3070
+ via,
3071
+ segments: [...sourceLayerPlan.segments, targetSegment],
3072
+ length:
3073
+ sourceLayerPlan.length + distance(boundaryViaPoint, finalExitPoint),
3074
+ trace: {
3075
+ ...sourceLayerPlan.trace,
3076
+ route: [
3077
+ ...sourceRoute,
3078
+ {
3079
+ route_type: "via",
3080
+ ...boundaryViaPoint,
3081
+ from_layer: preparedConnection.sourceLayer,
3082
+ to_layer: targetLayer,
3083
+ via_diameter: viaDiameter,
3084
+ via_hole_diameter: viaHoleDiameter,
3085
+ },
3086
+ {
3087
+ route_type: "wire",
3088
+ ...boundaryViaPoint,
3089
+ width: traceWidth,
3090
+ layer: targetLayer,
3091
+ },
3092
+ {
3093
+ route_type: "wire",
3094
+ ...finalExitPoint,
3095
+ width: traceWidth,
3096
+ layer: targetLayer,
3097
+ },
3098
+ ],
3099
+ },
3100
+ },
3101
+ ]
3102
+ const boundaryViaPlansAreClear = fanoutPlansAreClear({
3103
+ plans: [...acceptedPlans, ...plans],
3104
+ srj,
3105
+ sharedBoundary: bus.sharedBoundary,
3106
+ clearance,
3107
+ allowBlindAndBuriedVias,
3108
+ allowSameNetMerges,
3109
+ })
3110
+ const reservedViasAreClear = reservedVias.every((reserved) => {
3111
+ if (
3112
+ reserved.connectionName === preparedConnection.connection.name ||
3113
+ (allowSameNetMerges &&
3114
+ connectionsShareElectricalNet(
3115
+ srj,
3116
+ reserved.connectionName,
3117
+ preparedConnection.connection.name,
3118
+ ))
3119
+ )
3120
+ return true
3121
+ if (
3122
+ via.spanLayers.some((layer) =>
3123
+ reserved.via.spanLayers.includes(layer),
3124
+ ) &&
3125
+ distance(via.center, reserved.via.center) <
3126
+ (via.diameter + reserved.via.diameter) / 2 + clearance - 1e-9
3127
+ )
3128
+ return false
3129
+ return (
3130
+ !reserved.via.spanLayers.includes(targetLayer) ||
3131
+ distancePointToSegment(
3132
+ reserved.via.center,
3133
+ targetSegment.start,
3134
+ targetSegment.end,
3135
+ ) >=
3136
+ reserved.via.diameter / 2 + traceWidth / 2 + clearance - 1e-9
3137
+ )
3138
+ })
3139
+ if (boundaryViaPlansAreClear && reservedViasAreClear) {
3140
+ addAlternative(plans)
3141
+ return alternatives
3142
+ }
3143
+ }
3144
+ }
3145
+
2910
3146
  if (viaMinimalOnly) return alternatives
2911
3147
 
2912
3148
  const searchConnectionOrder = (
@@ -64,6 +64,12 @@ export interface RouteViaMinimalWindingParams {
64
64
  gridStepDivisor?: 1 | 2
65
65
  /** Bias bounded fixed-site searches toward the remote target band. */
66
66
  preferTargetDirectedLaneBias?: boolean
67
+ /** Internal path-only mode used before a boundary-side via is appended. */
68
+ allowSourceLayerRouting?: boolean
69
+ /** Promote a blocked terminal while staying within maximumRouteOrderAttempts. */
70
+ adaptiveRouteOrder?: boolean
71
+ /** Align a fine grid with pad/interstice centers instead of the boundary. */
72
+ alignGridToPads?: boolean
67
73
  }
68
74
 
69
75
  export interface RouteViaMinimalWindingProgress {
@@ -493,6 +499,7 @@ function buildPlan(params: {
493
499
  layer: connection.sourceLayer,
494
500
  }
495
501
  const hasSourceDogbone = distance(sourcePoint, terminal.viaPoint) > EPSILON
502
+ const changesLayer = connection.sourceLayer !== targetLayer
496
503
  const targetSegments = getSegments(targetLayerPoints, traceWidth, targetLayer)
497
504
  const spanLayers = getViaSpanLayers({
498
505
  fromLayer: connection.sourceLayer,
@@ -528,20 +535,24 @@ function buildPlan(params: {
528
535
  },
529
536
  ]
530
537
  : []),
531
- {
532
- route_type: "via",
533
- ...terminal.viaPoint,
534
- from_layer: connection.sourceLayer,
535
- to_layer: targetLayer,
536
- via_diameter: viaDiameter,
537
- via_hole_diameter: viaHoleDiameter,
538
- },
539
- {
540
- route_type: "wire",
541
- ...terminal.viaPoint,
542
- width: traceWidth,
543
- layer: targetLayer,
544
- },
538
+ ...(changesLayer
539
+ ? [
540
+ {
541
+ route_type: "via" as const,
542
+ ...terminal.viaPoint,
543
+ from_layer: connection.sourceLayer,
544
+ to_layer: targetLayer,
545
+ via_diameter: viaDiameter,
546
+ via_hole_diameter: viaHoleDiameter,
547
+ },
548
+ {
549
+ route_type: "wire" as const,
550
+ ...terminal.viaPoint,
551
+ width: traceWidth,
552
+ layer: targetLayer,
553
+ },
554
+ ]
555
+ : []),
545
556
  ...targetLayerPoints.slice(1).map((point) => ({
546
557
  route_type: "wire" as const,
547
558
  ...point,
@@ -589,7 +600,7 @@ function buildPlan(params: {
589
600
  route,
590
601
  },
591
602
  segments,
592
- via,
603
+ via: changesLayer ? via : undefined,
593
604
  length: segments.reduce(
594
605
  (total, segment) => total + distance(segment.start, segment.end),
595
606
  0,
@@ -624,6 +635,9 @@ export function* routeViaMinimalWindingAlternativesSteps(
624
635
  reservedVias = [],
625
636
  gridStepDivisor = 1,
626
637
  preferTargetDirectedLaneBias = false,
638
+ allowSourceLayerRouting = false,
639
+ adaptiveRouteOrder = false,
640
+ alignGridToPads = false,
627
641
  } = params
628
642
  if (
629
643
  maximumRouteOrderAttempts !== undefined &&
@@ -643,18 +657,33 @@ export function* routeViaMinimalWindingAlternativesSteps(
643
657
  if (
644
658
  terminals.length === 0 ||
645
659
  !bus.exitEdge ||
646
- terminals.some(
647
- (terminal) => terminal.connection.sourceLayer === targetLayer,
648
- )
660
+ (!allowSourceLayerRouting &&
661
+ terminals.some(
662
+ (terminal) => terminal.connection.sourceLayer === targetLayer,
663
+ ))
649
664
  ) {
650
665
  return []
651
666
  }
652
667
 
653
- const gridStep = (traceWidth + clearance) / gridStepDivisor
668
+ const baseGridStep = (traceWidth + clearance) / gridStepDivisor
669
+ const pitch = Math.min(bus.pitchX, bus.pitchY)
670
+ const alignGridToPitch =
671
+ alignGridToPads && gridStepDivisor === 2 && Number.isFinite(pitch)
672
+ const gridStep = alignGridToPitch
673
+ ? pitch / (2 * Math.ceil(pitch / (2 * baseGridStep)))
674
+ : baseGridStep
654
675
  if (!Number.isFinite(gridStep) || gridStep <= 0) return []
655
676
  const { minX, maxX, minY, maxY } = bus.sharedBoundary
656
- const columnCount = Math.floor((maxX - minX) / gridStep) + 1
657
- const rowCount = Math.floor((maxY - minY) / gridStep) + 1
677
+ const originX = bus.xCoordinates[0] ?? minX
678
+ const originY = bus.yCoordinates[0] ?? minY
679
+ const gridMinX = alignGridToPitch
680
+ ? originX + Math.ceil((minX - originX) / gridStep) * gridStep
681
+ : minX
682
+ const gridMinY = alignGridToPitch
683
+ ? originY + Math.ceil((minY - originY) / gridStep) * gridStep
684
+ : minY
685
+ const columnCount = Math.floor((maxX - gridMinX) / gridStep) + 1
686
+ const rowCount = Math.floor((maxY - gridMinY) / gridStep) + 1
658
687
  const nodeCount = columnCount * rowCount
659
688
  if (columnCount < 2 || rowCount < 2 || nodeCount > MAX_GRID_NODE_COUNT) {
660
689
  return []
@@ -665,7 +694,7 @@ export function* routeViaMinimalWindingAlternativesSteps(
665
694
  return {
666
695
  column,
667
696
  row,
668
- point: { x: minX + column * gridStep, y: minY + row * gridStep },
697
+ point: { x: gridMinX + column * gridStep, y: gridMinY + row * gridStep },
669
698
  }
670
699
  })
671
700
  const sampledGridPoints = includeVisualization
@@ -797,6 +826,18 @@ export function* routeViaMinimalWindingAlternativesSteps(
797
826
  }
798
827
  for (const blocker of acceptedAttemptSegments) {
799
828
  if (sharesNet(connectionName, blocker.connectionName)) continue
829
+ const margin = (segment.width + blocker.segment.width) / 2 + clearance
830
+ if (
831
+ Math.max(segment.start.x, segment.end.x) + margin <
832
+ Math.min(blocker.segment.start.x, blocker.segment.end.x) ||
833
+ Math.min(segment.start.x, segment.end.x) - margin >
834
+ Math.max(blocker.segment.start.x, blocker.segment.end.x) ||
835
+ Math.max(segment.start.y, segment.end.y) + margin <
836
+ Math.min(blocker.segment.start.y, blocker.segment.end.y) ||
837
+ Math.min(segment.start.y, segment.end.y) - margin >
838
+ Math.max(blocker.segment.start.y, blocker.segment.end.y)
839
+ )
840
+ continue
800
841
  if (
801
842
  distanceSegmentToSegment(
802
843
  segment.start,
@@ -1283,6 +1324,9 @@ export function* routeViaMinimalWindingAlternativesSteps(
1283
1324
  const initialRouteOrderFactories: Array<
1284
1325
  () => readonly ViaMinimalWindingTerminal[]
1285
1326
  > = []
1327
+ if (alignGridToPads && preferTargetDirectedLaneBias && viasAreBeforeTargets) {
1328
+ initialRouteOrderFactories.push(() => targetOrderedTerminals)
1329
+ }
1286
1330
  if (viasAreBeforeTargets) {
1287
1331
  initialRouteOrderFactories.push(() => [
1288
1332
  ...targetOrderedTerminals.slice(1),
@@ -1340,11 +1384,28 @@ export function* routeViaMinimalWindingAlternativesSteps(
1340
1384
  getItemKey: (terminal) => terminal.connection.connection.name,
1341
1385
  maximumOrderCount: maximumRouteOrderCount,
1342
1386
  })
1387
+ const pendingRouteOrders: ViaMinimalWindingTerminal[][] = []
1388
+ const seenAdaptiveOrders = new Set<string>()
1389
+ const adaptiveRouteOrders = function* () {
1390
+ while (true) {
1391
+ const pending = pendingRouteOrders.shift()
1392
+ const next = pending
1393
+ ? { done: false, value: pending }
1394
+ : routeOrders.next()
1395
+ if (next.done) return
1396
+ const key = next.value
1397
+ .map((terminal) => terminal.connection.connection.name)
1398
+ .join("|")
1399
+ if (seenAdaptiveOrders.has(key)) continue
1400
+ seenAdaptiveOrders.add(key)
1401
+ yield next.value
1402
+ }
1403
+ }
1343
1404
 
1344
1405
  const alternatives: FanoutRoutePlan[][] = []
1345
1406
  const seenAlternativeKeys = new Set<string>()
1346
1407
  let routeOrderAttemptCount = 0
1347
- for (const routeOrder of routeOrders) {
1408
+ for (const routeOrder of adaptiveRouteOrders()) {
1348
1409
  for (const laneBias of laneBiases) {
1349
1410
  if (
1350
1411
  maximumRouteOrderAttempts !== undefined &&
@@ -1415,6 +1476,16 @@ export function* routeViaMinimalWindingAlternativesSteps(
1415
1476
  : {}),
1416
1477
  }
1417
1478
  if (!points) {
1479
+ if (
1480
+ adaptiveRouteOrder &&
1481
+ maximumRouteOrderAttempts !== undefined &&
1482
+ terminalIndex > 0
1483
+ ) {
1484
+ const others = routeOrder.filter(
1485
+ (candidate) => candidate !== terminal,
1486
+ )
1487
+ pendingRouteOrders.push([terminal, ...others])
1488
+ }
1418
1489
  failed = true
1419
1490
  break
1420
1491
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.50",
3
+ "version": "0.0.51",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",