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