@tscircuit/fanout-solver 0.0.59 → 0.0.60

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,
@@ -1982,14 +1986,35 @@ export class FanoutSolver extends BaseSolver {
1982
1986
  : []),
1983
1987
  ...singletonDeferralCandidates.filter((bus) => {
1984
1988
  const containingBus = getContainingWideSourceField(bus)
1985
- const sharesContainingBusLayer =
1989
+ const sourcePoint = bus.connections[0]!.sourcePoint
1990
+ const componentCenter = {
1991
+ x: (bus.componentBounds.minX + bus.componentBounds.maxX) / 2,
1992
+ y: (bus.componentBounds.minY + bus.componentBounds.maxY) / 2,
1993
+ }
1994
+ const boundaryDirection = bus.exitEdge
1995
+ ? getDirectionForExitEdge(bus.exitEdge)
1996
+ : bus.direction
1997
+ const inwardProjection =
1998
+ boundaryDirection === "right"
1999
+ ? componentCenter.x - sourcePoint.x
2000
+ : boundaryDirection === "left"
2001
+ ? sourcePoint.x - componentCenter.x
2002
+ : boundaryDirection === "up"
2003
+ ? componentCenter.y - sourcePoint.y
2004
+ : sourcePoint.y - componentCenter.y
2005
+ // Crossing the component can consume an embedded singleton's source
2006
+ // dogbone even when the target layers differ. Keep outward boundary
2007
+ // escapes provisional unless they share the wide bus's target layer.
2008
+ const reserveEmbeddedSourceEscape =
1986
2009
  usePadAlignedDenseRouting &&
1987
2010
  !useConfiguredDensePlaneRouting &&
1988
2011
  containingBus &&
1989
- params.busLayerAssignments[containingBus.busId] ===
1990
- params.busLayerAssignments[bus.busId]
2012
+ (params.busLayerAssignments[containingBus.busId] ===
2013
+ params.busLayerAssignments[bus.busId] ||
2014
+ inwardProjection > 1e-9)
1991
2015
  return (
1992
- !leadingWideSingletonBuses.includes(bus) && !sharesContainingBusLayer
2016
+ !leadingWideSingletonBuses.includes(bus) &&
2017
+ !reserveEmbeddedSourceEscape
1993
2018
  )
1994
2019
  }),
1995
2020
  ...(hasThreeWideBoundaryBuses
@@ -2164,6 +2189,117 @@ export class FanoutSolver extends BaseSolver {
2164
2189
  bus,
2165
2190
  ]),
2166
2191
  ]
