@tscircuit/fanout-solver 0.0.21 → 0.0.23
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/README.md +15 -0
- package/lib/build-output.ts +30 -12
- package/lib/complete-original-endpoints.ts +1208 -0
- package/lib/fanout-solver.ts +150 -20
- package/lib/index.ts +4 -0
- package/lib/route-bus.ts +339 -87
- package/lib/route-single-layer-adaptive-exits.ts +1 -0
- package/lib/route-single-layer-push-shove.ts +1 -0
- package/lib/types.ts +50 -0
- package/lib/validate-fanout-solution.ts +152 -79
- package/lib/validate-original-endpoint-connectivity.ts +42 -1
- package/lib/validate-routed-copper-drc.ts +23 -2
- package/package.json +4 -4
|
@@ -0,0 +1,1208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AutoroutingPipelineSolver6,
|
|
3
|
+
type ConnectionPoint,
|
|
4
|
+
type Obstacle,
|
|
5
|
+
type SimpleRouteJson,
|
|
6
|
+
type SimplifiedPcbTrace,
|
|
7
|
+
} from "@tscircuit/capacity-autorouter"
|
|
8
|
+
import {
|
|
9
|
+
distance,
|
|
10
|
+
distancePointToSegment,
|
|
11
|
+
pointIsInsideObstacle,
|
|
12
|
+
} from "./geometry"
|
|
13
|
+
import { getCopperLayerNames, getLayerSpan } from "./layer-names"
|
|
14
|
+
import { obstacleSharesElectricalNet } from "./net-identity"
|
|
15
|
+
import type {
|
|
16
|
+
FanoutEndpointCompletionReport,
|
|
17
|
+
FanoutRoutePlan,
|
|
18
|
+
Point2D,
|
|
19
|
+
} from "./types"
|
|
20
|
+
import { validateOriginalEndpointConnectivity } from "./validate-original-endpoint-connectivity"
|
|
21
|
+
import { validateRoutedCopperDrc } from "./validate-routed-copper-drc"
|
|
22
|
+
|
|
23
|
+
const EPSILON = 1e-9
|
|
24
|
+
|
|
25
|
+
interface CompletionAttempt {
|
|
26
|
+
traces: SimplifiedPcbTrace[]
|
|
27
|
+
failedConnectionNames: string[]
|
|
28
|
+
blockingConnectionNames: string[]
|
|
29
|
+
connectivity: ReturnType<typeof validateOriginalEndpointConnectivity>
|
|
30
|
+
drc: ReturnType<typeof validateRoutedCopperDrc>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CompleteOriginalEndpointsResult {
|
|
34
|
+
simpleRouteJson: SimpleRouteJson
|
|
35
|
+
traces: SimplifiedPcbTrace[]
|
|
36
|
+
report: FanoutEndpointCompletionReport
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getPointLayer(
|
|
40
|
+
point: ConnectionPoint,
|
|
41
|
+
preferredLayer?: string,
|
|
42
|
+
): string {
|
|
43
|
+
if ("layer" in point) return point.layer
|
|
44
|
+
if (preferredLayer && point.layers.includes(preferredLayer)) {
|
|
45
|
+
return preferredLayer
|
|
46
|
+
}
|
|
47
|
+
const layer = point.layers[0]
|
|
48
|
+
if (!layer) throw new Error("Endpoint completion received a layerless point")
|
|
49
|
+
return layer
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function uniquePoints(points: Point2D[]): Point2D[] {
|
|
53
|
+
const unique: Point2D[] = []
|
|
54
|
+
for (const point of points) {
|
|
55
|
+
if (unique.at(-1) && distance(unique.at(-1)!, point) < EPSILON) continue
|
|
56
|
+
unique.push(point)
|
|
57
|
+
}
|
|
58
|
+
return unique
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function chamferOrthogonalPolyline(
|
|
62
|
+
rawPoints: Point2D[],
|
|
63
|
+
requestedChamfer: number,
|
|
64
|
+
): Point2D[] {
|
|
65
|
+
const points = uniquePoints(rawPoints)
|
|
66
|
+
if (points.length < 3) return points
|
|
67
|
+
const chamfered: Point2D[] = [points[0]!]
|
|
68
|
+
|
|
69
|
+
for (let index = 1; index < points.length - 1; index++) {
|
|
70
|
+
const previous = points[index - 1]!
|
|
71
|
+
const corner = points[index]!
|
|
72
|
+
const next = points[index + 1]!
|
|
73
|
+
const incomingLength = distance(previous, corner)
|
|
74
|
+
const outgoingLength = distance(corner, next)
|
|
75
|
+
const incoming = {
|
|
76
|
+
x: (corner.x - previous.x) / incomingLength,
|
|
77
|
+
y: (corner.y - previous.y) / incomingLength,
|
|
78
|
+
}
|
|
79
|
+
const outgoing = {
|
|
80
|
+
x: (next.x - corner.x) / outgoingLength,
|
|
81
|
+
y: (next.y - corner.y) / outgoingLength,
|
|
82
|
+
}
|
|
83
|
+
if (Math.abs(incoming.x * outgoing.x + incoming.y * outgoing.y) > 1e-6) {
|
|
84
|
+
chamfered.push(corner)
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
const chamfer = Math.min(
|
|
88
|
+
requestedChamfer,
|
|
89
|
+
incomingLength / 2,
|
|
90
|
+
outgoingLength / 2,
|
|
91
|
+
)
|
|
92
|
+
chamfered.push({
|
|
93
|
+
x: corner.x - incoming.x * chamfer,
|
|
94
|
+
y: corner.y - incoming.y * chamfer,
|
|
95
|
+
})
|
|
96
|
+
chamfered.push({
|
|
97
|
+
x: corner.x + outgoing.x * chamfer,
|
|
98
|
+
y: corner.y + outgoing.y * chamfer,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
chamfered.push(points.at(-1)!)
|
|
102
|
+
return uniquePoints(chamfered)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function findEndpointPad(params: {
|
|
106
|
+
inputSrj: SimpleRouteJson
|
|
107
|
+
connectionName: string
|
|
108
|
+
target: ConnectionPoint
|
|
109
|
+
targetLayer: string
|
|
110
|
+
}): Obstacle | undefined {
|
|
111
|
+
const { inputSrj, connectionName, target, targetLayer } = params
|
|
112
|
+
return inputSrj.obstacles.find(
|
|
113
|
+
(obstacle) =>
|
|
114
|
+
obstacle.layers.includes(targetLayer) &&
|
|
115
|
+
obstacleSharesElectricalNet(inputSrj, obstacle, connectionName) &&
|
|
116
|
+
pointIsInsideObstacle(target, obstacle, 1e-6),
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getTerminalDirections(params: {
|
|
121
|
+
inputSrj: SimpleRouteJson
|
|
122
|
+
plan: FanoutRoutePlan
|
|
123
|
+
}): { outward: Point2D; perpendicular: Point2D } {
|
|
124
|
+
const { inputSrj, plan } = params
|
|
125
|
+
const targetLayer = getPointLayer(plan.targetPoint)
|
|
126
|
+
const targetPad = findEndpointPad({
|
|
127
|
+
inputSrj,
|
|
128
|
+
connectionName: plan.connectionName,
|
|
129
|
+
target: plan.targetPoint,
|
|
130
|
+
targetLayer,
|
|
131
|
+
})
|
|
132
|
+
const body = inputSrj.obstacles.find(
|
|
133
|
+
(obstacle) =>
|
|
134
|
+
obstacle.componentId === targetPad?.componentId &&
|
|
135
|
+
obstacle.connectedTo.length === 0,
|
|
136
|
+
)
|
|
137
|
+
const rawOutward = body
|
|
138
|
+
? {
|
|
139
|
+
x: plan.targetPoint.x - body.center.x,
|
|
140
|
+
y: plan.targetPoint.y - body.center.y,
|
|
141
|
+
}
|
|
142
|
+
: {
|
|
143
|
+
x: plan.targetPoint.x - plan.sourcePoint.x,
|
|
144
|
+
y: plan.targetPoint.y - plan.sourcePoint.y,
|
|
145
|
+
}
|
|
146
|
+
const length = Math.hypot(rawOutward.x, rawOutward.y)
|
|
147
|
+
const outward =
|
|
148
|
+
length > EPSILON
|
|
149
|
+
? { x: rawOutward.x / length, y: rawOutward.y / length }
|
|
150
|
+
: { x: 1, y: 0 }
|
|
151
|
+
return {
|
|
152
|
+
outward,
|
|
153
|
+
perpendicular: { x: -outward.y, y: outward.x },
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function createBranchTrace(params: {
|
|
158
|
+
plan: FanoutRoutePlan
|
|
159
|
+
branchStart: Point2D & { layer: string }
|
|
160
|
+
viaPoint: Point2D
|
|
161
|
+
assignedLayerPath: Point2D[]
|
|
162
|
+
terminalApproach?: Point2D
|
|
163
|
+
traceWidth: number
|
|
164
|
+
viaDiameter: number
|
|
165
|
+
viaHoleDiameter: number
|
|
166
|
+
candidateIndex: number
|
|
167
|
+
chamfer: number
|
|
168
|
+
}): SimplifiedPcbTrace {
|
|
169
|
+
const {
|
|
170
|
+
plan,
|
|
171
|
+
branchStart,
|
|
172
|
+
viaPoint,
|
|
173
|
+
assignedLayerPath,
|
|
174
|
+
terminalApproach,
|
|
175
|
+
traceWidth,
|
|
176
|
+
viaDiameter,
|
|
177
|
+
viaHoleDiameter,
|
|
178
|
+
candidateIndex,
|
|
179
|
+
chamfer,
|
|
180
|
+
} = params
|
|
181
|
+
const targetLayer = getPointLayer(plan.targetPoint)
|
|
182
|
+
const firstPath = chamferOrthogonalPolyline(assignedLayerPath, chamfer)
|
|
183
|
+
const terminalPath = chamferOrthogonalPolyline(
|
|
184
|
+
[
|
|
185
|
+
viaPoint,
|
|
186
|
+
...(terminalApproach ? [terminalApproach] : []),
|
|
187
|
+
plan.targetPoint,
|
|
188
|
+
],
|
|
189
|
+
chamfer,
|
|
190
|
+
)
|
|
191
|
+
const route: SimplifiedPcbTrace["route"] = firstPath.map((point) => ({
|
|
192
|
+
route_type: "wire" as const,
|
|
193
|
+
x: point.x,
|
|
194
|
+
y: point.y,
|
|
195
|
+
width: traceWidth,
|
|
196
|
+
layer: branchStart.layer,
|
|
197
|
+
}))
|
|
198
|
+
|
|
199
|
+
if (branchStart.layer !== targetLayer) {
|
|
200
|
+
route.push({
|
|
201
|
+
route_type: "via",
|
|
202
|
+
x: viaPoint.x,
|
|
203
|
+
y: viaPoint.y,
|
|
204
|
+
from_layer: branchStart.layer,
|
|
205
|
+
to_layer: targetLayer,
|
|
206
|
+
via_diameter: viaDiameter,
|
|
207
|
+
via_hole_diameter: viaHoleDiameter,
|
|
208
|
+
})
|
|
209
|
+
route.push({
|
|
210
|
+
route_type: "wire",
|
|
211
|
+
x: viaPoint.x,
|
|
212
|
+
y: viaPoint.y,
|
|
213
|
+
width: traceWidth,
|
|
214
|
+
layer: targetLayer,
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
for (const point of terminalPath.slice(1)) {
|
|
218
|
+
route.push({
|
|
219
|
+
route_type: "wire",
|
|
220
|
+
x: point.x,
|
|
221
|
+
y: point.y,
|
|
222
|
+
width: traceWidth,
|
|
223
|
+
layer: targetLayer,
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
type: "pcb_trace",
|
|
229
|
+
pcb_trace_id: `fanout-completion:${plan.connectionName}:${candidateIndex}`,
|
|
230
|
+
connection_name: plan.connectionName,
|
|
231
|
+
route,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function connectionIsComplete(
|
|
236
|
+
report: ReturnType<typeof validateOriginalEndpointConnectivity>,
|
|
237
|
+
connectionName: string,
|
|
238
|
+
): boolean {
|
|
239
|
+
return !report.issues.some((issue) => issue.connectionName === connectionName)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function traceHasViaAtEndpoint(params: {
|
|
243
|
+
trace: SimplifiedPcbTrace
|
|
244
|
+
endpointSrjs: SimpleRouteJson[]
|
|
245
|
+
}): boolean {
|
|
246
|
+
const { trace, endpointSrjs } = params
|
|
247
|
+
const endpoints = endpointSrjs.flatMap((srj) =>
|
|
248
|
+
srj.connections.flatMap((connection) => connection.pointsToConnect),
|
|
249
|
+
)
|
|
250
|
+
return trace.route.some(
|
|
251
|
+
(routePoint) =>
|
|
252
|
+
routePoint.route_type === "via" &&
|
|
253
|
+
endpoints.some((endpoint) => distance(routePoint, endpoint) <= 1e-6),
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function findLocalBranch(params: {
|
|
258
|
+
inputSrj: SimpleRouteJson
|
|
259
|
+
fanoutSrj: SimpleRouteJson
|
|
260
|
+
plan: FanoutRoutePlan
|
|
261
|
+
acceptedTraces: SimplifiedPcbTrace[]
|
|
262
|
+
traceWidth: number
|
|
263
|
+
viaDiameter: number
|
|
264
|
+
viaHoleDiameter: number
|
|
265
|
+
clearance: number
|
|
266
|
+
}): {
|
|
267
|
+
trace?: SimplifiedPcbTrace
|
|
268
|
+
blockingConnectionNames: string[]
|
|
269
|
+
} {
|
|
270
|
+
const {
|
|
271
|
+
inputSrj,
|
|
272
|
+
fanoutSrj,
|
|
273
|
+
plan,
|
|
274
|
+
acceptedTraces,
|
|
275
|
+
traceWidth,
|
|
276
|
+
viaDiameter,
|
|
277
|
+
viaHoleDiameter,
|
|
278
|
+
clearance,
|
|
279
|
+
} = params
|
|
280
|
+
const { outward, perpendicular } = getTerminalDirections({ inputSrj, plan })
|
|
281
|
+
const branchStarts: Array<Point2D & { layer: string }> = [
|
|
282
|
+
...(plan.via
|
|
283
|
+
? [
|
|
284
|
+
{
|
|
285
|
+
x: plan.via.center.x,
|
|
286
|
+
y: plan.via.center.y,
|
|
287
|
+
layer: plan.targetLayer,
|
|
288
|
+
},
|
|
289
|
+
]
|
|
290
|
+
: []),
|
|
291
|
+
{
|
|
292
|
+
x: plan.sourcePoint.x,
|
|
293
|
+
y: plan.sourcePoint.y,
|
|
294
|
+
layer: plan.sourceLayer,
|
|
295
|
+
},
|
|
296
|
+
]
|
|
297
|
+
let bestBlockingConnectionNames: string[] = []
|
|
298
|
+
let bestIssueCount = Number.POSITIVE_INFINITY
|
|
299
|
+
let candidateIndex = 0
|
|
300
|
+
const maximumCandidateCount =
|
|
301
|
+
inputSrj.connections.length > 32 ? 600 : Number.POSITIVE_INFINITY
|
|
302
|
+
|
|
303
|
+
const planeTraceTransitionPoints =
|
|
304
|
+
plan.termination.type === "plane"
|
|
305
|
+
? getPointsBackAlongTrace({
|
|
306
|
+
trace: plan.trace,
|
|
307
|
+
endpoint: plan.exitPoint,
|
|
308
|
+
layer: plan.sourceLayer,
|
|
309
|
+
distances: [0.2, 0.3, 0.4],
|
|
310
|
+
})
|
|
311
|
+
: []
|
|
312
|
+
const candidateViaPoints: Point2D[] = [
|
|
313
|
+
...(plan.termination.type === "plane" && plan.via
|
|
314
|
+
? [{ ...plan.via.center }]
|
|
315
|
+
: []),
|
|
316
|
+
...planeTraceTransitionPoints,
|
|
317
|
+
...[0.4, 0.8, 1.2, 1.6].flatMap((outwardDistance) =>
|
|
318
|
+
[0, 0.4, -0.4, 0.8, -0.8, 1.2, -1.2, 1.6, -1.6].map(
|
|
319
|
+
(perpendicularDistance) => ({
|
|
320
|
+
x:
|
|
321
|
+
plan.sourcePoint.x +
|
|
322
|
+
outward.x * outwardDistance +
|
|
323
|
+
perpendicular.x * perpendicularDistance,
|
|
324
|
+
y:
|
|
325
|
+
plan.sourcePoint.y +
|
|
326
|
+
outward.y * outwardDistance +
|
|
327
|
+
perpendicular.y * perpendicularDistance,
|
|
328
|
+
}),
|
|
329
|
+
),
|
|
330
|
+
),
|
|
331
|
+
]
|
|
332
|
+
for (const viaPoint of candidateViaPoints) {
|
|
333
|
+
const candidateBranchStarts = [
|
|
334
|
+
...(plan.termination.type === "plane"
|
|
335
|
+
? [{ ...viaPoint, layer: plan.targetLayer }]
|
|
336
|
+
: []),
|
|
337
|
+
...branchStarts,
|
|
338
|
+
...planeTraceTransitionPoints.flatMap((transitionPoint) =>
|
|
339
|
+
distance(transitionPoint, viaPoint) <= 1e-6
|
|
340
|
+
? [{ ...transitionPoint, layer: plan.sourceLayer }]
|
|
341
|
+
: [],
|
|
342
|
+
),
|
|
343
|
+
]
|
|
344
|
+
for (const branchStart of candidateBranchStarts) {
|
|
345
|
+
const assignedLayerPaths =
|
|
346
|
+
distance(branchStart, viaPoint) <= 1e-6
|
|
347
|
+
? [[branchStart]]
|
|
348
|
+
: [
|
|
349
|
+
[branchStart, viaPoint],
|
|
350
|
+
[branchStart, { x: viaPoint.x, y: branchStart.y }, viaPoint],
|
|
351
|
+
[branchStart, { x: branchStart.x, y: viaPoint.y }, viaPoint],
|
|
352
|
+
...[0.4, -0.4].map((corridorOffset) => [
|
|
353
|
+
branchStart,
|
|
354
|
+
{ x: branchStart.x + corridorOffset, y: branchStart.y },
|
|
355
|
+
{ x: branchStart.x + corridorOffset, y: viaPoint.y },
|
|
356
|
+
viaPoint,
|
|
357
|
+
]),
|
|
358
|
+
]
|
|
359
|
+
for (const assignedLayerPath of assignedLayerPaths) {
|
|
360
|
+
const terminalApproaches: Array<Point2D | undefined> = [
|
|
361
|
+
undefined,
|
|
362
|
+
...[0.4, 0.8].map((terminalDistance) => ({
|
|
363
|
+
x: plan.targetPoint.x + outward.x * terminalDistance,
|
|
364
|
+
y: plan.targetPoint.y + outward.y * terminalDistance,
|
|
365
|
+
})),
|
|
366
|
+
...[0.4, -0.4].map((sideDistance) => ({
|
|
367
|
+
x:
|
|
368
|
+
plan.targetPoint.x +
|
|
369
|
+
outward.x * 0.4 +
|
|
370
|
+
perpendicular.x * sideDistance,
|
|
371
|
+
y:
|
|
372
|
+
plan.targetPoint.y +
|
|
373
|
+
outward.y * 0.4 +
|
|
374
|
+
perpendicular.y * sideDistance,
|
|
375
|
+
})),
|
|
376
|
+
]
|
|
377
|
+
for (const terminalApproach of terminalApproaches) {
|
|
378
|
+
if (candidateIndex >= maximumCandidateCount) {
|
|
379
|
+
return {
|
|
380
|
+
blockingConnectionNames: bestBlockingConnectionNames,
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const trace = createBranchTrace({
|
|
384
|
+
plan,
|
|
385
|
+
branchStart,
|
|
386
|
+
viaPoint,
|
|
387
|
+
assignedLayerPath,
|
|
388
|
+
terminalApproach,
|
|
389
|
+
traceWidth,
|
|
390
|
+
viaDiameter,
|
|
391
|
+
viaHoleDiameter,
|
|
392
|
+
candidateIndex: candidateIndex++,
|
|
393
|
+
// Preserve the raw Manhattan alternative. Some dense escape
|
|
394
|
+
// slots disappear when both sides of a short corner are moved.
|
|
395
|
+
chamfer: 0,
|
|
396
|
+
})
|
|
397
|
+
if (
|
|
398
|
+
traceHasViaAtEndpoint({
|
|
399
|
+
trace,
|
|
400
|
+
endpointSrjs: [inputSrj, fanoutSrj],
|
|
401
|
+
})
|
|
402
|
+
) {
|
|
403
|
+
continue
|
|
404
|
+
}
|
|
405
|
+
const candidateSrj = {
|
|
406
|
+
...fanoutSrj,
|
|
407
|
+
traces: [...(fanoutSrj.traces ?? []), ...acceptedTraces, trace],
|
|
408
|
+
}
|
|
409
|
+
const drc = validateRoutedCopperDrc({
|
|
410
|
+
inputSrj,
|
|
411
|
+
routedSrj: candidateSrj,
|
|
412
|
+
clearance,
|
|
413
|
+
})
|
|
414
|
+
if (!drc.valid) {
|
|
415
|
+
if (drc.issues.length < bestIssueCount) {
|
|
416
|
+
bestIssueCount = drc.issues.length
|
|
417
|
+
bestBlockingConnectionNames = [
|
|
418
|
+
...new Set(
|
|
419
|
+
drc.issues.flatMap((issue) =>
|
|
420
|
+
[issue.connectionName, issue.otherConnectionName].flatMap(
|
|
421
|
+
(connectionName) =>
|
|
422
|
+
connectionName && connectionName !== plan.connectionName
|
|
423
|
+
? [connectionName]
|
|
424
|
+
: [],
|
|
425
|
+
),
|
|
426
|
+
),
|
|
427
|
+
),
|
|
428
|
+
]
|
|
429
|
+
}
|
|
430
|
+
continue
|
|
431
|
+
}
|
|
432
|
+
const connectivity = validateOriginalEndpointConnectivity({
|
|
433
|
+
inputSrj,
|
|
434
|
+
routedSrj: candidateSrj,
|
|
435
|
+
})
|
|
436
|
+
if (connectionIsComplete(connectivity, plan.connectionName)) {
|
|
437
|
+
return { trace, blockingConnectionNames: [] }
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
blockingConnectionNames: bestBlockingConnectionNames,
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function runLocalCompletionPass(params: {
|
|
449
|
+
inputSrj: SimpleRouteJson
|
|
450
|
+
fanoutSrj: SimpleRouteJson
|
|
451
|
+
plans: FanoutRoutePlan[]
|
|
452
|
+
traceWidth: number
|
|
453
|
+
viaDiameter: number
|
|
454
|
+
viaHoleDiameter: number
|
|
455
|
+
clearance: number
|
|
456
|
+
}): CompletionAttempt {
|
|
457
|
+
const {
|
|
458
|
+
inputSrj,
|
|
459
|
+
fanoutSrj,
|
|
460
|
+
plans,
|
|
461
|
+
traceWidth,
|
|
462
|
+
viaDiameter,
|
|
463
|
+
viaHoleDiameter,
|
|
464
|
+
clearance,
|
|
465
|
+
} = params
|
|
466
|
+
const traces: SimplifiedPcbTrace[] = []
|
|
467
|
+
const failedConnectionNames: string[] = []
|
|
468
|
+
const blockingConnectionNames = new Set<string>()
|
|
469
|
+
for (const plan of plans) {
|
|
470
|
+
const result = findLocalBranch({
|
|
471
|
+
inputSrj,
|
|
472
|
+
fanoutSrj,
|
|
473
|
+
plan,
|
|
474
|
+
acceptedTraces: traces,
|
|
475
|
+
traceWidth,
|
|
476
|
+
viaDiameter,
|
|
477
|
+
viaHoleDiameter,
|
|
478
|
+
clearance,
|
|
479
|
+
})
|
|
480
|
+
if (result.trace) {
|
|
481
|
+
traces.push(result.trace)
|
|
482
|
+
} else {
|
|
483
|
+
failedConnectionNames.push(plan.connectionName)
|
|
484
|
+
for (const connectionName of result.blockingConnectionNames) {
|
|
485
|
+
blockingConnectionNames.add(connectionName)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const simpleRouteJson = {
|
|
490
|
+
...fanoutSrj,
|
|
491
|
+
traces: [...(fanoutSrj.traces ?? []), ...traces],
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
traces,
|
|
495
|
+
failedConnectionNames,
|
|
496
|
+
blockingConnectionNames: [...blockingConnectionNames],
|
|
497
|
+
connectivity: validateOriginalEndpointConnectivity({
|
|
498
|
+
inputSrj,
|
|
499
|
+
routedSrj: simpleRouteJson,
|
|
500
|
+
}),
|
|
501
|
+
drc: validateRoutedCopperDrc({
|
|
502
|
+
inputSrj,
|
|
503
|
+
routedSrj: simpleRouteJson,
|
|
504
|
+
clearance,
|
|
505
|
+
}),
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function traceLength(trace: SimplifiedPcbTrace): number {
|
|
510
|
+
let length = 0
|
|
511
|
+
let previousWire:
|
|
512
|
+
| Extract<SimplifiedPcbTrace["route"][number], { route_type: "wire" }>
|
|
513
|
+
| undefined
|
|
514
|
+
for (const point of trace.route) {
|
|
515
|
+
if (point.route_type !== "wire") {
|
|
516
|
+
previousWire = undefined
|
|
517
|
+
continue
|
|
518
|
+
}
|
|
519
|
+
if (previousWire?.layer === point.layer)
|
|
520
|
+
length += distance(previousWire, point)
|
|
521
|
+
previousWire = point
|
|
522
|
+
}
|
|
523
|
+
return length
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function trimCompletedFanoutTails(params: {
|
|
527
|
+
fanoutSrj: SimpleRouteJson
|
|
528
|
+
completionTraces: SimplifiedPcbTrace[]
|
|
529
|
+
}): SimpleRouteJson {
|
|
530
|
+
const { fanoutSrj, completionTraces } = params
|
|
531
|
+
const branchStartByConnectionName = new Map(
|
|
532
|
+
completionTraces.flatMap((trace) => {
|
|
533
|
+
const firstWire = trace.route.find(
|
|
534
|
+
(point): point is Extract<typeof point, { route_type: "wire" }> =>
|
|
535
|
+
point.route_type === "wire",
|
|
536
|
+
)
|
|
537
|
+
return firstWire
|
|
538
|
+
? [
|
|
539
|
+
[
|
|
540
|
+
trace.connection_name,
|
|
541
|
+
{
|
|
542
|
+
x: firstWire.x,
|
|
543
|
+
y: firstWire.y,
|
|
544
|
+
layer: firstWire.layer,
|
|
545
|
+
},
|
|
546
|
+
] as const,
|
|
547
|
+
]
|
|
548
|
+
: []
|
|
549
|
+
}),
|
|
550
|
+
)
|
|
551
|
+
const traces = (fanoutSrj.traces ?? []).map((trace) => {
|
|
552
|
+
if (!trace.pcb_trace_id.startsWith("fanout:")) return trace
|
|
553
|
+
const branchStart = branchStartByConnectionName.get(trace.connection_name)
|
|
554
|
+
if (!branchStart) return trace
|
|
555
|
+
|
|
556
|
+
let previousWireIndex: number | undefined
|
|
557
|
+
for (let index = 0; index < trace.route.length; index++) {
|
|
558
|
+
const point = trace.route[index]
|
|
559
|
+
if (point?.route_type !== "wire") {
|
|
560
|
+
previousWireIndex = undefined
|
|
561
|
+
continue
|
|
562
|
+
}
|
|
563
|
+
if (previousWireIndex !== undefined) {
|
|
564
|
+
const previous = trace.route[previousWireIndex]
|
|
565
|
+
if (
|
|
566
|
+
previous?.route_type === "wire" &&
|
|
567
|
+
previous.layer === branchStart.layer &&
|
|
568
|
+
point.layer === branchStart.layer &&
|
|
569
|
+
distancePointToSegment(branchStart, previous, point) <= 1e-6
|
|
570
|
+
) {
|
|
571
|
+
const route = trace.route.slice(0, previousWireIndex + 1)
|
|
572
|
+
if (distance(previous, branchStart) > 1e-6) {
|
|
573
|
+
route.push({
|
|
574
|
+
route_type: "wire",
|
|
575
|
+
x: branchStart.x,
|
|
576
|
+
y: branchStart.y,
|
|
577
|
+
width: point.width,
|
|
578
|
+
layer: branchStart.layer,
|
|
579
|
+
})
|
|
580
|
+
}
|
|
581
|
+
return { ...trace, route }
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
previousWireIndex = index
|
|
585
|
+
}
|
|
586
|
+
return trace
|
|
587
|
+
})
|
|
588
|
+
return { ...fanoutSrj, traces }
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function createFanoutCopperObstacles(srj: SimpleRouteJson): Obstacle[] {
|
|
592
|
+
const layerNames = getCopperLayerNames(srj.layerCount)
|
|
593
|
+
const obstacles: Obstacle[] = []
|
|
594
|
+
for (const trace of srj.traces ?? []) {
|
|
595
|
+
let previousWire:
|
|
596
|
+
| Extract<SimplifiedPcbTrace["route"][number], { route_type: "wire" }>
|
|
597
|
+
| undefined
|
|
598
|
+
for (const point of trace.route) {
|
|
599
|
+
if (point.route_type === "via") {
|
|
600
|
+
obstacles.push({
|
|
601
|
+
obstacleId: `endpoint-completion-keepout:${trace.pcb_trace_id}:via:${obstacles.length}`,
|
|
602
|
+
type: "rect",
|
|
603
|
+
center: { x: point.x, y: point.y },
|
|
604
|
+
width: point.via_diameter ?? srj.minViaPadDiameter ?? 0.3,
|
|
605
|
+
height: point.via_diameter ?? srj.minViaPadDiameter ?? 0.3,
|
|
606
|
+
layers: getLayerSpan(point.from_layer, point.to_layer, layerNames),
|
|
607
|
+
connectedTo: [trace.connection_name],
|
|
608
|
+
})
|
|
609
|
+
previousWire = undefined
|
|
610
|
+
continue
|
|
611
|
+
}
|
|
612
|
+
if (point.route_type !== "wire") {
|
|
613
|
+
previousWire = undefined
|
|
614
|
+
continue
|
|
615
|
+
}
|
|
616
|
+
if (previousWire?.layer === point.layer) {
|
|
617
|
+
const segmentLength = distance(previousWire, point)
|
|
618
|
+
if (segmentLength > EPSILON) {
|
|
619
|
+
obstacles.push({
|
|
620
|
+
obstacleId: `endpoint-completion-keepout:${trace.pcb_trace_id}:segment:${obstacles.length}`,
|
|
621
|
+
type: "rect",
|
|
622
|
+
center: {
|
|
623
|
+
x: (previousWire.x + point.x) / 2,
|
|
624
|
+
y: (previousWire.y + point.y) / 2,
|
|
625
|
+
},
|
|
626
|
+
width: segmentLength,
|
|
627
|
+
height: Math.max(previousWire.width, point.width),
|
|
628
|
+
ccwRotationDegrees:
|
|
629
|
+
(Math.atan2(point.y - previousWire.y, point.x - previousWire.x) *
|
|
630
|
+
180) /
|
|
631
|
+
Math.PI,
|
|
632
|
+
layers: [point.layer],
|
|
633
|
+
connectedTo: [trace.connection_name],
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
previousWire = point
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return obstacles
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function acceptDownstreamTraces(params: {
|
|
644
|
+
inputSrj: SimpleRouteJson
|
|
645
|
+
fanoutSrj: SimpleRouteJson
|
|
646
|
+
localTraces: SimplifiedPcbTrace[]
|
|
647
|
+
candidates: SimplifiedPcbTrace[]
|
|
648
|
+
clearance: number
|
|
649
|
+
}): SimplifiedPcbTrace[] {
|
|
650
|
+
const { inputSrj, fanoutSrj, localTraces, candidates, clearance } = params
|
|
651
|
+
const baselineTraces = [...(fanoutSrj.traces ?? []), ...localTraces]
|
|
652
|
+
const baselineSrj = { ...fanoutSrj, traces: baselineTraces }
|
|
653
|
+
const baselineConnectivity = validateOriginalEndpointConnectivity({
|
|
654
|
+
inputSrj,
|
|
655
|
+
routedSrj: baselineSrj,
|
|
656
|
+
})
|
|
657
|
+
const physicalCandidates = candidates.filter(
|
|
658
|
+
(trace) =>
|
|
659
|
+
trace.route.length > 1 &&
|
|
660
|
+
trace.route.every(
|
|
661
|
+
(routePoint) =>
|
|
662
|
+
routePoint.route_type === "wire" || routePoint.route_type === "via",
|
|
663
|
+
) &&
|
|
664
|
+
!traceHasViaAtEndpoint({
|
|
665
|
+
trace,
|
|
666
|
+
endpointSrjs: [inputSrj, fanoutSrj],
|
|
667
|
+
}),
|
|
668
|
+
)
|
|
669
|
+
const usefulCandidates = physicalCandidates.filter((trace) => {
|
|
670
|
+
const report = validateOriginalEndpointConnectivity({
|
|
671
|
+
inputSrj,
|
|
672
|
+
routedSrj: { ...fanoutSrj, traces: [...baselineTraces, trace] },
|
|
673
|
+
})
|
|
674
|
+
return (
|
|
675
|
+
report.connectedConnectionCount >
|
|
676
|
+
baselineConnectivity.connectedConnectionCount &&
|
|
677
|
+
connectionIsComplete(report, trace.connection_name)
|
|
678
|
+
)
|
|
679
|
+
})
|
|
680
|
+
const combinedSrj = {
|
|
681
|
+
...fanoutSrj,
|
|
682
|
+
traces: [...baselineTraces, ...usefulCandidates],
|
|
683
|
+
}
|
|
684
|
+
if (
|
|
685
|
+
validateRoutedCopperDrc({
|
|
686
|
+
inputSrj,
|
|
687
|
+
routedSrj: combinedSrj,
|
|
688
|
+
clearance,
|
|
689
|
+
}).valid
|
|
690
|
+
) {
|
|
691
|
+
return usefulCandidates
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const accepted: SimplifiedPcbTrace[] = []
|
|
695
|
+
for (const trace of usefulCandidates.toSorted(
|
|
696
|
+
(first, second) => traceLength(first) - traceLength(second),
|
|
697
|
+
)) {
|
|
698
|
+
const candidateSrj = {
|
|
699
|
+
...fanoutSrj,
|
|
700
|
+
traces: [...baselineTraces, ...accepted, trace],
|
|
701
|
+
}
|
|
702
|
+
const drc = validateRoutedCopperDrc({
|
|
703
|
+
inputSrj,
|
|
704
|
+
routedSrj: candidateSrj,
|
|
705
|
+
clearance,
|
|
706
|
+
})
|
|
707
|
+
if (!drc.valid) continue
|
|
708
|
+
const before = validateOriginalEndpointConnectivity({
|
|
709
|
+
inputSrj,
|
|
710
|
+
routedSrj: {
|
|
711
|
+
...fanoutSrj,
|
|
712
|
+
traces: [...baselineTraces, ...accepted],
|
|
713
|
+
},
|
|
714
|
+
})
|
|
715
|
+
const after = validateOriginalEndpointConnectivity({
|
|
716
|
+
inputSrj,
|
|
717
|
+
routedSrj: candidateSrj,
|
|
718
|
+
})
|
|
719
|
+
if (
|
|
720
|
+
after.connectedConnectionCount > before.connectedConnectionCount &&
|
|
721
|
+
connectionIsComplete(after, trace.connection_name)
|
|
722
|
+
) {
|
|
723
|
+
accepted.push(trace)
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return accepted
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function getPointsBackAlongTrace(params: {
|
|
730
|
+
trace: SimplifiedPcbTrace
|
|
731
|
+
endpoint: Point2D
|
|
732
|
+
layer: string
|
|
733
|
+
distances: number[]
|
|
734
|
+
}): Point2D[] {
|
|
735
|
+
const { trace, endpoint, layer, distances } = params
|
|
736
|
+
const endpointIndex = trace.route.findLastIndex(
|
|
737
|
+
(routePoint) =>
|
|
738
|
+
routePoint.route_type === "wire" &&
|
|
739
|
+
routePoint.layer === layer &&
|
|
740
|
+
distance(routePoint, endpoint) <= 1e-6,
|
|
741
|
+
)
|
|
742
|
+
if (endpointIndex < 0) return []
|
|
743
|
+
|
|
744
|
+
return distances.flatMap((requestedDistance) => {
|
|
745
|
+
let remainingDistance = requestedDistance
|
|
746
|
+
let current = endpoint
|
|
747
|
+
for (let index = endpointIndex - 1; index >= 0; index--) {
|
|
748
|
+
const previous = trace.route[index]
|
|
749
|
+
if (previous?.route_type !== "wire" || previous.layer !== layer) break
|
|
750
|
+
const segmentLength = distance(current, previous)
|
|
751
|
+
if (segmentLength <= EPSILON) continue
|
|
752
|
+
if (remainingDistance < segmentLength - 1e-6) {
|
|
753
|
+
return [
|
|
754
|
+
{
|
|
755
|
+
x:
|
|
756
|
+
current.x +
|
|
757
|
+
((previous.x - current.x) * remainingDistance) / segmentLength,
|
|
758
|
+
y:
|
|
759
|
+
current.y +
|
|
760
|
+
((previous.y - current.y) * remainingDistance) / segmentLength,
|
|
761
|
+
},
|
|
762
|
+
]
|
|
763
|
+
}
|
|
764
|
+
remainingDistance -= segmentLength
|
|
765
|
+
current = previous
|
|
766
|
+
}
|
|
767
|
+
return []
|
|
768
|
+
})
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function findDownstreamTerminalBranch(params: {
|
|
772
|
+
inputSrj: SimpleRouteJson
|
|
773
|
+
fanoutSrj: SimpleRouteJson
|
|
774
|
+
plan: FanoutRoutePlan
|
|
775
|
+
acceptedTraces: SimplifiedPcbTrace[]
|
|
776
|
+
traceWidth: number
|
|
777
|
+
viaDiameter: number
|
|
778
|
+
viaHoleDiameter: number
|
|
779
|
+
clearance: number
|
|
780
|
+
}): SimplifiedPcbTrace | undefined {
|
|
781
|
+
const {
|
|
782
|
+
inputSrj,
|
|
783
|
+
fanoutSrj,
|
|
784
|
+
plan,
|
|
785
|
+
acceptedTraces,
|
|
786
|
+
traceWidth,
|
|
787
|
+
viaDiameter,
|
|
788
|
+
viaHoleDiameter,
|
|
789
|
+
clearance,
|
|
790
|
+
} = params
|
|
791
|
+
const branchStart = {
|
|
792
|
+
x: plan.exitPoint.x,
|
|
793
|
+
y: plan.exitPoint.y,
|
|
794
|
+
layer: plan.targetLayer,
|
|
795
|
+
}
|
|
796
|
+
const target = plan.targetPoint
|
|
797
|
+
const targetLayer = getPointLayer(target)
|
|
798
|
+
let candidateIndex = 0
|
|
799
|
+
const tryCandidate = (candidate: {
|
|
800
|
+
start: Point2D & { layer: string }
|
|
801
|
+
viaPoint: Point2D
|
|
802
|
+
terminalApproach?: Point2D
|
|
803
|
+
}): SimplifiedPcbTrace | undefined => {
|
|
804
|
+
const trace = createBranchTrace({
|
|
805
|
+
plan,
|
|
806
|
+
branchStart: candidate.start,
|
|
807
|
+
viaPoint: candidate.viaPoint,
|
|
808
|
+
assignedLayerPath: [candidate.start, candidate.viaPoint],
|
|
809
|
+
terminalApproach: candidate.terminalApproach,
|
|
810
|
+
traceWidth,
|
|
811
|
+
viaDiameter,
|
|
812
|
+
viaHoleDiameter,
|
|
813
|
+
candidateIndex: 10_000 + candidateIndex++,
|
|
814
|
+
chamfer: Math.max(traceWidth, 0.1),
|
|
815
|
+
})
|
|
816
|
+
if (
|
|
817
|
+
traceHasViaAtEndpoint({
|
|
818
|
+
trace,
|
|
819
|
+
endpointSrjs: [inputSrj, fanoutSrj],
|
|
820
|
+
})
|
|
821
|
+
) {
|
|
822
|
+
return undefined
|
|
823
|
+
}
|
|
824
|
+
const candidateSrj = {
|
|
825
|
+
...fanoutSrj,
|
|
826
|
+
traces: [...(fanoutSrj.traces ?? []), ...acceptedTraces, trace],
|
|
827
|
+
}
|
|
828
|
+
if (
|
|
829
|
+
!validateRoutedCopperDrc({
|
|
830
|
+
inputSrj,
|
|
831
|
+
routedSrj: candidateSrj,
|
|
832
|
+
clearance,
|
|
833
|
+
}).valid
|
|
834
|
+
) {
|
|
835
|
+
return undefined
|
|
836
|
+
}
|
|
837
|
+
if (
|
|
838
|
+
connectionIsComplete(
|
|
839
|
+
validateOriginalEndpointConnectivity({
|
|
840
|
+
inputSrj,
|
|
841
|
+
routedSrj: candidateSrj,
|
|
842
|
+
}),
|
|
843
|
+
plan.connectionName,
|
|
844
|
+
)
|
|
845
|
+
) {
|
|
846
|
+
return trace
|
|
847
|
+
}
|
|
848
|
+
return undefined
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (branchStart.layer !== targetLayer) {
|
|
852
|
+
const existingTraceTransitionPoints = getPointsBackAlongTrace({
|
|
853
|
+
trace: plan.trace,
|
|
854
|
+
endpoint: plan.exitPoint,
|
|
855
|
+
layer: plan.targetLayer,
|
|
856
|
+
distances: [0.4, 0.8, 1.2, 1.6],
|
|
857
|
+
})
|
|
858
|
+
for (const viaPoint of existingTraceTransitionPoints) {
|
|
859
|
+
const transitionStart = { ...viaPoint, layer: plan.targetLayer }
|
|
860
|
+
for (const terminalApproach of [
|
|
861
|
+
undefined,
|
|
862
|
+
{ x: target.x, y: viaPoint.y },
|
|
863
|
+
{ x: viaPoint.x, y: target.y },
|
|
864
|
+
]) {
|
|
865
|
+
const trace = tryCandidate({
|
|
866
|
+
start: transitionStart,
|
|
867
|
+
viaPoint,
|
|
868
|
+
terminalApproach,
|
|
869
|
+
})
|
|
870
|
+
if (trace) return trace
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const routePaths: Point2D[][] = [
|
|
876
|
+
[target],
|
|
877
|
+
[{ x: target.x, y: branchStart.y }, target],
|
|
878
|
+
[{ x: branchStart.x, y: target.y }, target],
|
|
879
|
+
]
|
|
880
|
+
for (const routePath of routePaths) {
|
|
881
|
+
const firstTarget = routePath[0]!
|
|
882
|
+
const firstSegmentLength = distance(branchStart, firstTarget)
|
|
883
|
+
const transitionDistances =
|
|
884
|
+
branchStart.layer === targetLayer ? [0] : [0.4, 0.8, 1.2, 1.6]
|
|
885
|
+
for (const transitionDistance of transitionDistances) {
|
|
886
|
+
if (
|
|
887
|
+
branchStart.layer !== targetLayer &&
|
|
888
|
+
(firstSegmentLength <= transitionDistance + 0.2 ||
|
|
889
|
+
transitionDistance <= 1e-6)
|
|
890
|
+
) {
|
|
891
|
+
continue
|
|
892
|
+
}
|
|
893
|
+
const viaPoint =
|
|
894
|
+
branchStart.layer === targetLayer
|
|
895
|
+
? { x: branchStart.x, y: branchStart.y }
|
|
896
|
+
: {
|
|
897
|
+
x:
|
|
898
|
+
branchStart.x +
|
|
899
|
+
((firstTarget.x - branchStart.x) * transitionDistance) /
|
|
900
|
+
firstSegmentLength,
|
|
901
|
+
y:
|
|
902
|
+
branchStart.y +
|
|
903
|
+
((firstTarget.y - branchStart.y) * transitionDistance) /
|
|
904
|
+
firstSegmentLength,
|
|
905
|
+
}
|
|
906
|
+
const trace = tryCandidate({
|
|
907
|
+
start: branchStart,
|
|
908
|
+
viaPoint,
|
|
909
|
+
terminalApproach: routePath.length > 1 ? firstTarget : undefined,
|
|
910
|
+
})
|
|
911
|
+
if (trace) return trace
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return undefined
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Connects short opposite-layer terminal pairs with constrained interstitial
|
|
919
|
+
* vias, then delegates the remaining long routes to the capacity autorouter.
|
|
920
|
+
* Only metric-improving, independently DRC-clean physical copper is retained.
|
|
921
|
+
*/
|
|
922
|
+
export function completeOriginalEndpoints(params: {
|
|
923
|
+
inputSrj: SimpleRouteJson
|
|
924
|
+
fanoutSrj: SimpleRouteJson
|
|
925
|
+
plans: FanoutRoutePlan[]
|
|
926
|
+
traceWidth: number
|
|
927
|
+
viaDiameter: number
|
|
928
|
+
viaHoleDiameter: number
|
|
929
|
+
clearance: number
|
|
930
|
+
effort?: number
|
|
931
|
+
}): CompleteOriginalEndpointsResult {
|
|
932
|
+
const {
|
|
933
|
+
inputSrj,
|
|
934
|
+
fanoutSrj,
|
|
935
|
+
plans,
|
|
936
|
+
traceWidth,
|
|
937
|
+
viaDiameter,
|
|
938
|
+
viaHoleDiameter,
|
|
939
|
+
clearance,
|
|
940
|
+
effort = 1,
|
|
941
|
+
} = params
|
|
942
|
+
const errors: string[] = []
|
|
943
|
+
const baselineDrc = validateRoutedCopperDrc({
|
|
944
|
+
inputSrj,
|
|
945
|
+
routedSrj: fanoutSrj,
|
|
946
|
+
clearance,
|
|
947
|
+
})
|
|
948
|
+
const baselineConnectivity = validateOriginalEndpointConnectivity({
|
|
949
|
+
inputSrj,
|
|
950
|
+
routedSrj: fanoutSrj,
|
|
951
|
+
})
|
|
952
|
+
const localPlans = plans.filter((plan) => {
|
|
953
|
+
const sourceLayer = getPointLayer(plan.sourcePoint)
|
|
954
|
+
const targetLayer = getPointLayer(plan.targetPoint)
|
|
955
|
+
return (
|
|
956
|
+
sourceLayer !== targetLayer &&
|
|
957
|
+
distance(plan.sourcePoint, plan.targetPoint) <= 0.25 &&
|
|
958
|
+
!connectionIsComplete(baselineConnectivity, plan.connectionName)
|
|
959
|
+
)
|
|
960
|
+
})
|
|
961
|
+
let bestLocalAttempt: CompletionAttempt = {
|
|
962
|
+
traces: [],
|
|
963
|
+
failedConnectionNames: localPlans.map((plan) => plan.connectionName),
|
|
964
|
+
blockingConnectionNames: [],
|
|
965
|
+
connectivity: baselineConnectivity,
|
|
966
|
+
drc: baselineDrc,
|
|
967
|
+
}
|
|
968
|
+
let searchPassCount = 0
|
|
969
|
+
|
|
970
|
+
if (!baselineDrc.valid) {
|
|
971
|
+
errors.push("Fanout prefix failed emitted-copper DRC; skipped completion")
|
|
972
|
+
} else {
|
|
973
|
+
let priorityConnectionNames: string[] = []
|
|
974
|
+
const originalOrder = new Map(
|
|
975
|
+
localPlans.map((plan, index) => [plan.connectionName, index]),
|
|
976
|
+
)
|
|
977
|
+
const maximumLocalPasses = inputSrj.connections.length > 32 ? 1 : 3
|
|
978
|
+
for (let passIndex = 0; passIndex < maximumLocalPasses; passIndex++) {
|
|
979
|
+
const priority = new Map(
|
|
980
|
+
priorityConnectionNames.map((connectionName, index) => [
|
|
981
|
+
connectionName,
|
|
982
|
+
index,
|
|
983
|
+
]),
|
|
984
|
+
)
|
|
985
|
+
const orderedPlans = localPlans.toSorted(
|
|
986
|
+
(first, second) =>
|
|
987
|
+
(priority.get(first.connectionName) ?? Number.MAX_SAFE_INTEGER) -
|
|
988
|
+
(priority.get(second.connectionName) ?? Number.MAX_SAFE_INTEGER) ||
|
|
989
|
+
(originalOrder.get(first.connectionName) ?? 0) -
|
|
990
|
+
(originalOrder.get(second.connectionName) ?? 0),
|
|
991
|
+
)
|
|
992
|
+
const attempt = runLocalCompletionPass({
|
|
993
|
+
inputSrj,
|
|
994
|
+
fanoutSrj,
|
|
995
|
+
plans: orderedPlans,
|
|
996
|
+
traceWidth,
|
|
997
|
+
viaDiameter,
|
|
998
|
+
viaHoleDiameter,
|
|
999
|
+
clearance,
|
|
1000
|
+
})
|
|
1001
|
+
searchPassCount++
|
|
1002
|
+
if (
|
|
1003
|
+
attempt.drc.valid &&
|
|
1004
|
+
(attempt.connectivity.connectedConnectionCount >
|
|
1005
|
+
bestLocalAttempt.connectivity.connectedConnectionCount ||
|
|
1006
|
+
(attempt.connectivity.connectedConnectionCount ===
|
|
1007
|
+
bestLocalAttempt.connectivity.connectedConnectionCount &&
|
|
1008
|
+
attempt.traces.length < bestLocalAttempt.traces.length))
|
|
1009
|
+
) {
|
|
1010
|
+
bestLocalAttempt = attempt
|
|
1011
|
+
}
|
|
1012
|
+
priorityConnectionNames = [
|
|
1013
|
+
...new Set([
|
|
1014
|
+
...attempt.failedConnectionNames,
|
|
1015
|
+
...attempt.blockingConnectionNames,
|
|
1016
|
+
...priorityConnectionNames,
|
|
1017
|
+
]),
|
|
1018
|
+
]
|
|
1019
|
+
if (attempt.failedConnectionNames.length === 0) break
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const localConnectionNames = new Set(
|
|
1024
|
+
localPlans.map((plan) => plan.connectionName),
|
|
1025
|
+
)
|
|
1026
|
+
const downstreamPlans = plans.filter(
|
|
1027
|
+
(plan) =>
|
|
1028
|
+
!localConnectionNames.has(plan.connectionName) &&
|
|
1029
|
+
!connectionIsComplete(baselineConnectivity, plan.connectionName),
|
|
1030
|
+
)
|
|
1031
|
+
const directDownstreamTraces: SimplifiedPcbTrace[] = []
|
|
1032
|
+
if (baselineDrc.valid) {
|
|
1033
|
+
for (const plan of downstreamPlans) {
|
|
1034
|
+
const terminalBranch = findDownstreamTerminalBranch({
|
|
1035
|
+
inputSrj,
|
|
1036
|
+
fanoutSrj,
|
|
1037
|
+
plan,
|
|
1038
|
+
acceptedTraces: [...bestLocalAttempt.traces, ...directDownstreamTraces],
|
|
1039
|
+
traceWidth,
|
|
1040
|
+
viaDiameter,
|
|
1041
|
+
viaHoleDiameter,
|
|
1042
|
+
clearance,
|
|
1043
|
+
})
|
|
1044
|
+
if (terminalBranch) directDownstreamTraces.push(terminalBranch)
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
const directSrj = {
|
|
1048
|
+
...fanoutSrj,
|
|
1049
|
+
traces: [
|
|
1050
|
+
...(fanoutSrj.traces ?? []),
|
|
1051
|
+
...bestLocalAttempt.traces,
|
|
1052
|
+
...directDownstreamTraces,
|
|
1053
|
+
],
|
|
1054
|
+
}
|
|
1055
|
+
const unresolvedConnectionNames = new Set(
|
|
1056
|
+
validateOriginalEndpointConnectivity({
|
|
1057
|
+
inputSrj,
|
|
1058
|
+
routedSrj: directSrj,
|
|
1059
|
+
}).issues.map((issue) => issue.connectionName),
|
|
1060
|
+
)
|
|
1061
|
+
const downstreamConnections = fanoutSrj.connections.filter(
|
|
1062
|
+
(connection) =>
|
|
1063
|
+
!localConnectionNames.has(connection.name) &&
|
|
1064
|
+
unresolvedConnectionNames.has(connection.name),
|
|
1065
|
+
)
|
|
1066
|
+
let downstreamTraces: SimplifiedPcbTrace[] = []
|
|
1067
|
+
if (
|
|
1068
|
+
baselineDrc.valid &&
|
|
1069
|
+
downstreamConnections.length > 0 &&
|
|
1070
|
+
downstreamConnections.length <= 12
|
|
1071
|
+
) {
|
|
1072
|
+
const downstreamConnectionNames = new Set(
|
|
1073
|
+
downstreamConnections.map((connection) => connection.name),
|
|
1074
|
+
)
|
|
1075
|
+
const downstreamInput: SimpleRouteJson = {
|
|
1076
|
+
...fanoutSrj,
|
|
1077
|
+
connections: downstreamConnections,
|
|
1078
|
+
buses: fanoutSrj.buses
|
|
1079
|
+
?.map((bus) => ({
|
|
1080
|
+
...bus,
|
|
1081
|
+
connectionNames: bus.connectionNames.filter((connectionName) =>
|
|
1082
|
+
downstreamConnectionNames.has(connectionName),
|
|
1083
|
+
),
|
|
1084
|
+
}))
|
|
1085
|
+
.filter((bus) => bus.connectionNames.length > 0),
|
|
1086
|
+
obstacles: [
|
|
1087
|
+
...fanoutSrj.obstacles,
|
|
1088
|
+
...createFanoutCopperObstacles(fanoutSrj),
|
|
1089
|
+
],
|
|
1090
|
+
traces: [],
|
|
1091
|
+
}
|
|
1092
|
+
try {
|
|
1093
|
+
const downstreamSolver = new AutoroutingPipelineSolver6(downstreamInput, {
|
|
1094
|
+
effort,
|
|
1095
|
+
})
|
|
1096
|
+
downstreamSolver.solve()
|
|
1097
|
+
if (downstreamSolver.solved) {
|
|
1098
|
+
downstreamTraces = acceptDownstreamTraces({
|
|
1099
|
+
inputSrj,
|
|
1100
|
+
fanoutSrj,
|
|
1101
|
+
localTraces: [...bestLocalAttempt.traces, ...directDownstreamTraces],
|
|
1102
|
+
candidates:
|
|
1103
|
+
downstreamSolver
|
|
1104
|
+
.getOutputSimpleRouteJson()
|
|
1105
|
+
.traces?.filter((trace) =>
|
|
1106
|
+
downstreamConnectionNames.has(trace.connection_name),
|
|
1107
|
+
) ?? [],
|
|
1108
|
+
clearance,
|
|
1109
|
+
})
|
|
1110
|
+
} else {
|
|
1111
|
+
errors.push(
|
|
1112
|
+
`Downstream autorouter did not solve: ${downstreamSolver.error ?? "unknown error"}`,
|
|
1113
|
+
)
|
|
1114
|
+
}
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
errors.push(
|
|
1117
|
+
`Downstream autorouter failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1118
|
+
)
|
|
1119
|
+
}
|
|
1120
|
+
} else if (downstreamConnections.length > 12) {
|
|
1121
|
+
errors.push(
|
|
1122
|
+
`Skipped downstream autorouter for ${downstreamConnections.length} unresolved connections (bounded at 12)`,
|
|
1123
|
+
)
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const traces = [
|
|
1127
|
+
...bestLocalAttempt.traces,
|
|
1128
|
+
...directDownstreamTraces,
|
|
1129
|
+
...downstreamTraces,
|
|
1130
|
+
]
|
|
1131
|
+
for (const plan of downstreamPlans) {
|
|
1132
|
+
const currentSrj = {
|
|
1133
|
+
...fanoutSrj,
|
|
1134
|
+
traces: [...(fanoutSrj.traces ?? []), ...traces],
|
|
1135
|
+
}
|
|
1136
|
+
if (
|
|
1137
|
+
connectionIsComplete(
|
|
1138
|
+
validateOriginalEndpointConnectivity({
|
|
1139
|
+
inputSrj,
|
|
1140
|
+
routedSrj: currentSrj,
|
|
1141
|
+
}),
|
|
1142
|
+
plan.connectionName,
|
|
1143
|
+
)
|
|
1144
|
+
) {
|
|
1145
|
+
continue
|
|
1146
|
+
}
|
|
1147
|
+
const terminalBranch = findDownstreamTerminalBranch({
|
|
1148
|
+
inputSrj,
|
|
1149
|
+
fanoutSrj,
|
|
1150
|
+
plan,
|
|
1151
|
+
acceptedTraces: traces,
|
|
1152
|
+
traceWidth,
|
|
1153
|
+
viaDiameter,
|
|
1154
|
+
viaHoleDiameter,
|
|
1155
|
+
clearance,
|
|
1156
|
+
})
|
|
1157
|
+
if (terminalBranch) traces.push(terminalBranch)
|
|
1158
|
+
}
|
|
1159
|
+
const untrimmedSimpleRouteJson = {
|
|
1160
|
+
...fanoutSrj,
|
|
1161
|
+
traces: [...(fanoutSrj.traces ?? []), ...traces],
|
|
1162
|
+
}
|
|
1163
|
+
const simpleRouteJson = trimCompletedFanoutTails({
|
|
1164
|
+
fanoutSrj: untrimmedSimpleRouteJson,
|
|
1165
|
+
completionTraces: traces,
|
|
1166
|
+
})
|
|
1167
|
+
const connectivity = validateOriginalEndpointConnectivity({
|
|
1168
|
+
inputSrj,
|
|
1169
|
+
routedSrj: simpleRouteJson,
|
|
1170
|
+
})
|
|
1171
|
+
const drc = validateRoutedCopperDrc({
|
|
1172
|
+
inputSrj,
|
|
1173
|
+
routedSrj: simpleRouteJson,
|
|
1174
|
+
clearance,
|
|
1175
|
+
})
|
|
1176
|
+
if (!drc.valid) {
|
|
1177
|
+
errors.push("Final endpoint-completion output failed emitted-copper DRC")
|
|
1178
|
+
return {
|
|
1179
|
+
simpleRouteJson: fanoutSrj,
|
|
1180
|
+
traces: [],
|
|
1181
|
+
report: {
|
|
1182
|
+
attemptedLocalConnectionCount: localPlans.length,
|
|
1183
|
+
attemptedDownstreamConnectionCount: downstreamPlans.length,
|
|
1184
|
+
completionTraceCount: 0,
|
|
1185
|
+
searchPassCount,
|
|
1186
|
+
errors,
|
|
1187
|
+
connectivity: validateOriginalEndpointConnectivity({
|
|
1188
|
+
inputSrj,
|
|
1189
|
+
routedSrj: fanoutSrj,
|
|
1190
|
+
}),
|
|
1191
|
+
drc: baselineDrc,
|
|
1192
|
+
},
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
return {
|
|
1196
|
+
simpleRouteJson,
|
|
1197
|
+
traces,
|
|
1198
|
+
report: {
|
|
1199
|
+
attemptedLocalConnectionCount: localPlans.length,
|
|
1200
|
+
attemptedDownstreamConnectionCount: downstreamPlans.length,
|
|
1201
|
+
completionTraceCount: traces.length,
|
|
1202
|
+
searchPassCount,
|
|
1203
|
+
errors,
|
|
1204
|
+
connectivity,
|
|
1205
|
+
drc,
|
|
1206
|
+
},
|
|
1207
|
+
}
|
|
1208
|
+
}
|