@tscircuit/fanout-solver 0.0.36 → 0.0.37
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/build-output.ts +32 -10
- package/lib/fanout-solver.ts +214 -30
- package/lib/prepare-buses.ts +6 -2
- package/lib/route-bus.ts +378 -81
- package/lib/route-via-minimal-winding.ts +898 -0
- package/lib/types.ts +14 -3
- package/lib/validate-fanout-solution.ts +12 -10
- package/package.json +1 -1
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SimpleRouteJson,
|
|
3
|
+
SimplifiedPcbTrace,
|
|
4
|
+
} from "@tscircuit/capacity-autorouter"
|
|
5
|
+
import { getCornerBandSide, getDirectionForExitEdge } from "./boundary-exit"
|
|
6
|
+
import { createFanoutOutputIds } from "./fanout-output-ids"
|
|
7
|
+
import {
|
|
8
|
+
distance,
|
|
9
|
+
distancePointToSegment,
|
|
10
|
+
distanceSegmentToObstacle,
|
|
11
|
+
distanceSegmentToSegment,
|
|
12
|
+
} from "./geometry"
|
|
13
|
+
import { getAllRoutedTraceCopper } from "./get-routed-trace-copper"
|
|
14
|
+
import { getLayerSpan } from "./layer-names"
|
|
15
|
+
import {
|
|
16
|
+
connectionsShareElectricalNet,
|
|
17
|
+
obstacleSharesElectricalNet,
|
|
18
|
+
} from "./net-identity"
|
|
19
|
+
import type {
|
|
20
|
+
FanoutRoutePlan,
|
|
21
|
+
Point2D,
|
|
22
|
+
PreparedBus,
|
|
23
|
+
PreparedConnection,
|
|
24
|
+
RoutedSegment,
|
|
25
|
+
RoutedVia,
|
|
26
|
+
} from "./types"
|
|
27
|
+
|
|
28
|
+
const EPSILON = 1e-7
|
|
29
|
+
const MAX_GRID_NODE_COUNT = 120_000
|
|
30
|
+
const MAX_EXPANDED_STATE_COUNT = 240_000
|
|
31
|
+
const MAX_CONNECTOR_COUNT = 24
|
|
32
|
+
const CONNECTOR_RADIUS_IN_STEPS = 3.25
|
|
33
|
+
|
|
34
|
+
export interface ViaMinimalWindingTerminal {
|
|
35
|
+
connection: PreparedConnection
|
|
36
|
+
viaPoint: Point2D
|
|
37
|
+
exitPoint: Point2D
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RouteViaMinimalWindingParams {
|
|
41
|
+
srj: SimpleRouteJson
|
|
42
|
+
bus: PreparedBus
|
|
43
|
+
targetLayer: string
|
|
44
|
+
terminals: ViaMinimalWindingTerminal[]
|
|
45
|
+
acceptedPlans: FanoutRoutePlan[]
|
|
46
|
+
layerNames: string[]
|
|
47
|
+
traceWidth: number
|
|
48
|
+
viaDiameter: number
|
|
49
|
+
viaHoleDiameter: number
|
|
50
|
+
clearance: number
|
|
51
|
+
allowSameNetMerges?: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface GridNode {
|
|
55
|
+
point: Point2D
|
|
56
|
+
column: number
|
|
57
|
+
row: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface ConnectorCandidate {
|
|
61
|
+
nodeIndex: number
|
|
62
|
+
points: Point2D[]
|
|
63
|
+
radialDistance: number
|
|
64
|
+
length: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface BlockingSegment {
|
|
68
|
+
connectionName: string
|
|
69
|
+
segment: RoutedSegment
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface BlockingVia {
|
|
73
|
+
connectionName: string
|
|
74
|
+
via: Pick<RoutedVia, "center" | "diameter" | "spanLayers">
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface HeapEntry {
|
|
78
|
+
node: number
|
|
79
|
+
direction: number
|
|
80
|
+
score: number
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class MinHeap {
|
|
84
|
+
private values: HeapEntry[] = []
|
|
85
|
+
|
|
86
|
+
get size(): number {
|
|
87
|
+
return this.values.length
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
push(value: HeapEntry): void {
|
|
91
|
+
this.values.push(value)
|
|
92
|
+
let index = this.values.length - 1
|
|
93
|
+
while (index > 0) {
|
|
94
|
+
const parent = Math.floor((index - 1) / 2)
|
|
95
|
+
if (this.values[parent]!.score <= value.score) break
|
|
96
|
+
this.values[index] = this.values[parent]!
|
|
97
|
+
index = parent
|
|
98
|
+
}
|
|
99
|
+
this.values[index] = value
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
pop(): HeapEntry | undefined {
|
|
103
|
+
const result = this.values[0]
|
|
104
|
+
const last = this.values.pop()
|
|
105
|
+
if (!result || !last || this.values.length === 0) return result
|
|
106
|
+
let index = 0
|
|
107
|
+
while (true) {
|
|
108
|
+
const left = index * 2 + 1
|
|
109
|
+
const right = left + 1
|
|
110
|
+
if (left >= this.values.length) break
|
|
111
|
+
const child =
|
|
112
|
+
right < this.values.length &&
|
|
113
|
+
this.values[right]!.score < this.values[left]!.score
|
|
114
|
+
? right
|
|
115
|
+
: left
|
|
116
|
+
if (this.values[child]!.score >= last.score) break
|
|
117
|
+
this.values[index] = this.values[child]!
|
|
118
|
+
index = child
|
|
119
|
+
}
|
|
120
|
+
this.values[index] = last
|
|
121
|
+
return result
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getPerpendicularAxis(
|
|
126
|
+
point: Point2D,
|
|
127
|
+
direction: PreparedBus["direction"],
|
|
128
|
+
): number {
|
|
129
|
+
return direction === "left" || direction === "right" ? point.y : point.x
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getConnectorVariants(start: Point2D, end: Point2D): Point2D[][] {
|
|
133
|
+
const deltaX = end.x - start.x
|
|
134
|
+
const deltaY = end.y - start.y
|
|
135
|
+
const absoluteX = Math.abs(deltaX)
|
|
136
|
+
const absoluteY = Math.abs(deltaY)
|
|
137
|
+
if (
|
|
138
|
+
absoluteX < EPSILON ||
|
|
139
|
+
absoluteY < EPSILON ||
|
|
140
|
+
Math.abs(absoluteX - absoluteY) < EPSILON
|
|
141
|
+
) {
|
|
142
|
+
return [[start, end]]
|
|
143
|
+
}
|
|
144
|
+
if (absoluteX > absoluteY) {
|
|
145
|
+
return [
|
|
146
|
+
[start, { x: start.x + Math.sign(deltaX) * absoluteY, y: end.y }, end],
|
|
147
|
+
[start, { x: end.x - Math.sign(deltaX) * absoluteY, y: start.y }, end],
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
return [
|
|
151
|
+
[start, { x: end.x, y: start.y + Math.sign(deltaY) * absoluteX }, end],
|
|
152
|
+
[start, { x: start.x, y: end.y - Math.sign(deltaY) * absoluteX }, end],
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function compressPath(points: Point2D[]): Point2D[] {
|
|
157
|
+
if (points.length < 3) return points
|
|
158
|
+
const compressed = [points[0]!]
|
|
159
|
+
for (let index = 1; index < points.length - 1; index++) {
|
|
160
|
+
const previous = compressed.at(-1)!
|
|
161
|
+
const current = points[index]!
|
|
162
|
+
const next = points[index + 1]!
|
|
163
|
+
const incomingX = Math.sign(current.x - previous.x)
|
|
164
|
+
const incomingY = Math.sign(current.y - previous.y)
|
|
165
|
+
const outgoingX = Math.sign(next.x - current.x)
|
|
166
|
+
const outgoingY = Math.sign(next.y - current.y)
|
|
167
|
+
if (incomingX !== outgoingX || incomingY !== outgoingY) {
|
|
168
|
+
compressed.push(current)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
compressed.push(points.at(-1)!)
|
|
172
|
+
return compressed
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getSegments(
|
|
176
|
+
points: readonly Point2D[],
|
|
177
|
+
width: number,
|
|
178
|
+
layer: string,
|
|
179
|
+
): RoutedSegment[] {
|
|
180
|
+
return points.slice(1).flatMap((point, index) => {
|
|
181
|
+
const start = points[index]!
|
|
182
|
+
return distance(start, point) < EPSILON
|
|
183
|
+
? []
|
|
184
|
+
: [{ start, end: point, width, layer }]
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function segmentIsStraightOr45Degrees(segment: RoutedSegment): boolean {
|
|
189
|
+
const deltaX = Math.abs(segment.end.x - segment.start.x)
|
|
190
|
+
const deltaY = Math.abs(segment.end.y - segment.start.y)
|
|
191
|
+
return (
|
|
192
|
+
deltaX < EPSILON || deltaY < EPSILON || Math.abs(deltaX - deltaY) < EPSILON
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function pathHasNoProperSelfCrossing(segments: RoutedSegment[]): boolean {
|
|
197
|
+
for (let firstIndex = 0; firstIndex < segments.length; firstIndex++) {
|
|
198
|
+
for (
|
|
199
|
+
let secondIndex = firstIndex + 2;
|
|
200
|
+
secondIndex < segments.length;
|
|
201
|
+
secondIndex++
|
|
202
|
+
) {
|
|
203
|
+
if (
|
|
204
|
+
firstIndex === 0 &&
|
|
205
|
+
secondIndex === segments.length - 1 &&
|
|
206
|
+
distance(segments[firstIndex]!.start, segments[secondIndex]!.end) <
|
|
207
|
+
EPSILON
|
|
208
|
+
) {
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
if (
|
|
212
|
+
distanceSegmentToSegment(
|
|
213
|
+
segments[firstIndex]!.start,
|
|
214
|
+
segments[firstIndex]!.end,
|
|
215
|
+
segments[secondIndex]!.start,
|
|
216
|
+
segments[secondIndex]!.end,
|
|
217
|
+
) < EPSILON
|
|
218
|
+
) {
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return true
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getPlanVias(plan: FanoutRoutePlan): RoutedVia[] {
|
|
227
|
+
return [
|
|
228
|
+
plan.via,
|
|
229
|
+
...(plan.additionalVias ?? []),
|
|
230
|
+
plan.planeEndpointVia,
|
|
231
|
+
].filter((via): via is RoutedVia => Boolean(via))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function getBlockingCopper(params: {
|
|
235
|
+
srj: SimpleRouteJson
|
|
236
|
+
acceptedPlans: readonly FanoutRoutePlan[]
|
|
237
|
+
}): { segments: BlockingSegment[]; vias: BlockingVia[] } {
|
|
238
|
+
const { srj, acceptedPlans } = params
|
|
239
|
+
const routedTraceCopper = getAllRoutedTraceCopper(srj)
|
|
240
|
+
return {
|
|
241
|
+
segments: [
|
|
242
|
+
...routedTraceCopper.flatMap((copper) =>
|
|
243
|
+
copper.segments.map((segment) => ({
|
|
244
|
+
connectionName: copper.connectionName,
|
|
245
|
+
segment,
|
|
246
|
+
})),
|
|
247
|
+
),
|
|
248
|
+
...acceptedPlans.flatMap((plan) =>
|
|
249
|
+
[...plan.segments, ...(plan.planeEndpointSegments ?? [])].map(
|
|
250
|
+
(segment) => ({
|
|
251
|
+
connectionName: plan.connectionName,
|
|
252
|
+
segment,
|
|
253
|
+
}),
|
|
254
|
+
),
|
|
255
|
+
),
|
|
256
|
+
],
|
|
257
|
+
vias: [
|
|
258
|
+
...routedTraceCopper.flatMap((copper) =>
|
|
259
|
+
copper.vias.map((via) => ({
|
|
260
|
+
connectionName: copper.connectionName,
|
|
261
|
+
via,
|
|
262
|
+
})),
|
|
263
|
+
),
|
|
264
|
+
...acceptedPlans.flatMap((plan) =>
|
|
265
|
+
getPlanVias(plan).map((via) => ({
|
|
266
|
+
connectionName: plan.connectionName,
|
|
267
|
+
via,
|
|
268
|
+
})),
|
|
269
|
+
),
|
|
270
|
+
],
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function buildPlan(params: {
|
|
275
|
+
bus: PreparedBus
|
|
276
|
+
terminal: ViaMinimalWindingTerminal
|
|
277
|
+
targetLayer: string
|
|
278
|
+
targetLayerPoints: Point2D[]
|
|
279
|
+
layerNames: string[]
|
|
280
|
+
traceWidth: number
|
|
281
|
+
viaDiameter: number
|
|
282
|
+
viaHoleDiameter: number
|
|
283
|
+
}): FanoutRoutePlan {
|
|
284
|
+
const {
|
|
285
|
+
bus,
|
|
286
|
+
terminal,
|
|
287
|
+
targetLayer,
|
|
288
|
+
targetLayerPoints,
|
|
289
|
+
layerNames,
|
|
290
|
+
traceWidth,
|
|
291
|
+
viaDiameter,
|
|
292
|
+
viaHoleDiameter,
|
|
293
|
+
} = params
|
|
294
|
+
const connection = terminal.connection
|
|
295
|
+
const sourcePoint = {
|
|
296
|
+
x: connection.sourcePoint.x,
|
|
297
|
+
y: connection.sourcePoint.y,
|
|
298
|
+
}
|
|
299
|
+
const sourceSegment: RoutedSegment = {
|
|
300
|
+
start: sourcePoint,
|
|
301
|
+
end: terminal.viaPoint,
|
|
302
|
+
width: traceWidth,
|
|
303
|
+
layer: connection.sourceLayer,
|
|
304
|
+
}
|
|
305
|
+
const targetSegments = getSegments(targetLayerPoints, traceWidth, targetLayer)
|
|
306
|
+
const spanLayers = getLayerSpan(
|
|
307
|
+
connection.sourceLayer,
|
|
308
|
+
targetLayer,
|
|
309
|
+
layerNames,
|
|
310
|
+
)
|
|
311
|
+
const via: RoutedVia = {
|
|
312
|
+
center: terminal.viaPoint,
|
|
313
|
+
diameter: viaDiameter,
|
|
314
|
+
holeDiameter: viaHoleDiameter,
|
|
315
|
+
fromLayer: connection.sourceLayer,
|
|
316
|
+
toLayer: targetLayer,
|
|
317
|
+
spanLayers,
|
|
318
|
+
}
|
|
319
|
+
const route: SimplifiedPcbTrace["route"] = [
|
|
320
|
+
{
|
|
321
|
+
route_type: "wire",
|
|
322
|
+
...sourcePoint,
|
|
323
|
+
width: traceWidth,
|
|
324
|
+
layer: connection.sourceLayer,
|
|
325
|
+
...(connection.sourcePoint.pcb_port_id
|
|
326
|
+
? { start_pcb_port_id: connection.sourcePoint.pcb_port_id }
|
|
327
|
+
: {}),
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
route_type: "wire",
|
|
331
|
+
...terminal.viaPoint,
|
|
332
|
+
width: traceWidth,
|
|
333
|
+
layer: connection.sourceLayer,
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
route_type: "via",
|
|
337
|
+
...terminal.viaPoint,
|
|
338
|
+
from_layer: connection.sourceLayer,
|
|
339
|
+
to_layer: targetLayer,
|
|
340
|
+
via_diameter: viaDiameter,
|
|
341
|
+
via_hole_diameter: viaHoleDiameter,
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
route_type: "wire",
|
|
345
|
+
...terminal.viaPoint,
|
|
346
|
+
width: traceWidth,
|
|
347
|
+
layer: targetLayer,
|
|
348
|
+
},
|
|
349
|
+
...targetLayerPoints.slice(1).map((point) => ({
|
|
350
|
+
route_type: "wire" as const,
|
|
351
|
+
...point,
|
|
352
|
+
width: traceWidth,
|
|
353
|
+
layer: targetLayer,
|
|
354
|
+
})),
|
|
355
|
+
]
|
|
356
|
+
const outputIds = createFanoutOutputIds({
|
|
357
|
+
connectionName: connection.connection.name,
|
|
358
|
+
sourcePointIndex: connection.sourcePointIndex,
|
|
359
|
+
})
|
|
360
|
+
const segments = [sourceSegment, ...targetSegments]
|
|
361
|
+
const cornerBandSide = getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
362
|
+
return {
|
|
363
|
+
busId: bus.busId,
|
|
364
|
+
connectionName: connection.connection.name,
|
|
365
|
+
connectionIndex: connection.connectionIndex,
|
|
366
|
+
sourcePointIndex: connection.sourcePointIndex,
|
|
367
|
+
sourcePoint: connection.sourcePoint,
|
|
368
|
+
sourceObstacle: connection.sourceObstacle,
|
|
369
|
+
sourceLayer: connection.sourceLayer,
|
|
370
|
+
targetPoint: connection.targetPoint,
|
|
371
|
+
targetLayer,
|
|
372
|
+
termination: bus.termination,
|
|
373
|
+
direction: bus.direction,
|
|
374
|
+
...(bus.exitEdge ? { exitEdge: bus.exitEdge } : {}),
|
|
375
|
+
...(cornerBandSide ? { cornerBandSide } : {}),
|
|
376
|
+
exitPoint: terminal.exitPoint,
|
|
377
|
+
trace: {
|
|
378
|
+
type: "pcb_trace",
|
|
379
|
+
pcb_trace_id: outputIds.traceId,
|
|
380
|
+
connection_name: connection.connection.name,
|
|
381
|
+
connectsTo: [
|
|
382
|
+
...(connection.sourcePoint.pointId
|
|
383
|
+
? [connection.sourcePoint.pointId]
|
|
384
|
+
: []),
|
|
385
|
+
...(connection.sourcePoint.pcb_port_id
|
|
386
|
+
? [connection.sourcePoint.pcb_port_id]
|
|
387
|
+
: []),
|
|
388
|
+
outputIds.boundaryExitPointId,
|
|
389
|
+
],
|
|
390
|
+
route,
|
|
391
|
+
},
|
|
392
|
+
segments,
|
|
393
|
+
via,
|
|
394
|
+
length: segments.reduce(
|
|
395
|
+
(total, segment) => total + distance(segment.start, segment.end),
|
|
396
|
+
0,
|
|
397
|
+
),
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function routeViaMinimalWinding(
|
|
402
|
+
params: RouteViaMinimalWindingParams,
|
|
403
|
+
): FanoutRoutePlan[] | null {
|
|
404
|
+
const {
|
|
405
|
+
srj,
|
|
406
|
+
bus,
|
|
407
|
+
targetLayer,
|
|
408
|
+
terminals,
|
|
409
|
+
acceptedPlans,
|
|
410
|
+
layerNames,
|
|
411
|
+
traceWidth,
|
|
412
|
+
viaDiameter,
|
|
413
|
+
viaHoleDiameter,
|
|
414
|
+
clearance,
|
|
415
|
+
allowSameNetMerges = false,
|
|
416
|
+
} = params
|
|
417
|
+
if (
|
|
418
|
+
terminals.length === 0 ||
|
|
419
|
+
!bus.exitEdge ||
|
|
420
|
+
terminals.some(
|
|
421
|
+
(terminal) => terminal.connection.sourceLayer === targetLayer,
|
|
422
|
+
)
|
|
423
|
+
) {
|
|
424
|
+
return null
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const gridStep = traceWidth + clearance
|
|
428
|
+
if (!Number.isFinite(gridStep) || gridStep <= 0) return null
|
|
429
|
+
const { minX, maxX, minY, maxY } = bus.sharedBoundary
|
|
430
|
+
const columnCount = Math.floor((maxX - minX) / gridStep) + 1
|
|
431
|
+
const rowCount = Math.floor((maxY - minY) / gridStep) + 1
|
|
432
|
+
const nodeCount = columnCount * rowCount
|
|
433
|
+
if (columnCount < 2 || rowCount < 2 || nodeCount > MAX_GRID_NODE_COUNT) {
|
|
434
|
+
return null
|
|
435
|
+
}
|
|
436
|
+
const nodes: GridNode[] = Array.from({ length: nodeCount }, (_, index) => {
|
|
437
|
+
const column = index % columnCount
|
|
438
|
+
const row = Math.floor(index / columnCount)
|
|
439
|
+
return {
|
|
440
|
+
column,
|
|
441
|
+
row,
|
|
442
|
+
point: { x: minX + column * gridStep, y: minY + row * gridStep },
|
|
443
|
+
}
|
|
444
|
+
})
|
|
445
|
+
const targetLayerObstacles = srj.obstacles.filter((obstacle) =>
|
|
446
|
+
obstacle.layers.includes(targetLayer),
|
|
447
|
+
)
|
|
448
|
+
const blockingCopper = getBlockingCopper({ srj, acceptedPlans })
|
|
449
|
+
const blockingSegments = blockingCopper.segments.filter(({ segment }) => {
|
|
450
|
+
if (segment.layer !== targetLayer) return false
|
|
451
|
+
const margin = (segment.width + traceWidth) / 2 + clearance
|
|
452
|
+
return !(
|
|
453
|
+
Math.max(segment.start.x, segment.end.x) < minX - margin ||
|
|
454
|
+
Math.min(segment.start.x, segment.end.x) > maxX + margin ||
|
|
455
|
+
Math.max(segment.start.y, segment.end.y) < minY - margin ||
|
|
456
|
+
Math.min(segment.start.y, segment.end.y) > maxY + margin
|
|
457
|
+
)
|
|
458
|
+
})
|
|
459
|
+
const blockingVias = blockingCopper.vias.filter(({ via }) => {
|
|
460
|
+
if (!via.spanLayers.includes(targetLayer)) return false
|
|
461
|
+
const margin = via.diameter / 2 + traceWidth / 2 + clearance
|
|
462
|
+
return !(
|
|
463
|
+
via.center.x < minX - margin ||
|
|
464
|
+
via.center.x > maxX + margin ||
|
|
465
|
+
via.center.y < minY - margin ||
|
|
466
|
+
via.center.y > maxY + margin
|
|
467
|
+
)
|
|
468
|
+
})
|
|
469
|
+
const terminalVias: BlockingVia[] = terminals.map((terminal) => ({
|
|
470
|
+
connectionName: terminal.connection.connection.name,
|
|
471
|
+
via: {
|
|
472
|
+
center: terminal.viaPoint,
|
|
473
|
+
diameter: viaDiameter,
|
|
474
|
+
spanLayers: getLayerSpan(
|
|
475
|
+
terminal.connection.sourceLayer,
|
|
476
|
+
targetLayer,
|
|
477
|
+
layerNames,
|
|
478
|
+
),
|
|
479
|
+
},
|
|
480
|
+
}))
|
|
481
|
+
const boundaryDirection = getDirectionForExitEdge(bus.exitEdge)
|
|
482
|
+
const sharesNet = (first: string, second: string): boolean =>
|
|
483
|
+
first === second ||
|
|
484
|
+
(allowSameNetMerges && connectionsShareElectricalNet(srj, first, second))
|
|
485
|
+
|
|
486
|
+
const segmentIsClear = (params: {
|
|
487
|
+
segment: RoutedSegment
|
|
488
|
+
terminal: ViaMinimalWindingTerminal
|
|
489
|
+
acceptedAttemptSegments: BlockingSegment[]
|
|
490
|
+
}): boolean => {
|
|
491
|
+
const { segment, terminal, acceptedAttemptSegments } = params
|
|
492
|
+
const connectionName = terminal.connection.connection.name
|
|
493
|
+
for (const obstacle of targetLayerObstacles) {
|
|
494
|
+
if (
|
|
495
|
+
obstacle.connectedTo.includes(connectionName) ||
|
|
496
|
+
(allowSameNetMerges &&
|
|
497
|
+
obstacleSharesElectricalNet(srj, obstacle, connectionName))
|
|
498
|
+
) {
|
|
499
|
+
continue
|
|
500
|
+
}
|
|
501
|
+
if (
|
|
502
|
+
distanceSegmentToObstacle(segment, obstacle) <
|
|
503
|
+
segment.width / 2 + clearance - EPSILON
|
|
504
|
+
) {
|
|
505
|
+
return false
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
for (const blocker of blockingSegments) {
|
|
509
|
+
if (sharesNet(connectionName, blocker.connectionName)) continue
|
|
510
|
+
if (
|
|
511
|
+
distanceSegmentToSegment(
|
|
512
|
+
segment.start,
|
|
513
|
+
segment.end,
|
|
514
|
+
blocker.segment.start,
|
|
515
|
+
blocker.segment.end,
|
|
516
|
+
) <
|
|
517
|
+
(segment.width + blocker.segment.width) / 2 + clearance - EPSILON
|
|
518
|
+
) {
|
|
519
|
+
return false
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
for (const blocker of acceptedAttemptSegments) {
|
|
523
|
+
if (sharesNet(connectionName, blocker.connectionName)) continue
|
|
524
|
+
if (
|
|
525
|
+
distanceSegmentToSegment(
|
|
526
|
+
segment.start,
|
|
527
|
+
segment.end,
|
|
528
|
+
blocker.segment.start,
|
|
529
|
+
blocker.segment.end,
|
|
530
|
+
) <
|
|
531
|
+
(segment.width + blocker.segment.width) / 2 + clearance - EPSILON
|
|
532
|
+
) {
|
|
533
|
+
return false
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
for (const blocker of blockingVias) {
|
|
537
|
+
if (sharesNet(connectionName, blocker.connectionName)) continue
|
|
538
|
+
if (
|
|
539
|
+
distancePointToSegment(blocker.via.center, segment.start, segment.end) <
|
|
540
|
+
blocker.via.diameter / 2 + segment.width / 2 + clearance - EPSILON
|
|
541
|
+
) {
|
|
542
|
+
return false
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
for (const blocker of terminalVias) {
|
|
546
|
+
if (sharesNet(connectionName, blocker.connectionName)) continue
|
|
547
|
+
if (
|
|
548
|
+
distancePointToSegment(blocker.via.center, segment.start, segment.end) <
|
|
549
|
+
blocker.via.diameter / 2 + segment.width / 2 + clearance - EPSILON
|
|
550
|
+
) {
|
|
551
|
+
return false
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return true
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const connectorCandidates = (params: {
|
|
558
|
+
terminal: ViaMinimalWindingTerminal
|
|
559
|
+
endpoint: Point2D
|
|
560
|
+
acceptedAttemptSegments: BlockingSegment[]
|
|
561
|
+
}): ConnectorCandidate[] => {
|
|
562
|
+
const { terminal, endpoint, acceptedAttemptSegments } = params
|
|
563
|
+
const candidates: ConnectorCandidate[] = []
|
|
564
|
+
for (let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex++) {
|
|
565
|
+
const node = nodes[nodeIndex]!
|
|
566
|
+
const connectorDistance = distance(endpoint, node.point)
|
|
567
|
+
if (connectorDistance > gridStep * CONNECTOR_RADIUS_IN_STEPS) continue
|
|
568
|
+
for (const points of getConnectorVariants(endpoint, node.point)) {
|
|
569
|
+
const segments = getSegments(points, traceWidth, targetLayer)
|
|
570
|
+
if (
|
|
571
|
+
!segments.every((segment) =>
|
|
572
|
+
segmentIsClear({
|
|
573
|
+
segment,
|
|
574
|
+
terminal,
|
|
575
|
+
acceptedAttemptSegments,
|
|
576
|
+
}),
|
|
577
|
+
)
|
|
578
|
+
) {
|
|
579
|
+
continue
|
|
580
|
+
}
|
|
581
|
+
candidates.push({
|
|
582
|
+
nodeIndex,
|
|
583
|
+
points,
|
|
584
|
+
radialDistance: connectorDistance,
|
|
585
|
+
length: getSegments(points, traceWidth, targetLayer).reduce(
|
|
586
|
+
(total, segment) => total + distance(segment.start, segment.end),
|
|
587
|
+
0,
|
|
588
|
+
),
|
|
589
|
+
})
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return candidates
|
|
593
|
+
.toSorted(
|
|
594
|
+
(first, second) =>
|
|
595
|
+
first.radialDistance - second.radialDistance ||
|
|
596
|
+
first.length - second.length ||
|
|
597
|
+
first.nodeIndex - second.nodeIndex,
|
|
598
|
+
)
|
|
599
|
+
.slice(0, MAX_CONNECTOR_COUNT)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const routeOne = (params: {
|
|
603
|
+
terminal: ViaMinimalWindingTerminal
|
|
604
|
+
acceptedAttemptSegments: BlockingSegment[]
|
|
605
|
+
laneBias: -1 | 0 | 1
|
|
606
|
+
}): Point2D[] | null => {
|
|
607
|
+
const { terminal, acceptedAttemptSegments, laneBias } = params
|
|
608
|
+
const starts = connectorCandidates({
|
|
609
|
+
terminal,
|
|
610
|
+
endpoint: terminal.viaPoint,
|
|
611
|
+
acceptedAttemptSegments,
|
|
612
|
+
})
|
|
613
|
+
const ends = connectorCandidates({
|
|
614
|
+
terminal,
|
|
615
|
+
endpoint: terminal.exitPoint,
|
|
616
|
+
acceptedAttemptSegments,
|
|
617
|
+
})
|
|
618
|
+
if (starts.length === 0 || ends.length === 0) return null
|
|
619
|
+
const endByNode = new Map<number, ConnectorCandidate[]>()
|
|
620
|
+
for (const end of ends) {
|
|
621
|
+
const values = endByNode.get(end.nodeIndex) ?? []
|
|
622
|
+
values.push(end)
|
|
623
|
+
endByNode.set(end.nodeIndex, values)
|
|
624
|
+
}
|
|
625
|
+
const stateCount = nodeCount * 9
|
|
626
|
+
const distances = new Float64Array(stateCount).fill(
|
|
627
|
+
Number.POSITIVE_INFINITY,
|
|
628
|
+
)
|
|
629
|
+
const previous = new Int32Array(stateCount).fill(-1)
|
|
630
|
+
const heap = new MinHeap()
|
|
631
|
+
const heuristic = (point: Point2D): number => {
|
|
632
|
+
const deltaX = Math.abs(point.x - terminal.exitPoint.x)
|
|
633
|
+
const deltaY = Math.abs(point.y - terminal.exitPoint.y)
|
|
634
|
+
return (
|
|
635
|
+
Math.max(deltaX, deltaY) + (Math.SQRT2 - 1) * Math.min(deltaX, deltaY)
|
|
636
|
+
)
|
|
637
|
+
}
|
|
638
|
+
for (const start of starts) {
|
|
639
|
+
const state = start.nodeIndex * 9 + 8
|
|
640
|
+
if (start.length >= distances[state]!) continue
|
|
641
|
+
distances[state] = start.length
|
|
642
|
+
const remaining = heuristic(nodes[start.nodeIndex]!.point)
|
|
643
|
+
heap.push({
|
|
644
|
+
node: start.nodeIndex,
|
|
645
|
+
direction: 8,
|
|
646
|
+
score: start.length + remaining,
|
|
647
|
+
})
|
|
648
|
+
}
|
|
649
|
+
const directions = [
|
|
650
|
+
[1, 0],
|
|
651
|
+
[1, 1],
|
|
652
|
+
[0, 1],
|
|
653
|
+
[-1, 1],
|
|
654
|
+
[-1, 0],
|
|
655
|
+
[-1, -1],
|
|
656
|
+
[0, -1],
|
|
657
|
+
[1, -1],
|
|
658
|
+
] as const
|
|
659
|
+
const startsByNode = new Map<number, ConnectorCandidate[]>()
|
|
660
|
+
for (const start of starts) {
|
|
661
|
+
const values = startsByNode.get(start.nodeIndex) ?? []
|
|
662
|
+
values.push(start)
|
|
663
|
+
startsByNode.set(start.nodeIndex, values)
|
|
664
|
+
}
|
|
665
|
+
let bestGoalCost = Number.POSITIVE_INFINITY
|
|
666
|
+
let bestGoalPoints: Point2D[] | null = null
|
|
667
|
+
let expandedStateCount = 0
|
|
668
|
+
while (heap.size > 0 && expandedStateCount < MAX_EXPANDED_STATE_COUNT) {
|
|
669
|
+
const current = heap.pop()!
|
|
670
|
+
if (current.score >= bestGoalCost - EPSILON) break
|
|
671
|
+
const state = current.node * 9 + current.direction
|
|
672
|
+
const currentDistance = distances[state]!
|
|
673
|
+
if (
|
|
674
|
+
current.score >
|
|
675
|
+
currentDistance + heuristic(nodes[current.node]!.point) + EPSILON
|
|
676
|
+
)
|
|
677
|
+
continue
|
|
678
|
+
expandedStateCount++
|
|
679
|
+
const endConnectors = endByNode.get(current.node)
|
|
680
|
+
if (endConnectors) {
|
|
681
|
+
const gridPoints: Point2D[] = []
|
|
682
|
+
let pathState = state
|
|
683
|
+
while (pathState >= 0) {
|
|
684
|
+
gridPoints.push(nodes[Math.floor(pathState / 9)]!.point)
|
|
685
|
+
pathState = previous[pathState]!
|
|
686
|
+
}
|
|
687
|
+
gridPoints.reverse()
|
|
688
|
+
let firstState = state
|
|
689
|
+
while (previous[firstState]! >= 0) {
|
|
690
|
+
firstState = previous[firstState]!
|
|
691
|
+
}
|
|
692
|
+
const startNodeIndex = Math.floor(firstState / 9)
|
|
693
|
+
const startConnectors = startsByNode.get(startNodeIndex) ?? []
|
|
694
|
+
const shortestStartLength = Math.min(
|
|
695
|
+
...startConnectors.map((candidate) => candidate.length),
|
|
696
|
+
)
|
|
697
|
+
for (const startConnector of startConnectors) {
|
|
698
|
+
for (const endConnector of endConnectors) {
|
|
699
|
+
const candidateCost =
|
|
700
|
+
currentDistance -
|
|
701
|
+
shortestStartLength +
|
|
702
|
+
startConnector.length +
|
|
703
|
+
endConnector.length
|
|
704
|
+
if (candidateCost >= bestGoalCost - EPSILON) continue
|
|
705
|
+
const points = compressPath([
|
|
706
|
+
...startConnector.points,
|
|
707
|
+
...gridPoints.slice(1),
|
|
708
|
+
...endConnector.points.toReversed().slice(1),
|
|
709
|
+
])
|
|
710
|
+
const segments = getSegments(points, traceWidth, targetLayer)
|
|
711
|
+
if (
|
|
712
|
+
!segments.every(segmentIsStraightOr45Degrees) ||
|
|
713
|
+
!pathHasNoProperSelfCrossing(segments) ||
|
|
714
|
+
!segments.every((segment) =>
|
|
715
|
+
segmentIsClear({
|
|
716
|
+
segment,
|
|
717
|
+
terminal,
|
|
718
|
+
acceptedAttemptSegments,
|
|
719
|
+
}),
|
|
720
|
+
)
|
|
721
|
+
) {
|
|
722
|
+
continue
|
|
723
|
+
}
|
|
724
|
+
bestGoalCost = candidateCost
|
|
725
|
+
bestGoalPoints = points
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
const node = nodes[current.node]!
|
|
730
|
+
for (
|
|
731
|
+
let directionIndex = 0;
|
|
732
|
+
directionIndex < directions.length;
|
|
733
|
+
directionIndex++
|
|
734
|
+
) {
|
|
735
|
+
if (current.direction !== 8) {
|
|
736
|
+
const rawDirectionDelta = Math.abs(current.direction - directionIndex)
|
|
737
|
+
if (Math.min(rawDirectionDelta, 8 - rawDirectionDelta) > 1) {
|
|
738
|
+
continue
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const [deltaColumn, deltaRow] = directions[directionIndex]!
|
|
742
|
+
const column = node.column + deltaColumn
|
|
743
|
+
const row = node.row + deltaRow
|
|
744
|
+
if (column < 0 || column >= columnCount || row < 0 || row >= rowCount) {
|
|
745
|
+
continue
|
|
746
|
+
}
|
|
747
|
+
const nextNode = row * columnCount + column
|
|
748
|
+
const nextPoint = nodes[nextNode]!.point
|
|
749
|
+
const segment: RoutedSegment = {
|
|
750
|
+
start: node.point,
|
|
751
|
+
end: nextPoint,
|
|
752
|
+
width: traceWidth,
|
|
753
|
+
layer: targetLayer,
|
|
754
|
+
}
|
|
755
|
+
if (
|
|
756
|
+
!segmentIsClear({
|
|
757
|
+
segment,
|
|
758
|
+
terminal,
|
|
759
|
+
acceptedAttemptSegments,
|
|
760
|
+
})
|
|
761
|
+
) {
|
|
762
|
+
continue
|
|
763
|
+
}
|
|
764
|
+
const addsTurn =
|
|
765
|
+
current.direction !== 8 && current.direction !== directionIndex
|
|
766
|
+
const nextTrack = getPerpendicularAxis(nextPoint, boundaryDirection)
|
|
767
|
+
const targetTrack = getPerpendicularAxis(
|
|
768
|
+
terminal.exitPoint,
|
|
769
|
+
boundaryDirection,
|
|
770
|
+
)
|
|
771
|
+
const lanePenalty =
|
|
772
|
+
laneBias === 0
|
|
773
|
+
? 0
|
|
774
|
+
: laneBias > 0
|
|
775
|
+
? Math.max(0, targetTrack - nextTrack) * 0.2
|
|
776
|
+
: Math.max(0, nextTrack - targetTrack) * 0.2
|
|
777
|
+
const nextDistance =
|
|
778
|
+
currentDistance +
|
|
779
|
+
(deltaColumn !== 0 && deltaRow !== 0
|
|
780
|
+
? gridStep * Math.SQRT2
|
|
781
|
+
: gridStep) +
|
|
782
|
+
(addsTurn ? gridStep * 0.2 : 0) +
|
|
783
|
+
lanePenalty
|
|
784
|
+
const nextState = nextNode * 9 + directionIndex
|
|
785
|
+
if (nextDistance >= distances[nextState]! - EPSILON) continue
|
|
786
|
+
distances[nextState] = nextDistance
|
|
787
|
+
previous[nextState] = state
|
|
788
|
+
const remaining = heuristic(nextPoint)
|
|
789
|
+
heap.push({
|
|
790
|
+
node: nextNode,
|
|
791
|
+
direction: directionIndex,
|
|
792
|
+
score: nextDistance + remaining,
|
|
793
|
+
})
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return bestGoalPoints
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const targetOrderedTerminals = terminals.toSorted((first, second) => {
|
|
800
|
+
const axisDifference =
|
|
801
|
+
getPerpendicularAxis(first.exitPoint, boundaryDirection) -
|
|
802
|
+
getPerpendicularAxis(second.exitPoint, boundaryDirection)
|
|
803
|
+
return (
|
|
804
|
+
axisDifference ||
|
|
805
|
+
first.connection.connection.name.localeCompare(
|
|
806
|
+
second.connection.connection.name,
|
|
807
|
+
)
|
|
808
|
+
)
|
|
809
|
+
})
|
|
810
|
+
const routeOrderCandidates: ViaMinimalWindingTerminal[][] = [
|
|
811
|
+
targetOrderedTerminals,
|
|
812
|
+
[...targetOrderedTerminals].reverse(),
|
|
813
|
+
terminals.toSorted(
|
|
814
|
+
(first, second) =>
|
|
815
|
+
first.viaPoint.x - second.viaPoint.x ||
|
|
816
|
+
first.viaPoint.y - second.viaPoint.y,
|
|
817
|
+
),
|
|
818
|
+
terminals.toSorted(
|
|
819
|
+
(first, second) =>
|
|
820
|
+
second.viaPoint.x - first.viaPoint.x ||
|
|
821
|
+
second.viaPoint.y - first.viaPoint.y,
|
|
822
|
+
),
|
|
823
|
+
terminals.toSorted(
|
|
824
|
+
(first, second) =>
|
|
825
|
+
first.viaPoint.y - second.viaPoint.y ||
|
|
826
|
+
first.viaPoint.x - second.viaPoint.x,
|
|
827
|
+
),
|
|
828
|
+
terminals.toSorted(
|
|
829
|
+
(first, second) =>
|
|
830
|
+
second.viaPoint.y - first.viaPoint.y ||
|
|
831
|
+
second.viaPoint.x - first.viaPoint.x,
|
|
832
|
+
),
|
|
833
|
+
]
|
|
834
|
+
for (let offset = 1; offset < targetOrderedTerminals.length; offset++) {
|
|
835
|
+
routeOrderCandidates.push([
|
|
836
|
+
...targetOrderedTerminals.slice(offset),
|
|
837
|
+
...targetOrderedTerminals.slice(0, offset),
|
|
838
|
+
])
|
|
839
|
+
}
|
|
840
|
+
const seenRouteOrders = new Set<string>()
|
|
841
|
+
const routeOrders = routeOrderCandidates.filter((routeOrder) => {
|
|
842
|
+
const key = routeOrder
|
|
843
|
+
.map((terminal) => terminal.connection.connection.name)
|
|
844
|
+
.join("\u0000")
|
|
845
|
+
if (seenRouteOrders.has(key)) return false
|
|
846
|
+
seenRouteOrders.add(key)
|
|
847
|
+
return true
|
|
848
|
+
})
|
|
849
|
+
|
|
850
|
+
for (const routeOrder of routeOrders) {
|
|
851
|
+
for (const laneBias of [0, 1, -1] as const) {
|
|
852
|
+
const acceptedAttemptSegments: BlockingSegment[] = []
|
|
853
|
+
const routedPointsByConnectionName = new Map<string, Point2D[]>()
|
|
854
|
+
let failed = false
|
|
855
|
+
for (const terminal of routeOrder) {
|
|
856
|
+
const points = routeOne({
|
|
857
|
+
terminal,
|
|
858
|
+
acceptedAttemptSegments,
|
|
859
|
+
laneBias,
|
|
860
|
+
})
|
|
861
|
+
if (!points) {
|
|
862
|
+
failed = true
|
|
863
|
+
break
|
|
864
|
+
}
|
|
865
|
+
const connectionName = terminal.connection.connection.name
|
|
866
|
+
routedPointsByConnectionName.set(connectionName, points)
|
|
867
|
+
acceptedAttemptSegments.push(
|
|
868
|
+
...getSegments(points, traceWidth, targetLayer).map((segment) => ({
|
|
869
|
+
connectionName,
|
|
870
|
+
segment,
|
|
871
|
+
})),
|
|
872
|
+
)
|
|
873
|
+
}
|
|
874
|
+
if (failed) continue
|
|
875
|
+
return terminals.map((terminal) => {
|
|
876
|
+
const targetLayerPoints = routedPointsByConnectionName.get(
|
|
877
|
+
terminal.connection.connection.name,
|
|
878
|
+
)
|
|
879
|
+
if (!targetLayerPoints) {
|
|
880
|
+
throw new Error(
|
|
881
|
+
`FanoutSolver: via-minimal winding route omitted "${terminal.connection.connection.name}"`,
|
|
882
|
+
)
|
|
883
|
+
}
|
|
884
|
+
return buildPlan({
|
|
885
|
+
bus,
|
|
886
|
+
terminal,
|
|
887
|
+
targetLayer,
|
|
888
|
+
targetLayerPoints,
|
|
889
|
+
layerNames,
|
|
890
|
+
traceWidth,
|
|
891
|
+
viaDiameter,
|
|
892
|
+
viaHoleDiameter,
|
|
893
|
+
})
|
|
894
|
+
})
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return null
|
|
898
|
+
}
|