@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.
- package/lib/fanout-solver.ts +1617 -143
- package/lib/match-component-dogbone-via-sites.ts +101 -14
- package/lib/route-bus.ts +345 -52
- package/lib/route-via-minimal-winding.ts +94 -23
- package/lib/types.ts +11 -0
- package/package.json +1 -1
|
@@ -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 (!
|
|
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
|
|
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
|
|
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
|
-
:
|
|
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
|
-
!
|
|
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
|
-
|
|
588
|
-
first: candidate,
|
|
589
|
-
second: assignedCandidate,
|
|
590
|
-
rules,
|
|
591
|
-
}),
|
|
678
|
+
candidatesAreCompatible(candidate, assignedCandidate),
|
|
592
679
|
),
|
|
593
680
|
)
|
|
594
681
|
|