@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,430 @@
1
+ import { getCornerBandSide } from "./boundary-exit"
2
+ import { getFreeBoundaryTracks } from "./get-free-boundary-tracks"
3
+ import { routeViaMinimalWindingAlternativesSteps } from "./route-via-minimal-winding"
4
+ import {
5
+ getCornerTargetTrack,
6
+ routeBusAlternativesSteps,
7
+ type RouteBusAlternativesProgress,
8
+ type RouteBusParams,
9
+ } from "./route-bus"
10
+ import type { FanoutBorderTarget, FanoutRoutePlan, PreparedBus } from "./types"
11
+
12
+ interface Params extends Omit<RouteBusParams, "bus"> {
13
+ buses: readonly PreparedBus[]
14
+ }
15
+
16
+ /** Route fixed-via small buses together so an early lane cannot trap a later bus. */
17
+ export function* routeReservedNarrowBusesSteps(
18
+ params: Params,
19
+ ): Generator<RouteBusAlternativesProgress, FanoutRoutePlan[] | null, void> {
20
+ if (
21
+ params.buses.some(
22
+ (bus) =>
23
+ bus.termination.type !== "boundary" || bus.connections.length > 2,
24
+ )
25
+ )
26
+ return null
27
+ let attempts = 0
28
+ const maximumAttempts = 192
29
+ const pitch = params.traceWidth + params.clearance
30
+ function* route(
31
+ bus: PreparedBus,
32
+ accepted: FanoutRoutePlan[],
33
+ preferredExit = bus.preferredExit,
34
+ offset = 0,
35
+ ) {
36
+ if (attempts++ >= maximumAttempts) return []
37
+ return yield* routeBusAlternativesSteps(
38
+ {
39
+ ...params,
40
+ bus: { ...bus, preferredExit },
41
+ acceptedPlans: accepted,
42
+ reservedVias: params.reservedVias?.filter(
43
+ (reserved) =>
44
+ !bus.connections.some(
45
+ (connection) =>
46
+ connection.connection.name === reserved.connectionName,
47
+ ),
48
+ ),
49
+ viaMinimalOnly: true,
50
+ adaptiveWindingRouteOrder: true,
51
+ alignWindingGridToPads: true,
52
+ windingGridStep: pitch / 2,
53
+ fixedViaFallbackRouteOrderAttempts: 32,
54
+ cornerBandTargetTrackOffset: offset,
55
+ },
56
+ 1,
57
+ false,
58
+ )
59
+ }
60
+ const restore = (plans: FanoutRoutePlan[]) =>
61
+ plans.map((plan) => {
62
+ const bus = params.buses.find((bus) => bus.busId === plan.busId)
63
+ return bus
64
+ ? {
65
+ ...plan,
66
+ cornerBandSide: getCornerBandSide(bus.exitEdge, bus.preferredExit),
67
+ }
68
+ : plan
69
+ })
70
+ // Retain successful ordinary routing before searching alternate bands/orders.
71
+ let ordinary = [...params.acceptedPlans]
72
+ for (const bus of params.buses) {
73
+ const plans = yield* route(bus, ordinary)
74
+ if (!plans.length) {
75
+ ordinary = []
76
+ break
77
+ }
78
+ ordinary.push(...plans[0]!)
79
+ }
80
+ if (ordinary.length)
81
+ return restore(ordinary.slice(params.acceptedPlans.length))
82
+
83
+ const sourceMean = (bus: PreparedBus, axis: "x" | "y") =>
84
+ bus.connections.reduce(
85
+ (sum, c) =>
86
+ sum +
87
+ (params.fixedViaPointsByConnectionIndex?.get(c.connectionIndex) ??
88
+ c.sourcePoint)[axis],
89
+ 0,
90
+ ) / bus.connections.length
91
+ // Commit the most constrained corner exits first, then sweep across the
92
+ // source field. Two complete greedy orders cheaply resolve cases where the
93
+ // recursive search otherwise spends its budget revisiting center lanes.
94
+ if (params.buses.length > 1) {
95
+ for (const sign of [1, -1]) {
96
+ const sweep = params.buses.toSorted((a, b) => {
97
+ const aCorner =
98
+ getCornerBandSide(a.exitEdge, a.preferredExit) !== undefined
99
+ const bCorner =
100
+ getCornerBandSide(b.exitEdge, b.preferredExit) !== undefined
101
+ if (aCorner !== bCorner) return Number(bCorner) - Number(aCorner)
102
+ const axis = a.exitEdge === "left" || a.exitEdge === "right" ? "y" : "x"
103
+ return sign * (sourceMean(a, axis) - sourceMean(b, axis))
104
+ })
105
+ let accepted = [...params.acceptedPlans]
106
+ for (const bus of sweep) {
107
+ const alternatives = yield* route(bus, accepted)
108
+ if (!alternatives.length) {
109
+ accepted = []
110
+ break
111
+ }
112
+ accepted.push(...alternatives[0]!)
113
+ }
114
+ if (accepted.length)
115
+ return restore(accepted.slice(params.acceptedPlans.length))
116
+ }
117
+ }
118
+ const ordered = params.buses.toSorted((a, b) => {
119
+ const aSide = getCornerBandSide(a.exitEdge, a.preferredExit)
120
+ const bSide = getCornerBandSide(b.exitEdge, b.preferredExit)
121
+ const aLower = aSide === "minimum",
122
+ bLower = bSide === "minimum"
123
+ if (aLower !== bLower) return Number(bLower) - Number(aLower)
124
+ if (aLower) return sourceMean(a, "y") - sourceMean(b, "y")
125
+ if (a.connections.length !== b.connections.length)
126
+ return a.connections.length - b.connections.length
127
+ if (a.connections.length === 1)
128
+ return sourceMean(a, "x") - sourceMean(b, "x")
129
+ if (aSide !== bSide)
130
+ return Number(aSide === "maximum") - Number(bSide === "maximum")
131
+ return (
132
+ (aSide === "maximum" ? 1 : -1) * (sourceMean(a, "y") - sourceMean(b, "y"))
133
+ )
134
+ })
135
+ // An edge-only singleton can reserve a separating channel before the
136
+ // corner-guided pairs. Keep this ordered retry bounded; each bus may also
137
+ // move its complete, ordered track envelope into a free edge interval.
138
+ if (params.buses.length > 1) {
139
+ const intervalOrder = ordered.toSorted((a, b) => {
140
+ const singleton = (bus: PreparedBus) =>
141
+ bus.connections.length === 1 &&
142
+ !getCornerBandSide(bus.exitEdge, bus.preferredExit)
143
+ return Number(singleton(b)) - Number(singleton(a))
144
+ })
145
+ const intervalAttemptLimit = Math.min(maximumAttempts, attempts + 48)
146
+ function* orderedIntervals(
147
+ index: number,
148
+ accepted: FanoutRoutePlan[],
149
+ ): Generator<RouteBusAlternativesProgress, FanoutRoutePlan[] | null, void> {
150
+ if (index === intervalOrder.length) return accepted
151
+ if (attempts >= intervalAttemptLimit) return null
152
+ const bus = intervalOrder[index]!
153
+ const ordinary = yield* route(bus, accepted)
154
+ for (const plans of ordinary) {
155
+ const result = yield* orderedIntervals(index + 1, [
156
+ ...accepted,
157
+ ...plans,
158
+ ])
159
+ if (result) return result
160
+ }
161
+ const alternatives = yield* freeIntervals(
162
+ bus,
163
+ accepted,
164
+ intervalAttemptLimit,
165
+ )
166
+ for (const plans of alternatives) {
167
+ const result = yield* orderedIntervals(index + 1, [
168
+ ...accepted,
169
+ ...plans,
170
+ ])
171
+ if (result) return result
172
+ }
173
+ return null
174
+ }
175
+ const intervalPlans = yield* orderedIntervals(0, [...params.acceptedPlans])
176
+ if (intervalPlans)
177
+ return restore(intervalPlans.slice(params.acceptedPlans.length))
178
+ }
179
+ function* freeIntervals(
180
+ bus: PreparedBus,
181
+ accepted: FanoutRoutePlan[],
182
+ attemptLimit: number,
183
+ ): Generator<RouteBusAlternativesProgress, FanoutRoutePlan[][], void> {
184
+ if (!bus.exitEdge) return []
185
+ const reservedVias = params.reservedVias?.filter(
186
+ (reserved) =>
187
+ !bus.connections.some(
188
+ (connection) =>
189
+ connection.connection.name === reserved.connectionName,
190
+ ),
191
+ )
192
+ const common = { ...params, bus, acceptedPlans: accepted, reservedVias }
193
+ const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
194
+ const vertical = bus.exitEdge === "left" || bus.exitEdge === "right"
195
+ const boundary = bus.sharedBoundary
196
+ const lower = vertical ? boundary.minY : boundary.minX
197
+ const upper = vertical ? boundary.maxY : boundary.maxX
198
+ const middle = (lower + upper) / 2
199
+ const alternatives: FanoutRoutePlan[][] = []
200
+ for (const track of getFreeBoundaryTracks(common)) {
201
+ if (attempts >= attemptLimit) break
202
+ const preferredExit = side
203
+ ? bus.preferredExit
204
+ : guidance(bus).find(
205
+ (candidate) =>
206
+ getCornerBandSide(bus.exitEdge, candidate) ===
207
+ (track > middle ? "maximum" : "minimum"),
208
+ )
209
+ const reference = bus.connections.map((connection) =>
210
+ getCornerTargetTrack({
211
+ ...common,
212
+ bus: { ...bus, preferredExit },
213
+ connection,
214
+ cornerExitLaneOffset: 0,
215
+ windingOrderIndex: 0,
216
+ }),
217
+ )
218
+ const mean =
219
+ reference.reduce((sum, value) => sum + value, 0) / reference.length
220
+ const tracks = reference.map((value) => value - mean + track)
221
+ if (
222
+ tracks.some(
223
+ (value) =>
224
+ value <= lower + params.traceWidth / 2 ||
225
+ value >= upper - params.traceWidth / 2 ||
226
+ (side === "minimum" && value >= middle) ||
227
+ (side === "maximum" && value <= middle),
228
+ )
229
+ )
230
+ continue
231
+ const terminals = bus.connections.map((connection, index) => {
232
+ const viaPoint = params.fixedViaPointsByConnectionIndex?.get(
233
+ connection.connectionIndex,
234
+ )
235
+ const along = tracks[index]!
236
+ const exitPoint =
237
+ bus.exitEdge === "left"
238
+ ? { x: boundary.minX, y: along }
239
+ : bus.exitEdge === "right"
240
+ ? { x: boundary.maxX, y: along }
241
+ : bus.exitEdge === "bottom"
242
+ ? { x: along, y: boundary.minY }
243
+ : { x: along, y: boundary.maxY }
244
+ return { connection, viaPoint: viaPoint!, exitPoint }
245
+ })
246
+ if (terminals.some((terminal) => !terminal.viaPoint)) return []
247
+ attempts++
248
+ const steps = routeViaMinimalWindingAlternativesSteps(
249
+ {
250
+ ...common,
251
+ terminals,
252
+ gridStep: pitch / 2,
253
+ alignGridToPads: true,
254
+ maximumRouteOrderAttempts: bus.connections.length === 1 ? 3 : 16,
255
+ adaptiveRouteOrder: true,
256
+ },
257
+ 1,
258
+ false,
259
+ )
260
+ let result = steps.next()
261
+ while (!result.done) {
262
+ yield {
263
+ phase: "via-minimal-winding",
264
+ busId: bus.busId,
265
+ targetLayer: params.targetLayer,
266
+ winding: result.value,
267
+ }
268
+ result = steps.next()
269
+ }
270
+ alternatives.push(...result.value)
271
+ }
272
+ return alternatives
273
+ }
274
+ function guidance(bus: PreparedBus): (FanoutBorderTarget | undefined)[] {
275
+ if (getCornerBandSide(bus.exitEdge, bus.preferredExit))
276
+ return [bus.preferredExit]
277
+ switch (bus.exitEdge) {
278
+ case "right":
279
+ return [bus.preferredExit, "top-right", "bottom-right"]
280
+ case "left":
281
+ return [bus.preferredExit, "top-left", "bottom-left"]
282
+ case "top":
283
+ return [bus.preferredExit, "top-left", "top-right"]
284
+ case "bottom":
285
+ return [bus.preferredExit, "bottom-left", "bottom-right"]
286
+ default:
287
+ return [bus.preferredExit]
288
+ }
289
+ }
290
+ function offsets(
291
+ bus: PreparedBus,
292
+ accepted: FanoutRoutePlan[],
293
+ preferredExit: FanoutBorderTarget | undefined,
294
+ ) {
295
+ const side = getCornerBandSide(bus.exitEdge, preferredExit)
296
+ if (!side || params.buses.length > 1) return [0]
297
+ const horizontal = bus.exitEdge === "left" || bus.exitEdge === "right"
298
+ const lower = horizontal ? bus.sharedBoundary.minY : bus.sharedBoundary.minX
299
+ const upper = horizontal ? bus.sharedBoundary.maxY : bus.sharedBoundary.maxX
300
+ const middle = (lower + upper) / 2
301
+ const cornerExitLaneOffset = accepted.filter(
302
+ (plan) => plan.exitEdge === bus.exitEdge && plan.cornerBandSide === side,
303
+ ).length
304
+ const tracks = bus.connections.map((connection) =>
305
+ getCornerTargetTrack({
306
+ ...params,
307
+ bus: { ...bus, preferredExit },
308
+ connection,
309
+ cornerExitLaneOffset,
310
+ windingOrderIndex: 0,
311
+ }),
312
+ )
313
+ const sign = side === "minimum" ? -1 : 1
314
+ const padPitch = Math.min(bus.pitchX, bus.pitchY)
315
+ const mean = tracks.reduce((sum, value) => sum + value, 0) / tracks.length
316
+ const edgeTrack = side === "minimum" ? lower + pitch : upper - pitch
317
+ const candidates = [
318
+ 0,
319
+ sign * 2 * pitch,
320
+ -sign * 2 * pitch,
321
+ sign * padPitch,
322
+ sign * 2 * padPitch,
323
+ sign * 3 * padPitch,
324
+ sign * 4 * padPitch,
325
+ edgeTrack - mean,
326
+ ]
327
+ return candidates.filter(
328
+ (offset, i) =>
329
+ candidates.indexOf(offset) === i &&
330
+ tracks.every(
331
+ (track) =>
332
+ track + offset > lower + params.traceWidth / 2 &&
333
+ track + offset < upper - params.traceWidth / 2 &&
334
+ (side === "minimum"
335
+ ? track + offset < middle
336
+ : track + offset > middle),
337
+ ),
338
+ )
339
+ }
340
+ function* search(
341
+ remaining: readonly PreparedBus[],
342
+ accepted: FanoutRoutePlan[],
343
+ ): Generator<RouteBusAlternativesProgress, FanoutRoutePlan[] | null, void> {
344
+ if (!remaining.length) return accepted
345
+ if (attempts >= maximumAttempts) return null
346
+ for (const bus of remaining) {
347
+ for (const preferredExit of guidance(bus)) {
348
+ for (const offset of offsets(bus, accepted, preferredExit)) {
349
+ const alternatives = yield* route(
350
+ bus,
351
+ accepted,
352
+ preferredExit,
353
+ offset,
354
+ )
355
+ if (!alternatives.length) continue
356
+ const result = yield* search(
357
+ remaining.filter((candidate) => candidate !== bus),
358
+ [...accepted, ...alternatives[0]!],
359
+ )
360
+ if (result) return result
361
+ if (attempts >= maximumAttempts) return null
362
+ }
363
+ }
364
+ }
365
+ // A singleton has no intra-bus track ordering to preserve. If the ordinary
366
+ // bands fail, use free intervals on its original edge and declared half-band.
367
+ for (const bus of remaining) {
368
+ if (bus.connections.length !== 1 || !bus.exitEdge) continue
369
+ const connection = bus.connections[0]!
370
+ const viaPoint = params.fixedViaPointsByConnectionIndex?.get(
371
+ connection.connectionIndex,
372
+ )
373
+ if (!viaPoint) continue
374
+ const reservedVias = params.reservedVias?.filter(
375
+ (reserved) => reserved.connectionName !== connection.connection.name,
376
+ )
377
+ const tracks = getFreeBoundaryTracks({
378
+ ...params,
379
+ bus,
380
+ acceptedPlans: accepted,
381
+ reservedVias,
382
+ })
383
+ for (const track of tracks) {
384
+ if (attempts++ >= maximumAttempts) return null
385
+ const boundary = bus.sharedBoundary
386
+ const exitPoint =
387
+ bus.exitEdge === "left"
388
+ ? { x: boundary.minX, y: track }
389
+ : bus.exitEdge === "right"
390
+ ? { x: boundary.maxX, y: track }
391
+ : bus.exitEdge === "bottom"
392
+ ? { x: track, y: boundary.minY }
393
+ : { x: track, y: boundary.maxY }
394
+ const steps = routeViaMinimalWindingAlternativesSteps(
395
+ {
396
+ ...params,
397
+ bus,
398
+ acceptedPlans: accepted,
399
+ reservedVias,
400
+ terminals: [{ connection, viaPoint, exitPoint }],
401
+ gridStep: pitch / 2,
402
+ alignGridToPads: true,
403
+ maximumRouteOrderAttempts: 3,
404
+ },
405
+ 1,
406
+ false,
407
+ )
408
+ let result = steps.next()
409
+ while (!result.done) {
410
+ yield {
411
+ phase: "via-minimal-winding",
412
+ busId: bus.busId,
413
+ targetLayer: params.targetLayer,
414
+ winding: result.value,
415
+ }
416
+ result = steps.next()
417
+ }
418
+ if (!result.value.length) continue
419
+ const complete = yield* search(
420
+ remaining.filter((candidate) => candidate !== bus),
421
+ [...accepted, ...result.value[0]!],
422
+ )
423
+ if (complete) return complete
424
+ }
425
+ }
426
+ return null
427
+ }
428
+ const result = yield* search(ordered, [...params.acceptedPlans])
429
+ return result && restore(result.slice(params.acceptedPlans.length))
430
+ }
@@ -0,0 +1,243 @@
1
+ import { routeAdaptiveLeftCrossbarBusSteps } from "./route-adaptive-left-crossbar-bus"
2
+ import { routeOppositeBottomCrossbarBusSteps } from "./route-opposite-bottom-crossbar-bus"
3
+ import { reflectFanoutX } from "./reflect-fanout-x"
4
+ import { routeReservedNarrowBusesSteps } from "./route-reserved-narrow-buses"
5
+ import { routeBottomCrossbarBusSteps } from "./route-bottom-crossbar-bus"
6
+ import { routeLeftCrossbarBusSteps } from "./route-left-crossbar-bus"
7
+ import type { Bounds, FanoutRoutePlan, PreparedBus } from "./types"
8
+ import type { PeripheralSourceEscape } from "./route-peripheral-source-escapes"
9
+ import {
10
+ fanoutPlansAreClear,
11
+ routeBusAlternativesSteps,
12
+ type RouteBusAlternativesProgress,
13
+ type RouteBusParams,
14
+ } from "./route-bus"
15
+
16
+ export interface RouteReservedSourceBusesParams
17
+ extends Omit<RouteBusParams, "bus" | "targetLayer" | "acceptedPlans"> {
18
+ buses: readonly PreparedBus[]
19
+ sourceEscapes: readonly PeripheralSourceEscape[]
20
+ initialPlans: readonly FanoutRoutePlan[]
21
+ targetLayerByBusId?: ReadonlyMap<string, string>
22
+ sourceBoundary?: Bounds
23
+ }
24
+
25
+ /** Finish buses while preserving every already matched source escape and via. */
26
+ export function* routeReservedSourceBusesSteps(
27
+ params: RouteReservedSourceBusesParams,
28
+ ): Generator<RouteBusAlternativesProgress, FanoutRoutePlan[] | null, void> {
29
+ if (
30
+ params.buses.some((bus) => bus.termination.type === "boundary") &&
31
+ params.buses.every(
32
+ (bus) => bus.termination.type === "plane" || bus.exitEdge === "left",
33
+ )
34
+ ) {
35
+ const mirrored = reflectFanoutX(params)
36
+ const plans = yield* routeReservedSourceBusesSteps(mirrored)
37
+ if (!plans) return null
38
+ const restored = reflectFanoutX(plans)
39
+ const originalConnections = new Map(
40
+ params.buses.flatMap((bus) =>
41
+ bus.connections.map(
42
+ (connection) => [connection.connectionIndex, connection] as const,
43
+ ),
44
+ ),
45
+ )
46
+ return restored.map((plan) => ({
47
+ ...plan,
48
+ sourceObstacle: originalConnections.get(plan.connectionIndex)!
49
+ .sourceObstacle,
50
+ }))
51
+ }
52
+ const { buses, sourceEscapes, initialPlans } = params
53
+ const byIndex = new Map(
54
+ sourceEscapes.map((source) => [source.connectionIndex, source]),
55
+ )
56
+ const committedIndices = new Set(
57
+ initialPlans.map((plan) => plan.connectionIndex),
58
+ )
59
+ const pending = buses.filter((bus) =>
60
+ bus.connections.some(
61
+ (connection) => !committedIndices.has(connection.connectionIndex),
62
+ ),
63
+ )
64
+ if (
65
+ pending.some((bus) =>
66
+ bus.connections.some((connection) =>
67
+ committedIndices.has(connection.connectionIndex),
68
+ ),
69
+ )
70
+ ) {
71
+ throw new Error(
72
+ "FanoutSolver: a reserved-source continuation cannot start with a partially committed bus",
73
+ )
74
+ }
75
+ for (const bus of pending)
76
+ for (const connection of bus.connections) {
77
+ if (!byIndex.has(connection.connectionIndex))
78
+ throw new Error(
79
+ `FanoutSolver: missing reserved source escape for ${connection.connection.name}`,
80
+ )
81
+ }
82
+ const fixedViaPointsByConnectionIndex = new Map(
83
+ sourceEscapes.map((source) => [source.connectionIndex, source.via.center]),
84
+ )
85
+ const sourceEscapePaths = new Map(
86
+ sourceEscapes.map((source) => [
87
+ source.connectionIndex,
88
+ [
89
+ source.segments[0]!.start,
90
+ ...source.segments.map((segment) => segment.end),
91
+ ],
92
+ ]),
93
+ )
94
+ const accepted = [...initialPlans]
95
+ const occupiedLayerCount = (bus: PreparedBus) => {
96
+ const layer =
97
+ params.targetLayerByBusId?.get(bus.busId) ??
98
+ (bus.allowedLayers ?? params.layerNames).find(
99
+ (layer) => layer !== bus.connections[0]!.sourceLayer,
100
+ )
101
+ return initialPlans.filter((plan) => plan.targetLayer === layer).length
102
+ }
103
+ const boundaryBuses = pending
104
+ .filter((bus) => bus.termination.type === "boundary")
105
+ .sort(
106
+ (a, b) =>
107
+ b.connections.length - a.connections.length ||
108
+ occupiedLayerCount(a) - occupiedLayerCount(b),
109
+ )
110
+ const groupedNarrow = new Set<string>()
111
+ for (const bus of [
112
+ ...boundaryBuses,
113
+ ...pending.filter((bus) => bus.termination.type === "plane"),
114
+ ]) {
115
+ if (groupedNarrow.has(bus.busId)) continue
116
+ if (
117
+ bus.termination.type === "boundary" &&
118
+ bus.connections.length <= 2 &&
119
+ params.targetLayerByBusId
120
+ ) {
121
+ const targetLayer = params.targetLayerByBusId.get(bus.busId)
122
+ if (!targetLayer) return null
123
+ const group = boundaryBuses.filter(
124
+ (candidate) =>
125
+ candidate.connections.length <= 2 &&
126
+ params.targetLayerByBusId!.get(candidate.busId) === targetLayer,
127
+ )
128
+ const plans = yield* routeReservedNarrowBusesSteps({
129
+ ...params,
130
+ buses: group,
131
+ targetLayer,
132
+ acceptedPlans: accepted,
133
+ fixedViaPointsByConnectionIndex,
134
+ sourceEscapePaths,
135
+ reservedVias: sourceEscapes.map((source) => ({
136
+ connectionName: source.connectionName,
137
+ via: source.via,
138
+ })),
139
+ })
140
+ if (!plans) return null
141
+ accepted.push(...plans)
142
+ for (const candidate of group) groupedNarrow.add(candidate.busId)
143
+ continue
144
+ }
145
+ const ownIndices = new Set(
146
+ bus.connections.map((connection) => connection.connectionIndex),
147
+ )
148
+ const reservedVias = sourceEscapes
149
+ .filter((source) => !ownIndices.has(source.connectionIndex))
150
+ .map((source) => ({
151
+ connectionName: source.connectionName,
152
+ via: source.via,
153
+ ...(source.segments.length === 1
154
+ ? { sourceEscapeSegment: source.segments[0] }
155
+ : {}),
156
+ }))
157
+ const assignedLayer = params.targetLayerByBusId?.get(bus.busId)
158
+ const layers = assignedLayer
159
+ ? [assignedLayer]
160
+ : bus.termination.type === "plane"
161
+ ? [bus.termination.layer]
162
+ : (
163
+ bus.routableEscapeLayers ??
164
+ bus.allowedLayers ??
165
+ params.layerNames
166
+ ).filter((layer) => layer !== bus.connections[0]!.sourceLayer)
167
+ let routed: FanoutRoutePlan[] | undefined
168
+ for (const targetLayer of layers) {
169
+ const routeParams = {
170
+ ...params,
171
+ bus,
172
+ targetLayer,
173
+ acceptedPlans: accepted,
174
+ fixedViaPointsByConnectionIndex,
175
+ sourceEscapePaths,
176
+ reservedVias,
177
+ }
178
+ if (
179
+ params.sourceBoundary &&
180
+ bus.termination.type === "boundary" &&
181
+ bus.connections.length > 2
182
+ ) {
183
+ for (const routeCrossbar of [
184
+ routeBottomCrossbarBusSteps,
185
+ routeLeftCrossbarBusSteps,
186
+ routeAdaptiveLeftCrossbarBusSteps,
187
+ routeOppositeBottomCrossbarBusSteps,
188
+ ]) {
189
+ const steps = routeCrossbar({
190
+ ...routeParams,
191
+ sourceBoundary: params.sourceBoundary,
192
+ })
193
+ let step = steps.next()
194
+ while (!step.done) {
195
+ yield {
196
+ phase: "via-minimal-winding",
197
+ busId: bus.busId,
198
+ targetLayer,
199
+ winding: step.value,
200
+ }
201
+ step = steps.next()
202
+ }
203
+ if (step.value) {
204
+ routed = step.value
205
+ break
206
+ }
207
+ }
208
+ if (routed) break
209
+ }
210
+ const alternatives = yield* routeBusAlternativesSteps(
211
+ {
212
+ ...params,
213
+ bus,
214
+ targetLayer,
215
+ acceptedPlans: accepted,
216
+ fixedViaPointsByConnectionIndex,
217
+ sourceEscapePaths,
218
+ reservedVias,
219
+ viaMinimalOnly: true,
220
+ adaptiveWindingRouteOrder: true,
221
+ alignWindingGridToPads: true,
222
+ windingGridStep: (params.traceWidth + params.clearance) / 2,
223
+ fixedViaFallbackRouteOrderAttempts: 32,
224
+ },
225
+ 1,
226
+ false,
227
+ )
228
+ if (alternatives.length) {
229
+ routed = alternatives[0]
230
+ break
231
+ }
232
+ }
233
+ if (!routed) return null
234
+ accepted.push(...routed)
235
+ }
236
+ const sharedBoundary = buses[0]?.sharedBoundary
237
+ if (
238
+ !sharedBoundary ||
239
+ !fanoutPlansAreClear({ ...params, plans: accepted, sharedBoundary })
240
+ )
241
+ return null
242
+ return accepted
243
+ }