@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.
- package/lib/fanout-solver.ts +198 -6
- 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 +18 -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,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
|