@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.
@@ -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
  }
@@ -19,7 +19,11 @@ import {
19
19
  getConnectionNetKey,
20
20
  obstacleSharesElectricalNet,
21
21
  } from "./net-identity"
22
- import type { Point2D, RoutedSegment } from "./types"
22
+ import type {
23
+ Point2D,
24
+ RoutedSegment,
25
+ SimpleRouteJsonWithFanoutPlanes,
26
+ } from "./types"
23
27
 
24
28
  const EPSILON = 1e-6
25
29
 
@@ -64,11 +68,17 @@ interface ViaCopper {
64
68
  layers: string[]
65
69
  }
66
70
 
71
+ interface PlaneCopper {
72
+ type: "plane"
73
+ layer: string
74
+ }
75
+
67
76
  type CopperPrimitive =
68
77
  | EndpointCopper
69
78
  | ObstacleCopper
70
79
  | SegmentCopper
71
80
  | ViaCopper
81
+ | PlaneCopper
72
82
 
73
83
  function getPointLayers(point: ConnectionPoint): string[] {
74
84
  return "layer" in point ? [point.layer] : point.layers
@@ -124,6 +134,21 @@ function extractTraceCopper(
124
134
  }
125
135
 
126
136
  function primitivesTouch(first: CopperPrimitive, second: CopperPrimitive) {
137
+ if (first.type === "plane" && second.type === "plane") {
138
+ return first.layer === second.layer
139
+ }
140
+ if (first.type === "plane" && second.type === "via") {
141
+ return second.layers.includes(first.layer)
142
+ }
143
+ if (first.type === "via" && second.type === "plane") {
144
+ return primitivesTouch(second, first)
145
+ }
146
+ if (first.type === "plane" && second.type === "segment") {
147
+ return first.layer === second.segment.layer
148
+ }
149
+ if (first.type === "segment" && second.type === "plane") {
150
+ return primitivesTouch(second, first)
151
+ }
127
152
  if (first.type === "endpoint" && second.type === "endpoint") {
128
153
  return (
129
154
  layersOverlap(first.layers, second.layers) &&
@@ -265,6 +290,8 @@ export function validateOriginalEndpointConnectivity(params: {
265
290
  routedSrj: SimpleRouteJson
266
291
  }): OriginalEndpointConnectivityReport {
267
292
  const { inputSrj, routedSrj } = params
293
+ const planeConnectivity = (routedSrj as SimpleRouteJsonWithFanoutPlanes)
294
+ .fanoutPlaneConnectivity
268
295
  const layerNames = getCopperLayerNames(routedSrj.layerCount)
269
296
  const connectionsByNet = new Map<string, SimpleRouteConnection[]>()
270
297
  for (const connection of inputSrj.connections) {
@@ -303,10 +330,24 @@ export function validateOriginalEndpointConnectivity(params: {
303
330
  traceBelongsToNet(inputSrj, trace, representativeConnection),
304
331
  )
305
332
  .flatMap((trace) => extractTraceCopper(trace, layerNames))
333
+ const planeCopper: PlaneCopper[] = [
334
+ ...new Set(
335
+ (planeConnectivity ?? []).flatMap((plane) =>
336
+ connectionsShareElectricalNet(
337
+ inputSrj,
338
+ plane.connectionName,
339
+ representativeConnection.name,
340
+ )
341
+ ? [plane.layer]
342
+ : [],
343
+ ),
344
+ ),
345
+ ].map((layer) => ({ type: "plane", layer }))
306
346
  const copper: CopperPrimitive[] = [
307
347
  ...endpointCopper,
308
348
  ...obstacleCopper,
309
349
  ...routeCopper,
350
+ ...planeCopper,
310
351
  ]
311
352
  const connectedCopper = new DisjointSet(copper.length)
312
353
  for (let firstIndex = 0; firstIndex < copper.length; firstIndex++) {
@@ -23,6 +23,7 @@ export type RoutedCopperDrcIssueCode =
23
23
  | "unknown-trace-connection"
24
24
  | "unsupported-route-point"
25
25
  | "disconnected-trace"
26
+ | "via-at-endpoint"
26
27
  | "trace-obstacle-clearance"
27
28
  | "via-obstacle-clearance"
28
29
  | "different-net-trace-clearance"
@@ -61,7 +62,7 @@ interface TraceCopper {
61
62
  vias: RoutedVia[]
62
63
  }
63
64
 
64
- function isLegalTerminalBodyEscape(params: {
65
+ export function segmentIsLegalTerminalBodyEscape(params: {
65
66
  inputSrj: SimpleRouteJson
66
67
  segment: RoutedSegment
67
68
  bodyObstacle: SimpleRouteJson["obstacles"][number]
@@ -235,6 +236,14 @@ export function validateRoutedCopperDrc(params: {
235
236
  const issues: RoutedCopperDrcIssue[] = []
236
237
  const layerNames = getCopperLayerNames(routedSrj.layerCount)
237
238
  const traceCopper: TraceCopper[] = []
239
+ const originalAndRoutedEndpoints = [inputSrj, routedSrj].flatMap((srj) =>
240
+ srj.connections.flatMap((connection) =>
241
+ connection.pointsToConnect.map((point) => ({
242
+ connectionName: connection.name,
243
+ point,
244
+ })),
245
+ ),
246
+ )
238
247
 
239
248
  for (const trace of routedSrj.traces ?? []) {
240
249
  const connectionName = trace.connection_name
@@ -273,7 +282,7 @@ export function validateRoutedCopperDrc(params: {
273
282
  continue
274
283
  }
275
284
  if (
276
- isLegalTerminalBodyEscape({
285
+ segmentIsLegalTerminalBodyEscape({
277
286
  inputSrj,
278
287
  segment,
279
288
  bodyObstacle: obstacle,
@@ -297,6 +306,18 @@ export function validateRoutedCopperDrc(params: {
297
306
  }
298
307
  }
299
308
  for (const via of copper.vias) {
309
+ const coincidentEndpoint = originalAndRoutedEndpoints.find(
310
+ ({ point }) => distance(via.center, point) <= EPSILON,
311
+ )
312
+ if (coincidentEndpoint) {
313
+ addIssue(issues, {
314
+ code: "via-at-endpoint",
315
+ traceId: copper.trace.pcb_trace_id,
316
+ connectionName: copper.connectionName,
317
+ otherConnectionName: coincidentEndpoint.connectionName,
318
+ message: `Via in ${copper.trace.pcb_trace_id} is placed directly at an original or routed connection endpoint`,
319
+ })
320
+ }
300
321
  for (const obstacle of inputSrj.obstacles) {
301
322
  if (
302
323
  !obstacle.layers.some((layer) => via.spanLayers.includes(layer)) ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  "devDependencies": {
42
42
  "@biomejs/biome": "2.5.5",
43
43
  "@tsci/tscircuit.dataset-srj19-bga-passive-overlays": "github:tscircuit/dataset-srj19#c1206f3",
44
- "@tsci/tscircuit.dataset-srj29-bga-decoupling": "github:tscircuit/dataset-srj29-bga-decoupling#a8a1e74",
44
+ "@tsci/tscircuit.dataset-srj29-bga-decoupling": "github:tscircuit/dataset-srj29-bga-decoupling#bfdf7bc",
45
45
  "@tscircuit/footprinter": "0.0.394",
46
46
  "@types/bun": "1.3.14",
47
47
  "@types/react": "19.2.14",