@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,268 @@
1
+ import {
2
+ distance,
3
+ distancePointToObstacle,
4
+ distancePointToSegment,
5
+ distanceSegmentToObstacle,
6
+ segmentsAreClear,
7
+ } from "./geometry"
8
+ import { matchComponentDogboneViaSites } from "./match-component-dogbone-via-sites"
9
+ import type { PeripheralSourceEscape } from "./route-peripheral-source-escapes"
10
+ import {
11
+ routeSplitPerimeterBusSteps,
12
+ type SplitBusParams,
13
+ } from "./route-split-perimeter-bus"
14
+ import type { SplitPerimeterSources } from "./route-split-perimeter-source-escapes"
15
+ import type { RouteViaMinimalWindingProgress } from "./route-via-minimal-winding"
16
+ import type { FanoutRoutePlan, PreparedBus } from "./types"
17
+
18
+ const EPSILON = 1e-7
19
+ export type ShallowSplitBusParams = SplitBusParams & {
20
+ buses: readonly PreparedBus[]
21
+ }
22
+ export type ShallowSplitBusResult = SplitPerimeterSources & {
23
+ plans: FanoutRoutePlan[]
24
+ }
25
+
26
+ function sourcesAreClear(
27
+ params: ShallowSplitBusParams,
28
+ sources: readonly PeripheralSourceEscape[],
29
+ ): boolean {
30
+ const connections = new Map(
31
+ params.buses
32
+ .flatMap((bus) => bus.connections)
33
+ .map((connection) => [connection.connectionIndex, connection]),
34
+ )
35
+ const boundary = params.bus.sharedBoundary
36
+ for (const source of sources) {
37
+ const own = connections.get(source.connectionIndex)!
38
+ const radius = source.via.diameter / 2
39
+ for (const obstacle of params.srj.obstacles) {
40
+ if (
41
+ obstacle.layers.some((layer) =>
42
+ source.via.spanLayers.includes(layer),
43
+ ) &&
44
+ distancePointToObstacle(source.via.center, obstacle) <
45
+ radius + params.clearance - EPSILON
46
+ )
47
+ return false
48
+ if (
49
+ obstacle !== own.sourceObstacle &&
50
+ source.segments.some(
51
+ (segment) =>
52
+ obstacle.layers.includes(segment.layer) &&
53
+ distanceSegmentToObstacle(segment, obstacle) <
54
+ segment.width / 2 + params.clearance - EPSILON,
55
+ )
56
+ )
57
+ return false
58
+ }
59
+ for (const segment of source.segments) {
60
+ if (
61
+ [segment.start, segment.end].some(
62
+ (point) =>
63
+ point.x < boundary.minX - EPSILON ||
64
+ point.x > boundary.maxX + EPSILON ||
65
+ point.y < boundary.minY - EPSILON ||
66
+ point.y > boundary.maxY + EPSILON,
67
+ )
68
+ )
69
+ return false
70
+ }
71
+ for (const other of sources) {
72
+ if (source.connectionIndex === other.connectionIndex) continue
73
+ if (
74
+ distance(source.via.center, other.via.center) <
75
+ radius + other.via.diameter / 2 + params.clearance - EPSILON
76
+ )
77
+ return false
78
+ if (
79
+ source.segments.some(
80
+ (segment) =>
81
+ other.via.spanLayers.includes(segment.layer) &&
82
+ distancePointToSegment(
83
+ other.via.center,
84
+ segment.start,
85
+ segment.end,
86
+ ) <
87
+ other.via.diameter / 2 +
88
+ segment.width / 2 +
89
+ params.clearance -
90
+ EPSILON,
91
+ )
92
+ )
93
+ return false
94
+ if (
95
+ source.segments.some((segment) =>
96
+ other.segments.some(
97
+ (otherSegment) =>
98
+ !segmentsAreClear(segment, otherSegment, params.clearance),
99
+ ),
100
+ )
101
+ )
102
+ return false
103
+ }
104
+ }
105
+ return true
106
+ }
107
+
108
+ /** Shorten the lower perimeter while preserving its source-layer escape topology. */
109
+ export function* routeShallowSplitPerimeterBusSteps(
110
+ params: ShallowSplitBusParams,
111
+ ): Generator<
112
+ RouteViaMinimalWindingProgress,
113
+ ShallowSplitBusResult | null,
114
+ void
115
+ > {
116
+ const { bus, traceWidth, clearance, viaDiameter } = params
117
+ if (
118
+ bus.exitEdge !== "left" ||
119
+ bus.termination.type !== "boundary" ||
120
+ params.bottomRemoteConnectionIndices.size !== 1
121
+ )
122
+ return null
123
+ const bottomIndex = [...params.bottomRemoteConnectionIndices][0]!
124
+ const originalBottom = params.sourceEscapes.find(
125
+ (source) => source.connectionIndex === bottomIndex,
126
+ )
127
+ const last = originalBottom?.segments.at(-1)
128
+ if (
129
+ !last ||
130
+ Math.abs(last.start.x - last.end.x) > EPSILON ||
131
+ last.start.y <= last.end.y
132
+ )
133
+ return null
134
+ const padPitch = Math.min(bus.pitchX, bus.pitchY)
135
+ if (!Number.isFinite(padPitch) || padPitch <= 0) return null
136
+ const pitch = traceWidth + clearance
137
+ const nativeRows = bus.yCoordinates
138
+ .slice(1)
139
+ .map((y, index) => (y + bus.yCoordinates[index]!) / 2)
140
+ .filter((y) => y > last.end.y + EPSILON && y < last.start.y - EPSILON)
141
+ .sort((a, b) => a - b)
142
+ .slice(0, 4)
143
+ const fixedIndices = new Set([
144
+ ...bus.connections.map((connection) => connection.connectionIndex),
145
+ // A nonlocal source escape already commits its topology before its first
146
+ // via. Only ordinary one-segment dogbones may move around the new band.
147
+ ...params.sourceEscapes
148
+ .filter((source) => source.segments.length > 1)
149
+ .map((source) => source.connectionIndex),
150
+ ])
151
+ const movableBuses = params.buses
152
+ .map((owner) => ({
153
+ ...owner,
154
+ connections: owner.connections.filter(
155
+ (connection) => !fixedIndices.has(connection.connectionIndex),
156
+ ),
157
+ }))
158
+ .filter((owner) => owner.connections.length)
159
+ const pendingIndices = new Set(
160
+ movableBuses
161
+ .flatMap((owner) => owner.connections)
162
+ .map((connection) => connection.connectionIndex),
163
+ )
164
+ for (const y of nativeRows) {
165
+ yield {
166
+ phase: "route-connection",
167
+ routeOrderAttempt: 0,
168
+ connectionIndex: 0,
169
+ connectionCount: bus.connections.length,
170
+ connectionName: originalBottom!.connectionName,
171
+ searchBatch: 0,
172
+ expandedStateCount: 0,
173
+ connectionComplete: false,
174
+ }
175
+ const sources = params.sourceEscapes.map((source) => ({
176
+ ...source,
177
+ segments: source.segments.map((segment) => ({
178
+ ...segment,
179
+ start: { ...segment.start },
180
+ end: { ...segment.end },
181
+ })),
182
+ via: { ...source.via, center: { ...source.via.center } },
183
+ }))
184
+ const viaPoints = new Map(params.viaPointsByConnectionIndex)
185
+ const bottom = sources.find(
186
+ (source) => source.connectionIndex === bottomIndex,
187
+ )!
188
+ bottom.via.center = { x: last.end.x, y }
189
+ bottom.segments.at(-1)!.end = bottom.via.center
190
+ viaPoints.set(bottomIndex, bottom.via.center)
191
+ const fixed = sources.filter((source) =>
192
+ fixedIndices.has(source.connectionIndex),
193
+ )
194
+ const rematch = (plans: FanoutRoutePlan[] = []) => {
195
+ const matched = matchComponentDogboneViaSites(movableBuses, {
196
+ ...params,
197
+ additionalObstacles: params.srj.obstacles,
198
+ maximumSearchStates: 300_000,
199
+ preferredViaPointsByConnectionIndex: viaPoints,
200
+ blockingSegments: [
201
+ ...fixed.flatMap((source) =>
202
+ source.segments.map((segment) => ({
203
+ connectionIndex: source.connectionIndex,
204
+ segment,
205
+ })),
206
+ ),
207
+ ...plans.flatMap((plan) =>
208
+ plan.segments
209
+ .filter((segment) => segment.layer === params.targetLayer)
210
+ .map((segment) => ({
211
+ connectionIndex: plan.connectionIndex,
212
+ segment,
213
+ })),
214
+ ),
215
+ ],
216
+ blockingVias: fixed.map((source) => ({
217
+ connectionIndex: source.connectionIndex,
218
+ ...source.via,
219
+ })),
220
+ })
221
+ if (!matched) return false
222
+ for (const source of sources)
223
+ if (pendingIndices.has(source.connectionIndex)) {
224
+ const end = matched.get(source.connectionIndex)!
225
+ source.via = { ...source.via, center: end }
226
+ source.segments = [{ ...source.segments[0]!, end }]
227
+ viaPoints.set(source.connectionIndex, end)
228
+ }
229
+ return sourcesAreClear(params, sources)
230
+ }
231
+ if (!rematch()) continue
232
+ // Center the middle lower rail on the preceding native interstitial row.
233
+ const sourceBoundary = {
234
+ ...params.sourceBoundary,
235
+ minY:
236
+ y -
237
+ padPitch +
238
+ viaDiameter / 2 +
239
+ traceWidth / 2 +
240
+ clearance +
241
+ 1e-5 +
242
+ pitch,
243
+ }
244
+ const plans = yield* routeSplitPerimeterBusSteps({
245
+ ...params,
246
+ sourceEscapes: sources,
247
+ sourceBoundary,
248
+ viaPointsByConnectionIndex: viaPoints,
249
+ rematchPendingSources: {
250
+ connectionIndices: pendingIndices,
251
+ afterLowerStage: rematch,
252
+ },
253
+ })
254
+ if (plans && sourcesAreClear(params, sources))
255
+ return {
256
+ plans,
257
+ sourceEscapes: sources,
258
+ sourceBoundary: { ...params.sourceBoundary },
259
+ viaPointsByConnectionIndex: viaPoints,
260
+ remoteConnectionIndices: new Set(params.remoteConnectionIndices),
261
+ bottomRemoteConnectionIndices: new Set(
262
+ params.bottomRemoteConnectionIndices,
263
+ ),
264
+ lowerConnectionIndices: [...params.lowerConnectionIndices],
265
+ }
266
+ }
267
+ return null
268
+ }
@@ -0,0 +1,397 @@
1
+ import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter"
2
+ import {
3
+ distance,
4
+ distancePointToSegment,
5
+ distanceSegmentToSegment,
6
+ } from "./geometry"
7
+ import { fanoutPlansAreClear } from "./route-bus"
8
+ import {
9
+ buildViaMinimalWindingPlan,
10
+ routeViaMinimalWindingAlternativesSteps,
11
+ type RouteViaMinimalWindingParams,
12
+ type RouteViaMinimalWindingProgress,
13
+ type ViaMinimalWindingTerminal,
14
+ } from "./route-via-minimal-winding"
15
+ import {
16
+ chamferSplitPerimeter,
17
+ type SplitPerimeterSources,
18
+ } from "./route-split-perimeter-source-escapes"
19
+ import type { FanoutRoutePlan, Point2D, PreparedBus } from "./types"
20
+
21
+ export type SplitBusParams = SplitPerimeterSources & {
22
+ srj: SimpleRouteJson
23
+ bus: PreparedBus
24
+ targetLayer: string
25
+ layerNames: string[]
26
+ traceWidth: number
27
+ clearance: number
28
+ viaDiameter: number
29
+ viaHoleDiameter: number
30
+ /** Rematch pending sources around a lower band before routing its upper lanes. */
31
+ rematchPendingSources?: {
32
+ connectionIndices: ReadonlySet<number>
33
+ afterLowerStage: (plans: FanoutRoutePlan[]) => boolean
34
+ }
35
+ }
36
+
37
+ /** Promote a blocked terminal in a bounded search over complete lane orders. */
38
+ function* routeOrderedStage(
39
+ params: RouteViaMinimalWindingParams,
40
+ ): Generator<RouteViaMinimalWindingProgress, FanoutRoutePlan[] | null, void> {
41
+ type Attempt = { order: number[]; bias: -1 | 0 | 1; completed: number }
42
+ const seen = new Set<string>(),
43
+ prefixes = new Set<string>(),
44
+ queue: Attempt[] = []
45
+ let evaluations = 0,
46
+ result: FanoutRoutePlan[] | null = null
47
+ function* evaluate(
48
+ order: number[],
49
+ bias: -1 | 0 | 1,
50
+ ): Generator<RouteViaMinimalWindingProgress, void, void> {
51
+ const key = order.join(",") + ":" + bias
52
+ if (seen.has(key) || evaluations >= 256) return
53
+ seen.add(key)
54
+ evaluations++
55
+ let completed = 0
56
+ const gen = routeViaMinimalWindingAlternativesSteps(
57
+ {
58
+ ...params,
59
+ routeOrder: order,
60
+ laneBias: bias,
61
+ maximumRouteOrderAttempts: 1,
62
+ },
63
+ 1,
64
+ false,
65
+ )
66
+ let r = gen.next()
67
+ while (!r.done) {
68
+ completed = Math.max(completed, r.value.connectionIndex)
69
+ yield r.value
70
+ r = gen.next()
71
+ }
72
+ if (r.value.length) {
73
+ result = r.value[0]!
74
+ return
75
+ }
76
+ const prefix = order.slice(0, completed + 1).join(",") + ":" + bias
77
+ if (!prefixes.has(prefix)) {
78
+ prefixes.add(prefix)
79
+ queue.push({ order, bias, completed })
80
+ }
81
+ }
82
+ const indices = params.terminals.map((_, i) => i)
83
+ const target = indices.toSorted(
84
+ (a, b) =>
85
+ params.terminals[a]!.exitPoint.x - params.terminals[b]!.exitPoint.x,
86
+ )
87
+ for (const bias of [0, 1, -1] as const) {
88
+ yield* evaluate(target, bias)
89
+ if (result) return result
90
+ yield* evaluate(target.toReversed(), bias)
91
+ if (result) return result
92
+ }
93
+ while (queue.length && evaluations < 256) {
94
+ queue.sort((a, b) => b.completed - a.completed)
95
+ const { order, bias, completed } = queue.shift()!
96
+ if (queue.length > 256) queue.length = 256
97
+ for (let at = completed - 1; at >= 0; at--) {
98
+ const moved = [...order],
99
+ [failed] = moved.splice(completed, 1)
100
+ moved.splice(at, 0, failed!)
101
+ yield* evaluate(moved, bias)
102
+ if (result) return result
103
+ const swapped = [...order]
104
+ ;[swapped[completed], swapped[at]] = [swapped[at]!, swapped[completed]!]
105
+ yield* evaluate(swapped, bias)
106
+ if (result) return result
107
+ }
108
+ }
109
+ return null
110
+ }
111
+
112
+ /** Route a lower separator and an ordered upper bundle around a source field. */
113
+ export function* routeSplitPerimeterBusSteps(
114
+ params: SplitBusParams,
115
+ ): Generator<RouteViaMinimalWindingProgress, FanoutRoutePlan[] | null, void> {
116
+ const {
117
+ bus,
118
+ srj,
119
+ sourceBoundary,
120
+ sourceEscapes,
121
+ remoteConnectionIndices,
122
+ bottomRemoteConnectionIndices,
123
+ lowerConnectionIndices,
124
+ targetLayer,
125
+ traceWidth: w,
126
+ clearance: c,
127
+ } = params
128
+ if (
129
+ bus.exitEdge !== "left" ||
130
+ bus.termination.type !== "boundary" ||
131
+ lowerConnectionIndices.length !== 3
132
+ )
133
+ return null
134
+ const padPitch = Math.min(bus.pitchX, bus.pitchY),
135
+ pitch = w + c,
136
+ byIndex = new Map(sourceEscapes.map((p) => [p.connectionIndex, p]))
137
+ const terminals: ViaMinimalWindingTerminal[] = bus.connections
138
+ .map((connection) => ({
139
+ connection,
140
+ viaPoint: byIndex.get(connection.connectionIndex)!.via.center,
141
+ exitPoint: {
142
+ x: bus.sharedBoundary.minX,
143
+ y: (connection.exitTargetPoint ?? connection.targetPoint).y,
144
+ },
145
+ }))
146
+ .sort((a, b) => a.exitPoint.y - b.exitPoint.y)
147
+ const byTerminal = new Map(
148
+ terminals.map((t) => [t.connection.connectionIndex, t]),
149
+ )
150
+ const lower = lowerConnectionIndices.map((i) => byTerminal.get(i)!),
151
+ lowerIds = new Set(lowerConnectionIndices)
152
+ const upper = terminals.filter(
153
+ (t) =>
154
+ !lowerIds.has(t.connection.connectionIndex) &&
155
+ !remoteConnectionIndices.has(t.connection.connectionIndex),
156
+ )
157
+ if (upper.length < 2 || lower.some((t) => !t)) return null
158
+ const sourcePoints = new Map(
159
+ sourceEscapes.map((p) => [
160
+ p.connectionIndex,
161
+ [p.segments[0]!.start, ...p.segments.map((s) => s.end)],
162
+ ]),
163
+ )
164
+ const build = (terminal: ViaMinimalWindingTerminal, points: Point2D[]) =>
165
+ buildViaMinimalWindingPlan({
166
+ ...params,
167
+ bus,
168
+ terminal,
169
+ targetLayerPoints: points,
170
+ sourceEscapePoints: sourcePoints.get(terminal.connection.connectionIndex),
171
+ allowBlindAndBuriedVias: false,
172
+ })
173
+ // A shifted exit can lie below the source field. Put every lower rail
174
+ // below its own endpoint before nesting the neighboring turns around it.
175
+ const lowerRoof = Math.min(
176
+ sourceBoundary.minY - params.viaDiameter / 2 - w / 2 - c - 1e-5,
177
+ ...lower.map(
178
+ (terminal, rank) =>
179
+ terminal.exitPoint.y + (lower.length - 1 - rank) * pitch - pitch - w,
180
+ ),
181
+ )
182
+ const outer = (
183
+ terminal: ViaMinimalWindingTerminal,
184
+ points: Point2D[],
185
+ up: boolean,
186
+ ) => {
187
+ const j = (up ? upper : lower).findIndex(
188
+ (t) =>
189
+ t.connection.connectionIndex === terminal.connection.connectionIndex,
190
+ )
191
+ const port = points.at(-1)!,
192
+ roof = up
193
+ ? sourceBoundary.maxY + padPitch - pitch / 2 + j * pitch
194
+ : lowerRoof - (lower.length - 1 - j) * pitch
195
+ const column =
196
+ sourceBoundary.minX -
197
+ params.viaDiameter / 2 -
198
+ w / 2 -
199
+ c -
200
+ 1e-5 -
201
+ (up ? j : lower.length - 1 - j) * pitch
202
+ const extra = chamferSplitPerimeter(
203
+ [
204
+ port,
205
+ { x: port.x, y: roof },
206
+ { x: column, y: roof },
207
+ { x: column, y: terminal.exitPoint.y },
208
+ terminal.exitPoint,
209
+ ],
210
+ w,
211
+ )
212
+ return build(terminal, [...points, ...extra.slice(1)])
213
+ }
214
+ const remote = terminals
215
+ .filter((t) => remoteConnectionIndices.has(t.connection.connectionIndex))
216
+ .map((t) =>
217
+ bottomRemoteConnectionIndices.has(t.connection.connectionIndex)
218
+ ? outer(t, [t.viaPoint], false)
219
+ : build(t, [t.viaPoint, t.exitPoint]),
220
+ )
221
+ let deferPendingViaReservations = !!params.rematchPendingSources
222
+ const make = (
223
+ ts: ViaMinimalWindingTerminal[],
224
+ acceptedPlans: FanoutRoutePlan[],
225
+ up: boolean,
226
+ ports: Point2D[],
227
+ fullBoundary = false,
228
+ ): RouteViaMinimalWindingParams => {
229
+ const ids = new Set(ts.map((t) => t.connection.connectionIndex)),
230
+ stageBus: PreparedBus = {
231
+ ...bus,
232
+ connections: ts.map((t) => t.connection),
233
+ exitEdge: up ? "top" : "bottom",
234
+ direction: up ? "up" : "down",
235
+ preferredExit: undefined,
236
+ sharedBoundary: fullBoundary
237
+ ? bus.sharedBoundary
238
+ : {
239
+ ...bus.sharedBoundary,
240
+ maxY: sourceBoundary.maxY,
241
+ ...(!up ? { minY: sourceBoundary.minY } : {}),
242
+ },
243
+ }
244
+ return {
245
+ ...params,
246
+ bus: stageBus,
247
+ terminals: ts.map((t, i) => ({ ...t, exitPoint: ports[i]! })),
248
+ acceptedPlans,
249
+ reservedVias: sourceEscapes
250
+ .filter(
251
+ (p) =>
252
+ !ids.has(p.connectionIndex) &&
253
+ (!deferPendingViaReservations ||
254
+ !params.rematchPendingSources?.connectionIndices.has(
255
+ p.connectionIndex,
256
+ )),
257
+ )
258
+ .map((p) => ({ connectionName: p.connectionName, via: p.via })),
259
+ sourceEscapePaths: sourcePoints,
260
+ allowBlindAndBuriedVias: false,
261
+ allowSameNetMerges: false,
262
+ gridStepDivisor: 2,
263
+ gridStep: pitch / 2,
264
+ alignGridToPads: true,
265
+ reserveTerminalExitPoints: true,
266
+ }
267
+ }
268
+ const outerLower = lower[0]!,
269
+ innerLower = lower.at(-1)!,
270
+ firstUpper = upper[0]!,
271
+ lowerSource = innerLower.connection.sourcePoint,
272
+ column = lowerSource.x + padPitch
273
+ // The separator goes below the first upper source, then between that source
274
+ // group and the inner lower source before descending to the outer lower lane.
275
+ const guides = [
276
+ {
277
+ x: firstUpper.connection.sourcePoint.x,
278
+ y: firstUpper.connection.sourcePoint.y - padPitch,
279
+ },
280
+ { x: lowerSource.x - padPitch, y: lowerSource.y },
281
+ { x: column, y: lowerSource.y },
282
+ ]
283
+ let separatorPoints: Point2D[] = [outerLower.viaPoint]
284
+ for (const goal of guides) {
285
+ const temporary = { ...outerLower, viaPoint: separatorPoints.at(-1)! },
286
+ stage = make([temporary], remote, true, [goal], true)
287
+ stage.bus = { ...stage.bus, exitEdge: "right", direction: "right" }
288
+ stage.sourceEscapePaths = undefined
289
+ const routed = yield* routeOrderedStage(stage)
290
+ if (!routed) return null
291
+ separatorPoints.push(
292
+ ...routed[0]!.segments
293
+ .filter((s) => s.layer === targetLayer)
294
+ .map((s) => s.end),
295
+ )
296
+ }
297
+ separatorPoints = chamferSplitPerimeter(
298
+ [...separatorPoints, { x: column, y: sourceBoundary.minY }],
299
+ w,
300
+ )
301
+ const separator = outer(outerLower, separatorPoints, false)
302
+ const bottomVia = sourceEscapes.find((p) =>
303
+ bottomRemoteConnectionIndices.has(p.connectionIndex),
304
+ )!.via.center
305
+ const bottomPort = { x: bottomVia.x - padPitch, y: sourceBoundary.minY }
306
+ deferPendingViaReservations = false
307
+ const innerResult = yield* routeOrderedStage(
308
+ make([innerLower], [...remote, separator], false, [bottomPort]),
309
+ )
310
+ if (!innerResult) return null
311
+ const ips = innerResult[0]!.segments.filter((s) => s.layer === targetLayer),
312
+ inner = outer(innerLower, [ips[0]!.start, ...ips.map((s) => s.end)], false)
313
+ if (params.rematchPendingSources) {
314
+ if (
315
+ !params.rematchPendingSources.afterLowerStage([
316
+ ...remote,
317
+ separator,
318
+ inner,
319
+ ])
320
+ )
321
+ return null
322
+ for (const source of sourceEscapes) {
323
+ sourcePoints.set(source.connectionIndex, [
324
+ source.segments[0]!.start,
325
+ ...source.segments.map((segment) => segment.end),
326
+ ])
327
+ }
328
+ }
329
+ const minPadX = Math.min(
330
+ ...bus.componentObstacles.map((o) => o.center.x - o.width / 2),
331
+ ),
332
+ far = minPadX + params.viaDiameter / 2
333
+ const near =
334
+ Math.max(...upper.map((t) => t.connection.sourcePoint.x)) -
335
+ padPitch / 2 -
336
+ w / 2
337
+ if (near - far < (upper.length - 1) * pitch) return null
338
+ const upperPorts = upper.map((_, i) => ({
339
+ x: far + ((near - far) * i) / (upper.length - 1),
340
+ y: sourceBoundary.maxY,
341
+ }))
342
+ const upperResult = yield* routeOrderedStage(
343
+ make(upper, [...remote, separator, inner], true, upperPorts),
344
+ )
345
+ if (!upperResult) return null
346
+ const tops = upperResult.map((p) => {
347
+ const ss = p.segments.filter((s) => s.layer === targetLayer),
348
+ last = ss.at(-1)!
349
+ if (last.end.y <= last.start.y) return null
350
+ return outer(
351
+ byTerminal.get(p.connectionIndex)!,
352
+ [ss[0]!.start, ...ss.map((s) => s.end)],
353
+ true,
354
+ )
355
+ })
356
+ if (tops.some((p) => !p)) return null
357
+ const plans = [...remote, separator, inner, ...(tops as FanoutRoutePlan[])]
358
+ if (plans.length !== bus.connections.length) return null
359
+ const lengths = plans.map((p) => p.length)
360
+ if (
361
+ bus.maxLengthSkew !== undefined &&
362
+ Math.max(...lengths) - Math.min(...lengths) > bus.maxLengthSkew + EPS
363
+ )
364
+ return null
365
+ if (
366
+ !fanoutPlansAreClear({
367
+ plans,
368
+ srj,
369
+ sharedBoundary: bus.sharedBoundary,
370
+ clearance: c,
371
+ allowBlindAndBuriedVias: false,
372
+ allowSameNetMerges: false,
373
+ })
374
+ )
375
+ return null
376
+ for (const p of plans)
377
+ for (const other of sourceEscapes) {
378
+ if (p.connectionIndex === other.connectionIndex) continue
379
+ for (const s of p.segments) {
380
+ if (
381
+ other.via.spanLayers.includes(s.layer) &&
382
+ distancePointToSegment(other.via.center, s.start, s.end) <
383
+ other.via.diameter / 2 + s.width / 2 + c - EPS
384
+ )
385
+ return null
386
+ for (const t of other.segments)
387
+ if (
388
+ s.layer === t.layer &&
389
+ distanceSegmentToSegment(s.start, s.end, t.start, t.end) <
390
+ (s.width + t.width) / 2 + c - EPS
391
+ )
392
+ return null
393
+ }
394
+ }
395
+ return plans
396
+ }
397
+ const EPS = 1e-7