@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.
package/lib/route-bus.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  } from "./boundary-exit"
11
11
  import { createFanoutOutputIds } from "./fanout-output-ids"
12
12
  import {
13
+ circleFitsInsideObstacle,
13
14
  distance,
14
15
  distancePointToObstacle,
15
16
  distancePointToSegment,
@@ -17,12 +18,15 @@ import {
17
18
  segmentsAreClear,
18
19
  } from "./geometry"
19
20
  import { getAllRoutedTraceCopper } from "./get-routed-trace-copper"
20
- import { getLayerSpan } from "./layer-names"
21
+ import { getViaSpanLayers } from "./layer-names"
21
22
  import {
22
23
  connectionsShareElectricalNet,
23
24
  obstacleSharesElectricalNet,
24
25
  } from "./net-identity"
25
- import { routeViaMinimalWinding } from "./route-via-minimal-winding"
26
+ import {
27
+ routeViaMinimalWindingAlternatives,
28
+ type ViaMinimalWindingReservedVia,
29
+ } from "./route-via-minimal-winding"
26
30
  import type {
27
31
  Bounds,
28
32
  FanoutDirection,
@@ -47,9 +51,15 @@ export interface RouteBusParams {
47
51
  viaHoleDiameter: number
48
52
  clearance: number
49
53
  compactBusTracks: boolean
54
+ allowBlindAndBuriedVias?: boolean
50
55
  allowSameNetMerges?: boolean
51
56
  staticClearanceCache?: RouteBusStaticClearanceCache
52
57
  blockingBusCounts?: Map<string, number>
58
+ rejectedViaMinimalCandidates?: FanoutRoutePlan[][]
59
+ stopAfterFirstRejectedViaMinimalCandidate?: boolean
60
+ fixedViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
61
+ reservedVias?: readonly ViaMinimalWindingReservedVia[]
62
+ viaMinimalOnly?: boolean
53
63
  }
54
64
 
55
65
  interface TrackCandidate {
@@ -59,6 +69,13 @@ interface TrackCandidate {
59
69
 
60
70
  type ViaHandedness = -1 | 0 | 1
61
71
 
72
+ function allowsViaInPad(srj: SimpleRouteJson): boolean {
73
+ return (
74
+ (srj as SimpleRouteJson & { allowViaInPad?: boolean }).allowViaInPad ===
75
+ true
76
+ )
77
+ }
78
+
62
79
  function isHorizontal(direction: FanoutDirection): boolean {
63
80
  return direction === "left" || direction === "right"
64
81
  }
@@ -114,14 +131,22 @@ function getStableConnectionIdentity(
114
131
  return connection.name
115
132
  }
116
133
 
117
- function getWindingTargetRank(params: {
134
+ function getWindingTargetOrders(params: {
118
135
  bus: PreparedBus
119
- connection: PreparedConnection
120
136
  boundaryDirection: FanoutDirection
121
137
  layerNames: readonly string[]
122
- }): { rank: number; connectionCount: number } {
123
- const { bus, connection, boundaryDirection, layerNames } = params
124
- const orderedConnections = bus.connections.toSorted((first, second) => {
138
+ targetLayer: string
139
+ }): {
140
+ orders: PreparedConnection[][]
141
+ legacyOrder: PreparedConnection[]
142
+ } {
143
+ const { bus, boundaryDirection, layerNames, targetLayer } = params
144
+ const getTargetLayer = (candidate: PreparedConnection): string =>
145
+ candidate.exitTargetPoint?.layer ?? getPointLayer(candidate.targetPoint)
146
+ const compareWithinLayer = (
147
+ first: PreparedConnection,
148
+ second: PreparedConnection,
149
+ ): number => {
125
150
  const axisDifference =
126
151
  getPerpendicularAxis(
127
152
  first.exitTargetPoint ?? first.targetPoint,
@@ -133,28 +158,114 @@ function getWindingTargetRank(params: {
133
158
  )
134
159
  if (axisDifference !== 0) return axisDifference
135
160
 
136
- // Equal target coordinates came from distinct target layers and do not
137
- // impose a physical order after this bus is collapsed onto one escape
138
- // layer. Keep that free choice independent of allowed-layer ordering so
139
- // the local fanout can choose a deterministic, via-minimal permutation.
140
- const identityDifference = first.connection.name.localeCompare(
141
- second.connection.name,
142
- )
143
- if (identityDifference !== 0) return identityDifference
144
-
145
161
  const firstStableId = getStableConnectionIdentity(first.connection)
146
162
  const secondStableId = getStableConnectionIdentity(second.connection)
147
163
  const stableIdentityDifference = firstStableId.localeCompare(secondStableId)
148
164
  if (stableIdentityDifference !== 0) return stableIdentityDifference
149
165
 
150
- const firstLayer = first.exitTargetPoint?.layer
151
- const secondLayer = second.exitTargetPoint?.layer
152
166
  return (
153
- layerNames.indexOf(firstLayer ?? "") -
154
- layerNames.indexOf(secondLayer ?? "") ||
167
+ first.connection.name.localeCompare(second.connection.name) ||
168
+ first.connectionIndex - second.connectionIndex
169
+ )
170
+ }
171
+ const legacyOrderedConnections = bus.connections.toSorted((first, second) => {
172
+ const axisDifference =
173
+ getPerpendicularAxis(
174
+ first.exitTargetPoint ?? first.targetPoint,
175
+ boundaryDirection,
176
+ ) -
177
+ getPerpendicularAxis(
178
+ second.exitTargetPoint ?? second.targetPoint,
179
+ boundaryDirection,
180
+ )
181
+ if (axisDifference !== 0) return axisDifference
182
+ return (
183
+ first.connection.name.localeCompare(second.connection.name) ||
184
+ getStableConnectionIdentity(first.connection).localeCompare(
185
+ getStableConnectionIdentity(second.connection),
186
+ ) ||
187
+ layerNames.indexOf(getTargetLayer(first)) -
188
+ layerNames.indexOf(getTargetLayer(second)) ||
155
189
  first.connectionIndex - second.connectionIndex
156
190
  )
157
191
  })
192
+ const connectionsByLayer = new Map<string, PreparedConnection[]>()
193
+ for (const candidate of bus.connections) {
194
+ const layer = getTargetLayer(candidate)
195
+ const layerConnections = connectionsByLayer.get(layer) ?? []
196
+ layerConnections.push(candidate)
197
+ connectionsByLayer.set(layer, layerConnections)
198
+ }
199
+ const orderedLayers = [...connectionsByLayer.keys()].toSorted(
200
+ (first, second) =>
201
+ Number(second === targetLayer) - Number(first === targetLayer) ||
202
+ layerNames.indexOf(first) - layerNames.indexOf(second) ||
203
+ first.localeCompare(second),
204
+ )
205
+ const layerOrderByName = new Map(
206
+ orderedLayers.map((layer, layerOrder) => [layer, layerOrder]),
207
+ )
208
+ const rankWithinLayerByConnectionIndex = new Map<number, number>()
209
+ for (const layer of orderedLayers) {
210
+ for (const [rank, candidate] of connectionsByLayer
211
+ .get(layer)!
212
+ .toSorted(compareWithinLayer)
213
+ .entries()) {
214
+ rankWithinLayerByConnectionIndex.set(candidate.connectionIndex, rank)
215
+ }
216
+ }
217
+ // Winding targets define an order within each copper layer. Their absolute
218
+ // offsets across different layers are not an ordering constraint: unrelated
219
+ // buses can move those layer bands without changing this bus's topology.
220
+ // Build a canonical interleave plus its adjacent linear extensions so a
221
+ // via-minimal search can choose a locally routable merge without violating
222
+ // any same-layer winding order.
223
+ const canonicalOrderedConnections = bus.connections.toSorted(
224
+ (first, second) =>
225
+ (rankWithinLayerByConnectionIndex.get(first.connectionIndex) ?? 0) -
226
+ (rankWithinLayerByConnectionIndex.get(second.connectionIndex) ?? 0) ||
227
+ (layerOrderByName.get(getTargetLayer(first)) ?? 0) -
228
+ (layerOrderByName.get(getTargetLayer(second)) ?? 0) ||
229
+ compareWithinLayer(first, second),
230
+ )
231
+ const candidateOrders: PreparedConnection[][] = [canonicalOrderedConnections]
232
+ for (let index = 0; index + 1 < canonicalOrderedConnections.length; index++) {
233
+ const first = canonicalOrderedConnections[index]!
234
+ const second = canonicalOrderedConnections[index + 1]!
235
+ if (getTargetLayer(first) === getTargetLayer(second)) continue
236
+ const adjacentExtension = [...canonicalOrderedConnections]
237
+ adjacentExtension[index] = second
238
+ adjacentExtension[index + 1] = first
239
+ candidateOrders.push(adjacentExtension)
240
+ }
241
+ // Retain the coordinate-total-order behavior as a compatibility fallback,
242
+ // but do not let sub-nanometer noise between unrelated layer bands choose
243
+ // the primary topology.
244
+ candidateOrders.push(legacyOrderedConnections)
245
+ const seenOrders = new Set<string>()
246
+ const orders = candidateOrders.filter((order) => {
247
+ const key = order.map((candidate) => candidate.connectionIndex).join(",")
248
+ if (seenOrders.has(key)) return false
249
+ seenOrders.add(key)
250
+ return true
251
+ })
252
+ return { orders, legacyOrder: legacyOrderedConnections }
253
+ }
254
+
255
+ function getWindingTargetRank(params: {
256
+ bus: PreparedBus
257
+ connection: PreparedConnection
258
+ boundaryDirection: FanoutDirection
259
+ layerNames: readonly string[]
260
+ targetLayer: string
261
+ windingOrderIndex?: number
262
+ }): { rank: number; connectionCount: number } {
263
+ const { connection, windingOrderIndex = 0 } = params
264
+ const { orders, legacyOrder } = getWindingTargetOrders(params)
265
+ const orderedConnections =
266
+ params.windingOrderIndex === undefined
267
+ ? legacyOrder
268
+ : (orders[windingOrderIndex] ?? orders[0] ?? legacyOrder)
158
269
  const rank = orderedConnections.findIndex(
159
270
  (candidate) => candidate.connectionIndex === connection.connectionIndex,
160
271
  )
@@ -184,6 +295,8 @@ function getCornerTargetTrack(params: {
184
295
  viaDiameter: number
185
296
  clearance: number
186
297
  layerNames: readonly string[]
298
+ targetLayer: string
299
+ windingOrderIndex?: number
187
300
  }): number {
188
301
  const {
189
302
  bus,
@@ -193,6 +306,8 @@ function getCornerTargetTrack(params: {
193
306
  viaDiameter,
194
307
  clearance,
195
308
  layerNames,
309
+ targetLayer,
310
+ windingOrderIndex,
196
311
  } = params
197
312
  const side = getCornerSide(bus)
198
313
  if (!side || !bus.exitEdge) {
@@ -218,6 +333,8 @@ function getCornerTargetTrack(params: {
218
333
  connection,
219
334
  boundaryDirection,
220
335
  layerNames,
336
+ targetLayer,
337
+ windingOrderIndex,
221
338
  })
222
339
  : undefined
223
340
  const bandConnectionCount = Math.max(
@@ -295,6 +412,83 @@ function getPerpendicularPitch(bus: PreparedBus): number {
295
412
  return isHorizontal(bus.direction) ? bus.pitchY : bus.pitchX
296
413
  }
297
414
 
415
+ function chamferOrthogonalCorners(
416
+ points: readonly Point2D[],
417
+ requestedChamfer: number,
418
+ ): Point2D[] {
419
+ if (points.length < 3) return [...points]
420
+ const output: Point2D[] = [points[0]!]
421
+ for (let index = 1; index < points.length - 1; index++) {
422
+ const previous = points[index - 1]!
423
+ const current = points[index]!
424
+ const next = points[index + 1]!
425
+ const incoming = { x: current.x - previous.x, y: current.y - previous.y }
426
+ const outgoing = { x: next.x - current.x, y: next.y - current.y }
427
+ const incomingLength = Math.hypot(incoming.x, incoming.y)
428
+ const outgoingLength = Math.hypot(outgoing.x, outgoing.y)
429
+ const incomingIsAxisAligned =
430
+ Math.abs(incoming.x) <= 1e-9 || Math.abs(incoming.y) <= 1e-9
431
+ const outgoingIsAxisAligned =
432
+ Math.abs(outgoing.x) <= 1e-9 || Math.abs(outgoing.y) <= 1e-9
433
+ const isOrthogonal =
434
+ Math.abs(incoming.x * outgoing.x + incoming.y * outgoing.y) <= 1e-9
435
+ if (
436
+ incomingLength <= 1e-9 ||
437
+ outgoingLength <= 1e-9 ||
438
+ !incomingIsAxisAligned ||
439
+ !outgoingIsAxisAligned ||
440
+ !isOrthogonal
441
+ ) {
442
+ output.push(current)
443
+ continue
444
+ }
445
+ const chamfer = Math.min(
446
+ requestedChamfer,
447
+ incomingLength / 3,
448
+ outgoingLength / 3,
449
+ )
450
+ output.push({
451
+ x: current.x - (incoming.x / incomingLength) * chamfer,
452
+ y: current.y - (incoming.y / incomingLength) * chamfer,
453
+ })
454
+ output.push({
455
+ x: current.x + (outgoing.x / outgoingLength) * chamfer,
456
+ y: current.y + (outgoing.y / outgoingLength) * chamfer,
457
+ })
458
+ }
459
+ output.push(points.at(-1)!)
460
+ return output.filter(
461
+ (point, index) => index === 0 || distance(point, output[index - 1]!) > 1e-9,
462
+ )
463
+ }
464
+
465
+ function getStraightOr45ConnectorVariants(
466
+ start: Point2D,
467
+ end: Point2D,
468
+ ): Point2D[][] {
469
+ const deltaX = end.x - start.x
470
+ const deltaY = end.y - start.y
471
+ const absoluteX = Math.abs(deltaX)
472
+ const absoluteY = Math.abs(deltaY)
473
+ if (
474
+ absoluteX <= 1e-9 ||
475
+ absoluteY <= 1e-9 ||
476
+ Math.abs(absoluteX - absoluteY) <= 1e-9
477
+ ) {
478
+ return [[start, end]]
479
+ }
480
+ if (absoluteX > absoluteY) {
481
+ return [
482
+ [start, { x: start.x + Math.sign(deltaX) * absoluteY, y: end.y }, end],
483
+ [start, { x: end.x - Math.sign(deltaX) * absoluteY, y: start.y }, end],
484
+ ]
485
+ }
486
+ return [
487
+ [start, { x: end.x, y: start.y + Math.sign(deltaY) * absoluteX }, end],
488
+ [start, { x: start.x, y: end.y - Math.sign(deltaY) * absoluteX }, end],
489
+ ]
490
+ }
491
+
298
492
  function getDepthInRows(bus: PreparedBus): number {
299
493
  const directionalCoordinates = (
300
494
  isHorizontal(bus.direction) ? bus.xCoordinates : bus.yCoordinates
@@ -748,6 +942,9 @@ function buildPlan(params: {
748
942
  cornerBoundaryChannelLaneOffset: number
749
943
  clearance: number
750
944
  terminateAtVia: boolean
945
+ allowBlindAndBuriedVias: boolean
946
+ initialViaPoint?: Point2D
947
+ sourceEscapePath?: readonly Point2D[]
751
948
  }): FanoutRoutePlan {
752
949
  const {
753
950
  preparedConnection,
@@ -767,6 +964,9 @@ function buildPlan(params: {
767
964
  cornerBoundaryChannelLaneOffset,
768
965
  clearance,
769
966
  terminateAtVia,
967
+ allowBlindAndBuriedVias,
968
+ initialViaPoint,
969
+ sourceEscapePath,
770
970
  } = params
771
971
  const sourcePoint = {
772
972
  x: preparedConnection.sourcePoint.x,
@@ -782,15 +982,33 @@ function buildPlan(params: {
782
982
  const usesLayeredWindingChannel = Boolean(windingCrossoverLayer)
783
983
  const sign = directionSign(bus.direction)
784
984
  const directionalPitch = getDirectionalPitch(bus)
785
- const viaPoint = getInitialViaPoint({
786
- preparedConnection,
787
- bus,
788
- targetLayer,
789
- traceWidth,
790
- viaDiameter,
791
- clearance,
792
- viaHandedness,
793
- })
985
+ const requestedViaPoint = sourceEscapePath?.at(-1)
986
+ const viaPoint =
987
+ requestedViaPoint !== undefined
988
+ ? { x: requestedViaPoint.x, y: requestedViaPoint.y }
989
+ : initialViaPoint === undefined
990
+ ? getInitialViaPoint({
991
+ preparedConnection,
992
+ bus,
993
+ targetLayer,
994
+ traceWidth,
995
+ viaDiameter,
996
+ clearance,
997
+ viaHandedness,
998
+ })
999
+ : { x: initialViaPoint.x, y: initialViaPoint.y }
1000
+ const resolvedSourceEscapePath = sourceEscapePath
1001
+ ? sourceEscapePath.map((point) => ({ x: point.x, y: point.y }))
1002
+ : [sourcePoint, viaPoint]
1003
+ if (
1004
+ resolvedSourceEscapePath.length < 2 ||
1005
+ distance(resolvedSourceEscapePath[0]!, sourcePoint) > 1e-9 ||
1006
+ distance(resolvedSourceEscapePath.at(-1)!, viaPoint) > 1e-9
1007
+ ) {
1008
+ throw new Error(
1009
+ `FanoutSolver: source escape path for "${preparedConnection.connection.name}" must run from its source point to its via`,
1010
+ )
1011
+ }
794
1012
  const viaAxis = getAxis(viaPoint, bus.direction)
795
1013
  const viaPerpendicularAxis = getPerpendicularAxis(viaPoint, bus.direction)
796
1014
  const spreadLaneDistance =
@@ -829,6 +1047,7 @@ function buildPlan(params: {
829
1047
  viaDiameter,
830
1048
  clearance,
831
1049
  layerNames,
1050
+ targetLayer,
832
1051
  })
833
1052
  : usesLayeredWindingChannel
834
1053
  ? getPerpendicularAxis(
@@ -899,28 +1118,38 @@ function buildPlan(params: {
899
1118
  layer: preparedConnection.sourceLayer,
900
1119
  start_pcb_port_id: preparedConnection.sourcePoint.pcb_port_id,
901
1120
  })
902
- appendSegment(
903
- segments,
904
- sourcePoint,
905
- viaPoint,
906
- traceWidth,
907
- preparedConnection.sourceLayer,
908
- )
909
- route.push({
910
- route_type: "wire",
911
- x: viaPoint.x,
912
- y: viaPoint.y,
913
- width: traceWidth,
914
- layer: preparedConnection.sourceLayer,
915
- })
1121
+ for (
1122
+ let pointIndex = 1;
1123
+ pointIndex < resolvedSourceEscapePath.length;
1124
+ pointIndex++
1125
+ ) {
1126
+ const previousPoint = resolvedSourceEscapePath[pointIndex - 1]!
1127
+ const point = resolvedSourceEscapePath[pointIndex]!
1128
+ appendSegment(
1129
+ segments,
1130
+ previousPoint,
1131
+ point,
1132
+ traceWidth,
1133
+ preparedConnection.sourceLayer,
1134
+ )
1135
+ if (distance(previousPoint, point) <= 1e-9) continue
1136
+ route.push({
1137
+ route_type: "wire",
1138
+ x: point.x,
1139
+ y: point.y,
1140
+ width: traceWidth,
1141
+ layer: preparedConnection.sourceLayer,
1142
+ })
1143
+ }
916
1144
 
917
1145
  let via: FanoutRoutePlan["via"]
918
1146
  if (targetLayer !== preparedConnection.sourceLayer) {
919
- const spanLayers = getLayerSpan(
920
- preparedConnection.sourceLayer,
921
- targetLayer,
1147
+ const spanLayers = getViaSpanLayers({
1148
+ fromLayer: preparedConnection.sourceLayer,
1149
+ toLayer: targetLayer,
922
1150
  layerNames,
923
- )
1151
+ allowBlindAndBuriedVias,
1152
+ })
924
1153
  via = {
925
1154
  center: viaPoint,
926
1155
  diameter: viaDiameter,
@@ -990,7 +1219,12 @@ function buildPlan(params: {
990
1219
  holeDiameter: viaHoleDiameter,
991
1220
  fromLayer,
992
1221
  toLayer,
993
- spanLayers: getLayerSpan(fromLayer, toLayer, layerNames),
1222
+ spanLayers: getViaSpanLayers({
1223
+ fromLayer,
1224
+ toLayer,
1225
+ layerNames,
1226
+ allowBlindAndBuriedVias,
1227
+ }),
994
1228
  })
995
1229
  route.push({
996
1230
  route_type: "via",
@@ -1066,7 +1300,7 @@ function buildPlan(params: {
1066
1300
  sourceObstacle: preparedConnection.sourceObstacle,
1067
1301
  sourceLayer: preparedConnection.sourceLayer,
1068
1302
  targetPoint: preparedConnection.targetPoint,
1069
- targetLayer: escapeLayer,
1303
+ targetLayer,
1070
1304
  termination: bus.termination,
1071
1305
  direction: bus.direction,
1072
1306
  ...(bus.exitEdge ? { exitEdge: bus.exitEdge } : {}),
@@ -1190,6 +1424,7 @@ function addPlaneEndpointTerminal(params: {
1190
1424
  traceWidth: number
1191
1425
  viaDiameter: number
1192
1426
  viaHoleDiameter: number
1427
+ allowBlindAndBuriedVias: boolean
1193
1428
  }): FanoutRoutePlan {
1194
1429
  const {
1195
1430
  plan,
@@ -1200,13 +1435,19 @@ function addPlaneEndpointTerminal(params: {
1200
1435
  traceWidth,
1201
1436
  viaDiameter,
1202
1437
  viaHoleDiameter,
1438
+ allowBlindAndBuriedVias,
1203
1439
  } = params
1204
1440
  const targetPoint = {
1205
1441
  x: preparedConnection.targetPoint.x,
1206
1442
  y: preparedConnection.targetPoint.y,
1207
1443
  }
1208
1444
  const targetEndpointLayer = getPointLayer(preparedConnection.targetPoint)
1209
- const spanLayers = getLayerSpan(planeLayer, targetEndpointLayer, layerNames)
1445
+ const spanLayers = getViaSpanLayers({
1446
+ fromLayer: planeLayer,
1447
+ toLayer: targetEndpointLayer,
1448
+ layerNames,
1449
+ allowBlindAndBuriedVias,
1450
+ })
1210
1451
  if (!spanLayers.includes(planeLayer)) {
1211
1452
  throw new Error(
1212
1453
  `FanoutSolver: via for "${preparedConnection.connection.name}" does not cross plane ${planeLayer}`,
@@ -1354,14 +1595,36 @@ function getPlanVias(plan: FanoutRoutePlan) {
1354
1595
  ].filter((via): via is NonNullable<FanoutRoutePlan["via"]> => Boolean(via))
1355
1596
  }
1356
1597
 
1598
+ function viaFitsInsidePlanSourcePad(
1599
+ plan: FanoutRoutePlan,
1600
+ via: NonNullable<FanoutRoutePlan["via"]>,
1601
+ ): boolean {
1602
+ return (
1603
+ distance(via.center, plan.sourcePoint) <= 1e-9 &&
1604
+ circleFitsInsideObstacle({
1605
+ center: via.center,
1606
+ diameter: via.diameter,
1607
+ obstacle: plan.sourceObstacle,
1608
+ })
1609
+ )
1610
+ }
1611
+
1357
1612
  function planIsStaticallyClear(params: {
1358
1613
  plan: FanoutRoutePlan
1359
1614
  srj: SimpleRouteJson
1360
1615
  sharedBoundary: Bounds
1361
1616
  clearance: number
1617
+ allowBlindAndBuriedVias: boolean
1362
1618
  allowSameNetMerges: boolean
1363
1619
  }): boolean {
1364
- const { plan, srj, sharedBoundary, clearance, allowSameNetMerges } = params
1620
+ const {
1621
+ plan,
1622
+ srj,
1623
+ sharedBoundary,
1624
+ clearance,
1625
+ allowBlindAndBuriedVias,
1626
+ allowSameNetMerges,
1627
+ } = params
1365
1628
  const routableBounds = getRoutableBounds(srj.bounds, sharedBoundary)
1366
1629
  if (
1367
1630
  !pointIsInsideBounds(plan.exitPoint, routableBounds) ||
@@ -1391,6 +1654,13 @@ function planIsStaticallyClear(params: {
1391
1654
  }
1392
1655
  for (const via of getPlanVias(plan)) {
1393
1656
  for (const obstacle of srj.obstacles) {
1657
+ if (
1658
+ allowsViaInPad(srj) &&
1659
+ obstacle === plan.sourceObstacle &&
1660
+ viaFitsInsidePlanSourcePad(plan, via)
1661
+ ) {
1662
+ continue
1663
+ }
1394
1664
  if (!obstacle.layers.some((layer) => via.spanLayers.includes(layer))) {
1395
1665
  continue
1396
1666
  }
@@ -1409,7 +1679,10 @@ function planIsStaticallyClear(params: {
1409
1679
  }
1410
1680
  }
1411
1681
 
1412
- for (const traceCopper of getAllRoutedTraceCopper(srj)) {
1682
+ for (const traceCopper of getAllRoutedTraceCopper(
1683
+ srj,
1684
+ allowBlindAndBuriedVias,
1685
+ )) {
1413
1686
  if (
1414
1687
  plan.connectionName === traceCopper.connectionName ||
1415
1688
  (allowSameNetMerges &&
@@ -1575,6 +1848,7 @@ function planIsClear(params: {
1575
1848
  srj: SimpleRouteJson
1576
1849
  sharedBoundary: Bounds
1577
1850
  clearance: number
1851
+ allowBlindAndBuriedVias: boolean
1578
1852
  allowSameNetMerges: boolean
1579
1853
  }): boolean {
1580
1854
  const {
@@ -1586,6 +1860,7 @@ function planIsClear(params: {
1586
1860
  srj,
1587
1861
  sharedBoundary,
1588
1862
  clearance,
1863
+ allowBlindAndBuriedVias,
1589
1864
  allowSameNetMerges,
1590
1865
  } = params
1591
1866
  let staticallyClear = staticClearanceCache?.get(cacheKey)
@@ -1595,6 +1870,7 @@ function planIsClear(params: {
1595
1870
  srj,
1596
1871
  sharedBoundary,
1597
1872
  clearance,
1873
+ allowBlindAndBuriedVias,
1598
1874
  allowSameNetMerges,
1599
1875
  })
1600
1876
  staticClearanceCache?.set(cacheKey, staticallyClear)
@@ -1622,6 +1898,7 @@ export function fanoutPlansAreClear(params: {
1622
1898
  srj: SimpleRouteJson
1623
1899
  sharedBoundary: Bounds
1624
1900
  clearance: number
1901
+ allowBlindAndBuriedVias?: boolean
1625
1902
  allowSameNetMerges?: boolean
1626
1903
  }): boolean {
1627
1904
  const {
@@ -1629,6 +1906,7 @@ export function fanoutPlansAreClear(params: {
1629
1906
  srj,
1630
1907
  sharedBoundary,
1631
1908
  clearance,
1909
+ allowBlindAndBuriedVias = true,
1632
1910
  allowSameNetMerges = false,
1633
1911
  } = params
1634
1912
  for (let index = 0; index < plans.length; index++) {
@@ -1639,15 +1917,17 @@ export function fanoutPlansAreClear(params: {
1639
1917
  srj,
1640
1918
  sharedBoundary,
1641
1919
  clearance,
1920
+ allowBlindAndBuriedVias,
1642
1921
  allowSameNetMerges,
1643
1922
  })
1644
1923
  ) {
1645
1924
  return false
1646
1925
  }
1926
+ const otherPlans = plans.filter((_, otherIndex) => otherIndex !== index)
1647
1927
  if (
1648
1928
  !planIsClearOfPlans({
1649
1929
  plan,
1650
- otherPlans: plans.filter((_, otherIndex) => otherIndex !== index),
1930
+ otherPlans,
1651
1931
  srj,
1652
1932
  allowSameNetMerges,
1653
1933
  clearance,
@@ -1674,13 +1954,163 @@ function routePlaneTerminatedBus(
1674
1954
  clearance,
1675
1955
  staticClearanceCache,
1676
1956
  blockingBusCounts,
1957
+ allowBlindAndBuriedVias = true,
1677
1958
  allowSameNetMerges = false,
1959
+ fixedViaPointsByConnectionIndex,
1678
1960
  } = params
1679
1961
  const sourceObstacle = bus.connections[0]?.sourceObstacle
1680
1962
  if (!sourceObstacle || bus.termination.type !== "plane") return null
1681
1963
  const sourceLayer = bus.connections[0]!.sourceLayer
1682
1964
  if (targetLayer === sourceLayer) return null
1683
1965
 
1966
+ if (fixedViaPointsByConnectionIndex) {
1967
+ const fixedPlans: FanoutRoutePlan[] = []
1968
+ for (const preparedConnection of bus.connections) {
1969
+ const fixedViaPoint = fixedViaPointsByConnectionIndex.get(
1970
+ preparedConnection.connectionIndex,
1971
+ )
1972
+ if (!fixedViaPoint) return null
1973
+ const sourcePoint = {
1974
+ x: preparedConnection.sourcePoint.x,
1975
+ y: preparedConnection.sourcePoint.y,
1976
+ }
1977
+ const basePlan = buildPlan({
1978
+ preparedConnection,
1979
+ bus,
1980
+ targetLayer,
1981
+ track: getPerpendicularAxis(sourcePoint, bus.direction),
1982
+ exitAxis: getExitAxis(bus),
1983
+ layerNames,
1984
+ traceWidth,
1985
+ viaDiameter,
1986
+ viaHoleDiameter,
1987
+ viaHandedness: 0,
1988
+ interstitialEscape: false,
1989
+ spreadLaneIndex: 0,
1990
+ cornerExitLaneOffset: 0,
1991
+ cornerLocalChannelLaneOffset: 0,
1992
+ cornerBoundaryChannelLaneOffset: 0,
1993
+ clearance,
1994
+ terminateAtVia: true,
1995
+ allowBlindAndBuriedVias,
1996
+ initialViaPoint: fixedViaPoint,
1997
+ sourceEscapePath: [sourcePoint, fixedViaPoint],
1998
+ })
1999
+ const endpointViaCandidates = getPlaneEndpointViaCandidates({
2000
+ preparedConnection,
2001
+ bus,
2002
+ viaDiameter,
2003
+ clearance,
2004
+ })
2005
+ const plansToTry = [
2006
+ ...endpointViaCandidates.map((viaPoint) =>
2007
+ addPlaneEndpointTerminal({
2008
+ plan: basePlan,
2009
+ preparedConnection,
2010
+ planeLayer: targetLayer,
2011
+ viaPoint,
2012
+ layerNames,
2013
+ traceWidth,
2014
+ viaDiameter,
2015
+ viaHoleDiameter,
2016
+ allowBlindAndBuriedVias,
2017
+ }),
2018
+ ),
2019
+ basePlan,
2020
+ ]
2021
+ const clearPlan = plansToTry.find((candidatePlan, candidateIndex) =>
2022
+ planIsClear({
2023
+ plan: candidatePlan,
2024
+ otherPlans: [...acceptedPlans, ...fixedPlans],
2025
+ staticClearanceCache,
2026
+ blockingBusCounts,
2027
+ cacheKey: `plane-fixed:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${candidateIndex}`,
2028
+ srj,
2029
+ sharedBoundary: bus.sharedBoundary,
2030
+ clearance,
2031
+ allowBlindAndBuriedVias,
2032
+ allowSameNetMerges,
2033
+ }),
2034
+ )
2035
+ if (!clearPlan) return null
2036
+ fixedPlans.push(clearPlan)
2037
+ }
2038
+ return fixedPlans
2039
+ }
2040
+
2041
+ if (
2042
+ allowsViaInPad(srj) &&
2043
+ bus.connections.length === 1 &&
2044
+ circleFitsInsideObstacle({
2045
+ center: bus.connections[0]!.sourcePoint,
2046
+ diameter: viaDiameter,
2047
+ obstacle: sourceObstacle,
2048
+ })
2049
+ ) {
2050
+ const preparedConnection = bus.connections[0]!
2051
+ const viaInPadPlan = buildPlan({
2052
+ preparedConnection,
2053
+ bus,
2054
+ targetLayer,
2055
+ track: getPerpendicularAxis(
2056
+ preparedConnection.sourcePoint,
2057
+ bus.direction,
2058
+ ),
2059
+ exitAxis: getExitAxis(bus),
2060
+ layerNames,
2061
+ traceWidth,
2062
+ viaDiameter,
2063
+ viaHoleDiameter,
2064
+ viaHandedness: 0,
2065
+ interstitialEscape: false,
2066
+ spreadLaneIndex: 0,
2067
+ cornerExitLaneOffset: 0,
2068
+ cornerLocalChannelLaneOffset: 0,
2069
+ cornerBoundaryChannelLaneOffset: 0,
2070
+ clearance,
2071
+ terminateAtVia: true,
2072
+ allowBlindAndBuriedVias,
2073
+ initialViaPoint: preparedConnection.sourcePoint,
2074
+ })
2075
+ const endpointViaCandidates = getPlaneEndpointViaCandidates({
2076
+ preparedConnection,
2077
+ bus,
2078
+ viaDiameter,
2079
+ clearance,
2080
+ })
2081
+ const plansToTry = [
2082
+ ...endpointViaCandidates.map((viaPoint) =>
2083
+ addPlaneEndpointTerminal({
2084
+ plan: viaInPadPlan,
2085
+ preparedConnection,
2086
+ planeLayer: targetLayer,
2087
+ viaPoint,
2088
+ layerNames,
2089
+ traceWidth,
2090
+ viaDiameter,
2091
+ viaHoleDiameter,
2092
+ allowBlindAndBuriedVias,
2093
+ }),
2094
+ ),
2095
+ viaInPadPlan,
2096
+ ]
2097
+ const clearPlan = plansToTry.find((candidatePlan, candidateIndex) =>
2098
+ planIsClear({
2099
+ plan: candidatePlan,
2100
+ otherPlans: acceptedPlans,
2101
+ staticClearanceCache,
2102
+ blockingBusCounts,
2103
+ cacheKey: `plane-via-in-pad:${bus.busId}:${targetLayer}:${candidateIndex}`,
2104
+ srj,
2105
+ sharedBoundary: bus.sharedBoundary,
2106
+ clearance,
2107
+ allowBlindAndBuriedVias,
2108
+ allowSameNetMerges,
2109
+ }),
2110
+ )
2111
+ if (clearPlan) return [clearPlan]
2112
+ }
2113
+
1684
2114
  const candidateDirections: FanoutDirection[] = [
1685
2115
  bus.direction,
1686
2116
  ...(["left", "right", "up", "down"] as const).filter(
@@ -1709,59 +2139,194 @@ function routePlaneTerminatedBus(
1709
2139
  preparedConnection.sourcePoint,
1710
2140
  direction,
1711
2141
  )
1712
- const basePlan = buildPlan({
2142
+ const adjacentViaPoint = getInitialViaPoint({
1713
2143
  preparedConnection,
1714
2144
  bus: directionalBus,
1715
2145
  targetLayer,
1716
- track: sourceTrack,
1717
- exitAxis: getExitAxis(directionalBus),
1718
- layerNames,
1719
2146
  traceWidth,
1720
2147
  viaDiameter,
1721
- viaHoleDiameter,
1722
- viaHandedness,
1723
- interstitialEscape: !pairChannelFitsVia,
1724
- spreadLaneIndex: 0,
1725
- cornerExitLaneOffset: 0,
1726
- cornerLocalChannelLaneOffset: 0,
1727
- cornerBoundaryChannelLaneOffset: 0,
1728
- clearance,
1729
- terminateAtVia: true,
1730
- })
1731
- const endpointViaCandidates = getPlaneEndpointViaCandidates({
1732
- preparedConnection,
1733
- bus: directionalBus,
1734
- viaDiameter,
1735
2148
  clearance,
2149
+ viaHandedness,
1736
2150
  })
1737
- const plansToTry = [
1738
- ...endpointViaCandidates.map((viaPoint) =>
1739
- addPlaneEndpointTerminal({
1740
- plan: basePlan,
1741
- preparedConnection,
1742
- planeLayer: targetLayer,
1743
- viaPoint,
1744
- layerNames,
1745
- traceWidth,
1746
- viaDiameter,
1747
- viaHoleDiameter,
1748
- }),
1749
- ),
1750
- basePlan,
1751
- ]
1752
- const plan = plansToTry.find((candidatePlan, candidateIndex) =>
1753
- planIsClear({
1754
- plan: candidatePlan,
1755
- otherPlans: [...acceptedPlans, ...candidatePlans],
1756
- staticClearanceCache,
1757
- blockingBusCounts,
1758
- cacheKey: `plane:${bus.busId}:${targetLayer}:${direction}:${preparedConnection.connectionIndex}:${viaHandedness}:${candidateIndex}`,
1759
- srj,
1760
- sharedBoundary: bus.sharedBoundary,
1761
- clearance,
1762
- allowSameNetMerges,
1763
- }),
2151
+ const directionPitch = getDirectionalPitch(directionalBus)
2152
+ const sign = directionSign(direction)
2153
+ const boundaryAxis = getExitAxis(directionalBus, direction)
2154
+ const availableTravel =
2155
+ sign * (boundaryAxis - getAxis(adjacentViaPoint, direction)) -
2156
+ (viaDiameter / 2 + clearance)
2157
+ const maximumEscapeSteps = Math.max(
2158
+ 0,
2159
+ Math.floor(availableTravel / directionPitch),
1764
2160
  )
2161
+ const sourcePoint = {
2162
+ x: preparedConnection.sourcePoint.x,
2163
+ y: preparedConnection.sourcePoint.y,
2164
+ }
2165
+ const adjacentAxis = getAxis(adjacentViaPoint, direction)
2166
+ const adjacentPerpendicularAxis = getPerpendicularAxis(
2167
+ adjacentViaPoint,
2168
+ direction,
2169
+ )
2170
+ const perpendicularPitch = getPerpendicularPitch(directionalBus)
2171
+ const sourceEscapePaths: Point2D[][] = []
2172
+ const maximumCandidatePaths = 32
2173
+ for (
2174
+ let totalSteps = 0;
2175
+ totalSteps <= maximumEscapeSteps &&
2176
+ sourceEscapePaths.length < maximumCandidatePaths;
2177
+ totalSteps++
2178
+ ) {
2179
+ const straightViaPoint = makePoint(
2180
+ adjacentAxis + sign * totalSteps * directionPitch,
2181
+ adjacentPerpendicularAxis,
2182
+ direction,
2183
+ )
2184
+ if (totalSteps === 0) {
2185
+ sourceEscapePaths.push([sourcePoint, adjacentViaPoint])
2186
+ } else {
2187
+ for (const connector of getStraightOr45ConnectorVariants(
2188
+ sourcePoint,
2189
+ straightViaPoint,
2190
+ )) {
2191
+ sourceEscapePaths.push(connector)
2192
+ if (sourceEscapePaths.length >= maximumCandidatePaths) break
2193
+ }
2194
+ if (sourceEscapePaths.length < maximumCandidatePaths) {
2195
+ sourceEscapePaths.push([
2196
+ sourcePoint,
2197
+ adjacentViaPoint,
2198
+ straightViaPoint,
2199
+ ])
2200
+ }
2201
+ }
2202
+
2203
+ for (
2204
+ let lateralSteps = 1;
2205
+ lateralSteps <= Math.min(3, totalSteps - 1) &&
2206
+ sourceEscapePaths.length < maximumCandidatePaths;
2207
+ lateralSteps++
2208
+ ) {
2209
+ const outwardSteps = totalSteps - lateralSteps
2210
+ if (outwardSteps < 1 || outwardSteps > maximumEscapeSteps) {
2211
+ continue
2212
+ }
2213
+ for (const lateralSign of [-1, 1] as const) {
2214
+ const lateralAxis =
2215
+ adjacentPerpendicularAxis +
2216
+ lateralSign * lateralSteps * perpendicularPitch
2217
+ const lateralPoint = makePoint(
2218
+ adjacentAxis,
2219
+ lateralAxis,
2220
+ direction,
2221
+ )
2222
+ const outwardPoint = makePoint(
2223
+ adjacentAxis + sign * outwardSteps * directionPitch,
2224
+ adjacentPerpendicularAxis,
2225
+ direction,
2226
+ )
2227
+ const detourViaPoint = makePoint(
2228
+ adjacentAxis + sign * outwardSteps * directionPitch,
2229
+ lateralAxis,
2230
+ direction,
2231
+ )
2232
+ const chamfer =
2233
+ Math.min(directionPitch, perpendicularPitch) * 0.2
2234
+ for (const connector of getStraightOr45ConnectorVariants(
2235
+ sourcePoint,
2236
+ detourViaPoint,
2237
+ )) {
2238
+ sourceEscapePaths.push(connector)
2239
+ if (sourceEscapePaths.length >= maximumCandidatePaths) break
2240
+ }
2241
+ if (sourceEscapePaths.length >= maximumCandidatePaths) break
2242
+ sourceEscapePaths.push(
2243
+ chamferOrthogonalCorners(
2244
+ [
2245
+ sourcePoint,
2246
+ adjacentViaPoint,
2247
+ lateralPoint,
2248
+ detourViaPoint,
2249
+ ],
2250
+ chamfer,
2251
+ ),
2252
+ chamferOrthogonalCorners(
2253
+ [
2254
+ sourcePoint,
2255
+ adjacentViaPoint,
2256
+ outwardPoint,
2257
+ detourViaPoint,
2258
+ ],
2259
+ chamfer,
2260
+ ),
2261
+ )
2262
+ if (sourceEscapePaths.length >= maximumCandidatePaths) break
2263
+ }
2264
+ }
2265
+ }
2266
+ let plan: FanoutRoutePlan | undefined
2267
+ for (const [
2268
+ pathIndex,
2269
+ sourceEscapePath,
2270
+ ] of sourceEscapePaths.entries()) {
2271
+ const basePlan = buildPlan({
2272
+ preparedConnection,
2273
+ bus: directionalBus,
2274
+ targetLayer,
2275
+ track: sourceTrack,
2276
+ exitAxis: getExitAxis(directionalBus),
2277
+ layerNames,
2278
+ traceWidth,
2279
+ viaDiameter,
2280
+ viaHoleDiameter,
2281
+ viaHandedness,
2282
+ interstitialEscape: !pairChannelFitsVia,
2283
+ spreadLaneIndex: 0,
2284
+ cornerExitLaneOffset: 0,
2285
+ cornerLocalChannelLaneOffset: 0,
2286
+ cornerBoundaryChannelLaneOffset: 0,
2287
+ clearance,
2288
+ terminateAtVia: true,
2289
+ allowBlindAndBuriedVias,
2290
+ sourceEscapePath,
2291
+ })
2292
+ const endpointViaCandidates = getPlaneEndpointViaCandidates({
2293
+ preparedConnection,
2294
+ bus: directionalBus,
2295
+ viaDiameter,
2296
+ clearance,
2297
+ })
2298
+ const plansToTry = [
2299
+ ...endpointViaCandidates.map((viaPoint) =>
2300
+ addPlaneEndpointTerminal({
2301
+ plan: basePlan,
2302
+ preparedConnection,
2303
+ planeLayer: targetLayer,
2304
+ viaPoint,
2305
+ layerNames,
2306
+ traceWidth,
2307
+ viaDiameter,
2308
+ viaHoleDiameter,
2309
+ allowBlindAndBuriedVias,
2310
+ }),
2311
+ ),
2312
+ basePlan,
2313
+ ]
2314
+ plan = plansToTry.find((candidatePlan, candidateIndex) =>
2315
+ planIsClear({
2316
+ plan: candidatePlan,
2317
+ otherPlans: [...acceptedPlans, ...candidatePlans],
2318
+ staticClearanceCache,
2319
+ blockingBusCounts,
2320
+ cacheKey: `plane:${bus.busId}:${targetLayer}:${direction}:${preparedConnection.connectionIndex}:${viaHandedness}:${pathIndex}:${candidateIndex}`,
2321
+ srj,
2322
+ sharedBoundary: bus.sharedBoundary,
2323
+ clearance,
2324
+ allowBlindAndBuriedVias,
2325
+ allowSameNetMerges,
2326
+ }),
2327
+ )
2328
+ if (plan) break
2329
+ }
1765
2330
  if (!plan) {
1766
2331
  orderIsClear = false
1767
2332
  break
@@ -1793,13 +2358,28 @@ export function routeBusAlternatives(
1793
2358
  compactBusTracks,
1794
2359
  staticClearanceCache,
1795
2360
  blockingBusCounts,
2361
+ allowBlindAndBuriedVias = true,
1796
2362
  allowSameNetMerges = false,
2363
+ rejectedViaMinimalCandidates,
2364
+ stopAfterFirstRejectedViaMinimalCandidate = false,
2365
+ fixedViaPointsByConnectionIndex,
2366
+ reservedVias = [],
2367
+ viaMinimalOnly = false,
1797
2368
  } = params
1798
2369
  if (!Number.isInteger(maxAlternatives) || maxAlternatives < 1) {
1799
2370
  throw new Error(
1800
2371
  `FanoutSolver: maxAlternatives must be a positive integer, received ${maxAlternatives}`,
1801
2372
  )
1802
2373
  }
2374
+ if (
2375
+ fixedViaPointsByConnectionIndex &&
2376
+ bus.connections.some(
2377
+ (connection) =>
2378
+ !fixedViaPointsByConnectionIndex.has(connection.connectionIndex),
2379
+ )
2380
+ ) {
2381
+ return []
2382
+ }
1803
2383
  if (bus.termination.type === "plane") {
1804
2384
  const plan = routePlaneTerminatedBus(params)
1805
2385
  return plan ? [plan] : []
@@ -1818,11 +2398,47 @@ export function routeBusAlternatives(
1818
2398
  viaDiameter / 2 + clearance - 1e-9
1819
2399
  const interstitialEscape =
1820
2400
  targetUsesVia && !outwardEdgeBus && !pairChannelFitsVia
1821
- const viaHandednesses: readonly ViaHandedness[] = targetUsesVia
2401
+ const availableViaHandednesses: readonly ViaHandedness[] = targetUsesVia
1822
2402
  ? pairChannelFitsVia || outwardEdgeBus
1823
2403
  ? [0]
1824
- : [1, -1]
2404
+ : allowBlindAndBuriedVias
2405
+ ? [1, -1]
2406
+ : [-1, 1]
1825
2407
  : [0]
2408
+ const viaHandednesses: readonly ViaHandedness[] = (() => {
2409
+ if (allowBlindAndBuriedVias) return availableViaHandednesses
2410
+ if (
2411
+ availableViaHandednesses.length !== 2 ||
2412
+ !availableViaHandednesses.includes(-1) ||
2413
+ !availableViaHandednesses.includes(1)
2414
+ ) {
2415
+ return availableViaHandednesses
2416
+ }
2417
+
2418
+ // Prefer placing the dogbone via away from the boundary targets. This
2419
+ // leaves the open routing chamber between the source field and the final
2420
+ // exit band, which is especially important when physical barrels span
2421
+ // every copper layer. The opposite hand remains an immediate fallback.
2422
+ const meanSourceTrack =
2423
+ bus.connections.reduce(
2424
+ (sum, connection) =>
2425
+ sum + getPerpendicularAxis(connection.sourcePoint, bus.direction),
2426
+ 0,
2427
+ ) / bus.connections.length
2428
+ const meanTargetTrack =
2429
+ bus.connections.reduce(
2430
+ (sum, connection) =>
2431
+ sum +
2432
+ getPerpendicularAxis(
2433
+ connection.exitTargetPoint ?? connection.targetPoint,
2434
+ bus.direction,
2435
+ ),
2436
+ 0,
2437
+ ) / bus.connections.length
2438
+ if (meanTargetTrack > meanSourceTrack + 1e-9) return [-1, 1]
2439
+ if (meanTargetTrack < meanSourceTrack - 1e-9) return [1, -1]
2440
+ return availableViaHandednesses
2441
+ })()
1826
2442
 
1827
2443
  const alternatives: FanoutRoutePlan[][] = []
1828
2444
  const seenAlternativeKeys = new Set<string>()
@@ -1844,8 +2460,233 @@ export function routeBusAlternatives(
1844
2460
  const boundaryDirection = getDirectionForExitEdge(bus.exitEdge)
1845
2461
  const boundaryExitAxis = getExitAxis(bus, boundaryDirection)
1846
2462
  const cornerSide = getCornerSide(bus)
1847
- for (const viaHandedness of viaHandednesses) {
2463
+ const canUseViaInPadTerminals =
2464
+ allowsViaInPad(srj) &&
2465
+ bus.connections.every((preparedConnection) =>
2466
+ circleFitsInsideObstacle({
2467
+ center: preparedConnection.sourcePoint,
2468
+ diameter: viaDiameter,
2469
+ obstacle: preparedConnection.sourceObstacle,
2470
+ }),
2471
+ )
2472
+ type CoordinatedTerminalPattern = {
2473
+ label: string
2474
+ useViaInPad: boolean
2475
+ getViaHandedness: (
2476
+ preparedConnection: PreparedConnection,
2477
+ ) => ViaHandedness
2478
+ getViaPoint?: (preparedConnection: PreparedConnection) => Point2D
2479
+ maximumRouteOrderAttempts?: number
2480
+ windingOrderIndex?: number
2481
+ preferTargetDirectedLaneBias?: boolean
2482
+ }
2483
+ const maximumThroughAllRouteOrderAttempts = 24
2484
+ const windingTargetOrderCount = cornerSide
2485
+ ? getWindingTargetOrders({
2486
+ bus,
2487
+ boundaryDirection,
2488
+ layerNames,
2489
+ targetLayer,
2490
+ }).orders.length
2491
+ : 1
2492
+ const uniformDogboneTerminalPatterns: CoordinatedTerminalPattern[] =
2493
+ viaHandednesses.map((viaHandedness) => ({
2494
+ label: `uniform-${viaHandedness}`,
2495
+ useViaInPad: false,
2496
+ getViaHandedness: () => viaHandedness,
2497
+ maximumRouteOrderAttempts: allowBlindAndBuriedVias
2498
+ ? undefined
2499
+ : maximumThroughAllRouteOrderAttempts,
2500
+ }))
2501
+ const connectionsBySourceTrack = bus.connections.toSorted(
2502
+ (first, second) =>
2503
+ getPerpendicularAxis(first.sourcePoint, bus.direction) -
2504
+ getPerpendicularAxis(second.sourcePoint, bus.direction) ||
2505
+ getAxis(first.sourcePoint, bus.direction) -
2506
+ getAxis(second.sourcePoint, bus.direction) ||
2507
+ first.connectionIndex - second.connectionIndex,
2508
+ )
2509
+ const sourceTrackRankByConnectionIndex = new Map(
2510
+ connectionsBySourceTrack.map((connection, rank) => [
2511
+ connection.connectionIndex,
2512
+ rank,
2513
+ ]),
2514
+ )
2515
+ const getTowardMedianHandedness = (rank: number): ViaHandedness =>
2516
+ rank < connectionsBySourceTrack.length / 2 ? 1 : -1
2517
+ const middleRank = Math.floor(connectionsBySourceTrack.length / 2)
2518
+ const singleFlipRanks = [
2519
+ 0,
2520
+ 1,
2521
+ middleRank,
2522
+ middleRank + 1,
2523
+ ...connectionsBySourceTrack.map((_, rank) => rank),
2524
+ ].filter(
2525
+ (rank, index, ranks) =>
2526
+ rank >= 0 &&
2527
+ rank < connectionsBySourceTrack.length &&
2528
+ ranks.indexOf(rank) === index,
2529
+ )
2530
+ const towardMedianFlipRankSets = [
2531
+ [middleRank],
2532
+ [middleRank + 1],
2533
+ [0],
2534
+ [1],
2535
+ [0, middleRank + 1],
2536
+ [1, middleRank],
2537
+ [0, middleRank],
2538
+ [1, middleRank + 1],
2539
+ [0, 1],
2540
+ [middleRank, middleRank + 1],
2541
+ ...singleFlipRanks.map((rank) => [rank]),
2542
+ ]
2543
+ .map((ranks) =>
2544
+ ranks
2545
+ .filter((rank) => rank >= 0 && rank < connectionsBySourceTrack.length)
2546
+ .toSorted((first, second) => first - second),
2547
+ )
2548
+ .filter(
2549
+ (ranks, index, rankSets) =>
2550
+ ranks.length > 0 &&
2551
+ rankSets.findIndex(
2552
+ (candidate) => candidate.join(",") === ranks.join(","),
2553
+ ) === index,
2554
+ )
2555
+ const mixedDogboneTerminalPatterns: CoordinatedTerminalPattern[] =
2556
+ !allowBlindAndBuriedVias &&
2557
+ (reservedVias.length > 0 ||
2558
+ acceptedPlans.some((plan) => plan.termination.type === "plane")) &&
2559
+ viaHandednesses.includes(-1) &&
2560
+ viaHandednesses.includes(1)
2561
+ ? [
2562
+ {
2563
+ label: "toward-source-median",
2564
+ useViaInPad: false,
2565
+ maximumRouteOrderAttempts: maximumThroughAllRouteOrderAttempts,
2566
+ getViaHandedness: (connection) =>
2567
+ getTowardMedianHandedness(
2568
+ sourceTrackRankByConnectionIndex.get(
2569
+ connection.connectionIndex,
2570
+ ) ?? 0,
2571
+ ),
2572
+ },
2573
+ ...towardMedianFlipRankSets.slice(0, 12).map((flippedRanks) => ({
2574
+ label: `toward-source-median-with-ranks-${flippedRanks.join("-")}-flipped`,
2575
+ useViaInPad: false,
2576
+ maximumRouteOrderAttempts: 6,
2577
+ getViaHandedness: (connection: PreparedConnection) => {
2578
+ const rank =
2579
+ sourceTrackRankByConnectionIndex.get(
2580
+ connection.connectionIndex,
2581
+ ) ?? 0
2582
+ const towardMedian = getTowardMedianHandedness(rank)
2583
+ return flippedRanks.includes(rank)
2584
+ ? (-towardMedian as ViaHandedness)
2585
+ : towardMedian
2586
+ },
2587
+ })),
2588
+ {
2589
+ label: "alternating-source-grid-a",
2590
+ useViaInPad: false,
2591
+ maximumRouteOrderAttempts: 3,
2592
+ getViaHandedness: (connection) =>
2593
+ (sourceTrackRankByConnectionIndex.get(
2594
+ connection.connectionIndex,
2595
+ ) ?? 0) %
2596
+ 2 ===
2597
+ 0
2598
+ ? -1
2599
+ : 1,
2600
+ },
2601
+ {
2602
+ label: "alternating-source-grid-b",
2603
+ useViaInPad: false,
2604
+ maximumRouteOrderAttempts: 3,
2605
+ getViaHandedness: (connection) =>
2606
+ (sourceTrackRankByConnectionIndex.get(
2607
+ connection.connectionIndex,
2608
+ ) ?? 0) %
2609
+ 2 ===
2610
+ 0
2611
+ ? 1
2612
+ : -1,
2613
+ },
2614
+ {
2615
+ label: "away-from-source-median",
2616
+ useViaInPad: false,
2617
+ maximumRouteOrderAttempts: maximumThroughAllRouteOrderAttempts,
2618
+ getViaHandedness: (connection) =>
2619
+ (sourceTrackRankByConnectionIndex.get(
2620
+ connection.connectionIndex,
2621
+ ) ?? 0) <
2622
+ connectionsBySourceTrack.length / 2
2623
+ ? -1
2624
+ : 1,
2625
+ },
2626
+ ]
2627
+ : []
2628
+ const viaInPadTerminalPattern = {
2629
+ label: "via-in-pad",
2630
+ useViaInPad: true,
2631
+ getViaHandedness: () => 0 as const,
2632
+ }
2633
+ const fixedViaTerminalPatterns: CoordinatedTerminalPattern[] =
2634
+ fixedViaPointsByConnectionIndex
2635
+ ? [
2636
+ ...Array.from(
2637
+ { length: windingTargetOrderCount },
2638
+ (_, windingOrderIndex) => ({
2639
+ label: `component-matched-vias-winding-${windingOrderIndex}`,
2640
+ useViaInPad: false,
2641
+ getViaHandedness: () => 0 as const,
2642
+ getViaPoint: (connection: PreparedConnection) =>
2643
+ fixedViaPointsByConnectionIndex.get(
2644
+ connection.connectionIndex,
2645
+ )!,
2646
+ maximumRouteOrderAttempts: 1,
2647
+ windingOrderIndex,
2648
+ preferTargetDirectedLaneBias: true,
2649
+ }),
2650
+ ),
2651
+ {
2652
+ label: "component-matched-vias-fallback",
2653
+ useViaInPad: false,
2654
+ getViaHandedness: () => 0,
2655
+ getViaPoint: (connection) =>
2656
+ fixedViaPointsByConnectionIndex.get(
2657
+ connection.connectionIndex,
2658
+ )!,
2659
+ // Preserve the existing bounded search after every inexpensive
2660
+ // layer-interleave candidate has had one deterministic attempt.
2661
+ maximumRouteOrderAttempts: maximumThroughAllRouteOrderAttempts,
2662
+ windingOrderIndex: 0,
2663
+ preferTargetDirectedLaneBias: true,
2664
+ },
2665
+ ]
2666
+ : []
2667
+ const planeTerminationsAlreadyOccupyTheFanout = acceptedPlans.some(
2668
+ (plan) => plan.termination.type === "plane",
2669
+ )
2670
+ const acceptedBoundaryPlansExist = acceptedPlans.some(
2671
+ (plan) => plan.termination.type === "boundary",
2672
+ )
2673
+ const dogboneTerminalPatterns =
2674
+ acceptedBoundaryPlansExist && !allowBlindAndBuriedVias
2675
+ ? [...mixedDogboneTerminalPatterns, ...uniformDogboneTerminalPatterns]
2676
+ : [...uniformDogboneTerminalPatterns, ...mixedDogboneTerminalPatterns]
2677
+ const terminalPatterns: CoordinatedTerminalPattern[] =
2678
+ fixedViaTerminalPatterns.length > 0
2679
+ ? fixedViaTerminalPatterns
2680
+ : canUseViaInPadTerminals
2681
+ ? planeTerminationsAlreadyOccupyTheFanout
2682
+ ? [viaInPadTerminalPattern, ...dogboneTerminalPatterns]
2683
+ : [...dogboneTerminalPatterns, viaInPadTerminalPattern]
2684
+ : dogboneTerminalPatterns
2685
+ const seenTerminalSignatures = new Set<string>()
2686
+ for (const terminalPattern of terminalPatterns) {
1848
2687
  const terminals = bus.connections.map((preparedConnection) => {
2688
+ const viaHandedness =
2689
+ terminalPattern.getViaHandedness(preparedConnection)
1849
2690
  const boundaryTrack = cornerSide
1850
2691
  ? getCornerTargetTrack({
1851
2692
  bus,
@@ -1855,6 +2696,8 @@ export function routeBusAlternatives(
1855
2696
  viaDiameter,
1856
2697
  clearance,
1857
2698
  layerNames,
2699
+ targetLayer,
2700
+ windingOrderIndex: terminalPattern.windingOrderIndex,
1858
2701
  })
1859
2702
  : getPerpendicularAxis(
1860
2703
  preparedConnection.exitTargetPoint ??
@@ -1863,15 +2706,22 @@ export function routeBusAlternatives(
1863
2706
  )
1864
2707
  return {
1865
2708
  connection: preparedConnection,
1866
- viaPoint: getInitialViaPoint({
1867
- preparedConnection,
1868
- bus,
1869
- targetLayer,
1870
- traceWidth,
1871
- viaDiameter,
1872
- clearance,
1873
- viaHandedness,
1874
- }),
2709
+ viaPoint: terminalPattern.getViaPoint
2710
+ ? terminalPattern.getViaPoint(preparedConnection)
2711
+ : terminalPattern.useViaInPad
2712
+ ? {
2713
+ x: preparedConnection.sourcePoint.x,
2714
+ y: preparedConnection.sourcePoint.y,
2715
+ }
2716
+ : getInitialViaPoint({
2717
+ preparedConnection,
2718
+ bus,
2719
+ targetLayer,
2720
+ traceWidth,
2721
+ viaDiameter,
2722
+ clearance,
2723
+ viaHandedness,
2724
+ }),
1875
2725
  exitPoint: makePoint(
1876
2726
  boundaryExitAxis,
1877
2727
  boundaryTrack,
@@ -1879,36 +2729,80 @@ export function routeBusAlternatives(
1879
2729
  ),
1880
2730
  }
1881
2731
  })
1882
- const viaMinimalPlans = routeViaMinimalWinding({
1883
- srj,
1884
- bus,
1885
- targetLayer,
1886
- terminals,
1887
- acceptedPlans,
1888
- layerNames,
1889
- traceWidth,
1890
- viaDiameter,
1891
- viaHoleDiameter,
1892
- clearance,
1893
- allowSameNetMerges,
1894
- })
1895
- if (
1896
- !viaMinimalPlans ||
1897
- !fanoutPlansAreClear({
2732
+ const terminalSignature = `${terminals
2733
+ .map(
2734
+ (terminal) =>
2735
+ `${terminal.connection.connectionIndex}:${terminal.viaPoint.x}:${terminal.viaPoint.y}:${terminal.exitPoint.x}:${terminal.exitPoint.y}`,
2736
+ )
2737
+ .join("|")}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}`
2738
+ if (seenTerminalSignatures.has(terminalSignature)) continue
2739
+ seenTerminalSignatures.add(terminalSignature)
2740
+ const viaMinimalAlternatives = routeViaMinimalWindingAlternatives(
2741
+ {
2742
+ srj,
2743
+ bus,
2744
+ targetLayer,
2745
+ terminals,
2746
+ acceptedPlans,
2747
+ layerNames,
2748
+ traceWidth,
2749
+ viaDiameter,
2750
+ viaHoleDiameter,
2751
+ clearance,
2752
+ allowBlindAndBuriedVias,
2753
+ allowSameNetMerges,
2754
+ maximumRouteOrderAttempts: terminalPattern.maximumRouteOrderAttempts,
2755
+ reservedVias,
2756
+ gridStepDivisor:
2757
+ fixedViaPointsByConnectionIndex &&
2758
+ Math.min(bus.pitchX, bus.pitchY) -
2759
+ 2 * (viaDiameter / 2 + traceWidth / 2 + clearance) <
2760
+ traceWidth + clearance
2761
+ ? 2
2762
+ : 1,
2763
+ preferTargetDirectedLaneBias:
2764
+ terminalPattern.preferTargetDirectedLaneBias,
2765
+ },
2766
+ fixedViaPointsByConnectionIndex && viaMinimalOnly
2767
+ ? Math.min(2, Math.max(1, maxAlternatives - alternatives.length))
2768
+ : terminalPattern.maximumRouteOrderAttempts === undefined
2769
+ ? Math.min(2, maxAlternatives - alternatives.length)
2770
+ : 2,
2771
+ )
2772
+ for (const viaMinimalPlans of viaMinimalAlternatives) {
2773
+ const combinedPlansAreClear = fanoutPlansAreClear({
1898
2774
  plans: [...acceptedPlans, ...viaMinimalPlans],
1899
2775
  srj,
1900
2776
  sharedBoundary: bus.sharedBoundary,
1901
2777
  clearance,
2778
+ allowBlindAndBuriedVias,
1902
2779
  allowSameNetMerges,
1903
2780
  })
1904
- ) {
1905
- continue
2781
+ if (!combinedPlansAreClear) {
2782
+ const candidateIsInternallyClear = fanoutPlansAreClear({
2783
+ plans: viaMinimalPlans,
2784
+ srj,
2785
+ sharedBoundary: bus.sharedBoundary,
2786
+ clearance,
2787
+ allowBlindAndBuriedVias,
2788
+ allowSameNetMerges,
2789
+ })
2790
+ if (candidateIsInternallyClear && rejectedViaMinimalCandidates) {
2791
+ rejectedViaMinimalCandidates.push(viaMinimalPlans)
2792
+ if (stopAfterFirstRejectedViaMinimalCandidate) {
2793
+ return alternatives
2794
+ }
2795
+ }
2796
+ continue
2797
+ }
2798
+ addAlternative(viaMinimalPlans)
2799
+ if (alternatives.length >= maxAlternatives) return alternatives
1906
2800
  }
1907
- addAlternative(viaMinimalPlans)
1908
- if (alternatives.length >= maxAlternatives) return alternatives
1909
2801
  }
1910
2802
  }
1911
2803
 
2804
+ if (viaMinimalOnly) return alternatives
2805
+
1912
2806
  const searchConnectionOrder = (
1913
2807
  connectionOrder: PreparedConnection[],
1914
2808
  viaHandedness: ViaHandedness,
@@ -1987,6 +2881,7 @@ export function routeBusAlternatives(
1987
2881
  cornerBoundaryChannelLaneOffset: cornerLaneOffsets.boundaryChannel,
1988
2882
  clearance,
1989
2883
  terminateAtVia: false,
2884
+ allowBlindAndBuriedVias,
1990
2885
  })
1991
2886
  if (
1992
2887
  !planIsClear({
@@ -1998,6 +2893,7 @@ export function routeBusAlternatives(
1998
2893
  srj,
1999
2894
  sharedBoundary: bus.sharedBoundary,
2000
2895
  clearance,
2896
+ allowBlindAndBuriedVias,
2001
2897
  allowSameNetMerges,
2002
2898
  })
2003
2899
  ) {