@tscircuit/fanout-solver 0.0.38 → 0.0.40

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