@tscircuit/fanout-solver 0.0.45 → 0.0.47

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.
@@ -447,6 +447,8 @@ export function shouldUseJointBoundaryViaReservation(
447
447
  boundaryBusConnectionCounts.length === 5 ||
448
448
  boundaryBusConnectionCounts.length === 6 ||
449
449
  boundaryBusConnectionCounts.length === 7 ||
450
+ boundaryBusConnectionCounts.length === 8 ||
451
+ boundaryBusConnectionCounts.length === 9 ||
450
452
  (boundaryBusConnectionCounts.length === 4 &&
451
453
  new Set(boundaryBusConnectionCounts).size > 1)
452
454
  )
@@ -455,25 +457,106 @@ export function shouldUseJointBoundaryViaReservation(
455
457
  export function shouldDeferSingletonBoundaryViaReservation(
456
458
  boundaryBusConnectionCounts: readonly number[],
457
459
  ): boolean {
460
+ const boundaryBusCount = boundaryBusConnectionCounts.length
461
+ const singletonBusCount = boundaryBusConnectionCounts.filter(
462
+ (count) => count === 1,
463
+ ).length
458
464
  return (
459
- (boundaryBusConnectionCounts.length === 5 ||
460
- boundaryBusConnectionCounts.length === 6 ||
461
- boundaryBusConnectionCounts.length === 7) &&
462
- boundaryBusConnectionCounts.filter((count) => count === 1).length === 1
465
+ ((boundaryBusCount === 5 ||
466
+ boundaryBusCount === 6 ||
467
+ boundaryBusCount === 7) &&
468
+ singletonBusCount === 1) ||
469
+ (boundaryBusCount === 8 &&
470
+ (singletonBusCount === 1 || singletonBusCount === 2)) ||
471
+ (boundaryBusCount === 9 && singletonBusCount >= 1 && singletonBusCount <= 3)
463
472
  )
464
473
  }
465
474
 
475
+ export function getDenseSingletonDeferralCandidateCount(
476
+ boundaryBusConnectionCounts: readonly number[],
477
+ ): number {
478
+ if (
479
+ !shouldDeferSingletonBoundaryViaReservation(boundaryBusConnectionCounts)
480
+ ) {
481
+ return 0
482
+ }
483
+ const singletonBusCount = boundaryBusConnectionCounts.filter(
484
+ (count) => count === 1,
485
+ ).length
486
+ return Math.max(1, singletonBusCount - 1)
487
+ }
488
+
489
+ interface DenseSourceFieldBus {
490
+ componentId: string
491
+ exitEdge?: PreparedBus["exitEdge"]
492
+ allowedLayers?: readonly string[]
493
+ routableEscapeLayers?: readonly string[]
494
+ connections: readonly { sourcePoint: { x: number; y: number } }[]
495
+ }
496
+
497
+ export function isDenseSingletonEmbeddedInMultiLayerWideBus(params: {
498
+ singletonBus: DenseSourceFieldBus
499
+ singletonTargetLayer: string
500
+ wideBuses: readonly DenseSourceFieldBus[]
501
+ }): boolean {
502
+ const sourcePoint = params.singletonBus.connections[0]?.sourcePoint
503
+ if (params.singletonBus.connections.length !== 1 || !sourcePoint) return false
504
+ return params.wideBuses.some((wideBus) => {
505
+ const wideLayers =
506
+ wideBus.routableEscapeLayers ?? wideBus.allowedLayers ?? []
507
+ if (
508
+ wideBus.connections.length < 8 ||
509
+ wideBus.componentId !== params.singletonBus.componentId ||
510
+ wideBus.exitEdge !== params.singletonBus.exitEdge ||
511
+ wideLayers.length < 2 ||
512
+ !wideLayers.includes(params.singletonTargetLayer)
513
+ ) {
514
+ return false
515
+ }
516
+ const sourceXs = wideBus.connections.map(
517
+ (connection) => connection.sourcePoint.x,
518
+ )
519
+ const sourceYs = wideBus.connections.map(
520
+ (connection) => connection.sourcePoint.y,
521
+ )
522
+ return (
523
+ sourcePoint.x >= Math.min(...sourceXs) - 1e-9 &&
524
+ sourcePoint.x <= Math.max(...sourceXs) + 1e-9 &&
525
+ sourcePoint.y >= Math.min(...sourceYs) - 1e-9 &&
526
+ sourcePoint.y <= Math.max(...sourceYs) + 1e-9
527
+ )
528
+ })
529
+ }
530
+
531
+ export function getDenseLeadingCornerBandTargetTrackOffset(params: {
532
+ leadingLaneCount: number
533
+ traceWidth: number
534
+ viaDiameter: number
535
+ clearance: number
536
+ }): number {
537
+ const cornerPitch = Math.max(
538
+ params.traceWidth + params.clearance,
539
+ params.viaDiameter + params.clearance,
540
+ )
541
+ return (-params.leadingLaneCount * cornerPitch) / 2
542
+ }
543
+
466
544
  export function shouldSearchAdditionalBoundaryRouteTopologies(params: {
467
545
  boundaryBusCount: number
468
546
  connectionCount: number
469
547
  rawSkew: number
470
548
  maximumSkew: number
471
549
  }): boolean {
472
- if (params.boundaryBusCount === 5 || params.boundaryBusCount > 7) {
550
+ if (params.boundaryBusCount === 5 || params.boundaryBusCount > 9) {
473
551
  return false
474
552
  }
475
- if (params.boundaryBusCount === 6 || params.boundaryBusCount === 7) {
476
- // Six or seven buses remove enough meander space that a severely skewed wide
553
+ if (
554
+ params.boundaryBusCount === 6 ||
555
+ params.boundaryBusCount === 7 ||
556
+ params.boundaryBusCount === 8 ||
557
+ params.boundaryBusCount === 9
558
+ ) {
559
+ // Six through nine buses remove enough meander space that a severely skewed wide
477
560
  // topology can be impossible to tune. Keep the retry away from narrow
478
561
  // differential/control groups and from modest deficits that the atomic
479
562
  // length matcher can absorb directly.
@@ -488,6 +571,72 @@ export function shouldSearchAdditionalBoundaryRouteTopologies(params: {
488
571
  )
489
572
  }
490
573
 
574
+ interface DenseSingletonBoundaryGeometryBus {
575
+ busId: string
576
+ direction: PreparedBus["direction"]
577
+ exitEdge?: PreparedBus["exitEdge"]
578
+ preferredExit?: PreparedBus["preferredExit"]
579
+ connections: readonly {
580
+ sourcePoint: { x: number; y: number }
581
+ exitTargetPoint?: { x: number; y: number }
582
+ }[]
583
+ }
584
+
585
+ export function getDenseSingletonBoundaryGeometry(
586
+ bus: DenseSingletonBoundaryGeometryBus,
587
+ ): { isCorner: boolean; targetProjection: number } {
588
+ const connection = bus.connections[0]
589
+ const target = connection?.exitTargetPoint
590
+ let targetProjection = 0
591
+ if (connection && target) {
592
+ const deltaX = target.x - connection.sourcePoint.x
593
+ const deltaY = target.y - connection.sourcePoint.y
594
+ targetProjection =
595
+ bus.direction === "right"
596
+ ? deltaX
597
+ : bus.direction === "left"
598
+ ? -deltaX
599
+ : bus.direction === "up"
600
+ ? deltaY
601
+ : -deltaY
602
+ }
603
+ return {
604
+ isCorner: Boolean(getCornerBandSide(bus.exitEdge, bus.preferredExit)),
605
+ targetProjection,
606
+ }
607
+ }
608
+
609
+ export function compareDenseSingletonBoundaryDeferralPriority(
610
+ first: DenseSingletonBoundaryGeometryBus,
611
+ second: DenseSingletonBoundaryGeometryBus,
612
+ ): number {
613
+ const firstGeometry = getDenseSingletonBoundaryGeometry(first)
614
+ const secondGeometry = getDenseSingletonBoundaryGeometry(second)
615
+ return (
616
+ Number(firstGeometry.isCorner) - Number(secondGeometry.isCorner) ||
617
+ (firstGeometry.isCorner && secondGeometry.isCorner
618
+ ? firstGeometry.targetProjection - secondGeometry.targetProjection
619
+ : 0) ||
620
+ first.busId.localeCompare(second.busId)
621
+ )
622
+ }
623
+
624
+ function compareReleasedDenseSingletonBoundaryDeferralPriority(
625
+ first: DenseSingletonBoundaryGeometryBus,
626
+ second: DenseSingletonBoundaryGeometryBus,
627
+ ): number {
628
+ const firstGeometry = getDenseSingletonBoundaryGeometry(first)
629
+ const secondGeometry = getDenseSingletonBoundaryGeometry(second)
630
+ return (
631
+ Number(firstGeometry.isCorner) - Number(secondGeometry.isCorner) ||
632
+ (firstGeometry.isCorner && secondGeometry.isCorner
633
+ ? Number(firstGeometry.targetProjection > 0) -
634
+ Number(secondGeometry.targetProjection > 0)
635
+ : 0) ||
636
+ first.busId.localeCompare(second.busId)
637
+ )
638
+ }
639
+
491
640
  interface DensePairRoutingPriorityBus {
492
641
  componentId: string
493
642
  exitEdge?: PreparedBus["exitEdge"]
@@ -499,7 +648,7 @@ interface DensePairRoutingPriorityBus {
499
648
  }[]
500
649
  }
501
650
 
502
- export function getDenseSevenBusPairRoutingPriorityKeys(params: {
651
+ export function getDenseBoundaryPairRoutingPriorityKeys(params: {
503
652
  boundaryBusCount: number
504
653
  pairBuses: readonly DensePairRoutingPriorityBus[]
505
654
  }): number[] | null {
@@ -507,7 +656,9 @@ export function getDenseSevenBusPairRoutingPriorityKeys(params: {
507
656
  const firstBus = pairBuses[0]
508
657
  const firstSourceLayer = firstBus?.connections[0]?.sourceLayer
509
658
  if (
510
- params.boundaryBusCount !== 7 ||
659
+ (params.boundaryBusCount !== 7 &&
660
+ params.boundaryBusCount !== 8 &&
661
+ params.boundaryBusCount !== 9) ||
511
662
  pairBuses.length !== 3 ||
512
663
  firstBus === undefined ||
513
664
  firstBus.exitEdge === undefined ||
@@ -535,23 +686,24 @@ export function getDenseSevenBusPairRoutingPriorityKeys(params: {
535
686
  return null
536
687
  }
537
688
 
538
- const getMeanSourceToExitTargetDistance = (
689
+ const getMaximumSourceToExitTargetDistance = (
539
690
  bus: DensePairRoutingPriorityBus,
540
691
  ): number =>
541
- bus.connections.reduce((total, connection) => {
542
- const exitTarget = connection.exitTargetPoint!
543
- return (
544
- total +
545
- Math.hypot(
692
+ Math.max(
693
+ ...bus.connections.map((connection) => {
694
+ const exitTarget = connection.exitTargetPoint!
695
+ return Math.hypot(
546
696
  exitTarget.x - connection.sourcePoint.x,
547
697
  exitTarget.y - connection.sourcePoint.y,
548
698
  )
549
- )
550
- }, 0) / bus.connections.length
699
+ }),
700
+ )
701
+ // A pair is constrained by the lane with the farthest explicit reach. A
702
+ // mean can hide that lane behind its shorter mate and reverse channel order.
551
703
  // Quantize once so equality is transitive. An epsilon-based pairwise
552
704
  // comparator can otherwise produce A = B, B = C, but A != C.
553
705
  return pairBuses.map((bus) =>
554
- Number(getMeanSourceToExitTargetDistance(bus).toFixed(9)),
706
+ Number(getMaximumSourceToExitTargetDistance(bus).toFixed(9)),
555
707
  )
556
708
  }
557
709
 
@@ -846,7 +998,7 @@ export class FanoutSolver extends BaseSolver {
846
998
  const twoConnectionBoundaryBuses = unsortedBoundaryBuses.filter(
847
999
  (bus) => bus.connections.length === 2,
848
1000
  )
849
- const pairRoutingPriorityKeys = getDenseSevenBusPairRoutingPriorityKeys({
1001
+ const pairRoutingPriorityKeys = getDenseBoundaryPairRoutingPriorityKeys({
850
1002
  boundaryBusCount: unsortedBoundaryBuses.length,
851
1003
  pairBuses: twoConnectionBoundaryBuses.map((bus) => ({
852
1004
  ...bus,
@@ -885,8 +1037,8 @@ export class FanoutSolver extends BaseSolver {
885
1037
  firstPairRoutingPriority !== secondPairRoutingPriority
886
1038
  ) {
887
1039
  // The third pair can be fenced off by two earlier pair windings. Let
888
- // the pair with the shortest explicit boundary reach claim its channel
889
- // first without relying on caller-specific bus identifiers.
1040
+ // the pair with the shortest farthest-lane boundary reach claim its
1041
+ // channel first without relying on caller-specific bus identifiers.
890
1042
  return firstPairRoutingPriority - secondPairRoutingPriority
891
1043
  }
892
1044
  const cornerBandDifference =
@@ -925,13 +1077,15 @@ export class FanoutSolver extends BaseSolver {
925
1077
  if (layerDifference !== 0) return -layerDifference
926
1078
  if (
927
1079
  unsortedBoundaryBuses.length !== 6 &&
928
- unsortedBoundaryBuses.length !== 7
1080
+ unsortedBoundaryBuses.length !== 7 &&
1081
+ unsortedBoundaryBuses.length !== 8 &&
1082
+ unsortedBoundaryBuses.length !== 9
929
1083
  ) {
930
1084
  return 0
931
1085
  }
932
1086
  // The general routing order can differ across the two components as a
933
1087
  // function of local pad geometry. Keep otherwise-equivalent corner buses
934
- // in one deterministic order for the six- and seven-bus paths so their
1088
+ // in one deterministic order for the six- through nine-bus paths so their
935
1089
  // boundary lanes do not swap between the two ends of a direct
936
1090
  // interconnect. Leave the released four- and five-bus tie behavior
937
1091
  // unchanged.
@@ -947,7 +1101,7 @@ export class FanoutSolver extends BaseSolver {
947
1101
  )
948
1102
  if (
949
1103
  boundaryBuses.length === 0 ||
950
- boundaryBuses.length > 7 ||
1104
+ boundaryBuses.length > 9 ||
951
1105
  planeBuses.length < 8 ||
952
1106
  boundaryBuses.some((bus) => !busUsesCoordinatedWinding(bus)) ||
953
1107
  planeBuses.some((bus) => bus.connections.length !== 1) ||
@@ -965,13 +1119,26 @@ export class FanoutSolver extends BaseSolver {
965
1119
  ),
966
1120
  ),
967
1121
  )
1122
+ const boundaryBusConnectionCounts = boundaryBuses.map(
1123
+ (bus) => bus.connections.length,
1124
+ )
1125
+ const singletonBoundaryBusCount = boundaryBusConnectionCounts.filter(
1126
+ (connectionCount) => connectionCount === 1,
1127
+ ).length
1128
+ const useGeometryAwareSingletonOutwardPreference =
1129
+ singletonBoundaryBusCount > 1 &&
1130
+ shouldDeferSingletonBoundaryViaReservation(boundaryBusConnectionCounts)
968
1131
  const preferredBoundaryPerpendicularSideByBusId = new Map(
969
1132
  boundaryBuses.map((bus) => [bus.busId, 1 as const]),
970
1133
  )
971
1134
  const preferBoundaryOutwardByBusId = new Map(
972
1135
  boundaryBuses.map((bus) => [
973
1136
  bus.busId,
974
- getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
1137
+ useGeometryAwareSingletonOutwardPreference &&
1138
+ bus.connections.length === 1 &&
1139
+ getCornerBandSide(bus.exitEdge, bus.preferredExit)
1140
+ ? getDenseSingletonBoundaryGeometry(bus).targetProjection > 0
1141
+ : getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
975
1142
  ]),
976
1143
  )
977
1144
  const canShareCopper = (
@@ -994,23 +1161,94 @@ export class FanoutSolver extends BaseSolver {
994
1161
  ),
995
1162
  )
996
1163
  }
997
- // Five through seven boundary buses, and heterogeneous four-bus groups, leave too little
1164
+ // Five through nine boundary buses, and heterogeneous four-bus groups, leave too little
998
1165
  // slack for incremental site allocation: a valid early trace can consume
999
1166
  // the last dogbone site of a later narrow bus. Reserve the multi-line bus
1000
- // barrels before routing copper. A single one-line bus stays provisional
1001
- // because its flexible site can be rematched around the wide-bus copper.
1167
+ // barrels before routing copper. Classify the least-constrained eligible
1168
+ // one-line buses as deferral candidates, preferring a centered singleton,
1169
+ // then a corner singleton whose explicit target lies inward along its local
1170
+ // escape direction. A candidate embedded in a multi-layer wide-bus source
1171
+ // field must instead reserve an outward dogbone and route before that wide
1172
+ // bus; the remaining candidates stay provisional and can be rematched
1173
+ // around completed copper.
1002
1174
  // Plane sites are likewise rematched around completed boundary plans.
1003
- const boundaryBusConnectionCounts = boundaryBuses.map(
1004
- (bus) => bus.connections.length,
1175
+ const singletonBoundaryBuses = boundaryBuses.filter(
1176
+ (bus) => bus.connections.length === 1,
1005
1177
  )
1006
- const provisionalSingletonBus = shouldDeferSingletonBoundaryViaReservation(
1007
- boundaryBusConnectionCounts,
1178
+ const singletonDeferralCandidates =
1179
+ shouldDeferSingletonBoundaryViaReservation(boundaryBusConnectionCounts)
1180
+ ? singletonBoundaryBuses
1181
+ .toSorted(
1182
+ boundaryBuses.length === 9
1183
+ ? compareDenseSingletonBoundaryDeferralPriority
1184
+ : compareReleasedDenseSingletonBoundaryDeferralPriority,
1185
+ )
1186
+ .slice(
1187
+ 0,
1188
+ getDenseSingletonDeferralCandidateCount(
1189
+ boundaryBusConnectionCounts,
1190
+ ),
1191
+ )
1192
+ : []
1193
+ const leadingWideSingletonBuses =
1194
+ boundaryBuses.length === 9
1195
+ ? singletonDeferralCandidates.filter((singletonBus) => {
1196
+ const singletonTargetLayer =
1197
+ params.busLayerAssignments[singletonBus.busId]
1198
+ return Boolean(
1199
+ singletonTargetLayer &&
1200
+ isDenseSingletonEmbeddedInMultiLayerWideBus({
1201
+ singletonBus,
1202
+ singletonTargetLayer,
1203
+ wideBuses: boundaryBuses,
1204
+ }),
1205
+ )
1206
+ })
1207
+ : []
1208
+ const leadingLaneCountByWideCornerBand = new Map<string, number>()
1209
+ if (boundaryBuses.length === 9 && leadingWideSingletonBuses.length > 0) {
1210
+ for (const bus of leadingWideSingletonBuses) {
1211
+ preferBoundaryOutwardByBusId.set(bus.busId, true)
1212
+ const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
1213
+ if (!bus.exitEdge || !side) continue
1214
+ const bandKey = `${bus.exitEdge}:${side}`
1215
+ const sharesBandWithWideBus = boundaryBuses.some((candidate) => {
1216
+ if (candidate === bus || candidate.connections.length < 8)
1217
+ return false
1218
+ return (
1219
+ candidate.exitEdge === bus.exitEdge &&
1220
+ getCornerBandSide(candidate.exitEdge, candidate.preferredExit) ===
1221
+ side
1222
+ )
1223
+ })
1224
+ if (!sharesBandWithWideBus) continue
1225
+ leadingLaneCountByWideCornerBand.set(
1226
+ bandKey,
1227
+ (leadingLaneCountByWideCornerBand.get(bandKey) ?? 0) +
1228
+ bus.connections.length,
1229
+ )
1230
+ }
1231
+ }
1232
+ const viaProvisionalSingletonBusSet = new Set(
1233
+ singletonDeferralCandidates.filter(
1234
+ (bus) => !leadingWideSingletonBuses.includes(bus),
1235
+ ),
1236
+ )
1237
+ const getCornerBandTargetTrackOffset = (bus: PreparedBus): number => {
1238
+ const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
1239
+ if (!bus.exitEdge || !side) return 0
1240
+ const leadingLaneCount =
1241
+ leadingLaneCountByWideCornerBand.get(`${bus.exitEdge}:${side}`) ?? 0
1242
+ return getDenseLeadingCornerBandTargetTrackOffset({
1243
+ leadingLaneCount,
1244
+ traceWidth: this.config.traceWidth,
1245
+ viaDiameter: this.config.viaDiameter,
1246
+ clearance: this.config.clearance,
1247
+ })
1248
+ }
1249
+ const initiallyMatchedBoundaryBuses = boundaryBuses.filter(
1250
+ (bus) => !viaProvisionalSingletonBusSet.has(bus),
1008
1251
  )
1009
- ? boundaryBuses.find((bus) => bus.connections.length === 1)
1010
- : undefined
1011
- const initiallyMatchedBoundaryBuses = provisionalSingletonBus
1012
- ? boundaryBuses.filter((bus) => bus !== provisionalSingletonBus)
1013
- : boundaryBuses
1014
1252
  const jointViaPoints = useJointBoundaryViaReservation
1015
1253
  ? matchComponentDogboneViaSites(
1016
1254
  [...planeBuses, ...initiallyMatchedBoundaryBuses],
@@ -1039,6 +1277,12 @@ export class FanoutSolver extends BaseSolver {
1039
1277
  canShareCopper,
1040
1278
  })
1041
1279
  if (seedViaPoints) {
1280
+ const denseBoundaryBusesInRoutingOrder = [
1281
+ ...leadingWideSingletonBuses,
1282
+ ...boundaryBuses.filter(
1283
+ (bus) => !leadingWideSingletonBuses.includes(bus),
1284
+ ),
1285
+ ]
1042
1286
  let fixedViaPointsByConnectionIndex: ReadonlyMap<
1043
1287
  number,
1044
1288
  { x: number; y: number }
@@ -1099,6 +1343,7 @@ export class FanoutSolver extends BaseSolver {
1099
1343
  fixedViaPointsByConnectionIndex,
1100
1344
  reservedVias: getReservedVias(bus),
1101
1345
  viaMinimalOnly: true,
1346
+ cornerBandTargetTrackOffset: getCornerBandTargetTrackOffset(bus),
1102
1347
  } as const
1103
1348
  let busPlans = routeBusAlternatives(routeParams, 1)[0]
1104
1349
  if (busPlans && bus.maxLengthSkew !== undefined) {
@@ -1135,14 +1380,14 @@ export class FanoutSolver extends BaseSolver {
1135
1380
  return true
1136
1381
  }
1137
1382
 
1138
- const firstBoundaryBus = boundaryBuses[0]!
1383
+ const firstBoundaryBus = denseBoundaryBusesInRoutingOrder[0]!
1139
1384
  const routedBoundaryBuses: PreparedBus[] = []
1140
1385
  if (routeMatchedBoundaryBus(firstBoundaryBus)) {
1141
1386
  routedBoundaryBuses.push(firstBoundaryBus)
1142
1387
  } else {
1143
1388
  matchedRoutingSucceeded = false
1144
1389
  }
1145
- const remainingBoundaryBuses = boundaryBuses.slice(1)
1390
+ const remainingBoundaryBuses = denseBoundaryBusesInRoutingOrder.slice(1)
1146
1391
  while (matchedRoutingSucceeded && remainingBoundaryBuses.length > 0) {
1147
1392
  const blockingSegments = matchedPlans.flatMap((plan) =>
1148
1393
  plan.segments.map((segment) => ({
@@ -525,10 +525,43 @@ function matchComponent(params: {
525
525
  }),
526
526
  )
527
527
  if (entries.some((entry) => entry.candidates.length === 0)) return null
528
+ // Every solution must include each sole candidate. Seed and validate those
529
+ // forced choices once so recursive matching only explores genuine choices.
530
+ const forcedCandidates = entries.flatMap((entry) =>
531
+ entry.candidates.length === 1 ? [entry.candidates[0]!] : [],
532
+ )
533
+ for (
534
+ let candidateIndex = 0;
535
+ candidateIndex < forcedCandidates.length;
536
+ candidateIndex++
537
+ ) {
538
+ const candidate = forcedCandidates[candidateIndex]!
539
+ for (
540
+ let previousIndex = 0;
541
+ previousIndex < candidateIndex;
542
+ previousIndex++
543
+ ) {
544
+ if (
545
+ !candidatesAreMutuallyClear({
546
+ first: candidate,
547
+ second: forcedCandidates[previousIndex]!,
548
+ rules,
549
+ })
550
+ ) {
551
+ return null
552
+ }
553
+ }
554
+ }
528
555
 
529
- const assignedCandidates = new Map<number, ViaSiteCandidate>()
556
+ const assignedCandidates = new Map(
557
+ forcedCandidates.map((candidate) => [candidate.connectionIndex, candidate]),
558
+ )
530
559
  const remaining = new Set(
531
- entries.map((entry) => entry.connection.preparedConnection.connectionIndex),
560
+ entries.flatMap((entry) =>
561
+ entry.candidates.length > 1
562
+ ? [entry.connection.preparedConnection.connectionIndex]
563
+ : [],
564
+ ),
532
565
  )
533
566
  const entryByConnectionIndex = new Map(
534
567
  entries.map((entry) => [
@@ -537,6 +570,15 @@ function matchComponent(params: {
537
570
  ]),
538
571
  )
539
572
 
573
+ if (remaining.size === 0) {
574
+ return new Map(
575
+ [...assignedCandidates.entries()].map(([connectionIndex, candidate]) => [
576
+ connectionIndex,
577
+ { ...candidate.point },
578
+ ]),
579
+ )
580
+ }
581
+
540
582
  const getViableCandidates = (
541
583
  entry: ConnectionCandidates,
542
584
  ): ViaSiteCandidate[] =>
package/lib/route-bus.ts CHANGED
@@ -60,6 +60,8 @@ export interface RouteBusParams {
60
60
  fixedViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
61
61
  reservedVias?: readonly ViaMinimalWindingReservedVia[]
62
62
  viaMinimalOnly?: boolean
63
+ /** Dense corner-band phase that preserves existing lane centers when leading lanes are prepended. */
64
+ cornerBandTargetTrackOffset?: number
63
65
  }
64
66
 
65
67
  interface TrackCandidate {
@@ -287,6 +289,24 @@ function getWindingCrossoverLayer(params: {
287
289
  )
288
290
  }
289
291
 
292
+ export function getBoundaryTargetTrack(params: {
293
+ bus: PreparedBus
294
+ connection: PreparedConnection
295
+ boundaryDirection: FanoutDirection
296
+ }): number {
297
+ const requestedTrack = getPerpendicularAxis(
298
+ params.connection.exitTargetPoint ?? params.connection.targetPoint,
299
+ params.boundaryDirection,
300
+ )
301
+ const boundaryMinimum = isHorizontal(params.boundaryDirection)
302
+ ? params.bus.sharedBoundary.minY
303
+ : params.bus.sharedBoundary.minX
304
+ const boundaryMaximum = isHorizontal(params.boundaryDirection)
305
+ ? params.bus.sharedBoundary.maxY
306
+ : params.bus.sharedBoundary.maxX
307
+ return Math.max(boundaryMinimum, Math.min(boundaryMaximum, requestedTrack))
308
+ }
309
+
290
310
  function getCornerTargetTrack(params: {
291
311
  bus: PreparedBus
292
312
  connection: PreparedConnection
@@ -297,6 +317,7 @@ function getCornerTargetTrack(params: {
297
317
  layerNames: readonly string[]
298
318
  targetLayer: string
299
319
  windingOrderIndex?: number
320
+ cornerBandTargetTrackOffset?: number
300
321
  }): number {
301
322
  const {
302
323
  bus,
@@ -308,6 +329,7 @@ function getCornerTargetTrack(params: {
308
329
  layerNames,
309
330
  targetLayer,
310
331
  windingOrderIndex,
332
+ cornerBandTargetTrackOffset = 0,
311
333
  } = params
312
334
  const side = getCornerSide(bus)
313
335
  if (!side || !bus.exitEdge) {
@@ -326,7 +348,8 @@ function getCornerTargetTrack(params: {
326
348
  const pitch = Math.max(traceWidth + clearance, viaDiameter + clearance)
327
349
  const baseBandCenter =
328
350
  boundaryMinimum +
329
- (boundaryMaximum - boundaryMinimum) * (side === "minimum" ? 0.25 : 0.75)
351
+ (boundaryMaximum - boundaryMinimum) * (side === "minimum" ? 0.25 : 0.75) +
352
+ cornerBandTargetTrackOffset
330
353
  const windingTarget = busUsesCoordinatedWindingChannel(bus)
331
354
  ? getWindingTargetRank({
332
355
  bus,
@@ -945,6 +968,7 @@ function buildPlan(params: {
945
968
  allowBlindAndBuriedVias: boolean
946
969
  initialViaPoint?: Point2D
947
970
  sourceEscapePath?: readonly Point2D[]
971
+ cornerBandTargetTrackOffset?: number
948
972
  }): FanoutRoutePlan {
949
973
  const {
950
974
  preparedConnection,
@@ -967,6 +991,7 @@ function buildPlan(params: {
967
991
  allowBlindAndBuriedVias,
968
992
  initialViaPoint,
969
993
  sourceEscapePath,
994
+ cornerBandTargetTrackOffset,
970
995
  } = params
971
996
  const sourcePoint = {
972
997
  x: preparedConnection.sourcePoint.x,
@@ -1048,13 +1073,14 @@ function buildPlan(params: {
1048
1073
  clearance,
1049
1074
  layerNames,
1050
1075
  targetLayer,
1076
+ cornerBandTargetTrackOffset,
1051
1077
  })
1052
1078
  : usesLayeredWindingChannel
1053
- ? getPerpendicularAxis(
1054
- preparedConnection.exitTargetPoint ??
1055
- preparedConnection.targetPoint,
1079
+ ? getBoundaryTargetTrack({
1080
+ bus,
1081
+ connection: preparedConnection,
1056
1082
  boundaryDirection,
1057
- )
1083
+ })
1058
1084
  : track
1059
1085
  const connectionRank = getConnectionRank(bus, preparedConnection)
1060
1086
  const localCornerSide = getLocalCornerSide(bus)
@@ -2365,6 +2391,7 @@ export function routeBusAlternatives(
2365
2391
  fixedViaPointsByConnectionIndex,
2366
2392
  reservedVias = [],
2367
2393
  viaMinimalOnly = false,
2394
+ cornerBandTargetTrackOffset,
2368
2395
  } = params
2369
2396
  if (!Number.isInteger(maxAlternatives) || maxAlternatives < 1) {
2370
2397
  throw new Error(
@@ -2698,12 +2725,13 @@ export function routeBusAlternatives(
2698
2725
  layerNames,
2699
2726
  targetLayer,
2700
2727
  windingOrderIndex: terminalPattern.windingOrderIndex,
2728
+ cornerBandTargetTrackOffset,
2701
2729
  })
2702
- : getPerpendicularAxis(
2703
- preparedConnection.exitTargetPoint ??
2704
- preparedConnection.targetPoint,
2730
+ : getBoundaryTargetTrack({
2731
+ bus,
2732
+ connection: preparedConnection,
2705
2733
  boundaryDirection,
2706
- )
2734
+ })
2707
2735
  return {
2708
2736
  connection: preparedConnection,
2709
2737
  viaPoint: terminalPattern.getViaPoint
@@ -2882,6 +2910,7 @@ export function routeBusAlternatives(
2882
2910
  clearance,
2883
2911
  terminateAtVia: false,
2884
2912
  allowBlindAndBuriedVias,
2913
+ cornerBandTargetTrackOffset,
2885
2914
  })
2886
2915
  if (
2887
2916
  !planIsClear({
@@ -2889,7 +2918,7 @@ export function routeBusAlternatives(
2889
2918
  otherPlans: [...acceptedPlans, ...candidatePlans],
2890
2919
  staticClearanceCache,
2891
2920
  blockingBusCounts,
2892
- cacheKey: `boundary:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${viaHandedness}:${trackIndex}:${bus.exitEdge ?? "legacy"}:${cornerLaneOffsets.exit}:${cornerLaneOffsets.localChannel}:${cornerLaneOffsets.boundaryChannel}`,
2921
+ cacheKey: `boundary:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${viaHandedness}:${trackIndex}:${bus.exitEdge ?? "legacy"}:${cornerLaneOffsets.exit}:${cornerLaneOffsets.localChannel}:${cornerLaneOffsets.boundaryChannel}:${cornerBandTargetTrackOffset ?? 0}`,
2893
2922
  srj,
2894
2923
  sharedBoundary: bus.sharedBoundary,
2895
2924
  clearance,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.45",
3
+ "version": "0.0.47",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",