@tscircuit/fanout-solver 0.0.20 → 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.
- package/README.md +25 -4
- 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 +15 -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 +415 -0
- package/lib/validate-routed-copper-drc.ts +463 -0
- package/package.json +2 -2
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
PreparedBus,
|
|
25
25
|
RoutedSegment,
|
|
26
26
|
} from "./types"
|
|
27
|
+
import { segmentIsLegalTerminalBodyEscape } from "./validate-routed-copper-drc"
|
|
27
28
|
|
|
28
29
|
const EPSILON = 1e-6
|
|
29
30
|
|
|
@@ -35,6 +36,16 @@ function getPointLayers(point: ConnectionPoint): string[] {
|
|
|
35
36
|
return "layer" in point ? [point.layer] : point.layers
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
function getPlanSegments(plan: FanoutRoutePlan): RoutedSegment[] {
|
|
40
|
+
return [...plan.segments, ...(plan.planeEndpointSegments ?? [])]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getPlanVias(plan: FanoutRoutePlan) {
|
|
44
|
+
return [plan.via, plan.planeEndpointVia].filter(
|
|
45
|
+
(via): via is NonNullable<FanoutRoutePlan["via"]> => Boolean(via),
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
function connectionPointsMatch(
|
|
39
50
|
first: ConnectionPoint,
|
|
40
51
|
second: ConnectionPoint,
|
|
@@ -322,6 +333,52 @@ function validatePlanStructure(params: {
|
|
|
322
333
|
)
|
|
323
334
|
}
|
|
324
335
|
|
|
336
|
+
if (
|
|
337
|
+
plan.planeEndpointTrace ||
|
|
338
|
+
plan.planeEndpointSegments ||
|
|
339
|
+
plan.planeEndpointVia
|
|
340
|
+
) {
|
|
341
|
+
if (
|
|
342
|
+
!plan.planeEndpointTrace ||
|
|
343
|
+
!plan.planeEndpointSegments ||
|
|
344
|
+
!plan.planeEndpointVia
|
|
345
|
+
) {
|
|
346
|
+
addIssue(
|
|
347
|
+
issues,
|
|
348
|
+
"trace-plan-mismatch",
|
|
349
|
+
`Plane endpoint geometry for ${plan.connectionName} is incomplete`,
|
|
350
|
+
plan,
|
|
351
|
+
)
|
|
352
|
+
} else {
|
|
353
|
+
const endpointSegments = extractTraceSegments({
|
|
354
|
+
trace: plan.planeEndpointTrace,
|
|
355
|
+
plan,
|
|
356
|
+
issues,
|
|
357
|
+
})
|
|
358
|
+
if (
|
|
359
|
+
endpointSegments.length !== plan.planeEndpointSegments.length ||
|
|
360
|
+
endpointSegments.some((segment, index) => {
|
|
361
|
+
const declared = plan.planeEndpointSegments?.[index]
|
|
362
|
+
return (
|
|
363
|
+
!declared ||
|
|
364
|
+
segment.layer !== declared.layer ||
|
|
365
|
+
Math.abs(segment.width - declared.width) > EPSILON ||
|
|
366
|
+
!pointsMatch(segment.start, declared.start) ||
|
|
367
|
+
!pointsMatch(segment.end, declared.end)
|
|
368
|
+
)
|
|
369
|
+
}) ||
|
|
370
|
+
distance(plan.planeEndpointVia.center, plan.targetPoint) <= EPSILON
|
|
371
|
+
) {
|
|
372
|
+
addIssue(
|
|
373
|
+
issues,
|
|
374
|
+
"trace-plan-mismatch",
|
|
375
|
+
`Trace ${plan.planeEndpointTrace.pcb_trace_id} does not encode a valid offset plane-to-endpoint dogbone`,
|
|
376
|
+
plan,
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
325
382
|
const firstRoutePoint = plan.trace.route.find(
|
|
326
383
|
(routePoint): routePoint is Extract<typeof routePoint, { x: number }> =>
|
|
327
384
|
"x" in routePoint && "y" in routePoint,
|
|
@@ -409,8 +466,12 @@ function plansHaveConnectedCopper(
|
|
|
409
466
|
first: FanoutRoutePlan,
|
|
410
467
|
second: FanoutRoutePlan,
|
|
411
468
|
): boolean {
|
|
412
|
-
|
|
413
|
-
|
|
469
|
+
const firstSegments = getPlanSegments(first)
|
|
470
|
+
const secondSegments = getPlanSegments(second)
|
|
471
|
+
const firstVias = getPlanVias(first)
|
|
472
|
+
const secondVias = getPlanVias(second)
|
|
473
|
+
for (const firstSegment of firstSegments) {
|
|
474
|
+
for (const secondSegment of secondSegments) {
|
|
414
475
|
if (
|
|
415
476
|
firstSegment.layer === secondSegment.layer &&
|
|
416
477
|
distanceSegmentToSegment(
|
|
@@ -424,41 +485,44 @@ function plansHaveConnectedCopper(
|
|
|
424
485
|
return true
|
|
425
486
|
}
|
|
426
487
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
488
|
+
for (const secondVia of secondVias) {
|
|
489
|
+
if (
|
|
490
|
+
secondVia.spanLayers.includes(firstSegment.layer) &&
|
|
491
|
+
distancePointToSegment(
|
|
492
|
+
secondVia.center,
|
|
493
|
+
firstSegment.start,
|
|
494
|
+
firstSegment.end,
|
|
495
|
+
) <=
|
|
496
|
+
secondVia.diameter / 2 + firstSegment.width / 2 + EPSILON
|
|
497
|
+
) {
|
|
498
|
+
return true
|
|
499
|
+
}
|
|
437
500
|
}
|
|
438
501
|
}
|
|
439
|
-
|
|
440
|
-
for (const secondSegment of
|
|
502
|
+
for (const firstVia of firstVias) {
|
|
503
|
+
for (const secondSegment of secondSegments) {
|
|
441
504
|
if (
|
|
442
|
-
|
|
505
|
+
firstVia.spanLayers.includes(secondSegment.layer) &&
|
|
443
506
|
distancePointToSegment(
|
|
444
|
-
|
|
507
|
+
firstVia.center,
|
|
445
508
|
secondSegment.start,
|
|
446
509
|
secondSegment.end,
|
|
447
510
|
) <=
|
|
448
|
-
|
|
511
|
+
firstVia.diameter / 2 + secondSegment.width / 2 + EPSILON
|
|
449
512
|
) {
|
|
450
513
|
return true
|
|
451
514
|
}
|
|
452
515
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
516
|
+
for (const secondVia of secondVias) {
|
|
517
|
+
if (
|
|
518
|
+
firstVia.spanLayers.some((layer) =>
|
|
519
|
+
secondVia.spanLayers.includes(layer),
|
|
520
|
+
) &&
|
|
521
|
+
distance(firstVia.center, secondVia.center) <=
|
|
522
|
+
(firstVia.diameter + secondVia.diameter) / 2 + EPSILON
|
|
523
|
+
) {
|
|
524
|
+
return true
|
|
525
|
+
}
|
|
462
526
|
}
|
|
463
527
|
}
|
|
464
528
|
return false
|
|
@@ -541,12 +605,9 @@ function validateClearances(params: {
|
|
|
541
605
|
}): void {
|
|
542
606
|
const { plans, inputSrj, clearance, issues } = params
|
|
543
607
|
for (const plan of plans) {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
segmentIndex++
|
|
548
|
-
) {
|
|
549
|
-
const segment = plan.segments[segmentIndex]!
|
|
608
|
+
const segments = getPlanSegments(plan)
|
|
609
|
+
for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) {
|
|
610
|
+
const segment = segments[segmentIndex]!
|
|
550
611
|
for (const obstacle of inputSrj.obstacles) {
|
|
551
612
|
if (!obstacle.layers.includes(segment.layer)) continue
|
|
552
613
|
if (
|
|
@@ -561,6 +622,16 @@ function validateClearances(params: {
|
|
|
561
622
|
) {
|
|
562
623
|
continue
|
|
563
624
|
}
|
|
625
|
+
if (
|
|
626
|
+
segmentIsLegalTerminalBodyEscape({
|
|
627
|
+
inputSrj,
|
|
628
|
+
segment,
|
|
629
|
+
bodyObstacle: obstacle,
|
|
630
|
+
connectionName: plan.connectionName,
|
|
631
|
+
})
|
|
632
|
+
) {
|
|
633
|
+
continue
|
|
634
|
+
}
|
|
564
635
|
const actual = distanceSegmentToObstacle(segment, obstacle)
|
|
565
636
|
const required = segment.width / 2 + clearance
|
|
566
637
|
if (actual < required - 1e-9) {
|
|
@@ -573,18 +644,16 @@ function validateClearances(params: {
|
|
|
573
644
|
}
|
|
574
645
|
}
|
|
575
646
|
}
|
|
576
|
-
|
|
647
|
+
for (const via of getPlanVias(plan)) {
|
|
577
648
|
for (const obstacle of inputSrj.obstacles) {
|
|
578
649
|
if (
|
|
579
|
-
!obstacle.layers.some((layer) =>
|
|
580
|
-
plan.via!.spanLayers.includes(layer),
|
|
581
|
-
) ||
|
|
650
|
+
!obstacle.layers.some((layer) => via.spanLayers.includes(layer)) ||
|
|
582
651
|
obstacleSharesElectricalNet(inputSrj, obstacle, plan.connectionName)
|
|
583
652
|
) {
|
|
584
653
|
continue
|
|
585
654
|
}
|
|
586
|
-
const actual = distancePointToObstacle(
|
|
587
|
-
const required =
|
|
655
|
+
const actual = distancePointToObstacle(via.center, obstacle)
|
|
656
|
+
const required = via.diameter / 2 + clearance
|
|
588
657
|
if (actual < required - 1e-9) {
|
|
589
658
|
addIssue(
|
|
590
659
|
issues,
|
|
@@ -614,8 +683,12 @@ function validateClearances(params: {
|
|
|
614
683
|
) {
|
|
615
684
|
continue
|
|
616
685
|
}
|
|
617
|
-
|
|
618
|
-
|
|
686
|
+
const firstSegments = getPlanSegments(first)
|
|
687
|
+
const secondSegments = getPlanSegments(second)
|
|
688
|
+
const firstVias = getPlanVias(first)
|
|
689
|
+
const secondVias = getPlanVias(second)
|
|
690
|
+
for (const firstSegment of firstSegments) {
|
|
691
|
+
for (const secondSegment of secondSegments) {
|
|
619
692
|
if (!segmentsAreClear(firstSegment, secondSegment, clearance)) {
|
|
620
693
|
addIssue(
|
|
621
694
|
issues,
|
|
@@ -626,37 +699,36 @@ function validateClearances(params: {
|
|
|
626
699
|
)
|
|
627
700
|
}
|
|
628
701
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
702
|
+
for (const secondVia of secondVias) {
|
|
703
|
+
if (
|
|
704
|
+
secondVia.spanLayers.includes(firstSegment.layer) &&
|
|
705
|
+
distancePointToSegment(
|
|
706
|
+
secondVia.center,
|
|
707
|
+
firstSegment.start,
|
|
708
|
+
firstSegment.end,
|
|
709
|
+
) <
|
|
710
|
+
secondVia.diameter / 2 + firstSegment.width / 2 + clearance - 1e-9
|
|
711
|
+
) {
|
|
712
|
+
addIssue(
|
|
713
|
+
issues,
|
|
714
|
+
"different-net-trace-via-clearance",
|
|
715
|
+
`Trace ${first.connectionName} violates via clearance to ${second.connectionName} on ${firstSegment.layer}`,
|
|
716
|
+
first,
|
|
717
|
+
second.connectionName,
|
|
718
|
+
)
|
|
719
|
+
}
|
|
645
720
|
}
|
|
646
721
|
}
|
|
647
|
-
|
|
648
|
-
for (const secondSegment of
|
|
722
|
+
for (const firstVia of firstVias) {
|
|
723
|
+
for (const secondSegment of secondSegments) {
|
|
649
724
|
if (
|
|
650
|
-
|
|
725
|
+
firstVia.spanLayers.includes(secondSegment.layer) &&
|
|
651
726
|
distancePointToSegment(
|
|
652
|
-
|
|
727
|
+
firstVia.center,
|
|
653
728
|
secondSegment.start,
|
|
654
729
|
secondSegment.end,
|
|
655
730
|
) <
|
|
656
|
-
|
|
657
|
-
secondSegment.width / 2 +
|
|
658
|
-
clearance -
|
|
659
|
-
1e-9
|
|
731
|
+
firstVia.diameter / 2 + secondSegment.width / 2 + clearance - 1e-9
|
|
660
732
|
) {
|
|
661
733
|
addIssue(
|
|
662
734
|
issues,
|
|
@@ -667,21 +739,22 @@ function validateClearances(params: {
|
|
|
667
739
|
)
|
|
668
740
|
}
|
|
669
741
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
742
|
+
for (const secondVia of secondVias) {
|
|
743
|
+
if (
|
|
744
|
+
firstVia.spanLayers.some((layer) =>
|
|
745
|
+
secondVia.spanLayers.includes(layer),
|
|
746
|
+
) &&
|
|
747
|
+
distance(firstVia.center, secondVia.center) <
|
|
748
|
+
(firstVia.diameter + secondVia.diameter) / 2 + clearance - 1e-9
|
|
749
|
+
) {
|
|
750
|
+
addIssue(
|
|
751
|
+
issues,
|
|
752
|
+
"different-net-via-clearance",
|
|
753
|
+
`Vias ${first.connectionName} and ${second.connectionName} violate clearance on an overlapping layer span`,
|
|
754
|
+
first,
|
|
755
|
+
second.connectionName,
|
|
756
|
+
)
|
|
757
|
+
}
|
|
685
758
|
}
|
|
686
759
|
}
|
|
687
760
|
}
|