2192
+ const areAdjacentInvertedNarrowBuses = (
2193
+ first: PreparedBus,
2194
+ second: PreparedBus,
2195
+ ): boolean => {
2196
+ if (
2197
+ !usePadAlignedDenseRouting ||
2198
+ useConfiguredDensePlaneRouting ||
2199
+ first.componentId !== second.componentId ||
2200
+ first.exitEdge !== second.exitEdge ||
2201
+ first.direction !== second.direction ||
2202
+ getCornerBandSide(first.exitEdge, first.preferredExit) !==
2203
+ getCornerBandSide(second.exitEdge, second.preferredExit) ||
2204
+ params.busLayerAssignments[first.busId] !==
2205
+ params.busLayerAssignments[second.busId] ||
2206
+ getContainingWideSourceField(first) !==
2207
+ getContainingWideSourceField(second) ||
2208
+ !getContainingWideSourceField(first)
2209
+ )
2210
+ return false
2211
+ const firstCenter = {
2212
+ x:
2213
+ first.connections.reduce(
2214
+ (sum, connection) => sum + connection.sourcePoint.x,
2215
+ 0,
2216
+ ) / first.connections.length,
2217
+ y:
2218
+ first.connections.reduce(
2219
+ (sum, connection) => sum + connection.sourcePoint.y,
2220
+ 0,
2221
+ ) / first.connections.length,
2222
+ }
2223
+ const secondCenter = {
2224
+ x:
2225
+ second.connections.reduce(
2226
+ (sum, connection) => sum + connection.sourcePoint.x,
2227
+ 0,
2228
+ ) / second.connections.length,
2229
+ y:
2230
+ second.connections.reduce(
2231
+ (sum, connection) => sum + connection.sourcePoint.y,
2232
+ 0,
2233
+ ) / second.connections.length,
2234
+ }
2235
+ if (
2236
+ Math.hypot(
2237
+ firstCenter.x - secondCenter.x,
2238
+ firstCenter.y - secondCenter.y,
2239
+ ) >
2240
+ 1.5 * Math.min(first.pitchX, first.pitchY)
2241
+ )
2242
+ return false
2243
+ const axis =
2244
+ first.exitEdge === "left" || first.exitEdge === "right" ? "y" : "x"
2245
+ const targetTrack = (bus: PreparedBus) =>
2246
+ bus.connections.reduce(
2247
+ (sum, connection) =>
2248
+ sum +
2249
+ (connection.exitTargetPoint ?? connection.targetPoint)[axis],
2250
+ 0,
2251
+ ) / bus.connections.length
2252
+ return (
2253
+ (firstCenter[axis] - secondCenter[axis]) *
2254
+ (targetTrack(first) - targetTrack(second)) <
2255
+ -1e-9
2256
+ )
2257
+ }
2258
+ // Preserve a centered pair's channel before its leading singleton. A
2259
+ // turning pair instead leaves its adjacent singleton room to escape
2260
+ // before searching for an outside-package via.
2261
+ const centeredPairPromotionGroups = new Set<string>()
2262
+ for (const singleton of multiLayerLeadingSingletonBuses) {
2263
+ if (getCornerBandSide(singleton.exitEdge, singleton.preferredExit))
2264
+ continue
2265
+ for (const pair of boundaryBuses) {
2266
+ if (
2267
+ pair.connections.length !== 2 ||
2268
+ !areAdjacentInvertedNarrowBuses(pair, singleton)
2269
+ )
2270
+ continue
2271
+ const pairIndex = denseBoundaryBusesInRoutingOrder.indexOf(pair)
2272
+ const singletonIndex =
2273
+ denseBoundaryBusesInRoutingOrder.indexOf(singleton)
2274
+ if (pairIndex > singletonIndex) {
2275
+ denseBoundaryBusesInRoutingOrder.splice(pairIndex, 1)
2276
+ denseBoundaryBusesInRoutingOrder.splice(singletonIndex, 0, pair)
2277
+ centeredPairPromotionGroups.add(
2278
+ `${pair.componentId}:${pair.exitEdge}`,
2279
+ )
2280
+ }
2281
+ }
2282
+ }
2283
+ for (const pair of boundaryBuses) {
2284
+ if (
2285
+ !centeredPairPromotionGroups.has(
2286
+ `${pair.componentId}:${pair.exitEdge}`,
2287
+ ) ||
2288
+ pair.connections.length !== 2 ||
2289
+ !getCornerBandSide(pair.exitEdge, pair.preferredExit)
2290
+ )
2291
+ continue
2292
+ for (const singleton of singletonBoundaryBuses) {
2293
+ if (!areAdjacentInvertedNarrowBuses(singleton, pair)) continue
2294
+ const singletonIndex =
2295
+ denseBoundaryBusesInRoutingOrder.indexOf(singleton)
2296
+ const pairIndex = denseBoundaryBusesInRoutingOrder.indexOf(pair)
2297
+ if (singletonIndex > pairIndex) {
2298
+ denseBoundaryBusesInRoutingOrder.splice(singletonIndex, 1)
2299
+ denseBoundaryBusesInRoutingOrder.splice(pairIndex, 0, singleton)
2300
+ }
2301
+ }
2302
+ }
2167
2303
  let fixedViaPointsByConnectionIndex: ReadonlyMap<
2168
2304
  number,
2169
2305
  { x: number; y: number }
