@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,1179 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Obstacle,
|
|
3
|
+
SimpleRouteJson,
|
|
4
|
+
SimplifiedPcbTrace,
|
|
5
|
+
} from "@tscircuit/capacity-autorouter"
|
|
6
|
+
import {
|
|
7
|
+
distance,
|
|
8
|
+
distancePointToObstacle,
|
|
9
|
+
distanceSegmentToObstacle,
|
|
10
|
+
distanceSegmentToSegment,
|
|
11
|
+
} from "./geometry"
|
|
12
|
+
import type {
|
|
13
|
+
FanoutDirection,
|
|
14
|
+
FanoutRoutePlan,
|
|
15
|
+
Point2D,
|
|
16
|
+
PreparedBus,
|
|
17
|
+
PreparedConnection,
|
|
18
|
+
RoutedSegment,
|
|
19
|
+
} from "./types"
|
|
20
|
+
|
|
21
|
+
interface FlowRoutingParams {
|
|
22
|
+
srj: SimpleRouteJson
|
|
23
|
+
buses: PreparedBus[]
|
|
24
|
+
traceWidth: number
|
|
25
|
+
clearance: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface FlowItem {
|
|
29
|
+
bus: PreparedBus
|
|
30
|
+
connection: PreparedConnection
|
|
31
|
+
source: Point2D
|
|
32
|
+
netKey: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface FlowTerminal {
|
|
36
|
+
item: FlowItem
|
|
37
|
+
equivalentItems: FlowItem[]
|
|
38
|
+
candidates: Array<{ node: number; connectorPoints: Point2D[] }>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface FlowRoute {
|
|
42
|
+
item: FlowItem
|
|
43
|
+
points: Point2D[]
|
|
44
|
+
segments: RoutedSegment[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface DirectionGroupResult {
|
|
48
|
+
routes: FlowRoute[]
|
|
49
|
+
usedNodes: number[]
|
|
50
|
+
unmatchedItems: FlowItem[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface FlowGrid {
|
|
54
|
+
boundary: PreparedBus["sharedBoundary"]
|
|
55
|
+
step: number
|
|
56
|
+
columnCount: number
|
|
57
|
+
rowCount: number
|
|
58
|
+
nodeCount: number
|
|
59
|
+
points: Point2D[]
|
|
60
|
+
obstacleFreeNodes: Uint8Array
|
|
61
|
+
neighbors: number[][]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface FlowEdge {
|
|
65
|
+
to: number
|
|
66
|
+
reverseIndex: number
|
|
67
|
+
capacity: number
|
|
68
|
+
initialCapacity: number
|
|
69
|
+
gridNode?: number
|
|
70
|
+
isSink?: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const EPSILON = 1e-9
|
|
74
|
+
const OBSTACLE_BIN_SIZE = 1
|
|
75
|
+
const FANOUT_FLOW_DEBUG_ENABLED =
|
|
76
|
+
(
|
|
77
|
+
globalThis as typeof globalThis & {
|
|
78
|
+
process?: {
|
|
79
|
+
env?: {
|
|
80
|
+
FANOUT_FLOW_DEBUG?: string
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
).process?.env?.FANOUT_FLOW_DEBUG === "1"
|
|
85
|
+
|
|
86
|
+
function getNetKey(connection: PreparedConnection): string {
|
|
87
|
+
const simpleRouteConnection =
|
|
88
|
+
connection.connection as typeof connection.connection & {
|
|
89
|
+
netConnectionName?: string
|
|
90
|
+
}
|
|
91
|
+
const connectivityNet = connection.sourceObstacle.connectedTo.find(
|
|
92
|
+
(connectionName) => connectionName.startsWith("connectivity_net"),
|
|
93
|
+
)
|
|
94
|
+
return (
|
|
95
|
+
connectivityNet ??
|
|
96
|
+
simpleRouteConnection.netConnectionName ??
|
|
97
|
+
connection.connection.name.replace(/::fanout:\d+$/, "")
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function obstacleBelongsToItem(obstacle: Obstacle, item: FlowItem): boolean {
|
|
102
|
+
return (
|
|
103
|
+
obstacle === item.connection.sourceObstacle ||
|
|
104
|
+
obstacle.connectedTo.includes(item.connection.connection.name) ||
|
|
105
|
+
obstacle.connectedTo.includes(item.netKey)
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function connectWith45DegreeSegments(start: Point2D, end: Point2D): Point2D[] {
|
|
110
|
+
const deltaX = end.x - start.x
|
|
111
|
+
const deltaY = end.y - start.y
|
|
112
|
+
const absoluteX = Math.abs(deltaX)
|
|
113
|
+
const absoluteY = Math.abs(deltaY)
|
|
114
|
+
if (
|
|
115
|
+
absoluteX < EPSILON ||
|
|
116
|
+
absoluteY < EPSILON ||
|
|
117
|
+
Math.abs(absoluteX - absoluteY) < EPSILON
|
|
118
|
+
) {
|
|
119
|
+
return [start, end]
|
|
120
|
+
}
|
|
121
|
+
if (absoluteX > absoluteY) {
|
|
122
|
+
return [
|
|
123
|
+
start,
|
|
124
|
+
{
|
|
125
|
+
x: start.x + Math.sign(deltaX) * absoluteY,
|
|
126
|
+
y: end.y,
|
|
127
|
+
},
|
|
128
|
+
end,
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
return [
|
|
132
|
+
start,
|
|
133
|
+
{
|
|
134
|
+
x: end.x,
|
|
135
|
+
y: start.y + Math.sign(deltaY) * absoluteX,
|
|
136
|
+
},
|
|
137
|
+
end,
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function enforceStraightOr45DegreeSegments(points: Point2D[]): Point2D[] {
|
|
142
|
+
if (points.length < 2) return points
|
|
143
|
+
const normalized = [points[0]!]
|
|
144
|
+
for (const end of points.slice(1)) {
|
|
145
|
+
const start = normalized.at(-1)!
|
|
146
|
+
const deltaX = Math.abs(end.x - start.x)
|
|
147
|
+
const deltaY = Math.abs(end.y - start.y)
|
|
148
|
+
if (
|
|
149
|
+
deltaX < EPSILON ||
|
|
150
|
+
deltaY < EPSILON ||
|
|
151
|
+
Math.abs(deltaX - deltaY) < EPSILON
|
|
152
|
+
) {
|
|
153
|
+
normalized.push(end)
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
normalized.push(
|
|
157
|
+
...connectWith45DegreeSegments(end, start).reverse().slice(1),
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
return compressPath(normalized)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function compressPath(points: Point2D[]): Point2D[] {
|
|
164
|
+
if (points.length < 3) return points
|
|
165
|
+
const compressed = [points[0]!]
|
|
166
|
+
for (let index = 1; index < points.length - 1; index++) {
|
|
167
|
+
const previous = compressed.at(-1)!
|
|
168
|
+
const current = points[index]!
|
|
169
|
+
const next = points[index + 1]!
|
|
170
|
+
if (
|
|
171
|
+
Math.sign(current.x - previous.x) !== Math.sign(next.x - current.x) ||
|
|
172
|
+
Math.sign(current.y - previous.y) !== Math.sign(next.y - current.y)
|
|
173
|
+
) {
|
|
174
|
+
compressed.push(current)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
compressed.push(points.at(-1)!)
|
|
178
|
+
return compressed
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function chamferOrthogonalPolyline(
|
|
182
|
+
points: Point2D[],
|
|
183
|
+
requestedChamfer: number,
|
|
184
|
+
): Point2D[] {
|
|
185
|
+
if (points.length < 3) return points
|
|
186
|
+
const chamfered = [points[0]!]
|
|
187
|
+
for (let index = 1; index < points.length - 1; index++) {
|
|
188
|
+
const previous = points[index - 1]!
|
|
189
|
+
const corner = points[index]!
|
|
190
|
+
const next = points[index + 1]!
|
|
191
|
+
const incomingLength = distance(previous, corner)
|
|
192
|
+
const outgoingLength = distance(corner, next)
|
|
193
|
+
if (incomingLength < EPSILON || outgoingLength < EPSILON) continue
|
|
194
|
+
const incoming = {
|
|
195
|
+
x: (corner.x - previous.x) / incomingLength,
|
|
196
|
+
y: (corner.y - previous.y) / incomingLength,
|
|
197
|
+
}
|
|
198
|
+
const outgoing = {
|
|
199
|
+
x: (next.x - corner.x) / outgoingLength,
|
|
200
|
+
y: (next.y - corner.y) / outgoingLength,
|
|
201
|
+
}
|
|
202
|
+
if (Math.abs(incoming.x * outgoing.x + incoming.y * outgoing.y) > 1e-6) {
|
|
203
|
+
chamfered.push(corner)
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
const chamfer = Math.min(
|
|
207
|
+
requestedChamfer,
|
|
208
|
+
incomingLength / 2,
|
|
209
|
+
outgoingLength / 2,
|
|
210
|
+
)
|
|
211
|
+
chamfered.push({
|
|
212
|
+
x: corner.x - incoming.x * chamfer,
|
|
213
|
+
y: corner.y - incoming.y * chamfer,
|
|
214
|
+
})
|
|
215
|
+
chamfered.push({
|
|
216
|
+
x: corner.x + outgoing.x * chamfer,
|
|
217
|
+
y: corner.y + outgoing.y * chamfer,
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
chamfered.push(points.at(-1)!)
|
|
221
|
+
return compressPath(chamfered)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getSegments(points: Point2D[], traceWidth: number): RoutedSegment[] {
|
|
225
|
+
const segments: RoutedSegment[] = []
|
|
226
|
+
for (let index = 1; index < points.length; index++) {
|
|
227
|
+
if (distance(points[index - 1]!, points[index]!) < EPSILON) continue
|
|
228
|
+
segments.push({
|
|
229
|
+
start: points[index - 1]!,
|
|
230
|
+
end: points[index]!,
|
|
231
|
+
width: traceWidth,
|
|
232
|
+
layer: "top",
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
return segments
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
class Dinic {
|
|
239
|
+
readonly edges: FlowEdge[][]
|
|
240
|
+
private readonly levels: Int32Array
|
|
241
|
+
private readonly nextEdges: Int32Array
|
|
242
|
+
|
|
243
|
+
constructor(nodeCount: number) {
|
|
244
|
+
this.edges = Array.from({ length: nodeCount }, () => [])
|
|
245
|
+
this.levels = new Int32Array(nodeCount)
|
|
246
|
+
this.nextEdges = new Int32Array(nodeCount)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
addEdge(
|
|
250
|
+
from: number,
|
|
251
|
+
to: number,
|
|
252
|
+
capacity: number,
|
|
253
|
+
metadata: Pick<FlowEdge, "gridNode" | "isSink"> = {},
|
|
254
|
+
): void {
|
|
255
|
+
const forward: FlowEdge = {
|
|
256
|
+
to,
|
|
257
|
+
reverseIndex: this.edges[to]!.length,
|
|
258
|
+
capacity,
|
|
259
|
+
initialCapacity: capacity,
|
|
260
|
+
...metadata,
|
|
261
|
+
}
|
|
262
|
+
const reverse: FlowEdge = {
|
|
263
|
+
to: from,
|
|
264
|
+
reverseIndex: this.edges[from]!.length,
|
|
265
|
+
capacity: 0,
|
|
266
|
+
initialCapacity: 0,
|
|
267
|
+
}
|
|
268
|
+
this.edges[from]!.push(forward)
|
|
269
|
+
this.edges[to]!.push(reverse)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private buildLevels(source: number, sink: number): boolean {
|
|
273
|
+
this.levels.fill(-1)
|
|
274
|
+
const queue = new Int32Array(this.edges.length)
|
|
275
|
+
let head = 0
|
|
276
|
+
let tail = 0
|
|
277
|
+
queue[tail++] = source
|
|
278
|
+
this.levels[source] = 0
|
|
279
|
+
while (head < tail) {
|
|
280
|
+
const node = queue[head++]!
|
|
281
|
+
for (const edge of this.edges[node]!) {
|
|
282
|
+
if (edge.capacity <= 0 || this.levels[edge.to] >= 0) continue
|
|
283
|
+
this.levels[edge.to] = this.levels[node]! + 1
|
|
284
|
+
queue[tail++] = edge.to
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return this.levels[sink] >= 0
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private sendFlow(node: number, sink: number): number {
|
|
291
|
+
if (node === sink) return 1
|
|
292
|
+
for (
|
|
293
|
+
let edgeIndex = this.nextEdges[node]!;
|
|
294
|
+
edgeIndex < this.edges[node]!.length;
|
|
295
|
+
edgeIndex++, this.nextEdges[node] = edgeIndex
|
|
296
|
+
) {
|
|
297
|
+
const edge = this.edges[node]![edgeIndex]!
|
|
298
|
+
if (
|
|
299
|
+
edge.capacity <= 0 ||
|
|
300
|
+
this.levels[edge.to] !== this.levels[node]! + 1
|
|
301
|
+
) {
|
|
302
|
+
continue
|
|
303
|
+
}
|
|
304
|
+
const sent = this.sendFlow(edge.to, sink)
|
|
305
|
+
if (sent === 0) continue
|
|
306
|
+
edge.capacity -= sent
|
|
307
|
+
this.edges[edge.to]![edge.reverseIndex]!.capacity += sent
|
|
308
|
+
return sent
|
|
309
|
+
}
|
|
310
|
+
return 0
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
maximumFlow(source: number, sink: number, limit: number): number {
|
|
314
|
+
let flow = 0
|
|
315
|
+
while (flow < limit && this.buildLevels(source, sink)) {
|
|
316
|
+
this.nextEdges.fill(0)
|
|
317
|
+
while (flow < limit) {
|
|
318
|
+
const sent = this.sendFlow(source, sink)
|
|
319
|
+
if (sent === 0) break
|
|
320
|
+
flow += sent
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return flow
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function createFlowGrid(params: {
|
|
328
|
+
boundary: PreparedBus["sharedBoundary"]
|
|
329
|
+
obstacles: Obstacle[]
|
|
330
|
+
traceWidth: number
|
|
331
|
+
clearance: number
|
|
332
|
+
}): FlowGrid {
|
|
333
|
+
const { boundary, obstacles, traceWidth, clearance } = params
|
|
334
|
+
const step = traceWidth + clearance
|
|
335
|
+
const columnCount = Math.round((boundary.maxX - boundary.minX) / step) + 1
|
|
336
|
+
const rowCount = Math.round((boundary.maxY - boundary.minY) / step) + 1
|
|
337
|
+
const nodeCount = columnCount * rowCount
|
|
338
|
+
const points = Array.from({ length: nodeCount }, (_, node) => ({
|
|
339
|
+
x: boundary.minX + (node % columnCount) * step,
|
|
340
|
+
y: boundary.minY + Math.floor(node / columnCount) * step,
|
|
341
|
+
}))
|
|
342
|
+
const requiredObstacleDistance = traceWidth / 2 + clearance
|
|
343
|
+
const obstacleIndexesByBin = new Map<string, number[]>()
|
|
344
|
+
for (
|
|
345
|
+
let obstacleIndex = 0;
|
|
346
|
+
obstacleIndex < obstacles.length;
|
|
347
|
+
obstacleIndex++
|
|
348
|
+
) {
|
|
349
|
+
const obstacle = obstacles[obstacleIndex]!
|
|
350
|
+
const minBinX = Math.floor(
|
|
351
|
+
(obstacle.center.x - obstacle.width / 2 - requiredObstacleDistance) /
|
|
352
|
+
OBSTACLE_BIN_SIZE,
|
|
353
|
+
)
|
|
354
|
+
const maxBinX = Math.floor(
|
|
355
|
+
(obstacle.center.x + obstacle.width / 2 + requiredObstacleDistance) /
|
|
356
|
+
OBSTACLE_BIN_SIZE,
|
|
357
|
+
)
|
|
358
|
+
const minBinY = Math.floor(
|
|
359
|
+
(obstacle.center.y - obstacle.height / 2 - requiredObstacleDistance) /
|
|
360
|
+
OBSTACLE_BIN_SIZE,
|
|
361
|
+
)
|
|
362
|
+
const maxBinY = Math.floor(
|
|
363
|
+
(obstacle.center.y + obstacle.height / 2 + requiredObstacleDistance) /
|
|
364
|
+
OBSTACLE_BIN_SIZE,
|
|
365
|
+
)
|
|
366
|
+
for (let binX = minBinX; binX <= maxBinX; binX++) {
|
|
367
|
+
for (let binY = minBinY; binY <= maxBinY; binY++) {
|
|
368
|
+
const key = `${binX}:${binY}`
|
|
369
|
+
const indexes = obstacleIndexesByBin.get(key) ?? []
|
|
370
|
+
indexes.push(obstacleIndex)
|
|
371
|
+
obstacleIndexesByBin.set(key, indexes)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const getNearbyObstacles = (first: Point2D, second = first): Obstacle[] => {
|
|
376
|
+
const minBinX = Math.floor(Math.min(first.x, second.x) / OBSTACLE_BIN_SIZE)
|
|
377
|
+
const maxBinX = Math.floor(Math.max(first.x, second.x) / OBSTACLE_BIN_SIZE)
|
|
378
|
+
const minBinY = Math.floor(Math.min(first.y, second.y) / OBSTACLE_BIN_SIZE)
|
|
379
|
+
const maxBinY = Math.floor(Math.max(first.y, second.y) / OBSTACLE_BIN_SIZE)
|
|
380
|
+
const indexes = new Set<number>()
|
|
381
|
+
for (let binX = minBinX; binX <= maxBinX; binX++) {
|
|
382
|
+
for (let binY = minBinY; binY <= maxBinY; binY++) {
|
|
383
|
+
for (const obstacleIndex of obstacleIndexesByBin.get(
|
|
384
|
+
`${binX}:${binY}`,
|
|
385
|
+
) ?? []) {
|
|
386
|
+
indexes.add(obstacleIndex)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return [...indexes].map((index) => obstacles[index]!)
|
|
391
|
+
}
|
|
392
|
+
const obstacleFreeNodes = new Uint8Array(nodeCount)
|
|
393
|
+
for (let node = 0; node < nodeCount; node++) {
|
|
394
|
+
const point = points[node]!
|
|
395
|
+
if (
|
|
396
|
+
getNearbyObstacles(point).every(
|
|
397
|
+
(obstacle) =>
|
|
398
|
+
distancePointToObstacle(point, obstacle) >=
|
|
399
|
+
requiredObstacleDistance - EPSILON,
|
|
400
|
+
)
|
|
401
|
+
) {
|
|
402
|
+
obstacleFreeNodes[node] = 1
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const neighbors: number[][] = Array.from({ length: nodeCount }, () => [])
|
|
406
|
+
for (let node = 0; node < nodeCount; node++) {
|
|
407
|
+
if (!obstacleFreeNodes[node]) continue
|
|
408
|
+
const column = node % columnCount
|
|
409
|
+
const row = Math.floor(node / columnCount)
|
|
410
|
+
for (const [deltaColumn, deltaRow] of [
|
|
411
|
+
[1, 0],
|
|
412
|
+
[0, 1],
|
|
413
|
+
] as const) {
|
|
414
|
+
const nextColumn = column + deltaColumn
|
|
415
|
+
const nextRow = row + deltaRow
|
|
416
|
+
if (nextColumn >= columnCount || nextRow >= rowCount) continue
|
|
417
|
+
const nextNode = nextRow * columnCount + nextColumn
|
|
418
|
+
if (!obstacleFreeNodes[nextNode]) continue
|
|
419
|
+
const segment: RoutedSegment = {
|
|
420
|
+
start: points[node]!,
|
|
421
|
+
end: points[nextNode]!,
|
|
422
|
+
width: traceWidth,
|
|
423
|
+
layer: "top",
|
|
424
|
+
}
|
|
425
|
+
if (
|
|
426
|
+
getNearbyObstacles(segment.start, segment.end).some(
|
|
427
|
+
(obstacle) =>
|
|
428
|
+
distanceSegmentToObstacle(segment, obstacle) <
|
|
429
|
+
requiredObstacleDistance - EPSILON,
|
|
430
|
+
)
|
|
431
|
+
) {
|
|
432
|
+
continue
|
|
433
|
+
}
|
|
434
|
+
neighbors[node]!.push(nextNode)
|
|
435
|
+
neighbors[nextNode]!.push(node)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
boundary,
|
|
440
|
+
step,
|
|
441
|
+
columnCount,
|
|
442
|
+
rowCount,
|
|
443
|
+
nodeCount,
|
|
444
|
+
points,
|
|
445
|
+
obstacleFreeNodes,
|
|
446
|
+
neighbors,
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function routeDirectionGroup(params: {
|
|
451
|
+
direction: FanoutDirection | "any"
|
|
452
|
+
items: FlowItem[]
|
|
453
|
+
grid: FlowGrid
|
|
454
|
+
obstacles: Obstacle[]
|
|
455
|
+
traceWidth: number
|
|
456
|
+
clearance: number
|
|
457
|
+
occupiedNodes: Uint8Array
|
|
458
|
+
acceptedSegments: RoutedSegment[]
|
|
459
|
+
connectorSelectionOffset?: number
|
|
460
|
+
}): DirectionGroupResult | null {
|
|
461
|
+
const {
|
|
462
|
+
direction,
|
|
463
|
+
items,
|
|
464
|
+
grid,
|
|
465
|
+
obstacles,
|
|
466
|
+
traceWidth,
|
|
467
|
+
clearance,
|
|
468
|
+
occupiedNodes,
|
|
469
|
+
acceptedSegments,
|
|
470
|
+
connectorSelectionOffset = 0,
|
|
471
|
+
} = params
|
|
472
|
+
if (items.length === 0) {
|
|
473
|
+
return { routes: [], usedNodes: [], unmatchedItems: [] }
|
|
474
|
+
}
|
|
475
|
+
const {
|
|
476
|
+
boundary,
|
|
477
|
+
step,
|
|
478
|
+
columnCount,
|
|
479
|
+
rowCount,
|
|
480
|
+
nodeCount: gridNodeCount,
|
|
481
|
+
} = grid
|
|
482
|
+
const pointForNode = (node: number): Point2D => grid.points[node]!
|
|
483
|
+
const requiredObstacleDistance = traceWidth / 2 + clearance
|
|
484
|
+
const requiredRouteDistance = traceWidth + clearance
|
|
485
|
+
const freeNodes = new Uint8Array(gridNodeCount)
|
|
486
|
+
for (let node = 0; node < gridNodeCount; node++) {
|
|
487
|
+
if (!occupiedNodes[node] && grid.obstacleFreeNodes[node]) {
|
|
488
|
+
freeNodes[node] = 1
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const connectorIsClear = (points: Point2D[], item: FlowItem) => {
|
|
492
|
+
const segments = getSegments(points, traceWidth)
|
|
493
|
+
return (
|
|
494
|
+
segments.every((segment) =>
|
|
495
|
+
obstacles.every(
|
|
496
|
+
(obstacle) =>
|
|
497
|
+
obstacleBelongsToItem(obstacle, item) ||
|
|
498
|
+
distanceSegmentToObstacle(segment, obstacle) >=
|
|
499
|
+
requiredObstacleDistance - EPSILON,
|
|
500
|
+
),
|
|
501
|
+
) &&
|
|
502
|
+
segments.every((segment) =>
|
|
503
|
+
acceptedSegments.every(
|
|
504
|
+
(acceptedSegment) =>
|
|
505
|
+
distanceSegmentToSegment(
|
|
506
|
+
segment.start,
|
|
507
|
+
segment.end,
|
|
508
|
+
acceptedSegment.start,
|
|
509
|
+
acceptedSegment.end,
|
|
510
|
+
) >=
|
|
511
|
+
requiredRouteDistance - EPSILON,
|
|
512
|
+
),
|
|
513
|
+
)
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const equivalentItemsByKey = new Map<string, FlowItem[]>()
|
|
518
|
+
for (const item of items) {
|
|
519
|
+
const key = `${item.source.x.toFixed(6)}:${item.source.y.toFixed(6)}:${item.netKey}`
|
|
520
|
+
const equivalents = equivalentItemsByKey.get(key) ?? []
|
|
521
|
+
equivalents.push(item)
|
|
522
|
+
equivalentItemsByKey.set(key, equivalents)
|
|
523
|
+
}
|
|
524
|
+
const offsetCandidates = Array.from({ length: 61 * 61 }, (_, index) => {
|
|
525
|
+
const x = (index % 61) - 30
|
|
526
|
+
const y = Math.floor(index / 61) - 30
|
|
527
|
+
return { x, y, distanceSquared: x * x + y * y }
|
|
528
|
+
}).sort((first, second) => first.distanceSquared - second.distanceSquared)
|
|
529
|
+
const terminals: FlowTerminal[] = []
|
|
530
|
+
for (const equivalentItems of equivalentItemsByKey.values()) {
|
|
531
|
+
const item = equivalentItems[0]!
|
|
532
|
+
const maxConnectorLength =
|
|
533
|
+
Math.hypot(
|
|
534
|
+
item.connection.sourceObstacle.width / 2,
|
|
535
|
+
item.connection.sourceObstacle.height / 2,
|
|
536
|
+
) +
|
|
537
|
+
clearance +
|
|
538
|
+
step * 2
|
|
539
|
+
const sourceColumn = Math.round((item.source.x - boundary.minX) / step)
|
|
540
|
+
const sourceRow = Math.round((item.source.y - boundary.minY) / step)
|
|
541
|
+
const candidates: FlowTerminal["candidates"] = []
|
|
542
|
+
const candidateNodes = new Set<number>()
|
|
543
|
+
for (const offset of offsetCandidates) {
|
|
544
|
+
const column = sourceColumn + offset.x
|
|
545
|
+
const row = sourceRow + offset.y
|
|
546
|
+
if (column < 0 || column >= columnCount || row < 0 || row >= rowCount) {
|
|
547
|
+
continue
|
|
548
|
+
}
|
|
549
|
+
const node = row * columnCount + column
|
|
550
|
+
if (!freeNodes[node] || candidateNodes.has(node)) continue
|
|
551
|
+
if (distance(item.source, pointForNode(node)) > maxConnectorLength) {
|
|
552
|
+
continue
|
|
553
|
+
}
|
|
554
|
+
const connectorPoints = connectWith45DegreeSegments(
|
|
555
|
+
item.source,
|
|
556
|
+
pointForNode(node),
|
|
557
|
+
)
|
|
558
|
+
if (!connectorIsClear(connectorPoints, item)) continue
|
|
559
|
+
candidateNodes.add(node)
|
|
560
|
+
candidates.push({ node, connectorPoints })
|
|
561
|
+
if (candidates.length >= 12) break
|
|
562
|
+
}
|
|
563
|
+
if (candidates.length === 0) return null
|
|
564
|
+
terminals.push({ item, equivalentItems, candidates })
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const selectedConnectorSegments: Array<{
|
|
568
|
+
netKey: string
|
|
569
|
+
segments: RoutedSegment[]
|
|
570
|
+
}> = []
|
|
571
|
+
for (const terminal of [...terminals].sort(
|
|
572
|
+
(first, second) => first.candidates.length - second.candidates.length,
|
|
573
|
+
)) {
|
|
574
|
+
const candidateOffset =
|
|
575
|
+
terminal.candidates.length === 0
|
|
576
|
+
? 0
|
|
577
|
+
: connectorSelectionOffset % terminal.candidates.length
|
|
578
|
+
const orderedCandidates = [
|
|
579
|
+
...terminal.candidates.slice(candidateOffset),
|
|
580
|
+
...terminal.candidates.slice(0, candidateOffset),
|
|
581
|
+
]
|
|
582
|
+
const candidate = orderedCandidates.find((value) => {
|
|
583
|
+
const segments = getSegments(value.connectorPoints, traceWidth)
|
|
584
|
+
return selectedConnectorSegments.every(
|
|
585
|
+
(selected) =>
|
|
586
|
+
selected.netKey === terminal.item.netKey ||
|
|
587
|
+
segments.every((segment) =>
|
|
588
|
+
selected.segments.every(
|
|
589
|
+
(otherSegment) =>
|
|
590
|
+
distanceSegmentToSegment(
|
|
591
|
+
segment.start,
|
|
592
|
+
segment.end,
|
|
593
|
+
otherSegment.start,
|
|
594
|
+
otherSegment.end,
|
|
595
|
+
) >=
|
|
596
|
+
requiredRouteDistance - EPSILON,
|
|
597
|
+
),
|
|
598
|
+
),
|
|
599
|
+
)
|
|
600
|
+
})
|
|
601
|
+
if (!candidate) return null
|
|
602
|
+
terminal.candidates = [candidate]
|
|
603
|
+
selectedConnectorSegments.push({
|
|
604
|
+
netKey: terminal.item.netKey,
|
|
605
|
+
segments: getSegments(candidate.connectorPoints, traceWidth),
|
|
606
|
+
})
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const terminalNodes = new Set(
|
|
610
|
+
terminals.map((terminal) => terminal.candidates[0]!.node),
|
|
611
|
+
)
|
|
612
|
+
for (const selected of selectedConnectorSegments) {
|
|
613
|
+
for (const segment of selected.segments) {
|
|
614
|
+
const minColumn = Math.max(
|
|
615
|
+
0,
|
|
616
|
+
Math.floor(
|
|
617
|
+
(Math.min(segment.start.x, segment.end.x) -
|
|
618
|
+
requiredRouteDistance -
|
|
619
|
+
boundary.minX) /
|
|
620
|
+
step,
|
|
621
|
+
),
|
|
622
|
+
)
|
|
623
|
+
const maxColumn = Math.min(
|
|
624
|
+
columnCount - 1,
|
|
625
|
+
Math.ceil(
|
|
626
|
+
(Math.max(segment.start.x, segment.end.x) +
|
|
627
|
+
requiredRouteDistance -
|
|
628
|
+
boundary.minX) /
|
|
629
|
+
step,
|
|
630
|
+
),
|
|
631
|
+
)
|
|
632
|
+
const minRow = Math.max(
|
|
633
|
+
0,
|
|
634
|
+
Math.floor(
|
|
635
|
+
(Math.min(segment.start.y, segment.end.y) -
|
|
636
|
+
requiredRouteDistance -
|
|
637
|
+
boundary.minY) /
|
|
638
|
+
step,
|
|
639
|
+
),
|
|
640
|
+
)
|
|
641
|
+
const maxRow = Math.min(
|
|
642
|
+
rowCount - 1,
|
|
643
|
+
Math.ceil(
|
|
644
|
+
(Math.max(segment.start.y, segment.end.y) +
|
|
645
|
+
requiredRouteDistance -
|
|
646
|
+
boundary.minY) /
|
|
647
|
+
step,
|
|
648
|
+
),
|
|
649
|
+
)
|
|
650
|
+
for (let row = minRow; row <= maxRow; row++) {
|
|
651
|
+
for (let column = minColumn; column <= maxColumn; column++) {
|
|
652
|
+
const node = row * columnCount + column
|
|
653
|
+
if (!freeNodes[node] || terminalNodes.has(node)) continue
|
|
654
|
+
const point = pointForNode(node)
|
|
655
|
+
if (
|
|
656
|
+
distanceSegmentToSegment(segment.start, segment.end, point, point) <
|
|
657
|
+
requiredRouteDistance - EPSILON
|
|
658
|
+
) {
|
|
659
|
+
freeNodes[node] = 0
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const source = 0
|
|
667
|
+
const terminalStart = 1
|
|
668
|
+
const gridInStart = terminalStart + terminals.length
|
|
669
|
+
const gridOutStart = gridInStart + gridNodeCount
|
|
670
|
+
const sink = gridOutStart + gridNodeCount
|
|
671
|
+
const flow = new Dinic(sink + 1)
|
|
672
|
+
for (
|
|
673
|
+
let terminalIndex = 0;
|
|
674
|
+
terminalIndex < terminals.length;
|
|
675
|
+
terminalIndex++
|
|
676
|
+
) {
|
|
677
|
+
const terminalNode = terminalStart + terminalIndex
|
|
678
|
+
flow.addEdge(source, terminalNode, 1)
|
|
679
|
+
for (const candidate of terminals[terminalIndex]!.candidates) {
|
|
680
|
+
flow.addEdge(terminalNode, gridInStart + candidate.node, 1, {
|
|
681
|
+
gridNode: candidate.node,
|
|
682
|
+
})
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
for (let node = 0; node < gridNodeCount; node++) {
|
|
686
|
+
if (!freeNodes[node]) continue
|
|
687
|
+
flow.addEdge(gridInStart + node, gridOutStart + node, 1)
|
|
688
|
+
const column = node % columnCount
|
|
689
|
+
const row = Math.floor(node / columnCount)
|
|
690
|
+
for (const nextNode of grid.neighbors[node]!) {
|
|
691
|
+
if (!freeNodes[nextNode]) continue
|
|
692
|
+
flow.addEdge(gridOutStart + node, gridInStart + nextNode, 1, {
|
|
693
|
+
gridNode: nextNode,
|
|
694
|
+
})
|
|
695
|
+
}
|
|
696
|
+
const isTarget =
|
|
697
|
+
(direction === "any" &&
|
|
698
|
+
(column === 0 ||
|
|
699
|
+
column === columnCount - 1 ||
|
|
700
|
+
row === 0 ||
|
|
701
|
+
row === rowCount - 1)) ||
|
|
702
|
+
(direction === "left" && column === 0) ||
|
|
703
|
+
(direction === "right" && column === columnCount - 1) ||
|
|
704
|
+
(direction === "down" && row === 0) ||
|
|
705
|
+
(direction === "up" && row === rowCount - 1)
|
|
706
|
+
if (isTarget) {
|
|
707
|
+
flow.addEdge(gridOutStart + node, sink, 1, { isSink: true })
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
const achievedFlow = flow.maximumFlow(source, sink, terminals.length)
|
|
711
|
+
const terminalWasMatched = (terminalIndex: number) => {
|
|
712
|
+
const terminalNode = terminalStart + terminalIndex
|
|
713
|
+
return flow.edges[source]!.some(
|
|
714
|
+
(edge) =>
|
|
715
|
+
edge.to === terminalNode &&
|
|
716
|
+
edge.initialCapacity === 1 &&
|
|
717
|
+
edge.capacity === 0,
|
|
718
|
+
)
|
|
719
|
+
}
|
|
720
|
+
if (achievedFlow !== terminals.length) {
|
|
721
|
+
if (FANOUT_FLOW_DEBUG_ENABLED) {
|
|
722
|
+
const unmatchedConnections = terminals.flatMap((terminal, index) => {
|
|
723
|
+
const terminalNode = terminalStart + index
|
|
724
|
+
return terminalWasMatched(index)
|
|
725
|
+
? []
|
|
726
|
+
: [terminal.item.connection.connection.name]
|
|
727
|
+
})
|
|
728
|
+
console.error("single-layer flow group failed", {
|
|
729
|
+
direction,
|
|
730
|
+
achievedFlow,
|
|
731
|
+
requiredFlow: terminals.length,
|
|
732
|
+
unmatchedConnections,
|
|
733
|
+
})
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const routes: FlowRoute[] = []
|
|
738
|
+
const usedNodes: number[] = []
|
|
739
|
+
const unmatchedItems: FlowItem[] = []
|
|
740
|
+
for (
|
|
741
|
+
let terminalIndex = 0;
|
|
742
|
+
terminalIndex < terminals.length;
|
|
743
|
+
terminalIndex++
|
|
744
|
+
) {
|
|
745
|
+
const terminal = terminals[terminalIndex]!
|
|
746
|
+
if (!terminalWasMatched(terminalIndex)) {
|
|
747
|
+
unmatchedItems.push(...terminal.equivalentItems)
|
|
748
|
+
continue
|
|
749
|
+
}
|
|
750
|
+
const terminalNode = terminalStart + terminalIndex
|
|
751
|
+
const candidateEdge = flow.edges[terminalNode]!.find(
|
|
752
|
+
(edge) =>
|
|
753
|
+
edge.initialCapacity === 1 &&
|
|
754
|
+
edge.capacity === 0 &&
|
|
755
|
+
edge.gridNode !== undefined,
|
|
756
|
+
)
|
|
757
|
+
if (candidateEdge?.gridNode === undefined) return null
|
|
758
|
+
const candidate = terminal.candidates.find(
|
|
759
|
+
(value) => value.node === candidateEdge.gridNode,
|
|
760
|
+
)
|
|
761
|
+
if (!candidate) return null
|
|
762
|
+
const gridPoints: Point2D[] = [pointForNode(candidate.node)]
|
|
763
|
+
let node = candidate.node
|
|
764
|
+
const routeNodes = [node]
|
|
765
|
+
for (let guard = 0; guard <= gridNodeCount; guard++) {
|
|
766
|
+
const outNode = gridOutStart + node
|
|
767
|
+
const nextEdge = flow.edges[outNode]!.find(
|
|
768
|
+
(edge) =>
|
|
769
|
+
edge.initialCapacity === 1 &&
|
|
770
|
+
edge.capacity === 0 &&
|
|
771
|
+
(edge.gridNode !== undefined || edge.isSink),
|
|
772
|
+
)
|
|
773
|
+
if (!nextEdge) return null
|
|
774
|
+
if (nextEdge.isSink) break
|
|
775
|
+
if (nextEdge.gridNode === undefined) return null
|
|
776
|
+
node = nextEdge.gridNode
|
|
777
|
+
routeNodes.push(node)
|
|
778
|
+
gridPoints.push(pointForNode(node))
|
|
779
|
+
}
|
|
780
|
+
usedNodes.push(...routeNodes)
|
|
781
|
+
const unchamferedPoints = compressPath([
|
|
782
|
+
...candidate.connectorPoints,
|
|
783
|
+
...gridPoints.slice(1),
|
|
784
|
+
])
|
|
785
|
+
const points = enforceStraightOr45DegreeSegments(
|
|
786
|
+
chamferOrthogonalPolyline(unchamferedPoints, step / 2),
|
|
787
|
+
)
|
|
788
|
+
const segments = getSegments(points, traceWidth)
|
|
789
|
+
for (const item of terminal.equivalentItems) {
|
|
790
|
+
routes.push({ item, points, segments })
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return { routes, usedNodes, unmatchedItems }
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function buildPlan(route: FlowRoute, traceWidth: number): FanoutRoutePlan {
|
|
797
|
+
const { item, points, segments } = route
|
|
798
|
+
const traceRoute: SimplifiedPcbTrace["route"] = points.map(
|
|
799
|
+
(point, index) => ({
|
|
800
|
+
route_type: "wire",
|
|
801
|
+
x: point.x,
|
|
802
|
+
y: point.y,
|
|
803
|
+
width: traceWidth,
|
|
804
|
+
layer: "top",
|
|
805
|
+
...(index === 0 && item.connection.sourcePoint.pcb_port_id
|
|
806
|
+
? { start_pcb_port_id: item.connection.sourcePoint.pcb_port_id }
|
|
807
|
+
: {}),
|
|
808
|
+
}),
|
|
809
|
+
)
|
|
810
|
+
return {
|
|
811
|
+
busId: item.bus.busId,
|
|
812
|
+
connectionName: item.connection.connection.name,
|
|
813
|
+
connectionIndex: item.connection.connectionIndex,
|
|
814
|
+
sourcePointIndex: item.connection.sourcePointIndex,
|
|
815
|
+
sourcePoint: item.connection.sourcePoint,
|
|
816
|
+
sourceObstacle: item.connection.sourceObstacle,
|
|
817
|
+
sourceLayer: item.connection.sourceLayer,
|
|
818
|
+
targetLayer: "top",
|
|
819
|
+
termination: item.bus.termination,
|
|
820
|
+
direction: item.bus.direction,
|
|
821
|
+
exitPoint: points.at(-1)!,
|
|
822
|
+
trace: {
|
|
823
|
+
type: "pcb_trace",
|
|
824
|
+
pcb_trace_id: `fanout:${item.connection.connection.name}`,
|
|
825
|
+
connection_name: item.connection.connection.name,
|
|
826
|
+
connectsTo: [
|
|
827
|
+
item.connection.connection.name,
|
|
828
|
+
item.netKey,
|
|
829
|
+
...(item.connection.sourcePoint.pointId
|
|
830
|
+
? [item.connection.sourcePoint.pointId]
|
|
831
|
+
: []),
|
|
832
|
+
...(item.connection.sourcePoint.pcb_port_id
|
|
833
|
+
? [item.connection.sourcePoint.pcb_port_id]
|
|
834
|
+
: []),
|
|
835
|
+
],
|
|
836
|
+
route: traceRoute,
|
|
837
|
+
},
|
|
838
|
+
segments,
|
|
839
|
+
length: segments.reduce(
|
|
840
|
+
(total, segment) => total + distance(segment.start, segment.end),
|
|
841
|
+
0,
|
|
842
|
+
),
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function getConnectorVariants(start: Point2D, end: Point2D): Point2D[][] {
|
|
847
|
+
const first = connectWith45DegreeSegments(start, end)
|
|
848
|
+
const second = connectWith45DegreeSegments(end, start).reverse()
|
|
849
|
+
return first.length === second.length &&
|
|
850
|
+
first.every((point, index) => distance(point, second[index]!) < EPSILON)
|
|
851
|
+
? [first]
|
|
852
|
+
: [first, second]
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function plansHaveRequiredClearance(params: {
|
|
856
|
+
plans: FanoutRoutePlan[]
|
|
857
|
+
items: FlowItem[]
|
|
858
|
+
obstacles: Obstacle[]
|
|
859
|
+
traceWidth: number
|
|
860
|
+
clearance: number
|
|
861
|
+
}): boolean {
|
|
862
|
+
const { plans, items, obstacles, traceWidth, clearance } = params
|
|
863
|
+
const netKeyByConnectionName = new Map(
|
|
864
|
+
items.map((item) => [item.connection.connection.name, item.netKey]),
|
|
865
|
+
)
|
|
866
|
+
const uniqueSegmentsByNet = new Map<string, RoutedSegment[]>()
|
|
867
|
+
const segmentKeysByNet = new Map<string, Set<string>>()
|
|
868
|
+
for (const plan of plans) {
|
|
869
|
+
const netKey = netKeyByConnectionName.get(plan.connectionName)!
|
|
870
|
+
const segments = uniqueSegmentsByNet.get(netKey) ?? []
|
|
871
|
+
const segmentKeys = segmentKeysByNet.get(netKey) ?? new Set<string>()
|
|
872
|
+
for (const segment of plan.segments) {
|
|
873
|
+
const endpoints = [segment.start, segment.end]
|
|
874
|
+
.map((point) => `${point.x.toFixed(6)}:${point.y.toFixed(6)}`)
|
|
875
|
+
.sort()
|
|
876
|
+
const key = endpoints.join(":")
|
|
877
|
+
if (segmentKeys.has(key)) continue
|
|
878
|
+
segmentKeys.add(key)
|
|
879
|
+
segments.push(segment)
|
|
880
|
+
}
|
|
881
|
+
uniqueSegmentsByNet.set(netKey, segments)
|
|
882
|
+
segmentKeysByNet.set(netKey, segmentKeys)
|
|
883
|
+
}
|
|
884
|
+
const requiredObstacleDistance = traceWidth / 2 + clearance
|
|
885
|
+
for (const [netKey, segments] of uniqueSegmentsByNet) {
|
|
886
|
+
for (const segment of segments) {
|
|
887
|
+
for (const obstacle of obstacles) {
|
|
888
|
+
if (
|
|
889
|
+
obstacle.connectedTo.includes(netKey) ||
|
|
890
|
+
distanceSegmentToObstacle(segment, obstacle) >=
|
|
891
|
+
requiredObstacleDistance - EPSILON
|
|
892
|
+
) {
|
|
893
|
+
continue
|
|
894
|
+
}
|
|
895
|
+
if (FANOUT_FLOW_DEBUG_ENABLED) {
|
|
896
|
+
console.error("single-layer route violates obstacle clearance", {
|
|
897
|
+
netKey,
|
|
898
|
+
segment,
|
|
899
|
+
obstacleId: obstacle.obstacleId,
|
|
900
|
+
distance: distanceSegmentToObstacle(segment, obstacle),
|
|
901
|
+
requiredObstacleDistance,
|
|
902
|
+
})
|
|
903
|
+
}
|
|
904
|
+
return false
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
const requiredRouteDistance = traceWidth + clearance
|
|
909
|
+
const entries = [...uniqueSegmentsByNet]
|
|
910
|
+
for (let firstIndex = 0; firstIndex < entries.length; firstIndex++) {
|
|
911
|
+
for (
|
|
912
|
+
let secondIndex = firstIndex + 1;
|
|
913
|
+
secondIndex < entries.length;
|
|
914
|
+
secondIndex++
|
|
915
|
+
) {
|
|
916
|
+
for (const first of entries[firstIndex]![1]) {
|
|
917
|
+
for (const second of entries[secondIndex]![1]) {
|
|
918
|
+
if (
|
|
919
|
+
distanceSegmentToSegment(
|
|
920
|
+
first.start,
|
|
921
|
+
first.end,
|
|
922
|
+
second.start,
|
|
923
|
+
second.end,
|
|
924
|
+
) <
|
|
925
|
+
requiredRouteDistance - EPSILON
|
|
926
|
+
) {
|
|
927
|
+
if (FANOUT_FLOW_DEBUG_ENABLED) {
|
|
928
|
+
console.error("single-layer routes violate copper clearance", {
|
|
929
|
+
firstNetKey: entries[firstIndex]![0],
|
|
930
|
+
secondNetKey: entries[secondIndex]![0],
|
|
931
|
+
first,
|
|
932
|
+
second,
|
|
933
|
+
distance: distanceSegmentToSegment(
|
|
934
|
+
first.start,
|
|
935
|
+
first.end,
|
|
936
|
+
second.start,
|
|
937
|
+
second.end,
|
|
938
|
+
),
|
|
939
|
+
requiredRouteDistance,
|
|
940
|
+
})
|
|
941
|
+
}
|
|
942
|
+
return false
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return true
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function getDirectionForBoundaryPoint(
|
|
952
|
+
point: Point2D,
|
|
953
|
+
boundary: PreparedBus["sharedBoundary"],
|
|
954
|
+
): FanoutDirection | null {
|
|
955
|
+
if (Math.abs(point.x - boundary.minX) < EPSILON) return "left"
|
|
956
|
+
if (Math.abs(point.x - boundary.maxX) < EPSILON) return "right"
|
|
957
|
+
if (Math.abs(point.y - boundary.minY) < EPSILON) return "down"
|
|
958
|
+
if (Math.abs(point.y - boundary.maxY) < EPSILON) return "up"
|
|
959
|
+
return null
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function routeWithAdaptiveExits(params: {
|
|
963
|
+
items: FlowItem[]
|
|
964
|
+
grid: FlowGrid
|
|
965
|
+
obstacles: Obstacle[]
|
|
966
|
+
traceWidth: number
|
|
967
|
+
clearance: number
|
|
968
|
+
}): FanoutRoutePlan[] | null {
|
|
969
|
+
const { items, grid, obstacles, traceWidth, clearance } = params
|
|
970
|
+
const mergeItems = new Set(
|
|
971
|
+
items.filter(
|
|
972
|
+
(item) =>
|
|
973
|
+
item.connection.sourceObstacle.width > 2 &&
|
|
974
|
+
item.connection.sourceObstacle.height > 2,
|
|
975
|
+
),
|
|
976
|
+
)
|
|
977
|
+
let unrestricted: ReturnType<typeof routeDirectionGroup> = null
|
|
978
|
+
for (let mergeRound = 0; mergeRound < 4; mergeRound++) {
|
|
979
|
+
const directlyRoutedItems = items.filter((item) => !mergeItems.has(item))
|
|
980
|
+
let bestResult: DirectionGroupResult | null = null
|
|
981
|
+
for (
|
|
982
|
+
let connectorSelectionOffset = 0;
|
|
983
|
+
connectorSelectionOffset < 4;
|
|
984
|
+
connectorSelectionOffset++
|
|
985
|
+
) {
|
|
986
|
+
const result = routeDirectionGroup({
|
|
987
|
+
direction: "any",
|
|
988
|
+
items: directlyRoutedItems,
|
|
989
|
+
grid,
|
|
990
|
+
obstacles,
|
|
991
|
+
traceWidth,
|
|
992
|
+
clearance,
|
|
993
|
+
occupiedNodes: new Uint8Array(grid.nodeCount),
|
|
994
|
+
acceptedSegments: [],
|
|
995
|
+
connectorSelectionOffset,
|
|
996
|
+
})
|
|
997
|
+
if (!result) continue
|
|
998
|
+
if (!bestResult || result.routes.length > bestResult.routes.length) {
|
|
999
|
+
bestResult = result
|
|
1000
|
+
}
|
|
1001
|
+
if (result.routes.length === directlyRoutedItems.length) break
|
|
1002
|
+
}
|
|
1003
|
+
if (!bestResult) return null
|
|
1004
|
+
if (bestResult.routes.length === directlyRoutedItems.length) {
|
|
1005
|
+
unrestricted = bestResult
|
|
1006
|
+
break
|
|
1007
|
+
}
|
|
1008
|
+
const directlyRoutedNetCounts = new Map<string, number>()
|
|
1009
|
+
for (const item of directlyRoutedItems) {
|
|
1010
|
+
directlyRoutedNetCounts.set(
|
|
1011
|
+
item.netKey,
|
|
1012
|
+
(directlyRoutedNetCounts.get(item.netKey) ?? 0) + 1,
|
|
1013
|
+
)
|
|
1014
|
+
}
|
|
1015
|
+
let addedMergeItem = false
|
|
1016
|
+
for (const item of bestResult.unmatchedItems) {
|
|
1017
|
+
if ((directlyRoutedNetCounts.get(item.netKey) ?? 0) < 2) continue
|
|
1018
|
+
mergeItems.add(item)
|
|
1019
|
+
addedMergeItem = true
|
|
1020
|
+
}
|
|
1021
|
+
if (!addedMergeItem) return null
|
|
1022
|
+
}
|
|
1023
|
+
if (!unrestricted) return null
|
|
1024
|
+
const routes = [...unrestricted.routes]
|
|
1025
|
+
const requiredObstacleDistance = traceWidth / 2 + clearance
|
|
1026
|
+
const requiredRouteDistance = traceWidth + clearance
|
|
1027
|
+
for (const mergeItem of mergeItems) {
|
|
1028
|
+
const candidates = routes
|
|
1029
|
+
.filter((route) => route.item.netKey === mergeItem.netKey)
|
|
1030
|
+
.flatMap((route) =>
|
|
1031
|
+
route.points.map((point, pointIndex) => ({
|
|
1032
|
+
route,
|
|
1033
|
+
point,
|
|
1034
|
+
pointIndex,
|
|
1035
|
+
})),
|
|
1036
|
+
)
|
|
1037
|
+
.sort(
|
|
1038
|
+
(first, second) =>
|
|
1039
|
+
Number(
|
|
1040
|
+
second.route.item.bus.componentId === mergeItem.bus.componentId,
|
|
1041
|
+
) -
|
|
1042
|
+
Number(
|
|
1043
|
+
first.route.item.bus.componentId === mergeItem.bus.componentId,
|
|
1044
|
+
) ||
|
|
1045
|
+
distance(mergeItem.source, first.point) -
|
|
1046
|
+
distance(mergeItem.source, second.point),
|
|
1047
|
+
)
|
|
1048
|
+
let mergedRoute: FlowRoute | null = null
|
|
1049
|
+
for (const candidate of candidates) {
|
|
1050
|
+
for (const connectorPoints of getConnectorVariants(
|
|
1051
|
+
mergeItem.source,
|
|
1052
|
+
candidate.point,
|
|
1053
|
+
)) {
|
|
1054
|
+
const connectorSegments = getSegments(connectorPoints, traceWidth)
|
|
1055
|
+
const clearsObstacles = connectorSegments.every((segment) =>
|
|
1056
|
+
obstacles.every(
|
|
1057
|
+
(obstacle) =>
|
|
1058
|
+
obstacleBelongsToItem(obstacle, mergeItem) ||
|
|
1059
|
+
distanceSegmentToObstacle(segment, obstacle) >=
|
|
1060
|
+
requiredObstacleDistance - EPSILON,
|
|
1061
|
+
),
|
|
1062
|
+
)
|
|
1063
|
+
const clearsRoutes = connectorSegments.every((segment) =>
|
|
1064
|
+
routes.every(
|
|
1065
|
+
(route) =>
|
|
1066
|
+
route.item.netKey === mergeItem.netKey ||
|
|
1067
|
+
route.segments.every(
|
|
1068
|
+
(otherSegment) =>
|
|
1069
|
+
distanceSegmentToSegment(
|
|
1070
|
+
segment.start,
|
|
1071
|
+
segment.end,
|
|
1072
|
+
otherSegment.start,
|
|
1073
|
+
otherSegment.end,
|
|
1074
|
+
) >=
|
|
1075
|
+
requiredRouteDistance - EPSILON,
|
|
1076
|
+
),
|
|
1077
|
+
),
|
|
1078
|
+
)
|
|
1079
|
+
if (!clearsObstacles || !clearsRoutes) continue
|
|
1080
|
+
const points = enforceStraightOr45DegreeSegments(
|
|
1081
|
+
compressPath([
|
|
1082
|
+
...connectorPoints,
|
|
1083
|
+
...candidate.route.points.slice(candidate.pointIndex + 1),
|
|
1084
|
+
]),
|
|
1085
|
+
)
|
|
1086
|
+
mergedRoute = {
|
|
1087
|
+
item: mergeItem,
|
|
1088
|
+
points,
|
|
1089
|
+
segments: getSegments(points, traceWidth),
|
|
1090
|
+
}
|
|
1091
|
+
break
|
|
1092
|
+
}
|
|
1093
|
+
if (mergedRoute) break
|
|
1094
|
+
}
|
|
1095
|
+
if (!mergedRoute) return null
|
|
1096
|
+
routes.push(mergedRoute)
|
|
1097
|
+
}
|
|
1098
|
+
const plans = routes.map((route) => buildPlan(route, traceWidth))
|
|
1099
|
+
if (
|
|
1100
|
+
!plansHaveRequiredClearance({
|
|
1101
|
+
plans,
|
|
1102
|
+
items,
|
|
1103
|
+
obstacles,
|
|
1104
|
+
traceWidth,
|
|
1105
|
+
clearance,
|
|
1106
|
+
})
|
|
1107
|
+
) {
|
|
1108
|
+
if (FANOUT_FLOW_DEBUG_ENABLED) {
|
|
1109
|
+
console.error("single-layer adaptive exits failed exact clearance")
|
|
1110
|
+
}
|
|
1111
|
+
return null
|
|
1112
|
+
}
|
|
1113
|
+
for (const plan of plans) {
|
|
1114
|
+
const direction = getDirectionForBoundaryPoint(
|
|
1115
|
+
plan.exitPoint,
|
|
1116
|
+
grid.boundary,
|
|
1117
|
+
)
|
|
1118
|
+
if (!direction) return null
|
|
1119
|
+
const item = items.find(
|
|
1120
|
+
(candidate) =>
|
|
1121
|
+
candidate.connection.connection.name === plan.connectionName,
|
|
1122
|
+
)!
|
|
1123
|
+
item.bus.direction = direction
|
|
1124
|
+
item.bus.preferredExit =
|
|
1125
|
+
direction === "up" ? "top" : direction === "down" ? "bottom" : direction
|
|
1126
|
+
plan.direction = direction
|
|
1127
|
+
}
|
|
1128
|
+
const planByConnectionName = new Map(
|
|
1129
|
+
plans.map((plan) => [plan.connectionName, plan]),
|
|
1130
|
+
)
|
|
1131
|
+
return items.map(
|
|
1132
|
+
(item) => planByConnectionName.get(item.connection.connection.name)!,
|
|
1133
|
+
)
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
export function routeSingleLayerWithAdaptiveExits(
|
|
1137
|
+
params: FlowRoutingParams,
|
|
1138
|
+
): FanoutRoutePlan[] | null {
|
|
1139
|
+
const { srj, buses, traceWidth, clearance } = params
|
|
1140
|
+
if (buses.some((bus) => bus.connections.length !== 1)) return null
|
|
1141
|
+
const items = buses.flatMap((bus) =>
|
|
1142
|
+
bus.connections.map(
|
|
1143
|
+
(connection): FlowItem => ({
|
|
1144
|
+
bus,
|
|
1145
|
+
connection,
|
|
1146
|
+
source: {
|
|
1147
|
+
x: connection.sourcePoint.x,
|
|
1148
|
+
y: connection.sourcePoint.y,
|
|
1149
|
+
},
|
|
1150
|
+
netKey: getNetKey(connection),
|
|
1151
|
+
}),
|
|
1152
|
+
),
|
|
1153
|
+
)
|
|
1154
|
+
const topObstacles = srj.obstacles.filter((obstacle) =>
|
|
1155
|
+
obstacle.layers.includes("top"),
|
|
1156
|
+
)
|
|
1157
|
+
const boundary = buses[0]?.sharedBoundary
|
|
1158
|
+
if (!boundary) return []
|
|
1159
|
+
const grid = createFlowGrid({
|
|
1160
|
+
boundary,
|
|
1161
|
+
obstacles: topObstacles,
|
|
1162
|
+
traceWidth,
|
|
1163
|
+
clearance,
|
|
1164
|
+
})
|
|
1165
|
+
if (FANOUT_FLOW_DEBUG_ENABLED) {
|
|
1166
|
+
console.error("single-layer adaptive-exit grid ready", {
|
|
1167
|
+
nodeCount: grid.nodeCount,
|
|
1168
|
+
})
|
|
1169
|
+
}
|
|
1170
|
+
const adaptivePlans = routeWithAdaptiveExits({
|
|
1171
|
+
items,
|
|
1172
|
+
grid,
|
|
1173
|
+
obstacles: topObstacles,
|
|
1174
|
+
traceWidth,
|
|
1175
|
+
clearance,
|
|
1176
|
+
})
|
|
1177
|
+
if (adaptivePlans) return adaptivePlans
|
|
1178
|
+
return null
|
|
1179
|
+
}
|