@tscircuit/fanout-solver 0.0.10
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/LICENSE +21 -0
- package/README.md +366 -0
- package/lib/build-output.ts +140 -0
- package/lib/fanout-solver.ts +617 -0
- package/lib/geometry.ts +162 -0
- package/lib/index.ts +20 -0
- package/lib/layer-colors.ts +21 -0
- package/lib/layer-names.ts +137 -0
- package/lib/prepare-buses.ts +1016 -0
- package/lib/route-bus.ts +934 -0
- package/lib/route-single-layer-adaptive-exits.ts +1179 -0
- package/lib/route-single-layer-push-shove.ts +1233 -0
- package/lib/types.ts +212 -0
- package/package.json +56 -0
|
@@ -0,0 +1,1016 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConnectionPoint,
|
|
3
|
+
Obstacle,
|
|
4
|
+
SimpleRouteConnection,
|
|
5
|
+
SimpleRouteJson,
|
|
6
|
+
} from "@tscircuit/capacity-autorouter"
|
|
7
|
+
import { distance, pointIsInsideObstacle } from "./geometry"
|
|
8
|
+
import type {
|
|
9
|
+
Bounds,
|
|
10
|
+
FanoutAvailableCornerAndSideInput,
|
|
11
|
+
FanoutBorderTarget,
|
|
12
|
+
FanoutBusSpec,
|
|
13
|
+
FanoutBusTermination,
|
|
14
|
+
FanoutDirection,
|
|
15
|
+
FanoutSolverOptions,
|
|
16
|
+
PreparedBus,
|
|
17
|
+
PreparedConnection,
|
|
18
|
+
} from "./types"
|
|
19
|
+
|
|
20
|
+
interface AvailableBoundaryRegion {
|
|
21
|
+
direction: FanoutDirection
|
|
22
|
+
preferredExit: FanoutBorderTarget
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const FANOUT_BORDER_TARGETS = new Set<FanoutBorderTarget>([
|
|
26
|
+
"left",
|
|
27
|
+
"right",
|
|
28
|
+
"top",
|
|
29
|
+
"bottom",
|
|
30
|
+
"top-left",
|
|
31
|
+
"top-right",
|
|
32
|
+
"bottom-left",
|
|
33
|
+
"bottom-right",
|
|
34
|
+
])
|
|
35
|
+
|
|
36
|
+
const AVAILABLE_BOUNDARY_REGIONS: Readonly<
|
|
37
|
+
Record<FanoutAvailableCornerAndSideInput, AvailableBoundaryRegion>
|
|
38
|
+
> = {
|
|
39
|
+
top_left: {
|
|
40
|
+
direction: "up",
|
|
41
|
+
preferredExit: "top-left",
|
|
42
|
+
},
|
|
43
|
+
top_middle: {
|
|
44
|
+
direction: "up",
|
|
45
|
+
preferredExit: "top",
|
|
46
|
+
},
|
|
47
|
+
top_right: {
|
|
48
|
+
direction: "up",
|
|
49
|
+
preferredExit: "top-right",
|
|
50
|
+
},
|
|
51
|
+
right_top: {
|
|
52
|
+
direction: "right",
|
|
53
|
+
preferredExit: "top-right",
|
|
54
|
+
},
|
|
55
|
+
right_middle: {
|
|
56
|
+
direction: "right",
|
|
57
|
+
preferredExit: "right",
|
|
58
|
+
},
|
|
59
|
+
right_bottom: {
|
|
60
|
+
direction: "right",
|
|
61
|
+
preferredExit: "bottom-right",
|
|
62
|
+
},
|
|
63
|
+
bottom_right: {
|
|
64
|
+
direction: "down",
|
|
65
|
+
preferredExit: "bottom-right",
|
|
66
|
+
},
|
|
67
|
+
bottom_middle: {
|
|
68
|
+
direction: "down",
|
|
69
|
+
preferredExit: "bottom",
|
|
70
|
+
},
|
|
71
|
+
bottom_left: {
|
|
72
|
+
direction: "down",
|
|
73
|
+
preferredExit: "bottom-left",
|
|
74
|
+
},
|
|
75
|
+
left_bottom: {
|
|
76
|
+
direction: "left",
|
|
77
|
+
preferredExit: "bottom-left",
|
|
78
|
+
},
|
|
79
|
+
left_middle: {
|
|
80
|
+
direction: "left",
|
|
81
|
+
preferredExit: "left",
|
|
82
|
+
},
|
|
83
|
+
left_top: {
|
|
84
|
+
direction: "left",
|
|
85
|
+
preferredExit: "top-left",
|
|
86
|
+
},
|
|
87
|
+
top: {
|
|
88
|
+
direction: "up",
|
|
89
|
+
preferredExit: "top",
|
|
90
|
+
},
|
|
91
|
+
right: {
|
|
92
|
+
direction: "right",
|
|
93
|
+
preferredExit: "right",
|
|
94
|
+
},
|
|
95
|
+
bottom: {
|
|
96
|
+
direction: "down",
|
|
97
|
+
preferredExit: "bottom",
|
|
98
|
+
},
|
|
99
|
+
left: {
|
|
100
|
+
direction: "left",
|
|
101
|
+
preferredExit: "left",
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface ComponentGrid {
|
|
106
|
+
componentId: string
|
|
107
|
+
obstacles: Obstacle[]
|
|
108
|
+
xCoordinates: number[]
|
|
109
|
+
yCoordinates: number[]
|
|
110
|
+
pitchX: number
|
|
111
|
+
pitchY: number
|
|
112
|
+
bounds: Bounds
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function uniqueSorted(values: number[]): number[] {
|
|
116
|
+
const sortedValues = [...values].sort((a, b) => a - b)
|
|
117
|
+
const result: number[] = []
|
|
118
|
+
for (const value of sortedValues) {
|
|
119
|
+
if (
|
|
120
|
+
result.length === 0 ||
|
|
121
|
+
Math.abs(result[result.length - 1]! - value) > 1e-6
|
|
122
|
+
) {
|
|
123
|
+
result.push(value)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return result
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getPitch(coordinates: number[]): number {
|
|
130
|
+
let pitch = Number.POSITIVE_INFINITY
|
|
131
|
+
for (let index = 1; index < coordinates.length; index++) {
|
|
132
|
+
const difference = coordinates[index]! - coordinates[index - 1]!
|
|
133
|
+
if (difference > 1e-6) pitch = Math.min(pitch, difference)
|
|
134
|
+
}
|
|
135
|
+
return pitch
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getAlignedPitch(obstacles: Obstacle[], axis: "x" | "y"): number {
|
|
139
|
+
const perpendicularAxis = axis === "x" ? "y" : "x"
|
|
140
|
+
let pitch = Number.POSITIVE_INFINITY
|
|
141
|
+
for (let firstIndex = 0; firstIndex < obstacles.length; firstIndex++) {
|
|
142
|
+
const first = obstacles[firstIndex]!
|
|
143
|
+
for (
|
|
144
|
+
let secondIndex = firstIndex + 1;
|
|
145
|
+
secondIndex < obstacles.length;
|
|
146
|
+
secondIndex++
|
|
147
|
+
) {
|
|
148
|
+
const second = obstacles[secondIndex]!
|
|
149
|
+
if (
|
|
150
|
+
Math.abs(
|
|
151
|
+
first.center[perpendicularAxis] - second.center[perpendicularAxis],
|
|
152
|
+
) > 1e-6
|
|
153
|
+
) {
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
const separation = Math.abs(first.center[axis] - second.center[axis])
|
|
157
|
+
if (separation > 1e-6) pitch = Math.min(pitch, separation)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return pitch
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getComponentBounds(obstacles: Obstacle[]): Bounds {
|
|
164
|
+
return {
|
|
165
|
+
minX: Math.min(
|
|
166
|
+
...obstacles.map((obstacle) => obstacle.center.x - obstacle.width / 2),
|
|
167
|
+
),
|
|
168
|
+
maxX: Math.max(
|
|
169
|
+
...obstacles.map((obstacle) => obstacle.center.x + obstacle.width / 2),
|
|
170
|
+
),
|
|
171
|
+
minY: Math.min(
|
|
172
|
+
...obstacles.map((obstacle) => obstacle.center.y - obstacle.height / 2),
|
|
173
|
+
),
|
|
174
|
+
maxY: Math.max(
|
|
175
|
+
...obstacles.map((obstacle) => obstacle.center.y + obstacle.height / 2),
|
|
176
|
+
),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function resolveComponentBounds(
|
|
181
|
+
grid: ComponentGrid,
|
|
182
|
+
options: FanoutSolverOptions,
|
|
183
|
+
): Bounds {
|
|
184
|
+
const requestedBounds = options.componentBounds?.[grid.componentId]
|
|
185
|
+
if (!requestedBounds) {
|
|
186
|
+
const inferredMarginX = grid.pitchX * 2.25
|
|
187
|
+
const inferredMarginY = grid.pitchY * 2.25
|
|
188
|
+
return {
|
|
189
|
+
minX: grid.bounds.minX - inferredMarginX,
|
|
190
|
+
maxX: grid.bounds.maxX + inferredMarginX,
|
|
191
|
+
minY: grid.bounds.minY - inferredMarginY,
|
|
192
|
+
maxY: grid.bounds.maxY + inferredMarginY,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const values = [
|
|
197
|
+
requestedBounds.minX,
|
|
198
|
+
requestedBounds.maxX,
|
|
199
|
+
requestedBounds.minY,
|
|
200
|
+
requestedBounds.maxY,
|
|
201
|
+
]
|
|
202
|
+
if (
|
|
203
|
+
values.some((value) => !Number.isFinite(value)) ||
|
|
204
|
+
requestedBounds.minX >= requestedBounds.maxX ||
|
|
205
|
+
requestedBounds.minY >= requestedBounds.maxY
|
|
206
|
+
) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`FanoutSolver: componentBounds for "${grid.componentId}" must contain finite, increasing bounds`,
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
if (
|
|
212
|
+
requestedBounds.minX > grid.bounds.minX + 1e-6 ||
|
|
213
|
+
requestedBounds.maxX < grid.bounds.maxX - 1e-6 ||
|
|
214
|
+
requestedBounds.minY > grid.bounds.minY + 1e-6 ||
|
|
215
|
+
requestedBounds.maxY < grid.bounds.maxY - 1e-6
|
|
216
|
+
) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`FanoutSolver: componentBounds for "${grid.componentId}" must contain every component pad`,
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { ...requestedBounds }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function validateSharedBoundary(
|
|
226
|
+
boundary: Bounds,
|
|
227
|
+
componentGrids: ComponentGrid[],
|
|
228
|
+
): Bounds {
|
|
229
|
+
const values = [boundary.minX, boundary.maxX, boundary.minY, boundary.maxY]
|
|
230
|
+
if (
|
|
231
|
+
values.some((value) => !Number.isFinite(value)) ||
|
|
232
|
+
boundary.minX >= boundary.maxX ||
|
|
233
|
+
boundary.minY >= boundary.maxY
|
|
234
|
+
) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
"FanoutSolver: sharedBoundary must contain finite, increasing bounds",
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
for (const grid of componentGrids) {
|
|
240
|
+
if (
|
|
241
|
+
boundary.minX > grid.bounds.minX + 1e-6 ||
|
|
242
|
+
boundary.maxX < grid.bounds.maxX - 1e-6 ||
|
|
243
|
+
boundary.minY > grid.bounds.minY + 1e-6 ||
|
|
244
|
+
boundary.maxY < grid.bounds.maxY - 1e-6
|
|
245
|
+
) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`FanoutSolver: sharedBoundary must contain every pad of component "${grid.componentId}"`,
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return { ...boundary }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function resolveSharedBoundary(
|
|
255
|
+
componentGrids: ComponentGrid[],
|
|
256
|
+
options: FanoutSolverOptions,
|
|
257
|
+
): Bounds {
|
|
258
|
+
if (options.sharedBoundary) {
|
|
259
|
+
return validateSharedBoundary(options.sharedBoundary, componentGrids)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const componentBounds = componentGrids.map((grid) =>
|
|
263
|
+
resolveComponentBounds(grid, options),
|
|
264
|
+
)
|
|
265
|
+
const maximumPitch = Math.max(
|
|
266
|
+
...componentGrids.flatMap((grid) => [grid.pitchX, grid.pitchY]),
|
|
267
|
+
)
|
|
268
|
+
const inferredMargin = maximumPitch * 2.25
|
|
269
|
+
return validateSharedBoundary(
|
|
270
|
+
{
|
|
271
|
+
minX:
|
|
272
|
+
Math.min(...componentBounds.map((bounds) => bounds.minX)) -
|
|
273
|
+
inferredMargin,
|
|
274
|
+
maxX:
|
|
275
|
+
Math.max(...componentBounds.map((bounds) => bounds.maxX)) +
|
|
276
|
+
inferredMargin,
|
|
277
|
+
minY:
|
|
278
|
+
Math.min(...componentBounds.map((bounds) => bounds.minY)) -
|
|
279
|
+
inferredMargin,
|
|
280
|
+
maxY:
|
|
281
|
+
Math.max(...componentBounds.map((bounds) => bounds.maxY)) +
|
|
282
|
+
inferredMargin,
|
|
283
|
+
},
|
|
284
|
+
componentGrids,
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function findComponentGrids(obstacles: Obstacle[]): ComponentGrid[] {
|
|
289
|
+
const obstaclesByComponent = new Map<string, Obstacle[]>()
|
|
290
|
+
for (const obstacle of obstacles) {
|
|
291
|
+
if (!obstacle.componentId || obstacle.isCopperPour) continue
|
|
292
|
+
const componentObstacles =
|
|
293
|
+
obstaclesByComponent.get(obstacle.componentId) ?? []
|
|
294
|
+
componentObstacles.push(obstacle)
|
|
295
|
+
obstaclesByComponent.set(obstacle.componentId, componentObstacles)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const grids: ComponentGrid[] = []
|
|
299
|
+
for (const [componentId, componentObstacles] of obstaclesByComponent) {
|
|
300
|
+
const xCoordinates = uniqueSorted(
|
|
301
|
+
componentObstacles.map((obstacle) => obstacle.center.x),
|
|
302
|
+
)
|
|
303
|
+
const yCoordinates = uniqueSorted(
|
|
304
|
+
componentObstacles.map((obstacle) => obstacle.center.y),
|
|
305
|
+
)
|
|
306
|
+
const alignedPitchX = getAlignedPitch(componentObstacles, "x")
|
|
307
|
+
const alignedPitchY = getAlignedPitch(componentObstacles, "y")
|
|
308
|
+
const coordinatePitchX = getPitch(xCoordinates)
|
|
309
|
+
const coordinatePitchY = getPitch(yCoordinates)
|
|
310
|
+
const fallbackPitch = Math.min(
|
|
311
|
+
...[
|
|
312
|
+
alignedPitchX,
|
|
313
|
+
alignedPitchY,
|
|
314
|
+
coordinatePitchX,
|
|
315
|
+
coordinatePitchY,
|
|
316
|
+
].filter(Number.isFinite),
|
|
317
|
+
)
|
|
318
|
+
const padSizeFallback = Math.max(
|
|
319
|
+
...componentObstacles.flatMap((obstacle) => [
|
|
320
|
+
obstacle.width,
|
|
321
|
+
obstacle.height,
|
|
322
|
+
]),
|
|
323
|
+
)
|
|
324
|
+
const resolvedFallback = Number.isFinite(fallbackPitch)
|
|
325
|
+
? fallbackPitch
|
|
326
|
+
: padSizeFallback
|
|
327
|
+
const pitchX = Number.isFinite(alignedPitchX)
|
|
328
|
+
? alignedPitchX
|
|
329
|
+
: Number.isFinite(coordinatePitchX)
|
|
330
|
+
? coordinatePitchX
|
|
331
|
+
: resolvedFallback
|
|
332
|
+
const pitchY = Number.isFinite(alignedPitchY)
|
|
333
|
+
? alignedPitchY
|
|
334
|
+
: Number.isFinite(coordinatePitchY)
|
|
335
|
+
? coordinatePitchY
|
|
336
|
+
: resolvedFallback
|
|
337
|
+
grids.push({
|
|
338
|
+
componentId,
|
|
339
|
+
obstacles: componentObstacles,
|
|
340
|
+
xCoordinates,
|
|
341
|
+
yCoordinates,
|
|
342
|
+
pitchX,
|
|
343
|
+
pitchY,
|
|
344
|
+
bounds: getComponentBounds(componentObstacles),
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
return grids
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function getPointLayers(point: ConnectionPoint): string[] {
|
|
351
|
+
return "layer" in point ? [point.layer] : point.layers
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function findPointObstacleMatches(params: {
|
|
355
|
+
point: ConnectionPoint
|
|
356
|
+
connection: SimpleRouteConnection
|
|
357
|
+
componentGrids: ComponentGrid[]
|
|
358
|
+
}): Array<{ grid: ComponentGrid; obstacle: Obstacle }> {
|
|
359
|
+
const { point, connection, componentGrids } = params
|
|
360
|
+
const pointLayers = getPointLayers(point)
|
|
361
|
+
const matches: Array<{ grid: ComponentGrid; obstacle: Obstacle }> = []
|
|
362
|
+
|
|
363
|
+
for (const grid of componentGrids) {
|
|
364
|
+
const candidateObstacles = grid.obstacles
|
|
365
|
+
.filter((obstacle) =>
|
|
366
|
+
obstacle.layers.some((layer) => pointLayers.includes(layer)),
|
|
367
|
+
)
|
|
368
|
+
.filter((obstacle) => pointIsInsideObstacle(point, obstacle, 1e-5))
|
|
369
|
+
.sort((a, b) => {
|
|
370
|
+
const aDirect =
|
|
371
|
+
a.connectedTo.includes(connection.name) ||
|
|
372
|
+
a.connectedTo.includes(point.pointId ?? "") ||
|
|
373
|
+
a.connectedTo.includes(point.pcb_port_id ?? "")
|
|
374
|
+
const bDirect =
|
|
375
|
+
b.connectedTo.includes(connection.name) ||
|
|
376
|
+
b.connectedTo.includes(point.pointId ?? "") ||
|
|
377
|
+
b.connectedTo.includes(point.pcb_port_id ?? "")
|
|
378
|
+
if (aDirect !== bDirect) return aDirect ? -1 : 1
|
|
379
|
+
return a.width * a.height - b.width * b.height
|
|
380
|
+
})
|
|
381
|
+
if (candidateObstacles[0]) {
|
|
382
|
+
matches.push({ grid, obstacle: candidateObstacles[0] })
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return matches
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function inferBusId(connection: SimpleRouteConnection): string | null {
|
|
390
|
+
for (const point of connection.pointsToConnect) {
|
|
391
|
+
if ("layers" in point && point.busId) return point.busId
|
|
392
|
+
}
|
|
393
|
+
const nameMatch = /^BUS[_:-]([^_:-]+)(?:[_:-]\d+)?$/i.exec(connection.name)
|
|
394
|
+
return nameMatch?.[1] ?? null
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function resolvePreferredExit(
|
|
398
|
+
busId: string,
|
|
399
|
+
value: FanoutBorderTarget | undefined,
|
|
400
|
+
): FanoutBorderTarget | undefined {
|
|
401
|
+
if (value === undefined) return undefined
|
|
402
|
+
if (!FANOUT_BORDER_TARGETS.has(value)) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
`FanoutSolver: bus "${busId}" has invalid preferredExit "${value}"`,
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
return value
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function resolveAvailableBoundaryRegions(
|
|
411
|
+
value: readonly FanoutAvailableCornerAndSideInput[] | undefined,
|
|
412
|
+
): AvailableBoundaryRegion[] | undefined {
|
|
413
|
+
if (value === undefined) return undefined
|
|
414
|
+
if (value.length === 0) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
"FanoutSolver: availableCornersAndSides must contain at least one boundary region",
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const regions: AvailableBoundaryRegion[] = []
|
|
421
|
+
const seen = new Set<string>()
|
|
422
|
+
for (const input of value) {
|
|
423
|
+
const region = AVAILABLE_BOUNDARY_REGIONS[input]
|
|
424
|
+
if (!region) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
`FanoutSolver: invalid availableCornersAndSides value "${input}"`,
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
const key = `${region.direction}:${region.preferredExit}`
|
|
430
|
+
if (seen.has(key)) continue
|
|
431
|
+
seen.add(key)
|
|
432
|
+
regions.push(region)
|
|
433
|
+
}
|
|
434
|
+
return regions
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function resolveTermination(
|
|
438
|
+
busId: string,
|
|
439
|
+
value: FanoutBusTermination | undefined,
|
|
440
|
+
): FanoutBusTermination {
|
|
441
|
+
if (value === undefined || value.type === "boundary") {
|
|
442
|
+
return { type: "boundary" }
|
|
443
|
+
}
|
|
444
|
+
if (
|
|
445
|
+
value.type !== "plane" ||
|
|
446
|
+
typeof value.layer !== "string" ||
|
|
447
|
+
value.layer.length === 0
|
|
448
|
+
) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`FanoutSolver: bus "${busId}" has an invalid termination target`,
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
return { type: "plane", layer: value.layer }
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function resolveBusSpecs(
|
|
457
|
+
srj: SimpleRouteJson,
|
|
458
|
+
options: FanoutSolverOptions,
|
|
459
|
+
): FanoutBusSpec[] {
|
|
460
|
+
const requestedBuses = options.buses ?? srj.buses
|
|
461
|
+
const specsById = new Map<string, FanoutBusSpec>()
|
|
462
|
+
const claimedConnectionNames = new Set<string>()
|
|
463
|
+
const knownConnectionNames = new Set(
|
|
464
|
+
srj.connections.map((connection) => connection.name),
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
for (const requestedBus of requestedBuses ?? []) {
|
|
468
|
+
if (specsById.has(requestedBus.busId)) {
|
|
469
|
+
throw new Error(`FanoutSolver: duplicate bus id "${requestedBus.busId}"`)
|
|
470
|
+
}
|
|
471
|
+
for (const connectionName of requestedBus.connectionNames) {
|
|
472
|
+
if (!knownConnectionNames.has(connectionName)) {
|
|
473
|
+
throw new Error(
|
|
474
|
+
`FanoutSolver: bus "${requestedBus.busId}" references unknown connection "${connectionName}"`,
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
if (claimedConnectionNames.has(connectionName)) {
|
|
478
|
+
throw new Error(
|
|
479
|
+
`FanoutSolver: connection "${connectionName}" belongs to more than one bus`,
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
claimedConnectionNames.add(connectionName)
|
|
483
|
+
}
|
|
484
|
+
const termination = resolveTermination(
|
|
485
|
+
requestedBus.busId,
|
|
486
|
+
(requestedBus as FanoutBusSpec).termination,
|
|
487
|
+
)
|
|
488
|
+
const preferredExit = resolvePreferredExit(
|
|
489
|
+
requestedBus.busId,
|
|
490
|
+
options.busExitPreferences?.[requestedBus.busId] ??
|
|
491
|
+
(requestedBus as FanoutBusSpec).preferredExit ??
|
|
492
|
+
options.defaultPreferredExit,
|
|
493
|
+
)
|
|
494
|
+
if (termination.type === "plane" && preferredExit !== undefined) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
`FanoutSolver: plane-terminated bus "${requestedBus.busId}" cannot also specify preferredExit`,
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
specsById.set(requestedBus.busId, {
|
|
500
|
+
...requestedBus,
|
|
501
|
+
sourceComponentId:
|
|
502
|
+
(requestedBus as FanoutBusSpec).sourceComponentId ??
|
|
503
|
+
options.sourceComponentId,
|
|
504
|
+
direction:
|
|
505
|
+
options.busDirections?.[requestedBus.busId] ??
|
|
506
|
+
(requestedBus as FanoutBusSpec).direction ??
|
|
507
|
+
options.defaultDirection,
|
|
508
|
+
preferredExit,
|
|
509
|
+
termination,
|
|
510
|
+
})
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
for (const connection of srj.connections) {
|
|
514
|
+
if (claimedConnectionNames.has(connection.name)) continue
|
|
515
|
+
const inferredBusId = inferBusId(connection)
|
|
516
|
+
if (inferredBusId) {
|
|
517
|
+
const existing = specsById.get(inferredBusId)
|
|
518
|
+
specsById.set(inferredBusId, {
|
|
519
|
+
busId: inferredBusId,
|
|
520
|
+
connectionNames: [
|
|
521
|
+
...(existing?.connectionNames ?? []),
|
|
522
|
+
connection.name,
|
|
523
|
+
],
|
|
524
|
+
direction:
|
|
525
|
+
options.busDirections?.[inferredBusId] ??
|
|
526
|
+
existing?.direction ??
|
|
527
|
+
options.defaultDirection,
|
|
528
|
+
sourceComponentId:
|
|
529
|
+
existing?.sourceComponentId ?? options.sourceComponentId,
|
|
530
|
+
preferredExit: resolvePreferredExit(
|
|
531
|
+
inferredBusId,
|
|
532
|
+
options.busExitPreferences?.[inferredBusId] ??
|
|
533
|
+
existing?.preferredExit ??
|
|
534
|
+
options.defaultPreferredExit,
|
|
535
|
+
),
|
|
536
|
+
termination: existing?.termination ?? { type: "boundary" },
|
|
537
|
+
})
|
|
538
|
+
} else {
|
|
539
|
+
const singletonBusId = `connection:${connection.name}`
|
|
540
|
+
specsById.set(singletonBusId, {
|
|
541
|
+
busId: singletonBusId,
|
|
542
|
+
connectionNames: [connection.name],
|
|
543
|
+
sourceComponentId: options.sourceComponentId,
|
|
544
|
+
direction:
|
|
545
|
+
options.busDirections?.[singletonBusId] ?? options.defaultDirection,
|
|
546
|
+
preferredExit: resolvePreferredExit(
|
|
547
|
+
singletonBusId,
|
|
548
|
+
options.busExitPreferences?.[singletonBusId] ??
|
|
549
|
+
options.defaultPreferredExit,
|
|
550
|
+
),
|
|
551
|
+
termination: { type: "boundary" },
|
|
552
|
+
})
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return [...specsById.values()]
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function chooseSourceGrid(params: {
|
|
560
|
+
busSpec: FanoutBusSpec
|
|
561
|
+
connections: SimpleRouteConnection[]
|
|
562
|
+
componentGrids: ComponentGrid[]
|
|
563
|
+
}): ComponentGrid {
|
|
564
|
+
const { busSpec, connections, componentGrids } = params
|
|
565
|
+
const matchCountByComponent = new Map<string, number>()
|
|
566
|
+
|
|
567
|
+
for (const connection of connections) {
|
|
568
|
+
const matchedComponents = new Set<string>()
|
|
569
|
+
for (const point of connection.pointsToConnect) {
|
|
570
|
+
for (const match of findPointObstacleMatches({
|
|
571
|
+
point,
|
|
572
|
+
connection,
|
|
573
|
+
componentGrids,
|
|
574
|
+
})) {
|
|
575
|
+
matchedComponents.add(match.grid.componentId)
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
for (const componentId of matchedComponents) {
|
|
579
|
+
matchCountByComponent.set(
|
|
580
|
+
componentId,
|
|
581
|
+
(matchCountByComponent.get(componentId) ?? 0) + 1,
|
|
582
|
+
)
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const selectedGrid = [...componentGrids].sort((a, b) => {
|
|
587
|
+
const countDifference =
|
|
588
|
+
(matchCountByComponent.get(b.componentId) ?? 0) -
|
|
589
|
+
(matchCountByComponent.get(a.componentId) ?? 0)
|
|
590
|
+
if (countDifference !== 0) return countDifference
|
|
591
|
+
return b.obstacles.length - a.obstacles.length
|
|
592
|
+
})[0]
|
|
593
|
+
const requestedGrid = busSpec.sourceComponentId
|
|
594
|
+
? componentGrids.find(
|
|
595
|
+
(grid) => grid.componentId === busSpec.sourceComponentId,
|
|
596
|
+
)
|
|
597
|
+
: undefined
|
|
598
|
+
if (busSpec.sourceComponentId && !requestedGrid) {
|
|
599
|
+
throw new Error(
|
|
600
|
+
`FanoutSolver: source component "${busSpec.sourceComponentId}" for bus "${busSpec.busId}" was not found`,
|
|
601
|
+
)
|
|
602
|
+
}
|
|
603
|
+
const sourceGrid = requestedGrid ?? selectedGrid
|
|
604
|
+
const sourceMatchCount = sourceGrid
|
|
605
|
+
? (matchCountByComponent.get(sourceGrid.componentId) ?? 0)
|
|
606
|
+
: 0
|
|
607
|
+
if (!sourceGrid || sourceMatchCount !== connections.length) {
|
|
608
|
+
throw new Error(
|
|
609
|
+
busSpec.sourceComponentId
|
|
610
|
+
? `FanoutSolver: source component "${busSpec.sourceComponentId}" is not an endpoint on every connection in bus "${busSpec.busId}"`
|
|
611
|
+
: `FanoutSolver: bus "${busSpec.busId}" does not have one component endpoint on every connection`,
|
|
612
|
+
)
|
|
613
|
+
}
|
|
614
|
+
return sourceGrid
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function chooseTargetPoint(
|
|
618
|
+
sourcePoint: ConnectionPoint,
|
|
619
|
+
connection: SimpleRouteConnection,
|
|
620
|
+
sourcePointIndex: number,
|
|
621
|
+
termination: FanoutBusTermination,
|
|
622
|
+
): ConnectionPoint {
|
|
623
|
+
const targetCandidates = connection.pointsToConnect.filter(
|
|
624
|
+
(_, pointIndex) => pointIndex !== sourcePointIndex,
|
|
625
|
+
)
|
|
626
|
+
const targetPoint = targetCandidates.sort(
|
|
627
|
+
(a, b) => distance(sourcePoint, b) - distance(sourcePoint, a),
|
|
628
|
+
)[0]
|
|
629
|
+
if (!targetPoint && termination.type === "plane") {
|
|
630
|
+
return sourcePoint
|
|
631
|
+
}
|
|
632
|
+
if (!targetPoint) {
|
|
633
|
+
throw new Error(
|
|
634
|
+
`FanoutSolver: connection "${connection.name}" has no target beyond its BGA pad`,
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
return targetPoint
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function prepareConnection(params: {
|
|
641
|
+
connection: SimpleRouteConnection
|
|
642
|
+
connectionIndex: number
|
|
643
|
+
sourceGrid: ComponentGrid
|
|
644
|
+
componentGrids: ComponentGrid[]
|
|
645
|
+
termination: FanoutBusTermination
|
|
646
|
+
}): PreparedConnection {
|
|
647
|
+
const {
|
|
648
|
+
connection,
|
|
649
|
+
connectionIndex,
|
|
650
|
+
sourceGrid,
|
|
651
|
+
componentGrids,
|
|
652
|
+
termination,
|
|
653
|
+
} = params
|
|
654
|
+
for (
|
|
655
|
+
let sourcePointIndex = 0;
|
|
656
|
+
sourcePointIndex < connection.pointsToConnect.length;
|
|
657
|
+
sourcePointIndex++
|
|
658
|
+
) {
|
|
659
|
+
const sourcePoint = connection.pointsToConnect[sourcePointIndex]!
|
|
660
|
+
const sourceMatch = findPointObstacleMatches({
|
|
661
|
+
point: sourcePoint,
|
|
662
|
+
connection,
|
|
663
|
+
componentGrids,
|
|
664
|
+
}).find((match) => match.grid.componentId === sourceGrid.componentId)
|
|
665
|
+
if (!sourceMatch) continue
|
|
666
|
+
const sourceLayer = getPointLayers(sourcePoint).find((layer) =>
|
|
667
|
+
sourceMatch.obstacle.layers.includes(layer),
|
|
668
|
+
)
|
|
669
|
+
if (!sourceLayer) {
|
|
670
|
+
throw new Error(
|
|
671
|
+
`FanoutSolver: connection "${connection.name}" has no source layer shared with its BGA pad`,
|
|
672
|
+
)
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
connection,
|
|
676
|
+
connectionIndex,
|
|
677
|
+
sourcePoint,
|
|
678
|
+
sourcePointIndex,
|
|
679
|
+
sourceLayer,
|
|
680
|
+
sourceObstacle: sourceMatch.obstacle,
|
|
681
|
+
targetPoint: chooseTargetPoint(
|
|
682
|
+
sourcePoint,
|
|
683
|
+
connection,
|
|
684
|
+
sourcePointIndex,
|
|
685
|
+
termination,
|
|
686
|
+
),
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
throw new Error(
|
|
690
|
+
`FanoutSolver: connection "${connection.name}" does not touch component "${sourceGrid.componentId}"`,
|
|
691
|
+
)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function inferDirection(
|
|
695
|
+
busId: string,
|
|
696
|
+
connections: PreparedConnection[],
|
|
697
|
+
): FanoutDirection {
|
|
698
|
+
let dx = 0
|
|
699
|
+
let dy = 0
|
|
700
|
+
for (const preparedConnection of connections) {
|
|
701
|
+
dx += preparedConnection.targetPoint.x - preparedConnection.sourcePoint.x
|
|
702
|
+
dy += preparedConnection.targetPoint.y - preparedConnection.sourcePoint.y
|
|
703
|
+
}
|
|
704
|
+
if (Math.abs(dx) < 1e-9 && Math.abs(dy) < 1e-9) {
|
|
705
|
+
throw new Error(
|
|
706
|
+
`FanoutSolver: cannot infer an escape direction for bus "${busId}"`,
|
|
707
|
+
)
|
|
708
|
+
}
|
|
709
|
+
if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? "right" : "left"
|
|
710
|
+
return dy >= 0 ? "up" : "down"
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function getDirectionsForBorderTarget(
|
|
714
|
+
target: FanoutBorderTarget,
|
|
715
|
+
): FanoutDirection[] {
|
|
716
|
+
switch (target) {
|
|
717
|
+
case "left":
|
|
718
|
+
return ["left"]
|
|
719
|
+
case "right":
|
|
720
|
+
return ["right"]
|
|
721
|
+
case "top":
|
|
722
|
+
return ["up"]
|
|
723
|
+
case "bottom":
|
|
724
|
+
return ["down"]
|
|
725
|
+
case "top-left":
|
|
726
|
+
return ["up", "left"]
|
|
727
|
+
case "top-right":
|
|
728
|
+
return ["up", "right"]
|
|
729
|
+
case "bottom-left":
|
|
730
|
+
return ["down", "left"]
|
|
731
|
+
case "bottom-right":
|
|
732
|
+
return ["down", "right"]
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function getAverageSourcePoint(connections: PreparedConnection[]): {
|
|
737
|
+
x: number
|
|
738
|
+
y: number
|
|
739
|
+
} {
|
|
740
|
+
return {
|
|
741
|
+
x:
|
|
742
|
+
connections.reduce(
|
|
743
|
+
(sum, connection) => sum + connection.sourcePoint.x,
|
|
744
|
+
0,
|
|
745
|
+
) / connections.length,
|
|
746
|
+
y:
|
|
747
|
+
connections.reduce(
|
|
748
|
+
(sum, connection) => sum + connection.sourcePoint.y,
|
|
749
|
+
0,
|
|
750
|
+
) / connections.length,
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function getDistanceToBoundary(
|
|
755
|
+
source: { x: number; y: number },
|
|
756
|
+
direction: FanoutDirection,
|
|
757
|
+
boundary: Bounds,
|
|
758
|
+
): number {
|
|
759
|
+
switch (direction) {
|
|
760
|
+
case "left":
|
|
761
|
+
return source.x - boundary.minX
|
|
762
|
+
case "right":
|
|
763
|
+
return boundary.maxX - source.x
|
|
764
|
+
case "up":
|
|
765
|
+
return boundary.maxY - source.y
|
|
766
|
+
case "down":
|
|
767
|
+
return source.y - boundary.minY
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function getRegionAnchor(
|
|
772
|
+
region: AvailableBoundaryRegion,
|
|
773
|
+
boundary: Bounds,
|
|
774
|
+
): number {
|
|
775
|
+
if (region.direction === "up" || region.direction === "down") {
|
|
776
|
+
if (region.preferredExit.endsWith("left")) return boundary.minX
|
|
777
|
+
if (region.preferredExit.endsWith("right")) return boundary.maxX
|
|
778
|
+
return (boundary.minX + boundary.maxX) / 2
|
|
779
|
+
}
|
|
780
|
+
if (region.preferredExit.startsWith("top")) return boundary.maxY
|
|
781
|
+
if (region.preferredExit.startsWith("bottom")) return boundary.minY
|
|
782
|
+
return (boundary.minY + boundary.maxY) / 2
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function getRegionSourceCoordinate(
|
|
786
|
+
source: { x: number; y: number },
|
|
787
|
+
direction: FanoutDirection,
|
|
788
|
+
): number {
|
|
789
|
+
return direction === "up" || direction === "down" ? source.x : source.y
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function tryInferDirection(
|
|
793
|
+
busId: string,
|
|
794
|
+
connections: PreparedConnection[],
|
|
795
|
+
): FanoutDirection | undefined {
|
|
796
|
+
try {
|
|
797
|
+
return inferDirection(busId, connections)
|
|
798
|
+
} catch {
|
|
799
|
+
return undefined
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function resolveAvailableBusExit(params: {
|
|
804
|
+
busId: string
|
|
805
|
+
explicitDirection?: FanoutDirection
|
|
806
|
+
preferredExit?: FanoutBorderTarget
|
|
807
|
+
connections: PreparedConnection[]
|
|
808
|
+
sharedBoundary: Bounds
|
|
809
|
+
availableRegions: AvailableBoundaryRegion[]
|
|
810
|
+
}): { direction: FanoutDirection; preferredExit: FanoutBorderTarget } {
|
|
811
|
+
const {
|
|
812
|
+
busId,
|
|
813
|
+
explicitDirection,
|
|
814
|
+
preferredExit,
|
|
815
|
+
connections,
|
|
816
|
+
sharedBoundary,
|
|
817
|
+
availableRegions,
|
|
818
|
+
} = params
|
|
819
|
+
const compatibleRegions = availableRegions.filter(
|
|
820
|
+
(region) =>
|
|
821
|
+
(explicitDirection === undefined ||
|
|
822
|
+
region.direction === explicitDirection) &&
|
|
823
|
+
(preferredExit === undefined || region.preferredExit === preferredExit),
|
|
824
|
+
)
|
|
825
|
+
if (compatibleRegions.length === 0) {
|
|
826
|
+
throw new Error(
|
|
827
|
+
`FanoutSolver: bus "${busId}" cannot use its requested exit with availableCornersAndSides`,
|
|
828
|
+
)
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const inferredDirection = explicitDirection
|
|
832
|
+
? undefined
|
|
833
|
+
: tryInferDirection(busId, connections)
|
|
834
|
+
const preferredDirectionRegions = inferredDirection
|
|
835
|
+
? compatibleRegions.filter(
|
|
836
|
+
(region) => region.direction === inferredDirection,
|
|
837
|
+
)
|
|
838
|
+
: []
|
|
839
|
+
const candidates =
|
|
840
|
+
preferredDirectionRegions.length > 0
|
|
841
|
+
? preferredDirectionRegions
|
|
842
|
+
: compatibleRegions
|
|
843
|
+
const averageSource = getAverageSourcePoint(connections)
|
|
844
|
+
return [...candidates].toSorted(
|
|
845
|
+
(first, second) =>
|
|
846
|
+
getDistanceToBoundary(averageSource, first.direction, sharedBoundary) -
|
|
847
|
+
getDistanceToBoundary(
|
|
848
|
+
averageSource,
|
|
849
|
+
second.direction,
|
|
850
|
+
sharedBoundary,
|
|
851
|
+
) ||
|
|
852
|
+
Math.abs(
|
|
853
|
+
getRegionSourceCoordinate(averageSource, first.direction) -
|
|
854
|
+
getRegionAnchor(first, sharedBoundary),
|
|
855
|
+
) -
|
|
856
|
+
Math.abs(
|
|
857
|
+
getRegionSourceCoordinate(averageSource, second.direction) -
|
|
858
|
+
getRegionAnchor(second, sharedBoundary),
|
|
859
|
+
) ||
|
|
860
|
+
first.preferredExit.localeCompare(second.preferredExit),
|
|
861
|
+
)[0]!
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function resolveBusDirection(params: {
|
|
865
|
+
busId: string
|
|
866
|
+
explicitDirection?: FanoutDirection
|
|
867
|
+
preferredExit?: FanoutBorderTarget
|
|
868
|
+
connections: PreparedConnection[]
|
|
869
|
+
sharedBoundary: Bounds
|
|
870
|
+
availableRegions?: AvailableBoundaryRegion[]
|
|
871
|
+
}): { direction: FanoutDirection; preferredExit?: FanoutBorderTarget } {
|
|
872
|
+
const {
|
|
873
|
+
busId,
|
|
874
|
+
explicitDirection,
|
|
875
|
+
preferredExit,
|
|
876
|
+
connections,
|
|
877
|
+
sharedBoundary,
|
|
878
|
+
availableRegions,
|
|
879
|
+
} = params
|
|
880
|
+
if (availableRegions) {
|
|
881
|
+
return resolveAvailableBusExit({
|
|
882
|
+
busId,
|
|
883
|
+
explicitDirection,
|
|
884
|
+
preferredExit,
|
|
885
|
+
connections,
|
|
886
|
+
sharedBoundary,
|
|
887
|
+
availableRegions,
|
|
888
|
+
})
|
|
889
|
+
}
|
|
890
|
+
if (!preferredExit) {
|
|
891
|
+
return {
|
|
892
|
+
direction: explicitDirection ?? inferDirection(busId, connections),
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const compatibleDirections = getDirectionsForBorderTarget(preferredExit)
|
|
897
|
+
if (explicitDirection) {
|
|
898
|
+
if (!compatibleDirections.includes(explicitDirection)) {
|
|
899
|
+
throw new Error(
|
|
900
|
+
`FanoutSolver: bus "${busId}" direction "${explicitDirection}" is incompatible with preferredExit "${preferredExit}"`,
|
|
901
|
+
)
|
|
902
|
+
}
|
|
903
|
+
return { direction: explicitDirection, preferredExit }
|
|
904
|
+
}
|
|
905
|
+
if (compatibleDirections.length === 1) {
|
|
906
|
+
return { direction: compatibleDirections[0]!, preferredExit }
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
let inferredDirection: FanoutDirection | undefined
|
|
910
|
+
try {
|
|
911
|
+
inferredDirection = inferDirection(busId, connections)
|
|
912
|
+
} catch {
|
|
913
|
+
inferredDirection = undefined
|
|
914
|
+
}
|
|
915
|
+
if (inferredDirection && compatibleDirections.includes(inferredDirection)) {
|
|
916
|
+
return { direction: inferredDirection, preferredExit }
|
|
917
|
+
}
|
|
918
|
+
const averageSource = getAverageSourcePoint(connections)
|
|
919
|
+
return {
|
|
920
|
+
direction: compatibleDirections.toSorted(
|
|
921
|
+
(first, second) =>
|
|
922
|
+
getDistanceToBoundary(averageSource, first, sharedBoundary) -
|
|
923
|
+
getDistanceToBoundary(averageSource, second, sharedBoundary) ||
|
|
924
|
+
first.localeCompare(second),
|
|
925
|
+
)[0]!,
|
|
926
|
+
preferredExit,
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
export function prepareFanoutBuses(
|
|
931
|
+
srj: SimpleRouteJson,
|
|
932
|
+
options: FanoutSolverOptions,
|
|
933
|
+
): PreparedBus[] {
|
|
934
|
+
const componentGrids = findComponentGrids(srj.obstacles)
|
|
935
|
+
if (componentGrids.length === 0 && srj.connections.length > 0) {
|
|
936
|
+
throw new Error(
|
|
937
|
+
"FanoutSolver: no componentId-tagged pad footprint was found",
|
|
938
|
+
)
|
|
939
|
+
}
|
|
940
|
+
const connectionIndexByName = new Map(
|
|
941
|
+
srj.connections.map((connection, index) => [connection.name, index]),
|
|
942
|
+
)
|
|
943
|
+
const resolvedBusInputs = resolveBusSpecs(srj, options).map((busSpec) => {
|
|
944
|
+
const connections = busSpec.connectionNames.map((connectionName) => {
|
|
945
|
+
const connectionIndex = connectionIndexByName.get(connectionName)
|
|
946
|
+
if (connectionIndex === undefined) {
|
|
947
|
+
throw new Error(
|
|
948
|
+
`FanoutSolver: connection "${connectionName}" is missing from the input`,
|
|
949
|
+
)
|
|
950
|
+
}
|
|
951
|
+
return srj.connections[connectionIndex]!
|
|
952
|
+
})
|
|
953
|
+
const sourceGrid = chooseSourceGrid({
|
|
954
|
+
busSpec,
|
|
955
|
+
connections,
|
|
956
|
+
componentGrids,
|
|
957
|
+
})
|
|
958
|
+
const preparedConnections = connections.map((connection) =>
|
|
959
|
+
prepareConnection({
|
|
960
|
+
connection,
|
|
961
|
+
connectionIndex: connectionIndexByName.get(connection.name)!,
|
|
962
|
+
sourceGrid,
|
|
963
|
+
componentGrids,
|
|
964
|
+
termination: busSpec.termination ?? { type: "boundary" },
|
|
965
|
+
}),
|
|
966
|
+
)
|
|
967
|
+
return { busSpec, sourceGrid, preparedConnections }
|
|
968
|
+
})
|
|
969
|
+
const sourceGrids = [
|
|
970
|
+
...new Map(
|
|
971
|
+
resolvedBusInputs.map(({ sourceGrid }) => [
|
|
972
|
+
sourceGrid.componentId,
|
|
973
|
+
sourceGrid,
|
|
974
|
+
]),
|
|
975
|
+
).values(),
|
|
976
|
+
]
|
|
977
|
+
const sharedBoundary = resolveSharedBoundary(sourceGrids, options)
|
|
978
|
+
const availableRegions = resolveAvailableBoundaryRegions(
|
|
979
|
+
options.availableCornersAndSides,
|
|
980
|
+
)
|
|
981
|
+
const buses: PreparedBus[] = []
|
|
982
|
+
|
|
983
|
+
for (const {
|
|
984
|
+
busSpec,
|
|
985
|
+
sourceGrid,
|
|
986
|
+
preparedConnections,
|
|
987
|
+
} of resolvedBusInputs) {
|
|
988
|
+
const resolvedExit = resolveBusDirection({
|
|
989
|
+
busId: busSpec.busId,
|
|
990
|
+
explicitDirection:
|
|
991
|
+
busSpec.direction ?? options.busDirections?.[busSpec.busId],
|
|
992
|
+
preferredExit: busSpec.preferredExit,
|
|
993
|
+
connections: preparedConnections,
|
|
994
|
+
sharedBoundary,
|
|
995
|
+
availableRegions:
|
|
996
|
+
busSpec.termination?.type === "plane" ? undefined : availableRegions,
|
|
997
|
+
})
|
|
998
|
+
buses.push({
|
|
999
|
+
busId: busSpec.busId,
|
|
1000
|
+
direction: resolvedExit.direction,
|
|
1001
|
+
preferredExit: resolvedExit.preferredExit,
|
|
1002
|
+
termination: busSpec.termination ?? { type: "boundary" },
|
|
1003
|
+
connections: preparedConnections,
|
|
1004
|
+
componentId: sourceGrid.componentId,
|
|
1005
|
+
componentObstacles: sourceGrid.obstacles,
|
|
1006
|
+
componentBounds: resolveComponentBounds(sourceGrid, options),
|
|
1007
|
+
sharedBoundary,
|
|
1008
|
+
xCoordinates: [...sourceGrid.xCoordinates],
|
|
1009
|
+
yCoordinates: [...sourceGrid.yCoordinates],
|
|
1010
|
+
pitchX: sourceGrid.pitchX,
|
|
1011
|
+
pitchY: sourceGrid.pitchY,
|
|
1012
|
+
})
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return buses
|
|
1016
|
+
}
|