@tscircuit/fanout-solver 0.0.37 → 0.0.39

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.
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ Obstacle,
2
3
  SimpleRouteJson,
3
4
  SimplifiedPcbTrace,
4
5
  } from "@tscircuit/capacity-autorouter"
@@ -11,7 +12,7 @@ import {
11
12
  distanceSegmentToSegment,
12
13
  } from "./geometry"
13
14
  import { getAllRoutedTraceCopper } from "./get-routed-trace-copper"
14
- import { getLayerSpan } from "./layer-names"
15
+ import { getViaSpanLayers } from "./layer-names"
15
16
  import {
16
17
  connectionsShareElectricalNet,
17
18
  obstacleSharesElectricalNet,
@@ -37,6 +38,11 @@ export interface ViaMinimalWindingTerminal {
37
38
  exitPoint: Point2D
38
39
  }
39
40
 
41
+ export interface ViaMinimalWindingReservedVia {
42
+ connectionName: string
43
+ via: Pick<RoutedVia, "center" | "diameter" | "spanLayers">
44
+ }
45
+
40
46
  export interface RouteViaMinimalWindingParams {
41
47
  srj: SimpleRouteJson
42
48
  bus: PreparedBus
@@ -48,7 +54,12 @@ export interface RouteViaMinimalWindingParams {
48
54
  viaDiameter: number
49
55
  viaHoleDiameter: number
50
56
  clearance: number
57
+ allowBlindAndBuriedVias?: boolean
51
58
  allowSameNetMerges?: boolean
59
+ maximumRouteOrderAttempts?: number
60
+ reservedVias?: readonly ViaMinimalWindingReservedVia[]
61
+ /** Use a finer uniform grid for narrow channels between reserved vias. */
62
+ gridStepDivisor?: 1 | 2
52
63
  }
53
64
 
54
65
  interface GridNode {
@@ -74,6 +85,157 @@ interface BlockingVia {
74
85
  via: Pick<RoutedVia, "center" | "diameter" | "spanLayers">
75
86
  }
76
87
 
88
+ interface IndexedObstacle {
89
+ obstacle: Obstacle
90
+ minX: number
91
+ maxX: number
92
+ minY: number
93
+ maxY: number
94
+ xRadius: number
95
+ }
96
+
97
+ type ShapeAwareObstacle = Obstacle & {
98
+ shape?: "circle"
99
+ ccwRotationDegrees?: number
100
+ }
101
+
102
+ function getObstacleAxisAlignedBounds(
103
+ obstacle: Obstacle,
104
+ ): Omit<IndexedObstacle, "obstacle"> {
105
+ const shapeAwareObstacle = obstacle as ShapeAwareObstacle
106
+ if (shapeAwareObstacle.shape === "circle") {
107
+ const radius = obstacle.width / 2
108
+ return {
109
+ minX: obstacle.center.x - radius,
110
+ maxX: obstacle.center.x + radius,
111
+ minY: obstacle.center.y - radius,
112
+ maxY: obstacle.center.y + radius,
113
+ xRadius: radius,
114
+ }
115
+ }
116
+
117
+ const rotationRadians =
118
+ ((shapeAwareObstacle.ccwRotationDegrees ?? 0) * Math.PI) / 180
119
+ const absoluteCosine = Math.abs(Math.cos(rotationRadians))
120
+ const absoluteSine = Math.abs(Math.sin(rotationRadians))
121
+ const halfWidth = obstacle.width / 2
122
+ const halfHeight = obstacle.height / 2
123
+ const xRadius = absoluteCosine * halfWidth + absoluteSine * halfHeight
124
+ const yRadius = absoluteSine * halfWidth + absoluteCosine * halfHeight
125
+ return {
126
+ minX: obstacle.center.x - xRadius,
127
+ maxX: obstacle.center.x + xRadius,
128
+ minY: obstacle.center.y - yRadius,
129
+ maxY: obstacle.center.y + yRadius,
130
+ xRadius,
131
+ }
132
+ }
133
+
134
+ /**
135
+ * X-sorted broad phase for exact segment-to-obstacle clearance checks.
136
+ * Rotation-aware bounds make the query conservative; callers still use the
137
+ * shape-aware distance function to decide whether copper is actually blocked.
138
+ */
139
+ export class ObstacleSpatialIndex {
140
+ private readonly obstaclesByCenterX: IndexedObstacle[]
141
+ private readonly maximumXRadius: number
142
+
143
+ constructor(obstacles: readonly Obstacle[]) {
144
+ this.obstaclesByCenterX = obstacles
145
+ .map((obstacle) => ({
146
+ obstacle,
147
+ ...getObstacleAxisAlignedBounds(obstacle),
148
+ }))
149
+ .toSorted(
150
+ (first, second) => first.obstacle.center.x - second.obstacle.center.x,
151
+ )
152
+ this.maximumXRadius = this.obstaclesByCenterX.reduce(
153
+ (maximum, obstacle) => Math.max(maximum, obstacle.xRadius),
154
+ 0,
155
+ )
156
+ }
157
+
158
+ querySegment(segment: RoutedSegment, margin: number): Obstacle[] {
159
+ const segmentMinX = Math.min(segment.start.x, segment.end.x)
160
+ const segmentMaxX = Math.max(segment.start.x, segment.end.x)
161
+ const segmentMinY = Math.min(segment.start.y, segment.end.y)
162
+ const segmentMaxY = Math.max(segment.start.y, segment.end.y)
163
+ const minimumCenterX = segmentMinX - margin - this.maximumXRadius
164
+ const maximumCenterX = segmentMaxX + margin + this.maximumXRadius
165
+ let low = 0
166
+ let high = this.obstaclesByCenterX.length
167
+ while (low < high) {
168
+ const middle = Math.floor((low + high) / 2)
169
+ if (this.obstaclesByCenterX[middle]!.obstacle.center.x < minimumCenterX) {
170
+ low = middle + 1
171
+ } else {
172
+ high = middle
173
+ }
174
+ }
175
+
176
+ const candidates: Obstacle[] = []
177
+ for (
178
+ let obstacleIndex = low;
179
+ obstacleIndex < this.obstaclesByCenterX.length;
180
+ obstacleIndex++
181
+ ) {
182
+ const indexedObstacle = this.obstaclesByCenterX[obstacleIndex]!
183
+ if (indexedObstacle.obstacle.center.x > maximumCenterX) break
184
+ if (
185
+ indexedObstacle.maxX < segmentMinX - margin ||
186
+ indexedObstacle.minX > segmentMaxX + margin ||
187
+ indexedObstacle.maxY < segmentMinY - margin ||
188
+ indexedObstacle.minY > segmentMaxY + margin
189
+ ) {
190
+ continue
191
+ }
192
+ candidates.push(indexedObstacle.obstacle)
193
+ }
194
+ return candidates
195
+ }
196
+ }
197
+
198
+ export function* iterateUniqueRouteOrders<T>(params: {
199
+ initialOrderFactories: ReadonlyArray<() => readonly T[]>
200
+ rotationBase: readonly T[]
201
+ getItemKey: (item: T) => string
202
+ maximumOrderCount?: number
203
+ }): Generator<readonly T[]> {
204
+ const {
205
+ initialOrderFactories,
206
+ rotationBase,
207
+ getItemKey,
208
+ maximumOrderCount = Number.POSITIVE_INFINITY,
209
+ } = params
210
+ const seenOrderKeys = new Set<string>()
211
+ let yieldedOrderCount = 0
212
+ const getOrderKey = (order: readonly T[]): string =>
213
+ order.map(getItemKey).join("\u0000")
214
+
215
+ for (const createOrder of initialOrderFactories) {
216
+ if (yieldedOrderCount >= maximumOrderCount) return
217
+ const order = createOrder()
218
+ const key = getOrderKey(order)
219
+ if (seenOrderKeys.has(key)) continue
220
+ seenOrderKeys.add(key)
221
+ yieldedOrderCount++
222
+ yield order
223
+ }
224
+
225
+ for (let offset = 1; offset < rotationBase.length; offset++) {
226
+ if (yieldedOrderCount >= maximumOrderCount) return
227
+ const order = [
228
+ ...rotationBase.slice(offset),
229
+ ...rotationBase.slice(0, offset),
230
+ ]
231
+ const key = getOrderKey(order)
232
+ if (seenOrderKeys.has(key)) continue
233
+ seenOrderKeys.add(key)
234
+ yieldedOrderCount++
235
+ yield order
236
+ }
237
+ }
238
+
77
239
  interface HeapEntry {
78
240
  node: number
79
241
  direction: number
@@ -234,9 +396,13 @@ function getPlanVias(plan: FanoutRoutePlan): RoutedVia[] {
234
396
  function getBlockingCopper(params: {
235
397
  srj: SimpleRouteJson
236
398
  acceptedPlans: readonly FanoutRoutePlan[]
399
+ allowBlindAndBuriedVias: boolean
237
400
  }): { segments: BlockingSegment[]; vias: BlockingVia[] } {
238
- const { srj, acceptedPlans } = params
239
- const routedTraceCopper = getAllRoutedTraceCopper(srj)
401
+ const { srj, acceptedPlans, allowBlindAndBuriedVias } = params
402
+ const routedTraceCopper = getAllRoutedTraceCopper(
403
+ srj,
404
+ allowBlindAndBuriedVias,
405
+ )
240
406
  return {
241
407
  segments: [
242
408
  ...routedTraceCopper.flatMap((copper) =>
@@ -280,6 +446,7 @@ function buildPlan(params: {
280
446
  traceWidth: number
281
447
  viaDiameter: number
282
448
  viaHoleDiameter: number
449
+ allowBlindAndBuriedVias: boolean
283
450
  }): FanoutRoutePlan {
284
451
  const {
285
452
  bus,
@@ -290,6 +457,7 @@ function buildPlan(params: {
290
457
  traceWidth,
291
458
  viaDiameter,
292
459
  viaHoleDiameter,
460
+ allowBlindAndBuriedVias,
293
461
  } = params
294
462
  const connection = terminal.connection
295
463
  const sourcePoint = {
@@ -302,12 +470,14 @@ function buildPlan(params: {
302
470
  width: traceWidth,
303
471
  layer: connection.sourceLayer,
304
472
  }
473
+ const hasSourceDogbone = distance(sourcePoint, terminal.viaPoint) > EPSILON
305
474
  const targetSegments = getSegments(targetLayerPoints, traceWidth, targetLayer)
306
- const spanLayers = getLayerSpan(
307
- connection.sourceLayer,
308
- targetLayer,
475
+ const spanLayers = getViaSpanLayers({
476
+ fromLayer: connection.sourceLayer,
477
+ toLayer: targetLayer,
309
478
  layerNames,
310
- )
479
+ allowBlindAndBuriedVias,
480
+ })
311
481
  const via: RoutedVia = {
312
482
  center: terminal.viaPoint,
313
483
  diameter: viaDiameter,
@@ -326,12 +496,16 @@ function buildPlan(params: {
326
496
  ? { start_pcb_port_id: connection.sourcePoint.pcb_port_id }
327
497
  : {}),
328
498
  },
329
- {
330
- route_type: "wire",
331
- ...terminal.viaPoint,
332
- width: traceWidth,
333
- layer: connection.sourceLayer,
334
- },
499
+ ...(hasSourceDogbone
500
+ ? [
501
+ {
502
+ route_type: "wire" as const,
503
+ ...terminal.viaPoint,
504
+ width: traceWidth,
505
+ layer: connection.sourceLayer,
506
+ },
507
+ ]
508
+ : []),
335
509
  {
336
510
  route_type: "via",
337
511
  ...terminal.viaPoint,
@@ -357,7 +531,10 @@ function buildPlan(params: {
357
531
  connectionName: connection.connection.name,
358
532
  sourcePointIndex: connection.sourcePointIndex,
359
533
  })
360
- const segments = [sourceSegment, ...targetSegments]
534
+ const segments = [
535
+ ...(hasSourceDogbone ? [sourceSegment] : []),
536
+ ...targetSegments,
537
+ ]
361
538
  const cornerBandSide = getCornerBandSide(bus.exitEdge, bus.preferredExit)
362
539
  return {
363
540
  busId: bus.busId,
@@ -398,9 +575,15 @@ function buildPlan(params: {
398
575
  }
399
576
  }
400
577
 
401
- export function routeViaMinimalWinding(
578
+ export function routeViaMinimalWindingAlternatives(
402
579
  params: RouteViaMinimalWindingParams,
403
- ): FanoutRoutePlan[] | null {
580
+ maximumAlternatives = 1,
581
+ ): FanoutRoutePlan[][] {
582
+ if (!Number.isInteger(maximumAlternatives) || maximumAlternatives < 1) {
583
+ throw new Error(
584
+ `FanoutSolver: maximum winding alternatives must be a positive integer, received ${maximumAlternatives}`,
585
+ )
586
+ }
404
587
  const {
405
588
  srj,
406
589
  bus,
@@ -412,8 +595,27 @@ export function routeViaMinimalWinding(
412
595
  viaDiameter,
413
596
  viaHoleDiameter,
414
597
  clearance,
598
+ allowBlindAndBuriedVias = true,
415
599
  allowSameNetMerges = false,
600
+ maximumRouteOrderAttempts,
601
+ reservedVias = [],
602
+ gridStepDivisor = 1,
416
603
  } = params
604
+ if (
605
+ maximumRouteOrderAttempts !== undefined &&
606
+ (!Number.isInteger(maximumRouteOrderAttempts) ||
607
+ maximumRouteOrderAttempts < 1)
608
+ ) {
609
+ throw new Error(
610
+ `FanoutSolver: maximumRouteOrderAttempts must be a positive integer, received ${maximumRouteOrderAttempts}`,
611
+ )
612
+ }
613
+
614
+ if (gridStepDivisor !== 1 && gridStepDivisor !== 2) {
615
+ throw new Error(
616
+ `FanoutSolver: gridStepDivisor must be 1 or 2, received ${gridStepDivisor}`,
617
+ )
618
+ }
417
619
  if (
418
620
  terminals.length === 0 ||
419
621
  !bus.exitEdge ||
@@ -421,17 +623,17 @@ export function routeViaMinimalWinding(
421
623
  (terminal) => terminal.connection.sourceLayer === targetLayer,
422
624
  )
423
625
  ) {
424
- return null
626
+ return []
425
627
  }
426
628
 
427
- const gridStep = traceWidth + clearance
428
- if (!Number.isFinite(gridStep) || gridStep <= 0) return null
629
+ const gridStep = (traceWidth + clearance) / gridStepDivisor
630
+ if (!Number.isFinite(gridStep) || gridStep <= 0) return []
429
631
  const { minX, maxX, minY, maxY } = bus.sharedBoundary
430
632
  const columnCount = Math.floor((maxX - minX) / gridStep) + 1
431
633
  const rowCount = Math.floor((maxY - minY) / gridStep) + 1
432
634
  const nodeCount = columnCount * rowCount
433
635
  if (columnCount < 2 || rowCount < 2 || nodeCount > MAX_GRID_NODE_COUNT) {
434
- return null
636
+ return []
435
637
  }
436
638
  const nodes: GridNode[] = Array.from({ length: nodeCount }, (_, index) => {
437
639
  const column = index % columnCount
@@ -445,7 +647,14 @@ export function routeViaMinimalWinding(
445
647
  const targetLayerObstacles = srj.obstacles.filter((obstacle) =>
446
648
  obstacle.layers.includes(targetLayer),
447
649
  )
448
- const blockingCopper = getBlockingCopper({ srj, acceptedPlans })
650
+ const targetLayerObstacleIndex = new ObstacleSpatialIndex(
651
+ targetLayerObstacles,
652
+ )
653
+ const blockingCopper = getBlockingCopper({
654
+ srj,
655
+ acceptedPlans,
656
+ allowBlindAndBuriedVias,
657
+ })
449
658
  const blockingSegments = blockingCopper.segments.filter(({ segment }) => {
450
659
  if (segment.layer !== targetLayer) return false
451
660
  const margin = (segment.width + traceWidth) / 2 + clearance
@@ -466,22 +675,54 @@ export function routeViaMinimalWinding(
466
675
  via.center.y > maxY + margin
467
676
  )
468
677
  })
678
+ blockingVias.push(
679
+ ...reservedVias.filter(({ via }) => {
680
+ if (!via.spanLayers.includes(targetLayer)) return false
681
+ const margin = via.diameter / 2 + traceWidth / 2 + clearance
682
+ return !(
683
+ via.center.x < minX - margin ||
684
+ via.center.x > maxX + margin ||
685
+ via.center.y < minY - margin ||
686
+ via.center.y > maxY + margin
687
+ )
688
+ }),
689
+ )
469
690
  const terminalVias: BlockingVia[] = terminals.map((terminal) => ({
470
691
  connectionName: terminal.connection.connection.name,
471
692
  via: {
472
693
  center: terminal.viaPoint,
473
694
  diameter: viaDiameter,
474
- spanLayers: getLayerSpan(
475
- terminal.connection.sourceLayer,
476
- targetLayer,
695
+ spanLayers: getViaSpanLayers({
696
+ fromLayer: terminal.connection.sourceLayer,
697
+ toLayer: targetLayer,
477
698
  layerNames,
478
- ),
699
+ allowBlindAndBuriedVias,
700
+ }),
479
701
  },
480
702
  }))
481
703
  const boundaryDirection = getDirectionForExitEdge(bus.exitEdge)
482
704
  const sharesNet = (first: string, second: string): boolean =>
483
705
  first === second ||
484
706
  (allowSameNetMerges && connectionsShareElectricalNet(srj, first, second))
707
+ const allBlockingVias = [...blockingVias, ...terminalVias]
708
+ const maximumViaToTraceDistance = allBlockingVias.reduce(
709
+ (maximum, { via }) =>
710
+ Math.max(maximum, via.diameter / 2 + traceWidth / 2 + clearance),
711
+ traceWidth / 2 + clearance,
712
+ )
713
+ const viasByX = allBlockingVias.toSorted(
714
+ (first, second) => first.via.center.x - second.via.center.x,
715
+ )
716
+ const getFirstViaAtOrAfterX = (minimumX: number): number => {
717
+ let low = 0
718
+ let high = viasByX.length
719
+ while (low < high) {
720
+ const middle = Math.floor((low + high) / 2)
721
+ if (viasByX[middle]!.via.center.x < minimumX) low = middle + 1
722
+ else high = middle
723
+ }
724
+ return low
725
+ }
485
726
 
486
727
  const segmentIsClear = (params: {
487
728
  segment: RoutedSegment
@@ -490,7 +731,11 @@ export function routeViaMinimalWinding(
490
731
  }): boolean => {
491
732
  const { segment, terminal, acceptedAttemptSegments } = params
492
733
  const connectionName = terminal.connection.connection.name
493
- for (const obstacle of targetLayerObstacles) {
734
+ const requiredObstacleClearance = segment.width / 2 + clearance
735
+ for (const obstacle of targetLayerObstacleIndex.querySegment(
736
+ segment,
737
+ requiredObstacleClearance,
738
+ )) {
494
739
  if (
495
740
  obstacle.connectedTo.includes(connectionName) ||
496
741
  (allowSameNetMerges &&
@@ -500,7 +745,7 @@ export function routeViaMinimalWinding(
500
745
  }
501
746
  if (
502
747
  distanceSegmentToObstacle(segment, obstacle) <
503
- segment.width / 2 + clearance - EPSILON
748
+ requiredObstacleClearance - EPSILON
504
749
  ) {
505
750
  return false
506
751
  }
@@ -533,20 +778,35 @@ export function routeViaMinimalWinding(
533
778
  return false
534
779
  }
535
780
  }
536
- for (const blocker of blockingVias) {
781
+ const segmentMinX = Math.min(segment.start.x, segment.end.x)
782
+ const segmentMaxX = Math.max(segment.start.x, segment.end.x)
783
+ const segmentMinY = Math.min(segment.start.y, segment.end.y)
784
+ const segmentMaxY = Math.max(segment.start.y, segment.end.y)
785
+ for (
786
+ let viaIndex = getFirstViaAtOrAfterX(
787
+ segmentMinX - maximumViaToTraceDistance,
788
+ );
789
+ viaIndex < viasByX.length;
790
+ viaIndex++
791
+ ) {
792
+ const blocker = viasByX[viaIndex]!
793
+ if (blocker.via.center.x > segmentMaxX + maximumViaToTraceDistance) {
794
+ break
795
+ }
537
796
  if (sharesNet(connectionName, blocker.connectionName)) continue
797
+ const requiredDistance =
798
+ blocker.via.diameter / 2 + segment.width / 2 + clearance
538
799
  if (
539
- distancePointToSegment(blocker.via.center, segment.start, segment.end) <
540
- blocker.via.diameter / 2 + segment.width / 2 + clearance - EPSILON
800
+ blocker.via.center.x < segmentMinX - requiredDistance ||
801
+ blocker.via.center.x > segmentMaxX + requiredDistance ||
802
+ blocker.via.center.y < segmentMinY - requiredDistance ||
803
+ blocker.via.center.y > segmentMaxY + requiredDistance
541
804
  ) {
542
- return false
805
+ continue
543
806
  }
544
- }
545
- for (const blocker of terminalVias) {
546
- if (sharesNet(connectionName, blocker.connectionName)) continue
547
807
  if (
548
808
  distancePointToSegment(blocker.via.center, segment.start, segment.end) <
549
- blocker.via.diameter / 2 + segment.width / 2 + clearance - EPSILON
809
+ requiredDistance - EPSILON
550
810
  ) {
551
811
  return false
552
812
  }
@@ -807,48 +1067,83 @@ export function routeViaMinimalWinding(
807
1067
  )
808
1068
  )
809
1069
  })
810
- const routeOrderCandidates: ViaMinimalWindingTerminal[][] = [
811
- targetOrderedTerminals,
812
- [...targetOrderedTerminals].reverse(),
813
- terminals.toSorted(
814
- (first, second) =>
815
- first.viaPoint.x - second.viaPoint.x ||
816
- first.viaPoint.y - second.viaPoint.y,
817
- ),
818
- terminals.toSorted(
819
- (first, second) =>
820
- second.viaPoint.x - first.viaPoint.x ||
821
- second.viaPoint.y - first.viaPoint.y,
822
- ),
823
- terminals.toSorted(
824
- (first, second) =>
825
- first.viaPoint.y - second.viaPoint.y ||
826
- first.viaPoint.x - second.viaPoint.x,
827
- ),
828
- terminals.toSorted(
829
- (first, second) =>
830
- second.viaPoint.y - first.viaPoint.y ||
831
- second.viaPoint.x - first.viaPoint.x,
832
- ),
833
- ]
834
- for (let offset = 1; offset < targetOrderedTerminals.length; offset++) {
835
- routeOrderCandidates.push([
836
- ...targetOrderedTerminals.slice(offset),
837
- ...targetOrderedTerminals.slice(0, offset),
1070
+ const viaTracks = terminals.map((terminal) =>
1071
+ getPerpendicularAxis(terminal.viaPoint, boundaryDirection),
1072
+ )
1073
+ const targetTracks = targetOrderedTerminals.map((terminal) =>
1074
+ getPerpendicularAxis(terminal.exitPoint, boundaryDirection),
1075
+ )
1076
+ const viasAreBeforeTargets =
1077
+ Math.max(...viaTracks) < Math.min(...targetTracks) - EPSILON
1078
+ const viasAreAfterTargets =
1079
+ Math.min(...viaTracks) > Math.max(...targetTracks) + EPSILON
1080
+ const laneBiases = viasAreBeforeTargets
1081
+ ? ([1, 0, -1] as const)
1082
+ : viasAreAfterTargets
1083
+ ? ([-1, 0, 1] as const)
1084
+ : ([0, 1, -1] as const)
1085
+ const initialRouteOrderFactories: Array<
1086
+ () => readonly ViaMinimalWindingTerminal[]
1087
+ > = []
1088
+ if (viasAreBeforeTargets) {
1089
+ initialRouteOrderFactories.push(() => [
1090
+ ...targetOrderedTerminals.slice(1),
1091
+ targetOrderedTerminals[0]!,
838
1092
  ])
1093
+ } else if (viasAreAfterTargets) {
1094
+ initialRouteOrderFactories.push(() => [...targetOrderedTerminals].reverse())
839
1095
  }
840
- const seenRouteOrders = new Set<string>()
841
- const routeOrders = routeOrderCandidates.filter((routeOrder) => {
842
- const key = routeOrder
843
- .map((terminal) => terminal.connection.connection.name)
844
- .join("\u0000")
845
- if (seenRouteOrders.has(key)) return false
846
- seenRouteOrders.add(key)
847
- return true
1096
+ initialRouteOrderFactories.push(
1097
+ () => targetOrderedTerminals,
1098
+ () => [...targetOrderedTerminals].reverse(),
1099
+ () =>
1100
+ terminals.toSorted(
1101
+ (first, second) =>
1102
+ first.viaPoint.x - second.viaPoint.x ||
1103
+ first.viaPoint.y - second.viaPoint.y,
1104
+ ),
1105
+ () =>
1106
+ terminals.toSorted(
1107
+ (first, second) =>
1108
+ second.viaPoint.x - first.viaPoint.x ||
1109
+ second.viaPoint.y - first.viaPoint.y,
1110
+ ),
1111
+ () =>
1112
+ terminals.toSorted(
1113
+ (first, second) =>
1114
+ first.viaPoint.y - second.viaPoint.y ||
1115
+ first.viaPoint.x - second.viaPoint.x,
1116
+ ),
1117
+ () =>
1118
+ terminals.toSorted(
1119
+ (first, second) =>
1120
+ second.viaPoint.y - first.viaPoint.y ||
1121
+ second.viaPoint.x - first.viaPoint.x,
1122
+ ),
1123
+ )
1124
+ const maximumRouteOrderCount =
1125
+ maximumRouteOrderAttempts === undefined
1126
+ ? undefined
1127
+ : Math.ceil(maximumRouteOrderAttempts / laneBiases.length)
1128
+ const routeOrders = iterateUniqueRouteOrders({
1129
+ initialOrderFactories: initialRouteOrderFactories,
1130
+ rotationBase: targetOrderedTerminals,
1131
+ getItemKey: (terminal) => terminal.connection.connection.name,
1132
+ maximumOrderCount: maximumRouteOrderCount,
848
1133
  })
849
1134
 
1135
+ const alternatives: FanoutRoutePlan[][] = []
1136
+ const seenAlternativeKeys = new Set<string>()
1137
+ let routeOrderAttemptCount = 0
850
1138
  for (const routeOrder of routeOrders) {
851
- for (const laneBias of [0, 1, -1] as const) {
1139
+ for (const laneBias of laneBiases) {
1140
+ if (
1141
+ maximumRouteOrderAttempts !== undefined &&
1142
+ routeOrderAttemptCount >= maximumRouteOrderAttempts
1143
+ ) {
1144
+ return alternatives
1145
+ }
1146
+ routeOrderAttemptCount++
852
1147
  const acceptedAttemptSegments: BlockingSegment[] = []
853
1148
  const routedPointsByConnectionName = new Map<string, Point2D[]>()
854
1149
  let failed = false
@@ -872,7 +1167,7 @@ export function routeViaMinimalWinding(
872
1167
  )
873
1168
  }
874
1169
  if (failed) continue
875
- return terminals.map((terminal) => {
1170
+ const plans = terminals.map((terminal) => {
876
1171
  const targetLayerPoints = routedPointsByConnectionName.get(
877
1172
  terminal.connection.connection.name,
878
1173
  )
@@ -890,9 +1185,30 @@ export function routeViaMinimalWinding(
890
1185
  traceWidth,
891
1186
  viaDiameter,
892
1187
  viaHoleDiameter,
1188
+ allowBlindAndBuriedVias,
893
1189
  })
894
1190
  })
1191
+ const alternativeKey = plans
1192
+ .map((plan) =>
1193
+ plan.segments
1194
+ .map(
1195
+ (segment) =>
1196
+ `${segment.start.x},${segment.start.y},${segment.end.x},${segment.end.y},${segment.layer}`,
1197
+ )
1198
+ .join(";"),
1199
+ )
1200
+ .join("|")
1201
+ if (seenAlternativeKeys.has(alternativeKey)) continue
1202
+ seenAlternativeKeys.add(alternativeKey)
1203
+ alternatives.push(plans)
1204
+ if (alternatives.length >= maximumAlternatives) return alternatives
895
1205
  }
896
1206
  }
897
- return null
1207
+ return alternatives
1208
+ }
1209
+
1210
+ export function routeViaMinimalWinding(
1211
+ params: RouteViaMinimalWindingParams,
1212
+ ): FanoutRoutePlan[] | null {
1213
+ return routeViaMinimalWindingAlternatives(params, 1)[0] ?? null
898
1214
  }