@tscircuit/fanout-solver 0.0.21 → 0.0.22

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.
@@ -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
+ }