@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,488 @@
|
|
|
1
|
+
import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter"
|
|
2
|
+
import {
|
|
3
|
+
distance,
|
|
4
|
+
distancePointToObstacle,
|
|
5
|
+
distancePointToSegment,
|
|
6
|
+
distanceSegmentToObstacle,
|
|
7
|
+
segmentsAreClear,
|
|
8
|
+
} from "./geometry"
|
|
9
|
+
import { getViaSpanLayers } from "./layer-names"
|
|
10
|
+
import { matchAngularlyOrderedLocalVias } from "./match-angularly-ordered-local-vias"
|
|
11
|
+
import {
|
|
12
|
+
getComponentDogboneViaSiteCandidates,
|
|
13
|
+
matchComponentDogboneViaSites,
|
|
14
|
+
type DogboneViaSiteGeometryRules,
|
|
15
|
+
} from "./match-component-dogbone-via-sites"
|
|
16
|
+
import { routeSingleLayerWithAdaptiveExitsSteps } from "./route-single-layer-adaptive-exits"
|
|
17
|
+
import type {
|
|
18
|
+
Bounds,
|
|
19
|
+
FanoutRoutePlan,
|
|
20
|
+
Point2D,
|
|
21
|
+
PreparedBus,
|
|
22
|
+
PreparedConnection,
|
|
23
|
+
RoutedSegment,
|
|
24
|
+
RoutedVia,
|
|
25
|
+
} from "./types"
|
|
26
|
+
|
|
27
|
+
const EPSILON = 1e-9
|
|
28
|
+
export interface PeripheralSourceEscape {
|
|
29
|
+
connectionIndex: number
|
|
30
|
+
connectionName: string
|
|
31
|
+
segments: RoutedSegment[]
|
|
32
|
+
via: RoutedVia
|
|
33
|
+
}
|
|
34
|
+
export interface PeripheralSourceEscapes {
|
|
35
|
+
sourceEscapes: PeripheralSourceEscape[]
|
|
36
|
+
viaPointsByConnectionIndex: Map<number, Point2D>
|
|
37
|
+
remoteConnectionIndices: Set<number>
|
|
38
|
+
localBus: PreparedBus
|
|
39
|
+
sourceBoundary: Bounds
|
|
40
|
+
}
|
|
41
|
+
export interface PeripheralSourceEscapeParams {
|
|
42
|
+
srj: SimpleRouteJson
|
|
43
|
+
buses: readonly PreparedBus[]
|
|
44
|
+
bus: PreparedBus
|
|
45
|
+
targetLayer: string
|
|
46
|
+
layerNames: string[]
|
|
47
|
+
traceWidth: number
|
|
48
|
+
clearance: number
|
|
49
|
+
viaDiameter: number
|
|
50
|
+
viaHoleDiameter: number
|
|
51
|
+
allowBlindAndBuriedVias?: boolean
|
|
52
|
+
initialViaPoints?: ReadonlyMap<number, Point2D>
|
|
53
|
+
targetPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
|
|
54
|
+
targetLayerByBusId?: ReadonlyMap<string, string>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function chamfer(points: Point2D[], amount: number): Point2D[] {
|
|
58
|
+
const result = [points[0]!]
|
|
59
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
60
|
+
const a = points[i - 1]!,
|
|
61
|
+
p = points[i]!,
|
|
62
|
+
b = points[i + 1]!
|
|
63
|
+
const dx = p.x - a.x,
|
|
64
|
+
dy = p.y - a.y,
|
|
65
|
+
ex = b.x - p.x,
|
|
66
|
+
ey = b.y - p.y
|
|
67
|
+
const before = Math.hypot(dx, dy),
|
|
68
|
+
after = Math.hypot(ex, ey)
|
|
69
|
+
if (before < EPSILON || after < EPSILON) continue
|
|
70
|
+
if (Math.abs(dx * ex + dy * ey) < EPSILON) {
|
|
71
|
+
const trim = Math.min(amount, before / 2, after / 2)
|
|
72
|
+
result.push(
|
|
73
|
+
{ x: p.x - (dx / before) * trim, y: p.y - (dy / before) * trim },
|
|
74
|
+
{ x: p.x + (ex / after) * trim, y: p.y + (ey / after) * trim },
|
|
75
|
+
)
|
|
76
|
+
} else result.push(p)
|
|
77
|
+
}
|
|
78
|
+
result.push(points.at(-1)!)
|
|
79
|
+
return result.filter(
|
|
80
|
+
(p, i) => i === 0 || distance(p, result[i - 1]!) > EPSILON,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A deterministic longest peripheral subsequence that preserves target order. */
|
|
85
|
+
function selectOrderedPaths(
|
|
86
|
+
paths: FanoutRoutePlan[],
|
|
87
|
+
ranks: ReadonlyMap<number, number>,
|
|
88
|
+
center: Point2D,
|
|
89
|
+
): FanoutRoutePlan[] {
|
|
90
|
+
const ordered = paths.toSorted(
|
|
91
|
+
(a, b) =>
|
|
92
|
+
Math.atan2(a.exitPoint.y - center.y, a.exitPoint.x - center.x) -
|
|
93
|
+
Math.atan2(b.exitPoint.y - center.y, b.exitPoint.x - center.x),
|
|
94
|
+
)
|
|
95
|
+
const lexicographicallyEarlier = (
|
|
96
|
+
a: FanoutRoutePlan[],
|
|
97
|
+
b: FanoutRoutePlan[],
|
|
98
|
+
) => {
|
|
99
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
100
|
+
const difference =
|
|
101
|
+
ranks.get(a[i]!.connectionIndex)! - ranks.get(b[i]!.connectionIndex)!
|
|
102
|
+
if (difference) return difference < 0
|
|
103
|
+
}
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
const better = (a: FanoutRoutePlan[], b: FanoutRoutePlan[]) =>
|
|
107
|
+
a.length > b.length ||
|
|
108
|
+
(a.length === b.length && lexicographicallyEarlier(a, b))
|
|
109
|
+
const suffixes: FanoutRoutePlan[][] = []
|
|
110
|
+
for (let i = ordered.length - 1; i >= 0; i--) {
|
|
111
|
+
let suffix: FanoutRoutePlan[] = []
|
|
112
|
+
for (let j = i + 1; j < ordered.length; j++)
|
|
113
|
+
if (
|
|
114
|
+
ranks.get(ordered[j]!.connectionIndex)! >
|
|
115
|
+
ranks.get(ordered[i]!.connectionIndex)! &&
|
|
116
|
+
better(suffixes[j]!, suffix)
|
|
117
|
+
)
|
|
118
|
+
suffix = suffixes[j]!
|
|
119
|
+
suffixes[i] = [ordered[i]!, ...suffix]
|
|
120
|
+
}
|
|
121
|
+
return suffixes.reduce(
|
|
122
|
+
(best, current) => (better(current, best) ? current : best),
|
|
123
|
+
[],
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Escape an ordered subset on its source layer before assigning the remaining
|
|
129
|
+
* vias. Returned prefixes stop at the first via; the caller must complete and
|
|
130
|
+
* validate the whole atomic bus before accepting any of them.
|
|
131
|
+
*/
|
|
132
|
+
export function* routePeripheralSourceEscapesSteps(
|
|
133
|
+
params: PeripheralSourceEscapeParams,
|
|
134
|
+
): Generator<void, PeripheralSourceEscapes | null, unknown> {
|
|
135
|
+
const { bus, srj, traceWidth: w, clearance: c, viaDiameter: d } = params
|
|
136
|
+
// This adjacent-band construction currently handles a right edge from the
|
|
137
|
+
// upper source perimeter. Other orientations retain the general fallback.
|
|
138
|
+
if (
|
|
139
|
+
bus.termination.type !== "boundary" ||
|
|
140
|
+
bus.exitEdge !== "right" ||
|
|
141
|
+
bus.connections.length < 6 ||
|
|
142
|
+
bus.connections.some((connection) => connection.sourceLayer !== "top") ||
|
|
143
|
+
!(bus.allowedLayers ?? params.layerNames).includes(params.targetLayer) ||
|
|
144
|
+
params.targetLayer === "top"
|
|
145
|
+
)
|
|
146
|
+
return null
|
|
147
|
+
const sourceObstacles = bus.componentObstacles.filter((o) =>
|
|
148
|
+
o.layers.includes("top"),
|
|
149
|
+
)
|
|
150
|
+
if (!sourceObstacles.length) return null
|
|
151
|
+
const padBounds = {
|
|
152
|
+
minX: Math.min(...sourceObstacles.map((o) => o.center.x - o.width / 2)),
|
|
153
|
+
maxX: Math.max(...sourceObstacles.map((o) => o.center.x + o.width / 2)),
|
|
154
|
+
minY: Math.min(...sourceObstacles.map((o) => o.center.y - o.height / 2)),
|
|
155
|
+
maxY: Math.max(...sourceObstacles.map((o) => o.center.y + o.height / 2)),
|
|
156
|
+
}
|
|
157
|
+
const center = {
|
|
158
|
+
x: (padBounds.minX + padBounds.maxX) / 2,
|
|
159
|
+
y: (padBounds.minY + padBounds.maxY) / 2,
|
|
160
|
+
}
|
|
161
|
+
const pitch = w + c,
|
|
162
|
+
padPitch = Math.max(bus.pitchX, bus.pitchY)
|
|
163
|
+
if (!Number.isFinite(padPitch) || padPitch <= 0) return null
|
|
164
|
+
const halfWidth =
|
|
165
|
+
Math.ceil(
|
|
166
|
+
((padBounds.maxX - padBounds.minX) / 2 + padPitch + pitch) / pitch,
|
|
167
|
+
) * pitch
|
|
168
|
+
const halfHeight =
|
|
169
|
+
Math.ceil(
|
|
170
|
+
((padBounds.maxY - padBounds.minY) / 2 + padPitch + pitch) / pitch,
|
|
171
|
+
) * pitch
|
|
172
|
+
const sourceBoundary = {
|
|
173
|
+
minX: center.x - halfWidth,
|
|
174
|
+
maxX: center.x + halfWidth,
|
|
175
|
+
minY: center.y - halfHeight,
|
|
176
|
+
maxY: center.y + halfHeight,
|
|
177
|
+
}
|
|
178
|
+
if (
|
|
179
|
+
sourceBoundary.minX <= bus.sharedBoundary.minX ||
|
|
180
|
+
sourceBoundary.maxX >= bus.sharedBoundary.maxX ||
|
|
181
|
+
sourceBoundary.minY <= bus.sharedBoundary.minY ||
|
|
182
|
+
sourceBoundary.maxY >= bus.sharedBoundary.maxY
|
|
183
|
+
)
|
|
184
|
+
return null
|
|
185
|
+
const singletonBuses = bus.connections.map((connection, index) => ({
|
|
186
|
+
...bus,
|
|
187
|
+
busId: `${bus.busId}:source-perimeter:${index}`,
|
|
188
|
+
sharedBoundary: sourceBoundary,
|
|
189
|
+
preferredExit: undefined,
|
|
190
|
+
exitEdge: undefined,
|
|
191
|
+
connections: [
|
|
192
|
+
{
|
|
193
|
+
...connection,
|
|
194
|
+
exitTargetPoint: undefined,
|
|
195
|
+
hasExplicitLayeredExitTarget: false,
|
|
196
|
+
hasExplicitExitTarget: false,
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
}))
|
|
200
|
+
const paths = yield* routeSingleLayerWithAdaptiveExitsSteps({
|
|
201
|
+
srj,
|
|
202
|
+
buses: singletonBuses,
|
|
203
|
+
traceWidth: w,
|
|
204
|
+
clearance: c,
|
|
205
|
+
})
|
|
206
|
+
if (!paths || paths.length !== bus.connections.length) return null
|
|
207
|
+
const target = (connection: PreparedConnection): Point2D =>
|
|
208
|
+
params.targetPointsByConnectionIndex?.get(connection.connectionIndex) ?? {
|
|
209
|
+
x: bus.sharedBoundary.maxX,
|
|
210
|
+
y: (connection.exitTargetPoint ?? connection.targetPoint).y,
|
|
211
|
+
}
|
|
212
|
+
const targetOrdered = bus.connections.toSorted(
|
|
213
|
+
(a, b) =>
|
|
214
|
+
target(a).y - target(b).y || a.connectionIndex - b.connectionIndex,
|
|
215
|
+
)
|
|
216
|
+
const ranks = new Map(
|
|
217
|
+
targetOrdered.map((connection, i) => [connection.connectionIndex, i]),
|
|
218
|
+
)
|
|
219
|
+
const selected = selectOrderedPaths(paths, ranks, center)
|
|
220
|
+
if (
|
|
221
|
+
selected.length < 3 ||
|
|
222
|
+
selected.length >= bus.connections.length ||
|
|
223
|
+
selected.some(
|
|
224
|
+
(p) =>
|
|
225
|
+
Math.abs(p.exitPoint.y - sourceBoundary.maxY) > EPSILON &&
|
|
226
|
+
(Math.abs(p.exitPoint.x - sourceBoundary.minX) > EPSILON ||
|
|
227
|
+
p.exitPoint.y < center.y),
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
return null
|
|
231
|
+
const byIndex = new Map(
|
|
232
|
+
params.buses.flatMap((b) =>
|
|
233
|
+
b.connections.map(
|
|
234
|
+
(connection) =>
|
|
235
|
+
[connection.connectionIndex, { bus: b, connection }] as const,
|
|
236
|
+
),
|
|
237
|
+
),
|
|
238
|
+
)
|
|
239
|
+
const viaTargetX = bus.sharedBoundary.maxX - Math.max(padPitch / 2, d / 2 + c)
|
|
240
|
+
const viaTraceDistance = d / 2 + w / 2 + c
|
|
241
|
+
const lastColumn = viaTargetX - viaTraceDistance - 1e-5
|
|
242
|
+
const firstColumn = lastColumn - (selected.length - 1) * pitch
|
|
243
|
+
const lastHeight = bus.sharedBoundary.maxY - pitch
|
|
244
|
+
const firstHeight = lastHeight - (selected.length - 1) * pitch
|
|
245
|
+
if (
|
|
246
|
+
firstColumn <= padBounds.maxX + viaTraceDistance ||
|
|
247
|
+
firstHeight <= sourceBoundary.maxY + pitch
|
|
248
|
+
)
|
|
249
|
+
return null
|
|
250
|
+
const makeVia = (
|
|
251
|
+
connection: PreparedConnection,
|
|
252
|
+
point: Point2D,
|
|
253
|
+
): RoutedVia => {
|
|
254
|
+
const owner = byIndex.get(connection.connectionIndex)!.bus
|
|
255
|
+
const toLayer =
|
|
256
|
+
owner.busId === bus.busId
|
|
257
|
+
? params.targetLayer
|
|
258
|
+
: owner.termination.type === "plane"
|
|
259
|
+
? owner.termination.layer
|
|
260
|
+
: (params.targetLayerByBusId?.get(owner.busId) ??
|
|
261
|
+
(owner.allowedLayers ?? params.layerNames).find(
|
|
262
|
+
(l) => l !== connection.sourceLayer,
|
|
263
|
+
))
|
|
264
|
+
if (!toLayer)
|
|
265
|
+
throw new Error(
|
|
266
|
+
"FanoutSolver: missing target layer for peripheral via assignment",
|
|
267
|
+
)
|
|
268
|
+
return {
|
|
269
|
+
center: point,
|
|
270
|
+
diameter: d,
|
|
271
|
+
holeDiameter: params.viaHoleDiameter,
|
|
272
|
+
fromLayer: connection.sourceLayer,
|
|
273
|
+
toLayer,
|
|
274
|
+
spanLayers: getViaSpanLayers({
|
|
275
|
+
fromLayer: connection.sourceLayer,
|
|
276
|
+
toLayer,
|
|
277
|
+
layerNames: params.layerNames,
|
|
278
|
+
allowBlindAndBuriedVias: params.allowBlindAndBuriedVias ?? false,
|
|
279
|
+
}),
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const makeEscape = (
|
|
283
|
+
connection: PreparedConnection,
|
|
284
|
+
points: Point2D[],
|
|
285
|
+
): PeripheralSourceEscape => ({
|
|
286
|
+
connectionIndex: connection.connectionIndex,
|
|
287
|
+
connectionName: connection.connection.name,
|
|
288
|
+
segments: points.slice(1).map((end, i) => ({
|
|
289
|
+
start: points[i]!,
|
|
290
|
+
end,
|
|
291
|
+
width: w,
|
|
292
|
+
layer: connection.sourceLayer,
|
|
293
|
+
})),
|
|
294
|
+
via: makeVia(connection, points.at(-1)!),
|
|
295
|
+
})
|
|
296
|
+
const fixed: PeripheralSourceEscape[] = selected.map((path, i) => {
|
|
297
|
+
const connection = byIndex.get(path.connectionIndex)!.connection,
|
|
298
|
+
exit = target(connection)
|
|
299
|
+
const height = firstHeight + i * pitch,
|
|
300
|
+
column = firstColumn + i * pitch
|
|
301
|
+
return makeEscape(
|
|
302
|
+
connection,
|
|
303
|
+
chamfer(
|
|
304
|
+
[
|
|
305
|
+
path.segments[0]!.start,
|
|
306
|
+
...path.segments.map((s) => s.end),
|
|
307
|
+
{ x: path.exitPoint.x, y: height },
|
|
308
|
+
{ x: column, y: height },
|
|
309
|
+
{ x: column, y: exit.y },
|
|
310
|
+
{ x: viaTargetX, y: exit.y },
|
|
311
|
+
],
|
|
312
|
+
w,
|
|
313
|
+
),
|
|
314
|
+
)
|
|
315
|
+
})
|
|
316
|
+
const remoteConnectionIndices = new Set(fixed.map((p) => p.connectionIndex))
|
|
317
|
+
const clears = (
|
|
318
|
+
escape: PeripheralSourceEscape,
|
|
319
|
+
others: readonly PeripheralSourceEscape[],
|
|
320
|
+
) => {
|
|
321
|
+
const connection = byIndex.get(escape.connectionIndex)!.connection
|
|
322
|
+
if (
|
|
323
|
+
escape.segments.some((segment) =>
|
|
324
|
+
[segment.start, segment.end].some(
|
|
325
|
+
(p) =>
|
|
326
|
+
p.x < bus.sharedBoundary.minX - EPSILON ||
|
|
327
|
+
p.x > bus.sharedBoundary.maxX + EPSILON ||
|
|
328
|
+
p.y < bus.sharedBoundary.minY - EPSILON ||
|
|
329
|
+
p.y > bus.sharedBoundary.maxY + EPSILON,
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
return false
|
|
334
|
+
for (const obstacle of srj.obstacles) {
|
|
335
|
+
if (
|
|
336
|
+
obstacle.layers.some((layer) =>
|
|
337
|
+
escape.via.spanLayers.includes(layer),
|
|
338
|
+
) &&
|
|
339
|
+
distancePointToObstacle(escape.via.center, obstacle) <
|
|
340
|
+
d / 2 + c - EPSILON
|
|
341
|
+
)
|
|
342
|
+
return false
|
|
343
|
+
if (
|
|
344
|
+
obstacle !== connection.sourceObstacle &&
|
|
345
|
+
escape.segments.some(
|
|
346
|
+
(segment) =>
|
|
347
|
+
obstacle.layers.includes(segment.layer) &&
|
|
348
|
+
distanceSegmentToObstacle(segment, obstacle) < w / 2 + c - EPSILON,
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
return false
|
|
352
|
+
}
|
|
353
|
+
for (const other of others) {
|
|
354
|
+
if (escape.connectionIndex === other.connectionIndex) continue
|
|
355
|
+
if (distance(escape.via.center, other.via.center) < d + c - EPSILON)
|
|
356
|
+
return false
|
|
357
|
+
if (
|
|
358
|
+
escape.segments.some(
|
|
359
|
+
(s) =>
|
|
360
|
+
distancePointToSegment(other.via.center, s.start, s.end) <
|
|
361
|
+
viaTraceDistance - EPSILON,
|
|
362
|
+
) ||
|
|
363
|
+
other.segments.some(
|
|
364
|
+
(s) =>
|
|
365
|
+
distancePointToSegment(escape.via.center, s.start, s.end) <
|
|
366
|
+
viaTraceDistance - EPSILON,
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
return false
|
|
370
|
+
if (
|
|
371
|
+
escape.segments.some((a) =>
|
|
372
|
+
other.segments.some((b) => !segmentsAreClear(a, b, c)),
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
return false
|
|
376
|
+
}
|
|
377
|
+
return true
|
|
378
|
+
}
|
|
379
|
+
if (fixed.some((escape) => !clears(escape, fixed))) return null
|
|
380
|
+
const remainingBuses = () => {
|
|
381
|
+
const held = new Set(fixed.map((p) => p.connectionIndex))
|
|
382
|
+
return params.buses
|
|
383
|
+
.map((b) => ({
|
|
384
|
+
...b,
|
|
385
|
+
connections: b.connections.filter(
|
|
386
|
+
(connection) => !held.has(connection.connectionIndex),
|
|
387
|
+
),
|
|
388
|
+
}))
|
|
389
|
+
.filter((b) => b.connections.length)
|
|
390
|
+
}
|
|
391
|
+
const rules = (): DogboneViaSiteGeometryRules => ({
|
|
392
|
+
viaDiameter: d,
|
|
393
|
+
viaHoleDiameter: params.viaHoleDiameter,
|
|
394
|
+
traceWidth: w,
|
|
395
|
+
clearance: c,
|
|
396
|
+
additionalObstacles: srj.obstacles,
|
|
397
|
+
maximumSearchStates: 300_000,
|
|
398
|
+
blockingSegments: fixed.flatMap((p) =>
|
|
399
|
+
p.segments.map((segment) => ({
|
|
400
|
+
connectionIndex: p.connectionIndex,
|
|
401
|
+
segment,
|
|
402
|
+
})),
|
|
403
|
+
),
|
|
404
|
+
blockingVias: fixed.map((p) => ({
|
|
405
|
+
connectionIndex: p.connectionIndex,
|
|
406
|
+
...p.via,
|
|
407
|
+
})),
|
|
408
|
+
preferredViaPointsByConnectionIndex: params.initialViaPoints,
|
|
409
|
+
})
|
|
410
|
+
let remaining = remainingBuses()
|
|
411
|
+
const available = new Set(
|
|
412
|
+
getComponentDogboneViaSiteCandidates(remaining, rules()).map(
|
|
413
|
+
(p) => p.connectionIndex,
|
|
414
|
+
),
|
|
415
|
+
)
|
|
416
|
+
for (const owner of remaining)
|
|
417
|
+
for (const connection of owner.connections) {
|
|
418
|
+
if (available.has(connection.connectionIndex)) continue
|
|
419
|
+
if (owner.termination.type !== "plane") return null
|
|
420
|
+
const source = connection.sourcePoint,
|
|
421
|
+
obstacle = connection.sourceObstacle
|
|
422
|
+
const directions = [
|
|
423
|
+
{ x: 0, y: 1, gap: padBounds.maxY - source.y },
|
|
424
|
+
{ x: -1, y: 0, gap: source.x - padBounds.minX },
|
|
425
|
+
{ x: 0, y: -1, gap: source.y - padBounds.minY },
|
|
426
|
+
{ x: 1, y: 0, gap: padBounds.maxX - source.x },
|
|
427
|
+
].sort((a, b) => a.gap - b.gap)
|
|
428
|
+
let additional: PeripheralSourceEscape | null = null
|
|
429
|
+
for (const direction of directions) {
|
|
430
|
+
for (let step = 0; step < 8; step++) {
|
|
431
|
+
const offset =
|
|
432
|
+
(direction.x ? obstacle.width : obstacle.height) / 2 +
|
|
433
|
+
d / 2 +
|
|
434
|
+
c +
|
|
435
|
+
1e-5 +
|
|
436
|
+
step * w
|
|
437
|
+
const candidate = makeEscape(connection, [
|
|
438
|
+
source,
|
|
439
|
+
{
|
|
440
|
+
x: source.x + direction.x * offset,
|
|
441
|
+
y: source.y + direction.y * offset,
|
|
442
|
+
},
|
|
443
|
+
])
|
|
444
|
+
if (clears(candidate, fixed)) {
|
|
445
|
+
additional = candidate
|
|
446
|
+
break
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (additional) break
|
|
450
|
+
}
|
|
451
|
+
if (!additional) return null
|
|
452
|
+
fixed.push(additional)
|
|
453
|
+
}
|
|
454
|
+
remaining = remainingBuses()
|
|
455
|
+
const geometryRules = rules()
|
|
456
|
+
const initial = matchComponentDogboneViaSites(remaining, geometryRules)
|
|
457
|
+
if (!initial) return null
|
|
458
|
+
yield
|
|
459
|
+
const matched = matchAngularlyOrderedLocalVias({
|
|
460
|
+
buses: remaining,
|
|
461
|
+
busId: bus.busId,
|
|
462
|
+
rules: { ...geometryRules, preferredViaPointsByConnectionIndex: initial },
|
|
463
|
+
})
|
|
464
|
+
if (!matched) return null
|
|
465
|
+
const sourceEscapes = [
|
|
466
|
+
...fixed,
|
|
467
|
+
...remaining.flatMap((owner) =>
|
|
468
|
+
owner.connections.map((connection) =>
|
|
469
|
+
makeEscape(connection, [
|
|
470
|
+
connection.sourcePoint,
|
|
471
|
+
matched.get(connection.connectionIndex)!,
|
|
472
|
+
]),
|
|
473
|
+
),
|
|
474
|
+
),
|
|
475
|
+
]
|
|
476
|
+
if (sourceEscapes.some((escape) => !clears(escape, sourceEscapes)))
|
|
477
|
+
return null
|
|
478
|
+
const viaPointsByConnectionIndex = new Map(
|
|
479
|
+
sourceEscapes.map((escape) => [escape.connectionIndex, escape.via.center]),
|
|
480
|
+
)
|
|
481
|
+
return {
|
|
482
|
+
sourceEscapes,
|
|
483
|
+
viaPointsByConnectionIndex,
|
|
484
|
+
remoteConnectionIndices,
|
|
485
|
+
localBus: remaining.find((b) => b.busId === bus.busId)!,
|
|
486
|
+
sourceBoundary,
|
|
487
|
+
}
|
|
488
|
+
}
|