@@ -2311,6 +2447,7 @@ export class FanoutSolver extends BaseSolver {
2311
2447
  useConfiguredDensePlaneRouting &&
2312
2448
  singleLayerBus !== bus &&
2313
2449
  !embeddedNarrowBusAlreadyRouted
2450
+ let usedSoftPlaneRepair = false
2314
2451
  let busPlans = (yield* routeAlternatives(
2315
2452
  preferSingleLayerWinding
2316
2453
  ? { ...routeParams, bus: singleLayerBus }
@@ -2417,6 +2554,86 @@ export class FanoutSolver extends BaseSolver {
2417
2554
  }
2418
2555
  }
2419
2556
  }
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.
2560
+ if (
2561
+ !busPlans &&
2562
+ bus.connections.length >= 8 &&
2563
+ boundaryBuses.some(
2564
+ (candidate) =>
2565
+ candidate.connections.length >= 8 &&
2566
+ matchedPlans.some((plan) => plan.busId === candidate.busId),
2567
+ )
2568
+ ) {
2569
+ const committedNames = new Set(
2570
+ matchedPlans.map((plan) => plan.connectionName),
2571
+ )
2572
+ const futurePlaneNames = new Set(
2573
+ this.preparedBuses
2574
+ .filter((candidate) => candidate.termination.type === "plane")
2575
+ .flatMap((candidate) =>
2576
+ candidate.connections.map(
2577
+ (connection) => connection.connection.name,
2578
+ ),
2579
+ )
2580
+ .filter((name) => !committedNames.has(name)),
2581
+ )
2582
+ const freePlans = (yield* routeAlternatives(
2583
+ {
2584
+ ...routeParams,
2585
+ fixedViaPointsByConnectionIndex: undefined,
2586
+ reservedVias: routeParams.reservedVias.filter(
2587
+ (reserved) => !futurePlaneNames.has(reserved.connectionName),
2588
+ ),
2589
+ softReservedVias: routeParams.reservedVias.filter((reserved) =>
2590
+ futurePlaneNames.has(reserved.connectionName),
2591
+ ),
2592
+ },
2593
+ 1,
2594
+ ))[0]
2595
+ if (freePlans) {
2596
+ const allPlans = [...matchedPlans, ...freePlans]
2597
+ const rematchedPoints = matchComponentDogboneViaSites(
2598
+ this.preparedBuses,
2599
+ {
2600
+ viaDiameter: this.config.viaDiameter,
2601
+ viaHoleDiameter: this.config.viaHoleDiameter,
2602
+ traceWidth: this.config.traceWidth,
2603
+ clearance: this.config.clearance,
2604
+ maximumSearchStates: 3_000_000,
2605
+ preferredBoundaryPerpendicularSideByBusId,
2606
+ preferBoundaryOutwardByBusId,
2607
+ fixedViaPointsByConnectionIndex: new Map(
2608
+ allPlans
2609
+ .filter((plan) => plan.via)
2610
+ .map((plan) => [plan.connectionIndex, plan.via!.center]),
2611
+ ),
2612
+ preferredViaPointsByConnectionIndex:
2613
+ fixedViaPointsByConnectionIndex,
2614
+ blockingSegments: allPlans.flatMap((plan) =>
2615
+ plan.segments.map((segment) => ({
2616
+ connectionIndex: plan.connectionIndex,
2617
+ segment,
2618
+ })),
2619
+ ),
2620
+ additionalObstacles: this.routingSrj.obstacles,
2621
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
2622
+ canShareCopper,
2623
+ },
2624
+ )
2625
+ debugDense(
2626
+ "free-sites:rematched",
2627
+ bus.busId,
2628
+ rematchedPoints?.size ?? "failed",
2629
+ )
2630
+ if (rematchedPoints) {
2631
+ busPlans = freePlans
2632
+ fixedViaPointsByConnectionIndex = rematchedPoints
2633
+ usedSoftPlaneRepair = true
2634
+ }
2635
+ }
2636
+ }
2420
2637
  if (busPlans && bus.maxLengthSkew !== undefined) {
2421
2638
  const lengths = busPlans.map((plan) => plan.length)
2422
2639
  const rawSkew = Math.max(...lengths) - Math.min(...lengths)
@@ -2430,7 +2647,13 @@ export class FanoutSolver extends BaseSolver {
2430
2647
  // Only pay for additional A* variants when the first topology is so
2431
2648
  // skewed that compact meanders are unlikely to absorb the deficit.
2432
2649
  // This keeps already-near-matched buses on the single-attempt path.
2433
- if (needsRouteDiversity && !matchLengthsAfterPlanes) {
2650
+ // Keep the jointly rematched repair: routeParams still carries the
2651
+ // earlier provisional sites and cannot safely replace its geometry.
2652
+ if (
2653
+ needsRouteDiversity &&
2654
+ !matchLengthsAfterPlanes &&
2655
+ !usedSoftPlaneRepair
2656
+ ) {
2434
2657
  busPlans = (yield* routeAlternatives(routeParams, 3)).toSorted(
2435
2658
  (first, second) => {
2436
2659
  const firstLengths = first.map((plan) => plan.length)
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
@@ -311,10 +313,89 @@ function getWindingCrossoverLayer(params: {
311
313
  )
312
314
  }
313
315
 
316
+ function getDistributedBoundaryTargetTracks(params: {
317
+ bus: PreparedBus
318
+ boundaryDirection: FanoutDirection
319
+ traceWidth: number
320
+ clearance: number
321
+ }): number[] | undefined {
322
+ const { bus, boundaryDirection, traceWidth, clearance } = params
323
+ if (getCornerSide(bus) || !busUsesCoordinatedWindingChannel(bus))
324
+ return undefined
325
+ const layers = new Set(
326
+ bus.connections.map(
327
+ (connection) =>
328
+ connection.exitTargetPoint?.layer ??
329
+ getPointLayer(connection.targetPoint),
330
+ ),
331
+ )
332
+ if (layers.size < 2) return undefined
333
+ const minimum = isHorizontal(boundaryDirection)
334
+ ? bus.sharedBoundary.minY
335
+ : bus.sharedBoundary.minX
336
+ const maximum = isHorizontal(boundaryDirection)
337
+ ? bus.sharedBoundary.maxY
338
+ : bus.sharedBoundary.maxX
339
+ const tracks = bus.connections
340
+ .map((connection) =>
341
+ Math.max(
342
+ minimum,
343
+ Math.min(
344
+ maximum,
345
+ getPerpendicularAxis(
346
+ connection.exitTargetPoint ?? connection.targetPoint,
347
+ boundaryDirection,
348
+ ),
349
+ ),
350
+ ),
351
+ )
352
+ .toSorted((a, b) => a - b)
353
+ const pitch = traceWidth + clearance
354
+ if (
355
+ tracks.every(
356
+ (track, index) =>
357
+ index === 0 || track - tracks[index - 1]! >= pitch - 1e-9,
358
+ )
359
+ )
360
+ return undefined
361
+ if (maximum - minimum < (tracks.length - 1) * pitch - 1e-9) return undefined
362
+ // Pool neighboring overlaps while preserving their mean requested location.
363
+ // Subtracting the pitch reduces the clearance constraint to monotonicity.
364
+ const blocks: { start: number; count: number; mean: number }[] = []
365
+ for (const [index, track] of tracks.entries()) {
366
+ blocks.push({ start: index, count: 1, mean: track - index * pitch })
367
+ while (blocks.length > 1 && blocks.at(-2)!.mean > blocks.at(-1)!.mean) {
368
+ const second = blocks.pop()!
369
+ const first = blocks.pop()!
370
+ blocks.push({
371
+ start: first.start,
372
+ count: first.count + second.count,
373
+ mean:
374
+ (first.mean * first.count + second.mean * second.count) /
375
+ (first.count + second.count),
376
+ })
377
+ }
378
+ }
379
+ for (const block of blocks) {
380
+ const mean = Math.max(
381
+ minimum,
382
+ Math.min(maximum - (tracks.length - 1) * pitch, block.mean),
383
+ )
384
+ for (let index = block.start; index < block.start + block.count; index++)
385
+ tracks[index] = mean + index * pitch
386
+ }
387
+ return tracks
388
+ }
389
+
314
390
  export function getBoundaryTargetTrack(params: {
315
391
  bus: PreparedBus
316
392
  connection: PreparedConnection
317
393
  boundaryDirection: FanoutDirection
394
+ traceWidth?: number
395
+ clearance?: number
396
+ layerNames?: readonly string[]
397
+ targetLayer?: string
398
+ windingOrderIndex?: number
318
399
  }): number {
319
400
  const requestedTrack = getPerpendicularAxis(
320
401
  params.connection.exitTargetPoint ?? params.connection.targetPoint,
@@ -326,6 +407,22 @@ export function getBoundaryTargetTrack(params: {
326
407
  const boundaryMaximum = isHorizontal(params.boundaryDirection)
327
408
  ? params.bus.sharedBoundary.maxY
328
409
  : params.bus.sharedBoundary.maxX
410
+ const distributedTracks =
411
+ params.traceWidth !== undefined && params.clearance !== undefined
412
+ ? getDistributedBoundaryTargetTracks({
413
+ ...params,
414
+ traceWidth: params.traceWidth,
415
+ clearance: params.clearance,
416
+ })
417
+ : undefined
418
+ if (distributedTracks && params.layerNames && params.targetLayer) {
419
+ const { rank } = getWindingTargetRank({
420
+ ...params,
421
+ layerNames: params.layerNames,
422
+ targetLayer: params.targetLayer,
423
+ })
424
+ return distributedTracks[rank]!
425
+ }
329
426
  return Math.max(boundaryMinimum, Math.min(boundaryMaximum, requestedTrack))
330
427
  }
331
428
 
@@ -1102,6 +1199,10 @@ function buildPlan(params: {
1102
1199
  bus,
1103
1200
  connection: preparedConnection,
1104
1201
  boundaryDirection,
1202
+ traceWidth,
1203
+ clearance,
1204
+ layerNames,
1205
+ targetLayer,
1105
1206
  })
1106
1207
  : track
1107
1208
  const connectionRank = getConnectionRank(bus, preparedConnection)
@@ -2459,6 +2560,7 @@ export function* routeBusAlternativesSteps(
2459
2560
  stopAfterFirstRejectedViaMinimalCandidate = false,
2460
2561
  fixedViaPointsByConnectionIndex,
2461
2562
  reservedVias = [],
2563
+ softReservedVias = [],
2462
2564
  viaMinimalOnly = false,
2463
2565
  allowBoundarySideViaFallback = false,
2464
2566
  preferCornerBoundaryVia = false,
@@ -2608,14 +2710,21 @@ export function* routeBusAlternativesSteps(
2608
2710
  localDogboneRepair?: boolean
2609
2711
  }
2610
2712
  const maximumThroughAllRouteOrderAttempts = 24
2611
- const windingTargetOrderCount = cornerSide
2612
- ? getWindingTargetOrders({
2613
- bus,
2614
- boundaryDirection,
2615
- layerNames,
2616
- targetLayer,
2617
- }).orders.length
2618
- : 1
2713
+ const windingTargetOrderCount =
2714
+ 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
2619
2728
  const uniformDogboneTerminalPatterns: CoordinatedTerminalPattern[] =
2620
2729
  viaHandednesses.map((viaHandedness) => ({
2621
2730
  label: `uniform-${viaHandedness}`,
@@ -2760,7 +2869,13 @@ export function* routeBusAlternativesSteps(
2760
2869
  const coordinatedViaPoints =
2761
2870
  fixedViaPointsByConnectionIndex ??
2762
2871
  (!allowBlindAndBuriedVias &&
2763
- bus.connections.length >= 8 &&
2872
+ (bus.connections.length >= 8 ||
2873
+ getDistributedBoundaryTargetTracks({
2874
+ bus,
2875
+ boundaryDirection,
2876
+ traceWidth,
2877
+ clearance,
2878
+ })) &&
2764
2879
  reservedVias.length === 0
2765
2880
  ? matchComponentDogboneViaSites([bus], {
2766
2881
  viaDiameter,
@@ -2902,6 +3017,11 @@ export function* routeBusAlternativesSteps(
2902
3017
  bus,
2903
3018
  connection: preparedConnection,
2904
3019
  boundaryDirection,
3020
+ traceWidth,
3021
+ clearance,
3022
+ layerNames,
3023
+ targetLayer,
3024
+ windingOrderIndex: terminalPattern.windingOrderIndex,
2905
3025
  })
2906
3026
  return {
2907
3027
  connection: preparedConnection,
@@ -3003,6 +3123,7 @@ export function* routeBusAlternativesSteps(
3003
3123
  alignGridToPads,
3004
3124
  includeReverseTargetRotation: terminalPattern.localDogboneRepair,
3005
3125
  reservedVias,
3126
+ softReservedVias,
3006
3127
  gridStepDivisor,
3007
3128
  preferTargetDirectedLaneBias:
3008
3129
  terminalPattern.preferTargetDirectedLaneBias,
@@ -3095,6 +3216,10 @@ export function* routeBusAlternativesSteps(
3095
3216
  bus,
3096
3217
  connection: preparedConnection,
3097
3218
  boundaryDirection,
3219
+ traceWidth,
3220
+ clearance,
3221
+ layerNames,
3222
+ targetLayer,
3098
3223
  }),
3099
3224
  )
3100
3225
  const finalExitPoints = finalTracks.map((track) =>
@@ -3182,47 +3307,64 @@ export function* routeBusAlternativesSteps(
3182
3307
  ),
3183
3308
  )
3184
3309
  : []
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
- : []
3310
+ const matchedVias = bus.connections.map(
3311
+ (connection) =>
3312
+ fixedViaPointsByConnectionIndex.get(connection.connectionIndex)!,
3313
+ )
3314
+ const packageEdgeViaCandidates = [
3315
+ {
3316
+ distance: sourceCenter.x - bus.componentBounds.minX,
3317
+ axis: "x" as const,
3318
+ value: bus.componentBounds.minX - 2 * insetStep,
3319
+ },
3320
+ {
3321
+ distance: bus.componentBounds.maxX - sourceCenter.x,
3322
+ axis: "x" as const,
3323
+ value: bus.componentBounds.maxX + 2 * insetStep,
3324
+ },
3325
+ {
3326
+ distance: sourceCenter.y - bus.componentBounds.minY,
3327
+ axis: "y" as const,
3328
+ value: bus.componentBounds.minY - 2 * insetStep,
3329
+ },
3330
+ {
3331
+ distance: bus.componentBounds.maxY - sourceCenter.y,
3332
+ axis: "y" as const,
3333
+ value: bus.componentBounds.maxY + 2 * insetStep,
3334
+ },
3335
+ ]
3336
+ .toSorted((a, b) => a.distance - b.distance)
3337
+ .flatMap(({ axis, value }) => {
3338
+ const otherAxis = axis === "x" ? "y" : "x"
3339
+ const mean =
3340
+ matchedVias.reduce((sum, via) => sum + via[otherAxis], 0) /
3341
+ matchedVias.length
3342
+ const order = matchedVias
3343
+ .map((via, index) => ({ index, track: via[otherAxis] }))
3344
+ .toSorted((a, b) => a.track - b.track || a.index - b.index)
3345
+ const pitch = viaDiameter + clearance
3346
+ const points = matchedVias.map((via, index) => ({
3347
+ ...via,
3348
+ [axis]: value,
3349
+ [otherAxis]:
3350
+ order.length === 2 &&
3351
+ Math.abs(order[1]!.track - order[0]!.track) < pitch
3352
+ ? mean + (order.findIndex((v) => v.index === index) - 0.5) * pitch
3353
+ : via[otherAxis],
3354
+ }))
3355
+ return points.length === 2
3356
+ ? [points, [points[1]!, points[0]!]]
3357
+ : [points]
3358
+ })
3359
+ const preferPackageEdgeVias =
3360
+ bus.connections.length === 2 && Boolean(getCornerSide(bus))
3225
3361
  const viaCandidates = [
3362
+ ...(preferPackageEdgeVias ? packageEdgeViaCandidates : []).map(
3363
+ (points) => ({
3364
+ points,
3365
+ boundarySide: false,
3366
+ }),
3367
+ ),
3226
3368
  ...displacedViaCandidates.map((points) => ({
3227
3369
  points,
3228
3370
  boundarySide: false,
@@ -3231,13 +3373,15 @@ export function* routeBusAlternativesSteps(
3231
3373
  points,
3232
3374
  boundarySide: true,
3233
3375
  })),
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
3376
+ // Preserve existing centered and singleton escapes before trying a short
3377
+ // source-layer route beyond the package. The target layer can then wind to the exit
3236
3378
  // without a local via being fenced in by an already-routed wide bus.
3237
- ...packageEdgeViaCandidates.map((points) => ({
3238
- points,
3239
- boundarySide: false,
3240
- })),
3379
+ ...(!preferPackageEdgeVias ? packageEdgeViaCandidates : []).map(
3380
+ (points) => ({
3381
+ points,
3382
+ boundarySide: false,
3383
+ }),
3384
+ ),
3241
3385
  ]
3242
3386
  for (const { points: boundaryViaPoints, boundarySide } of viaCandidates) {
3243
3387
  const boundaryVias = bus.connections.map((connection, index) => ({
@@ -3301,6 +3445,7 @@ export function* routeBusAlternativesSteps(
3301
3445
  allowBlindAndBuriedVias,
3302
3446
  allowSameNetMerges,
3303
3447
  maximumRouteOrderAttempts: bus.connections.length === 1 ? 3 : 6,
3448
+ softReservedVias,
3304
3449
  reservedVias:
3305
3450
  bus.connections.length > 1
3306
3451
  ? [...reservedVias, ...boundaryVias]
@@ -3350,6 +3495,7 @@ export function* routeBusAlternativesSteps(
3350
3495
  allowSameNetMerges,
3351
3496
  maximumRouteOrderAttempts: 6,
3352
3497
  reservedVias,
3498
+ softReservedVias,
3353
3499
  gridStepDivisor: 2,
3354
3500
  alignGridToPads: true,
3355
3501
  },
@@ -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. */
@@ -637,6 +639,7 @@ export function* routeViaMinimalWindingAlternativesSteps(
637
639
  allowSameNetMerges = false,
638
640
  maximumRouteOrderAttempts,
639
641
  reservedVias = [],
642
+ softReservedVias = [],
640
643
  gridStepDivisor = 1,
641
644
  preferTargetDirectedLaneBias = false,
642
645
  allowSourceLayerRouting = false,
@@ -702,6 +705,40 @@ export function* routeViaMinimalWindingAlternativesSteps(
702
705
  point: { x: gridMinX + column * gridStep, y: gridMinY + row * gridStep },
703
706
  }
704
707
  })
708
+ // Provisional plane barrels guide search without being fixed obstacles.
709
+ // Touch only the small grid rectangles around each disk, not every node.
710
+ const softViaCosts = softReservedVias.length
711
+ ? new Float32Array(nodeCount)
712
+ : undefined
713
+ if (softViaCosts) {
714
+ for (const { via } of softReservedVias) {
715
+ if (!via.spanLayers.includes(targetLayer)) continue
716
+ const radius = via.diameter / 2 + traceWidth / 2 + clearance
717
+ const minimumColumn = Math.max(
718
+ 0,
719
+ Math.ceil((via.center.x - radius - gridMinX) / gridStep),
720
+ )
721
+ const maximumColumn = Math.min(
722
+ columnCount - 1,
723
+ Math.floor((via.center.x + radius - gridMinX) / gridStep),
724
+ )
725
+ const minimumRow = Math.max(
726
+ 0,
727
+ Math.ceil((via.center.y - radius - gridMinY) / gridStep),
728
+ )
729
+ const maximumRow = Math.min(
730
+ rowCount - 1,
731
+ Math.floor((via.center.y + radius - gridMinY) / gridStep),
732
+ )
733
+ for (let row = minimumRow; row <= maximumRow; row++) {
734
+ for (let column = minimumColumn; column <= maximumColumn; column++) {
735
+ const index = row * columnCount + column
736
+ if (distance(nodes[index]!.point, via.center) < radius)
737
+ softViaCosts[index] = softViaCosts[index]! + 25 * gridStep
738
+ }
739
+ }
740
+ }
741
+ }
705
742
  const sampledGridPoints = includeVisualization
706
743
  ? nodes
707
744
  .filter(
@@ -1247,7 +1284,8 @@ export function* routeViaMinimalWindingAlternativesSteps(
1247
1284
  ? gridStep * Math.SQRT2
1248
1285
  : gridStep) +
1249
1286
  (addsTurn ? gridStep * 0.2 : 0) +
1250
- lanePenalty
1287
+ lanePenalty +
1288
+ (softViaCosts?.[nextNode] ?? 0)
1251
1289
  const nextState = nextNode * 9 + directionIndex
1252
1290
  if (nextDistance >= distances[nextState]! - EPSILON) continue
1253
1291
  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.60",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",