@tscircuit/fanout-solver 0.0.64 → 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[]
@@ -467,7 +471,7 @@ export function getBoundaryTargetTrack(params: {
467
471
  return Math.max(boundaryMinimum, Math.min(boundaryMaximum, requestedTrack))
468
472
  }
469
473
 
470
- function getCornerTargetTrack(params: {
474
+ export function getCornerTargetTrack(params: {
471
475
  bus: PreparedBus
472
476
  connection: PreparedConnection
473
477
  cornerExitLaneOffset: number
@@ -1332,6 +1336,7 @@ function buildPlan(params: {
1332
1336
  })
1333
1337
  }
1334
1338
 
1339
+ const sourceEscapeSegmentCount = segments.length
1335
1340
  let via: FanoutRoutePlan["via"]
1336
1341
  if (targetLayer !== preparedConnection.sourceLayer) {
1337
1342
  const spanLayers = getViaSpanLayers({
@@ -1512,6 +1517,7 @@ function buildPlan(params: {
1512
1517
  route,
1513
1518
  },
1514
1519
  segments,
1520
+ ...(sourceEscapeSegmentCount > 1 ? { sourceEscapeSegmentCount } : {}),
1515
1521
  via,
1516
1522
  ...(additionalVias.length > 0 ? { additionalVias } : {}),
1517
1523
  length: segments.reduce(
@@ -2224,7 +2230,15 @@ function routePlaneTerminatedBus(
2224
2230
  terminateAtVia: true,
2225
2231
  allowBlindAndBuriedVias,
2226
2232
  initialViaPoint: fixedViaPoint,
2227
- 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],
2228
2242
  })
2229
2243
  const endpointViaCandidates = getPlaneEndpointViaCandidates({
2230
2244
  preparedConnection,
@@ -3237,6 +3251,8 @@ export function* routeBusAlternativesSteps(
3237
3251
  softReservedVias,
3238
3252
  reserveTerminalExitPoints: terminalPattern.reserveTerminalExitPoints,
3239
3253
  gridStepDivisor,
3254
+ gridStep: params.windingGridStep,
3255
+ sourceEscapePaths: params.sourceEscapePaths,
3240
3256
  preferTargetDirectedLaneBias:
3241
3257
  terminalPattern.preferTargetDirectedLaneBias,
3242
3258
  },