@tscircuit/fanout-solver 0.0.63 → 0.0.65

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.
@@ -0,0 +1,559 @@
1
+ import type {
2
+ Obstacle,
3
+ SimplifiedPcbTrace,
4
+ } from "@tscircuit/capacity-autorouter"
5
+ import { getCornerBandSide } from "./boundary-exit"
6
+ import {
7
+ distance,
8
+ distancePointToSegment,
9
+ distanceSegmentToSegment,
10
+ } from "./geometry"
11
+ import {
12
+ fanoutPlansAreClear,
13
+ getCornerTargetTrack,
14
+ getBoundaryTargetTrack,
15
+ type RouteBusParams,
16
+ } from "./route-bus"
17
+ import { routeSingleLayerWithAdaptiveExitsSteps } from "./route-single-layer-adaptive-exits"
18
+ import {
19
+ buildViaMinimalWindingPlan,
20
+ type RouteViaMinimalWindingProgress,
21
+ } from "./route-via-minimal-winding"
22
+ import type { PeripheralSourceEscape } from "./route-peripheral-source-escapes"
23
+ import type {
24
+ Bounds,
25
+ FanoutRoutePlan,
26
+ Point2D,
27
+ RoutedSegment,
28
+ RoutedVia,
29
+ } from "./types"
30
+
31
+ /** Use two allowed layers to permute unordered bottom exits into either side of the boundary. */
32
+ export function* routeBottomCrossbarBusSteps(
33
+ params: RouteBusParams & {
34
+ sourceEscapes: readonly PeripheralSourceEscape[]
35
+ sourceBoundary: Bounds
36
+ oppositeLayout?: {
37
+ sourcePortOffset: number
38
+ rowOffset: number
39
+ compactRows: boolean
40
+ }
41
+ },
42
+ ): Generator<RouteViaMinimalWindingProgress, FanoutRoutePlan[] | null, void> {
43
+ const {
44
+ bus,
45
+ srj,
46
+ sourceEscapes,
47
+ sourceBoundary,
48
+ targetLayer,
49
+ acceptedPlans,
50
+ traceWidth: width,
51
+ clearance,
52
+ viaDiameter,
53
+ } = params
54
+ const oppositeSide = bus.exitEdge === "left"
55
+ const layout = oppositeSide ? params.oppositeLayout : undefined
56
+ const allowedLayers = bus.routableEscapeLayers ?? bus.allowedLayers ?? []
57
+ const sourceLayer = bus.connections[0]!.sourceLayer
58
+ const crossoverLayer = oppositeSide
59
+ ? allowedLayers.find((layer) => layer !== targetLayer)
60
+ : sourceLayer
61
+ if (
62
+ bus.termination.type !== "boundary" ||
63
+ (bus.exitEdge !== "right" && !oppositeSide) ||
64
+ !crossoverLayer ||
65
+ !allowedLayers.includes(targetLayer) ||
66
+ crossoverLayer === targetLayer ||
67
+ !(bus.routableEscapeLayers ?? bus.allowedLayers ?? []).includes(
68
+ crossoverLayer,
69
+ )
70
+ )
71
+ return null
72
+ const usesLowerBand =
73
+ getCornerBandSide(bus.exitEdge, bus.preferredExit) === "minimum"
74
+ const count = bus.connections.length,
75
+ pitch = width + clearance
76
+ const portPitch = Math.ceil((viaDiameter + clearance) / pitch) * pitch
77
+ const byIndex = new Map(
78
+ sourceEscapes.map((source) => [source.connectionIndex, source]),
79
+ )
80
+ const ownSources = bus.connections.map((connection) =>
81
+ byIndex.get(connection.connectionIndex),
82
+ )
83
+ if (count < 2 || ownSources.some((source) => !source)) return null
84
+ const viaPoints = ownSources.map((source) => source!.via.center)
85
+ const firstPortColumn = Math.round(
86
+ ((oppositeSide
87
+ ? (sourceBoundary.minX + sourceBoundary.maxX) / 2 +
88
+ (layout?.sourcePortOffset ?? 0)
89
+ : Math.min(...viaPoints.map((point) => point.x))) +
90
+ portPitch -
91
+ sourceBoundary.minX) /
92
+ pitch,
93
+ )
94
+ const portStride = Math.round(portPitch / pitch)
95
+ const portColumns = new Set(
96
+ Array.from(
97
+ { length: count },
98
+ (_, rank) => firstPortColumn + rank * portStride,
99
+ ),
100
+ )
101
+ const columnCount = Math.round(
102
+ (sourceBoundary.maxX - sourceBoundary.minX) / pitch,
103
+ )
104
+ if (firstPortColumn < 1 || Math.max(...portColumns) >= columnCount)
105
+ return null
106
+ const circle = (name: string, center: Point2D, diameter: number): Obstacle =>
107
+ ({
108
+ type: "rect",
109
+ shape: "circle",
110
+ center,
111
+ width: diameter,
112
+ height: diameter,
113
+ layers: ["top"],
114
+ connectedTo: [name],
115
+ }) as Obstacle
116
+ const obstacles: Obstacle[] = [
117
+ ...srj.obstacles
118
+ .filter((obstacle) => obstacle.layers.includes(targetLayer))
119
+ .map((obstacle) => ({ ...obstacle, layers: ["top"] })),
120
+ ...sourceEscapes.map((source) =>
121
+ circle(source.connectionName, source.via.center, source.via.diameter),
122
+ ),
123
+ ...acceptedPlans.flatMap((plan) =>
124
+ (plan.additionalVias ?? []).map((via) =>
125
+ circle(plan.connectionName, via.center, via.diameter),
126
+ ),
127
+ ),
128
+ ...acceptedPlans.flatMap((plan) =>
129
+ plan.segments
130
+ .filter((segment) => segment.layer === targetLayer)
131
+ .map((segment) => ({
132
+ type: "rect" as const,
133
+ center: {
134
+ x: (segment.start.x + segment.end.x) / 2,
135
+ y: (segment.start.y + segment.end.y) / 2,
136
+ },
137
+ width: distance(segment.start, segment.end),
138
+ height: segment.width,
139
+ ccwRotationDegrees:
140
+ (Math.atan2(
141
+ segment.end.y - segment.start.y,
142
+ segment.end.x - segment.start.x,
143
+ ) *
144
+ 180) /
145
+ Math.PI,
146
+ layers: ["top"],
147
+ connectedTo: [plan.connectionName],
148
+ })),
149
+ ),
150
+ ]
151
+ // Restrict only the internal flow sinks. The returned copper is checked against the original SRJ.
152
+ for (let column = 0; column <= columnCount; column++)
153
+ if (!portColumns.has(column))
154
+ obstacles.push(
155
+ circle(
156
+ `reserved-crossbar-port-${column}`,
157
+ { x: sourceBoundary.minX + column * pitch, y: sourceBoundary.minY },
158
+ width / 1000,
159
+ ),
160
+ )
161
+ const flowBuses = bus.connections.map((connection, index) => {
162
+ const viaPoint = viaPoints[index]!,
163
+ ownObstacle = obstacles.find(
164
+ (obstacle) =>
165
+ obstacle.connectedTo.includes(connection.connection.name) &&
166
+ distance(obstacle.center, viaPoint) < 1e-7,
167
+ )!
168
+ return {
169
+ ...bus,
170
+ busId: `${bus.busId}:source-${index}`,
171
+ sharedBoundary: sourceBoundary,
172
+ preferredExit: undefined,
173
+ exitEdge: undefined,
174
+ connections: [
175
+ {
176
+ ...connection,
177
+ sourcePoint: {
178
+ ...connection.sourcePoint,
179
+ ...viaPoint,
180
+ layer: "top",
181
+ },
182
+ sourceObstacle: ownObstacle,
183
+ sourceLayer: "top",
184
+ exitTargetPoint: undefined,
185
+ hasExplicitLayeredExitTarget: false,
186
+ hasExplicitExitTarget: false,
187
+ },
188
+ ],
189
+ }
190
+ })
191
+ const flow = routeSingleLayerWithAdaptiveExitsSteps({
192
+ srj: { ...srj, bounds: sourceBoundary, obstacles },
193
+ buses: flowBuses,
194
+ traceWidth: width,
195
+ clearance,
196
+ availableBoundaryRegions: [
197
+ { direction: "down", preferredExit: "bottom", exitEdge: "bottom" },
198
+ ],
199
+ })
200
+ let flowStep = flow.next()
201
+ while (!flowStep.done) {
202
+ yield {
203
+ phase: "route-connection",
204
+ routeOrderAttempt: 0,
205
+ connectionIndex: 0,
206
+ connectionCount: count,
207
+ connectionName: bus.connections[0]!.connection.name,
208
+ searchBatch: 0,
209
+ expandedStateCount: 0,
210
+ connectionComplete: false,
211
+ }
212
+ flowStep = flow.next()
213
+ }
214
+ if (!flowStep.value || flowStep.value.length !== count) return null
215
+ const prefixes = new Map(
216
+ flowStep.value.map((plan) => [plan.connectionIndex, plan]),
217
+ )
218
+ const targetTrack = (connection: (typeof bus.connections)[number]) =>
219
+ getCornerBandSide(bus.exitEdge, bus.preferredExit) !== undefined
220
+ ? getCornerTargetTrack({ ...params, connection, cornerExitLaneOffset: 0 })
221
+ : getBoundaryTargetTrack({
222
+ ...params,
223
+ connection,
224
+ boundaryDirection: oppositeSide ? "left" : "right",
225
+ })
226
+ const ordered = bus.connections.toSorted(
227
+ (a, b) => targetTrack(a) - targetTrack(b),
228
+ )
229
+
230
+ const padMaximumX = Math.max(
231
+ ...bus.componentObstacles.map(
232
+ (obstacle) => obstacle.center.x + obstacle.width / 2,
233
+ ),
234
+ )
235
+ const foreignRightVias = sourceEscapes
236
+ .filter((source) => source.via.center.x > sourceBoundary.maxX)
237
+ .map((source) => source.via.center.x)
238
+ const rightViaX = foreignRightVias.length
239
+ ? Math.min(...foreignRightVias)
240
+ : bus.sharedBoundary.maxX
241
+ const viaTraceDistance = viaDiameter / 2 + width / 2 + clearance
242
+ // Diagonally staggered crossbar vias need trace-to-via spacing on each
243
+ // axis; the final physical check also verifies their diagonal via clearance.
244
+ const crossingPitch =
245
+ usesLowerBand || layout?.compactRows
246
+ ? Math.max(viaTraceDistance, (viaDiameter + clearance) / Math.SQRT2) +
247
+ 1e-5
248
+ : portPitch
249
+ const lowestAcceptedCopper = Math.min(
250
+ ...acceptedPlans
251
+ .flatMap((plan) => plan.segments)
252
+ .filter((segment) => segment.layer !== crossoverLayer)
253
+ .flatMap((segment) => [segment.start.y, segment.end.y]),
254
+ )
255
+ const annulusTop = Math.min(
256
+ sourceBoundary.minY -
257
+ viaDiameter / 2 -
258
+ clearance -
259
+ (layout?.rowOffset ?? 0),
260
+ usesLowerBand ? lowestAcceptedCopper - viaTraceDistance - 1e-5 : Infinity,
261
+ )
262
+ const annulusBottom = annulusTop - (count - 1) * crossingPitch
263
+ const crossingSourceSegments = sourceEscapes
264
+ .flatMap((source) => source.segments)
265
+ .filter(
266
+ (segment) =>
267
+ segment.layer === crossoverLayer &&
268
+ (!usesLowerBand ||
269
+ Math.max(segment.start.x, segment.end.x) >=
270
+ Math.min(
271
+ ...[...prefixes.values()].map((prefix) => prefix.exitPoint.x),
272
+ ) -
273
+ viaTraceDistance) &&
274
+ Math.min(segment.start.y, segment.end.y) <=
275
+ annulusTop + viaTraceDistance &&
276
+ Math.max(segment.start.y, segment.end.y) >=
277
+ annulusBottom - viaTraceDistance,
278
+ )
279
+ const sourceCopperColumnLimit = crossingSourceSegments.length
280
+ ? Math.min(
281
+ ...crossingSourceSegments.map((segment) =>
282
+ Math.min(segment.start.x, segment.end.x),
283
+ ),
284
+ ) -
285
+ viaTraceDistance -
286
+ 1e-5
287
+ : bus.sharedBoundary.maxX
288
+ const highestColumn = Math.min(
289
+ bus.sharedBoundary.maxX - viaDiameter - clearance,
290
+ rightViaX - viaTraceDistance - width,
291
+ sourceCopperColumnLimit,
292
+ )
293
+ const columnBlockers = [
294
+ ...sourceEscapes.map((source) => source.via),
295
+ ...acceptedPlans.flatMap((plan) => plan.additionalVias ?? []),
296
+ ]
297
+ .filter((via) => via.spanLayers.includes(targetLayer))
298
+ .map((via) => ({
299
+ minimum: via.center.x - via.diameter / 2 - width / 2 - clearance - 1e-5,
300
+ maximum: via.center.x + via.diameter / 2 + width / 2 + clearance + 1e-5,
301
+ }))
302
+ const nominalColumns = Array.from(
303
+ { length: count },
304
+ (_, rank) => highestColumn - rank * crossingPitch,
305
+ )
306
+ const columns: number[] = []
307
+ if (
308
+ nominalColumns.every((column) =>
309
+ columnBlockers.every(
310
+ (interval) => column < interval.minimum || column > interval.maximum,
311
+ ),
312
+ )
313
+ )
314
+ columns.push(...nominalColumns)
315
+ else {
316
+ let nextColumn = highestColumn
317
+ for (let rank = 0; rank < count; rank++) {
318
+ for (let pass = 0; pass <= columnBlockers.length; pass++) {
319
+ const blockers = columnBlockers.filter(
320
+ (interval) =>
321
+ nextColumn >= interval.minimum && nextColumn <= interval.maximum,
322
+ )
323
+ if (!blockers.length) break
324
+ nextColumn =
325
+ Math.min(...blockers.map((interval) => interval.minimum)) - 1e-7
326
+ }
327
+ columns.push(nextColumn)
328
+ nextColumn -= usesLowerBand ? crossingPitch : viaDiameter + clearance
329
+ }
330
+ }
331
+ const minimumColumn = columns.at(-1)!
332
+ const blockedTracks = [
333
+ ...sourceEscapes.map((source) => source.via),
334
+ ...acceptedPlans.flatMap((plan) => plan.additionalVias ?? []),
335
+ ]
336
+ .filter(
337
+ (via) =>
338
+ via.spanLayers.includes(targetLayer) &&
339
+ via.center.x + via.diameter / 2 + width / 2 + clearance >=
340
+ (oppositeSide ? bus.sharedBoundary.minX : minimumColumn) &&
341
+ via.center.x - via.diameter / 2 - width / 2 - clearance <=
342
+ bus.sharedBoundary.maxX,
343
+ )
344
+ .map((via) => ({
345
+ minimum:
346
+ via.center.y - via.diameter / 2 - width / 2 - clearance - width / 100,
347
+ maximum:
348
+ via.center.y + via.diameter / 2 + width / 2 + clearance + width / 100,
349
+ }))
350
+ const tracks: number[] = []
351
+ let nextTrack = bus.sharedBoundary.maxY - width / 2
352
+ for (let rank = count - 1; rank >= 0; rank--) {
353
+ for (let pass = 0; pass <= blockedTracks.length; pass++) {
354
+ const blockers = blockedTracks.filter(
355
+ (interval) =>
356
+ nextTrack >= interval.minimum && nextTrack <= interval.maximum,
357
+ )
358
+ if (!blockers.length) break
359
+ nextTrack =
360
+ Math.min(...blockers.map((interval) => interval.minimum)) - 1e-7
361
+ }
362
+ tracks[rank] = nextTrack
363
+ nextTrack -= viaDiameter + clearance
364
+ }
365
+ if (usesLowerBand)
366
+ tracks.splice(0, tracks.length, ...ordered.map(targetTrack))
367
+ const lowestTrack = tracks[0]!
368
+ const topRow = annulusTop
369
+ if (
370
+ topRow - (count - 1) * crossingPitch - viaDiameter / 2 <
371
+ bus.sharedBoundary.minY ||
372
+ minimumColumn <=
373
+ Math.max(...[...prefixes.values()].map((prefix) => prefix.exitPoint.x)) +
374
+ viaTraceDistance ||
375
+ (!usesLowerBand &&
376
+ lowestTrack <= (bus.sharedBoundary.minY + bus.sharedBoundary.maxY) / 2)
377
+ )
378
+ return null
379
+ const baseLengths = ordered
380
+ .map((connection, rank) => {
381
+ const prefix = prefixes.get(connection.connectionIndex)!,
382
+ source = byIndex.get(connection.connectionIndex)!,
383
+ exitY = tracks[rank]!
384
+ return {
385
+ connectionIndex: connection.connectionIndex,
386
+ length:
387
+ source.segments.reduce(
388
+ (sum, segment) => sum + distance(segment.start, segment.end),
389
+ 0,
390
+ ) +
391
+ (layout?.compactRows ? 2 * columns[count - 1 - rank]! : 0) +
392
+ prefix.length +
393
+ exitY -
394
+ prefix.exitPoint.x,
395
+ }
396
+ })
397
+ .sort((a, b) => b.length - a.length)
398
+ const rowRank = new Map(
399
+ baseLengths.map((item, rank) => [item.connectionIndex, rank]),
400
+ )
401
+ const segments = (
402
+ points: readonly Point2D[],
403
+ layer: string,
404
+ ): RoutedSegment[] =>
405
+ points
406
+ .slice(1)
407
+ .flatMap((end, index) =>
408
+ distance(points[index]!, end) < 1e-9
409
+ ? []
410
+ : [{ start: points[index]!, end, width, layer }],
411
+ )
412
+ const wires = (
413
+ points: readonly Point2D[],
414
+ layer: string,
415
+ ): SimplifiedPcbTrace["route"] =>
416
+ points.map((point) => ({ route_type: "wire", ...point, width, layer }))
417
+ const viaRoute = (via: RoutedVia): SimplifiedPcbTrace["route"][number] => ({
418
+ route_type: "via",
419
+ ...via.center,
420
+ from_layer: via.fromLayer,
421
+ to_layer: via.toLayer,
422
+ via_diameter: via.diameter,
423
+ via_hole_diameter: via.holeDiameter,
424
+ })
425
+ const plans = ordered.map((connection, rank) => {
426
+ const source = byIndex.get(connection.connectionIndex)!,
427
+ prefix = prefixes.get(connection.connectionIndex)!,
428
+ row = topRow - rowRank.get(connection.connectionIndex)! * crossingPitch
429
+ const first = { x: prefix.exitPoint.x, y: row },
430
+ second = { x: columns[oppositeSide ? count - 1 - rank : rank]!, y: row },
431
+ exitPoint = {
432
+ x: oppositeSide ? bus.sharedBoundary.minX : bus.sharedBoundary.maxX,
433
+ y: tracks[rank]!,
434
+ }
435
+ const sourcePath = [
436
+ source.segments[0]!.start,
437
+ ...source.segments.map((segment) => segment.end),
438
+ ],
439
+ innerStart = [
440
+ prefix.segments[0]!.start,
441
+ ...prefix.segments.map((segment) => segment.end),
442
+ first,
443
+ ],
444
+ tail = [
445
+ second,
446
+ {
447
+ x: second.x,
448
+ y: exitPoint.y - Math.sign(exitPoint.y - second.y) * width,
449
+ },
450
+ { x: second.x + (oppositeSide ? -width : width), y: exitPoint.y },
451
+ exitPoint,
452
+ ]
453
+ const plan = buildViaMinimalWindingPlan({
454
+ ...params,
455
+ terminal: { connection, viaPoint: source.via.center, exitPoint },
456
+ sourceEscapePoints: sourcePath,
457
+ targetLayerPoints: [source.via.center, exitPoint],
458
+ allowBlindAndBuriedVias: false,
459
+ })
460
+ const additionalVias: RoutedVia[] = [
461
+ {
462
+ center: first,
463
+ diameter: viaDiameter,
464
+ holeDiameter: params.viaHoleDiameter,
465
+ spanLayers: params.layerNames,
466
+ fromLayer: targetLayer,
467
+ toLayer: crossoverLayer,
468
+ },
469
+ {
470
+ center: second,
471
+ diameter: viaDiameter,
472
+ holeDiameter: params.viaHoleDiameter,
473
+ spanLayers: params.layerNames,
474
+ fromLayer: crossoverLayer,
475
+ toLayer: targetLayer,
476
+ },
477
+ ]
478
+ plan.segments = [
479
+ ...source.segments,
480
+ ...segments(innerStart, targetLayer),
481
+ ...segments([first, second], crossoverLayer),
482
+ ...segments(tail, targetLayer),
483
+ ]
484
+ plan.additionalVias = additionalVias
485
+ plan.sourceEscapeSegmentCount = source.segments.length
486
+ plan.length = plan.segments.reduce(
487
+ (sum, segment) => sum + distance(segment.start, segment.end),
488
+ 0,
489
+ )
490
+ plan.trace.route = [
491
+ ...wires(sourcePath, sourceLayer),
492
+ viaRoute(plan.via!),
493
+ ...wires(innerStart, targetLayer),
494
+ viaRoute(additionalVias[0]!),
495
+ ...wires([first, second], crossoverLayer),
496
+ viaRoute(additionalVias[1]!),
497
+ ...wires(tail, targetLayer),
498
+ ]
499
+ return plan
500
+ })
501
+ // A compact opposite crossbar may need the caller's normal complete-bus
502
+ // length matching. Physical clearance is still checked before returning it.
503
+ if (bus.maxLengthSkew !== undefined && !layout?.compactRows) {
504
+ const lengths = plans.map((plan) => plan.length)
505
+ if (Math.max(...lengths) - Math.min(...lengths) > bus.maxLengthSkew + 1e-6)
506
+ return null
507
+ }
508
+ if (
509
+ !fanoutPlansAreClear({
510
+ ...params,
511
+ plans: [...acceptedPlans, ...plans],
512
+ sharedBoundary: bus.sharedBoundary,
513
+ })
514
+ )
515
+ return null
516
+ for (const plan of plans)
517
+ for (const source of sourceEscapes) {
518
+ if (plan.connectionIndex === source.connectionIndex) continue
519
+ for (const segment of plan.segments) {
520
+ if (
521
+ source.via.spanLayers.includes(segment.layer) &&
522
+ distancePointToSegment(
523
+ source.via.center,
524
+ segment.start,
525
+ segment.end,
526
+ ) <
527
+ source.via.diameter / 2 + width / 2 + clearance - 1e-7
528
+ )
529
+ return null
530
+ for (const other of source.segments)
531
+ if (
532
+ segment.layer === other.layer &&
533
+ distanceSegmentToSegment(
534
+ segment.start,
535
+ segment.end,
536
+ other.start,
537
+ other.end,
538
+ ) <
539
+ (width + other.width) / 2 + clearance - 1e-7
540
+ )
541
+ return null
542
+ }
543
+ for (const via of plan.additionalVias!) {
544
+ if (
545
+ distance(via.center, source.via.center) <
546
+ (via.diameter + source.via.diameter) / 2 + clearance - 1e-7
547
+ )
548
+ return null
549
+ for (const segment of source.segments)
550
+ if (
551
+ via.spanLayers.includes(segment.layer) &&
552
+ distancePointToSegment(via.center, segment.start, segment.end) <
553
+ via.diameter / 2 + segment.width / 2 + clearance - 1e-7
554
+ )
555
+ return null
556
+ }
557
+ }
558
+ return plans
559
+ }
package/lib/route-bus.ts CHANGED
@@ -61,6 +61,10 @@ export interface RouteBusParams {
61
61
  rejectedViaMinimalCandidates?: FanoutRoutePlan[][]
62
62
  stopAfterFirstRejectedViaMinimalCandidate?: boolean
63
63
  fixedViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
64
+ /** Actual copper before the first via for previously reserved source escapes. */
65
+ sourceEscapePaths?: ReadonlyMap<number, readonly Point2D[]>
66
+ /** Exact winding spacing for staged narrow-channel routing. */
67
+ windingGridStep?: number
64
68
  reservedVias?: readonly ViaMinimalWindingReservedVia[]
65
69
  /** Provisional site preferences; successful callers must rematch future vias. */
66
70
  softReservedVias?: readonly ViaMinimalWindingReservedVia[]
@@ -75,6 +79,8 @@ export interface RouteBusParams {
75
79
  alignWindingGridToPads?: boolean
76
80
  /** Bounds the final fixed-via winding fallback after ordered attempts. */
77
81
  fixedViaFallbackRouteOrderAttempts?: number
82
+ /** Retry caller-fixed sites while preserving future exit gaps during recovery. */
83
+ allowFixedViaReservedExitFallback?: boolean
78
84
  /** Skip this many otherwise-clear plane escapes when enumerating alternatives. */
79
85
  planeCandidateSkipCount?: number
80
86
  /** Dense corner-band phase that preserves existing lane centers when leading lanes are prepended. */
@@ -465,7 +471,7 @@ export function getBoundaryTargetTrack(params: {
465
471
  return Math.max(boundaryMinimum, Math.min(boundaryMaximum, requestedTrack))
466
472
  }
467
473
 
468
- function getCornerTargetTrack(params: {
474
+ export function getCornerTargetTrack(params: {
469
475
  bus: PreparedBus
470
476
  connection: PreparedConnection
471
477
  cornerExitLaneOffset: number
@@ -1330,6 +1336,7 @@ function buildPlan(params: {
1330
1336
  })
1331
1337
  }
1332
1338
 
1339
+ const sourceEscapeSegmentCount = segments.length
1333
1340
  let via: FanoutRoutePlan["via"]
1334
1341
  if (targetLayer !== preparedConnection.sourceLayer) {
1335
1342
  const spanLayers = getViaSpanLayers({
@@ -1510,6 +1517,7 @@ function buildPlan(params: {
1510
1517
  route,
1511
1518
  },
1512
1519
  segments,
1520
+ ...(sourceEscapeSegmentCount > 1 ? { sourceEscapeSegmentCount } : {}),
1513
1521
  via,
1514
1522
  ...(additionalVias.length > 0 ? { additionalVias } : {}),
1515
1523
  length: segments.reduce(
@@ -2222,7 +2230,15 @@ function routePlaneTerminatedBus(
2222
2230
  terminateAtVia: true,
2223
2231
  allowBlindAndBuriedVias,
2224
2232
  initialViaPoint: fixedViaPoint,
2225
- sourceEscapePath: [sourcePoint, fixedViaPoint],
2233
+ sourceEscapePath: params.sourceEscapePaths?.get(
2234
+ preparedConnection.connectionIndex,
2235
+ )
2236
+ ? [
2237
+ ...params.sourceEscapePaths.get(
2238
+ preparedConnection.connectionIndex,
2239
+ )!,
2240
+ ]
2241
+ : [sourcePoint, fixedViaPoint],
2226
2242
  })
2227
2243
  const endpointViaCandidates = getPlaneEndpointViaCandidates({
2228
2244
  preparedConnection,
@@ -2606,6 +2622,7 @@ export function* routeBusAlternativesSteps(
2606
2622
  adaptiveWindingRouteOrder = false,
2607
2623
  alignWindingGridToPads = false,
2608
2624
  fixedViaFallbackRouteOrderAttempts = 24,
2625
+ allowFixedViaReservedExitFallback = false,
2609
2626
  cornerBandTargetTrackOffset,
2610
2627
  } = params
2611
2628
  if (!Number.isInteger(maxAlternatives) || maxAlternatives < 1) {
@@ -2994,6 +3011,29 @@ export function* routeBusAlternativesSteps(
2994
3011
  })),
2995
3012
  ).flat()
2996
3013
  : []),
3014
+ // A single-layer target permutation can also let an early lane
3015
+ // close a future terminal's exit gap. Keep the original attempts
3016
+ // first, then retry the same fixed sites with every exit reserved.
3017
+ ...(allowFixedViaReservedExitFallback &&
3018
+ fixedViaPointsByConnectionIndex &&
3019
+ !getCornerSide(bus) &&
3020
+ windingTargetOrderCount === 1 &&
3021
+ bus.connections.length > 2
3022
+ ? [true, false].map((preferTargetDirectedLaneBias) => ({
3023
+ label: `fixed-vias-reserved-exits-${preferTargetDirectedLaneBias}`,
3024
+ useViaInPad: false,
3025
+ getViaHandedness: () => 0 as const,
3026
+ getViaPoint: (connection: PreparedConnection) =>
3027
+ coordinatedViaPoints.get(connection.connectionIndex)!,
3028
+ maximumRouteOrderAttempts: Math.min(
3029
+ maximumThroughAllRouteOrderAttempts,
3030
+ fixedViaFallbackRouteOrderAttempts,
3031
+ ),
3032
+ windingOrderIndex: 0,
3033
+ preferTargetDirectedLaneBias,
3034
+ reserveTerminalExitPoints: true,
3035
+ }))
3036
+ : []),
2997
3037
  ]
2998
3038
  : []
2999
3039
  const planeTerminationsAlreadyOccupyTheFanout = acceptedPlans.some(
@@ -3211,6 +3251,8 @@ export function* routeBusAlternativesSteps(
3211
3251
  softReservedVias,
3212
3252
  reserveTerminalExitPoints: terminalPattern.reserveTerminalExitPoints,
3213
3253
  gridStepDivisor,
3254
+ gridStep: params.windingGridStep,
3255
+ sourceEscapePaths: params.sourceEscapePaths,
3214
3256
  preferTargetDirectedLaneBias:
3215
3257
  terminalPattern.preferTargetDirectedLaneBias,
3216
3258
  },