@tscircuit/fanout-solver 0.0.49 → 0.0.51

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.
@@ -35,11 +35,24 @@ export interface DogboneViaSiteGeometryRules {
35
35
  preferBoundaryOutwardByBusId?: ReadonlyMap<string, boolean>
36
36
  /** Existing assignments that must be preserved while matching other pads. */
37
37
  fixedViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
38
+ /** Prefer these provisional sites, but allow rematching when they conflict. */
39
+ preferredViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
38
40
  /** Routed copper that every newly assigned through-via/dogbone must clear. */
39
41
  blockingSegments?: readonly {
40
42
  connectionIndex: number
41
43
  segment: RoutedSegment
42
44
  }[]
45
+ /** Routed vias that every newly assigned through-via/dogbone must clear. */
46
+ blockingVias?: readonly {
47
+ connectionIndex: number
48
+ center: Point2D
49
+ diameter: number
50
+ spanLayers: readonly string[]
51
+ }[]
52
+ /** Board obstacles outside the source component that dogbones must clear. */
53
+ additionalObstacles?: readonly Obstacle[]
54
+ /** Alternate horizontal plane dogbones across BGA rows. */
55
+ preferPlaneCheckerboardSites?: boolean
43
56
  /** True only when the two connections are allowed to merge copper. */
44
57
  canShareCopper?: (
45
58
  firstConnectionIndex: number,
@@ -148,6 +161,7 @@ function getComponentMatchingInputs(
148
161
  ): ComponentMatchingInput[] {
149
162
  const byComponent = new Map<string, ComponentMatchingInput>()
150
163
  const componentByConnectionIndex = new Map<number, string>()
164
+ const obstacleSetByComponentId = new Map<string, Set<Obstacle>>()
151
165
 
152
166
  for (const bus of preparedBuses) {
153
167
  let component = byComponent.get(bus.componentId)
@@ -162,6 +176,7 @@ function getComponentMatchingInputs(
162
176
  pitchY: Number.POSITIVE_INFINITY,
163
177
  }
164
178
  byComponent.set(bus.componentId, component)
179
+ obstacleSetByComponentId.set(bus.componentId, new Set())
165
180
  }
166
181
 
167
182
  component.xCoordinates.push(...bus.xCoordinates)
@@ -172,8 +187,10 @@ function getComponentMatchingInputs(
172
187
  if (Number.isFinite(bus.pitchY) && bus.pitchY > EPSILON) {
173
188
  component.pitchY = Math.min(component.pitchY, bus.pitchY)
174
189
  }
190
+ const obstacleSet = obstacleSetByComponentId.get(bus.componentId)!
175
191
  for (const obstacle of bus.componentObstacles) {
176
- if (!component.obstacles.includes(obstacle)) {
192
+ if (!obstacleSet.has(obstacle)) {
193
+ obstacleSet.add(obstacle)
177
194
  component.obstacles.push(obstacle)
178
195
  }
179
196
  }
@@ -325,6 +342,9 @@ function getConnectionCandidates(params: {
325
342
  }): ViaSiteCandidate[] {
326
343
  const { connection, component, rules } = params
327
344
  const { preparedConnection, direction } = connection
345
+ const obstacles = rules.additionalObstacles
346
+ ? [...new Set([...component.obstacles, ...rules.additionalObstacles])]
347
+ : component.obstacles
328
348
  const source = {
329
349
  x: preparedConnection.sourcePoint.x,
330
350
  y: preparedConnection.sourcePoint.y,
@@ -368,7 +388,7 @@ function getConnectionCandidates(params: {
368
388
  if (
369
389
  !viaSiteClearsObstacles({
370
390
  point,
371
- obstacles: component.obstacles,
391
+ obstacles,
372
392
  viaDiameter: rules.viaDiameter,
373
393
  clearance: rules.clearance,
374
394
  })
@@ -385,7 +405,7 @@ function getConnectionCandidates(params: {
385
405
  !sourceSegmentClearsOtherObstacles({
386
406
  segment: sourceSegment,
387
407
  sourceObstacle: preparedConnection.sourceObstacle,
388
- obstacles: component.obstacles,
408
+ obstacles,
389
409
  clearance: rules.clearance,
390
410
  })
391
411
  ) {
@@ -423,6 +443,35 @@ function getConnectionCandidates(params: {
423
443
  },
424
444
  )
425
445
  if (!candidateClearsRoutedCopper) continue
446
+ const candidateClearsRoutedVias = (rules.blockingVias ?? []).every(
447
+ (blocker) => {
448
+ if (blocker.connectionIndex === preparedConnection.connectionIndex) {
449
+ return true
450
+ }
451
+ if (
452
+ distance(point, blocker.center) <
453
+ (rules.viaDiameter + blocker.diameter) / 2 + rules.clearance - EPSILON
454
+ ) {
455
+ return false
456
+ }
457
+ if (
458
+ blocker.spanLayers.includes(sourceSegment.layer) &&
459
+ distancePointToSegment(
460
+ blocker.center,
461
+ sourceSegment.start,
462
+ sourceSegment.end,
463
+ ) <
464
+ blocker.diameter / 2 +
465
+ sourceSegment.width / 2 +
466
+ rules.clearance -
467
+ EPSILON
468
+ ) {
469
+ return false
470
+ }
471
+ return true
472
+ },
473
+ )
474
+ if (!candidateClearsRoutedVias) continue
426
475
  candidates.push({
427
476
  connectionIndex: preparedConnection.connectionIndex,
428
477
  point,
@@ -431,14 +480,37 @@ function getConnectionCandidates(params: {
431
480
  })
432
481
  }
433
482
 
483
+ const perpendicularCoordinates =
484
+ direction === "left" || direction === "right"
485
+ ? component.yCoordinates
486
+ : component.xCoordinates
487
+ const sourcePerpendicularCoordinate =
488
+ direction === "left" || direction === "right" ? source.y : source.x
489
+ const perpendicularGridIndex = perpendicularCoordinates.findIndex(
490
+ (coordinate) =>
491
+ Math.abs(coordinate - sourcePerpendicularCoordinate) <= EPSILON,
492
+ )
493
+ const planeCheckerboardSide =
494
+ perpendicularGridIndex >= 0
495
+ ? perpendicularGridIndex % 2 === 0
496
+ ? 1
497
+ : -1
498
+ : undefined
434
499
  const preferredPerpendicularSide =
435
500
  connection.terminationType === "boundary"
436
501
  ? rules.preferredBoundaryPerpendicularSideByBusId?.get(connection.busId)
437
- : undefined
502
+ : rules.preferPlaneCheckerboardSites &&
503
+ connection.terminationType === "plane" &&
504
+ (direction === "left" || direction === "right")
505
+ ? planeCheckerboardSide
506
+ : undefined
438
507
  const preferOutward =
439
508
  connection.terminationType === "boundary"
440
509
  ? (rules.preferBoundaryOutwardByBusId?.get(connection.busId) ?? true)
441
510
  : true
511
+ const preferredViaPoint = rules.preferredViaPointsByConnectionIndex?.get(
512
+ preparedConnection.connectionIndex,
513
+ )
442
514
  const getPerpendicularPreferenceRank = (
443
515
  candidate: ViaSiteCandidate,
444
516
  ): number => {
@@ -455,6 +527,10 @@ function getConnectionCandidates(params: {
455
527
  }
456
528
  return candidates.toSorted(
457
529
  (first, second) =>
530
+ (preferredViaPoint
531
+ ? Number(distance(first.point, preferredViaPoint) > EPSILON) -
532
+ Number(distance(second.point, preferredViaPoint) > EPSILON)
533
+ : 0) ||
458
534
  (preferOutward
459
535
  ? first.outwardRank - second.outwardRank
460
536
  : second.outwardRank - first.outwardRank) ||
@@ -525,6 +601,25 @@ function matchComponent(params: {
525
601
  }),
526
602
  )
527
603
  if (entries.some((entry) => entry.candidates.length === 0)) return null
604
+ const compatibilityCache = new Map<
605
+ ViaSiteCandidate,
606
+ Map<ViaSiteCandidate, boolean>
607
+ >()
608
+ const candidatesAreCompatible = (
609
+ first: ViaSiteCandidate,
610
+ second: ViaSiteCandidate,
611
+ ): boolean => {
612
+ const cached = compatibilityCache.get(first)?.get(second)
613
+ if (cached !== undefined) return cached
614
+ const compatible = candidatesAreMutuallyClear({ first, second, rules })
615
+ const firstCache = compatibilityCache.get(first) ?? new Map()
616
+ firstCache.set(second, compatible)
617
+ compatibilityCache.set(first, firstCache)
618
+ const secondCache = compatibilityCache.get(second) ?? new Map()
619
+ secondCache.set(first, compatible)
620
+ compatibilityCache.set(second, secondCache)
621
+ return compatible
622
+ }
528
623
  // Every solution must include each sole candidate. Seed and validate those
529
624
  // forced choices once so recursive matching only explores genuine choices.
530
625
  const forcedCandidates = entries.flatMap((entry) =>
@@ -542,11 +637,7 @@ function matchComponent(params: {
542
637
  previousIndex++
543
638
  ) {
544
639
  if (
545
- !candidatesAreMutuallyClear({
546
- first: candidate,
547
- second: forcedCandidates[previousIndex]!,
548
- rules,
549
- })
640
+ !candidatesAreCompatible(candidate, forcedCandidates[previousIndex]!)
550
641
  ) {
551
642
  return null
552
643
  }
@@ -584,11 +675,7 @@ function matchComponent(params: {
584
675
  ): ViaSiteCandidate[] =>
585
676
  entry.candidates.filter((candidate) =>
586
677
  [...assignedCandidates.values()].every((assignedCandidate) =>
587
- candidatesAreMutuallyClear({
588
- first: candidate,
589
- second: assignedCandidate,
590
- rules,
591
- }),
678
+ candidatesAreCompatible(candidate, assignedCandidate),
592
679
  ),
593
680
  )
594
681