@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.
@@ -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
- for (const firstSegment of first.segments) {
413
- for (const secondSegment of second.segments) {
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
- if (
428
- second.via?.spanLayers.includes(firstSegment.layer) &&
429
- distancePointToSegment(
430
- second.via.center,
431
- firstSegment.start,
432
- firstSegment.end,
433
- ) <=
434
- second.via.diameter / 2 + firstSegment.width / 2 + EPSILON
435
- ) {
436
- return true
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
- if (first.via) {
440
- for (const secondSegment of second.segments) {
502
+ for (const firstVia of firstVias) {
503
+ for (const secondSegment of secondSegments) {
441
504
  if (
442
- first.via.spanLayers.includes(secondSegment.layer) &&
505
+ firstVia.spanLayers.includes(secondSegment.layer) &&
443
506
  distancePointToSegment(
444
- first.via.center,
507
+ firstVia.center,
445
508
  secondSegment.start,
446
509
  secondSegment.end,
447
510
  ) <=
448
- first.via.diameter / 2 + secondSegment.width / 2 + EPSILON
511
+ firstVia.diameter / 2 + secondSegment.width / 2 + EPSILON
449
512
  ) {
450
513
  return true
451
514
  }
452
515
  }
453
- if (
454
- second.via &&
455
- first.via.spanLayers.some((layer) =>
456
- second.via!.spanLayers.includes(layer),
457
- ) &&
458
- distance(first.via.center, second.via.center) <=
459
- (first.via.diameter + second.via.diameter) / 2 + EPSILON
460
- ) {
461
- return true
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
- for (
545
- let segmentIndex = 0;
546
- segmentIndex < plan.segments.length;
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
- if (plan.via) {
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(plan.via.center, obstacle)
587
- const required = plan.via.diameter / 2 + clearance
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
- for (const firstSegment of first.segments) {
618
- for (const secondSegment of second.segments) {
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
- if (
630
- second.via?.spanLayers.includes(firstSegment.layer) &&
631
- distancePointToSegment(
632
- second.via.center,
633
- firstSegment.start,
634
- firstSegment.end,
635
- ) <
636
- second.via.diameter / 2 + firstSegment.width / 2 + clearance - 1e-9
637
- ) {
638
- addIssue(
639
- issues,
640
- "different-net-trace-via-clearance",
641
- `Trace ${first.connectionName} violates via clearance to ${second.connectionName} on ${firstSegment.layer}`,
642
- first,
643
- second.connectionName,
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
- if (first.via) {
648
- for (const secondSegment of second.segments) {
722
+ for (const firstVia of firstVias) {
723
+ for (const secondSegment of secondSegments) {
649
724
  if (
650
- first.via.spanLayers.includes(secondSegment.layer) &&
725
+ firstVia.spanLayers.includes(secondSegment.layer) &&
651
726
  distancePointToSegment(
652
- first.via.center,
727
+ firstVia.center,
653
728
  secondSegment.start,
654
729
  secondSegment.end,
655
730
  ) <
656
- first.via.diameter / 2 +
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
- if (
671
- second.via &&
672
- first.via.spanLayers.some((layer) =>
673
- second.via!.spanLayers.includes(layer),
674
- ) &&
675
- distance(first.via.center, second.via.center) <
676
- (first.via.diameter + second.via.diameter) / 2 + clearance - 1e-9
677
- ) {
678
- addIssue(
679
- issues,
680
- "different-net-via-clearance",
681
- `Vias ${first.connectionName} and ${second.connectionName} violate clearance on an overlapping layer span`,
682
- first,
683
- second.connectionName,
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
  }