@tscircuit/fanout-solver 0.0.64 → 0.0.65
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 +198 -6
- package/lib/get-free-boundary-tracks.ts +102 -0
- package/lib/match-angularly-ordered-local-vias.ts +239 -0
- package/lib/reflect-fanout-x.ts +67 -0
- package/lib/repair-peripheral-bus-lengths.ts +167 -0
- package/lib/route-adaptive-left-crossbar-bus.ts +425 -0
- package/lib/route-bottom-crossbar-bus.ts +559 -0
- package/lib/route-bus.ts +18 -2
- package/lib/route-left-crossbar-bus.ts +399 -0
- package/lib/route-opposite-bottom-crossbar-bus.ts +70 -0
- package/lib/route-peripheral-source-escapes.ts +488 -0
- package/lib/route-reserved-narrow-buses.ts +430 -0
- package/lib/route-reserved-source-buses.ts +243 -0
- package/lib/route-shallow-split-perimeter-bus.ts +268 -0
- package/lib/route-split-perimeter-bus.ts +397 -0
- package/lib/route-split-perimeter-source-escapes.ts +579 -0
- package/lib/route-staged-perimeter-bus.ts +257 -0
- package/lib/route-via-minimal-winding.ts +78 -30
- package/package.json +1 -1
package/lib/fanout-solver.ts
CHANGED
|
@@ -39,6 +39,13 @@ import {
|
|
|
39
39
|
routeBusAlternatives,
|
|
40
40
|
routeBusAlternativesSteps,
|
|
41
41
|
} from "./route-bus"
|
|
42
|
+
import { routePeripheralSourceEscapesSteps } from "./route-peripheral-source-escapes"
|
|
43
|
+
import { routeStagedPerimeterBusSteps } from "./route-staged-perimeter-bus"
|
|
44
|
+
import { routeReservedSourceBusesSteps } from "./route-reserved-source-buses"
|
|
45
|
+
import { repairPeripheralBusLengthsSteps } from "./repair-peripheral-bus-lengths"
|
|
46
|
+
import { routeSplitPerimeterSourceEscapesSteps } from "./route-split-perimeter-source-escapes"
|
|
47
|
+
import { routeSplitPerimeterBusSteps } from "./route-split-perimeter-bus"
|
|
48
|
+
import { routeShallowSplitPerimeterBusSteps } from "./route-shallow-split-perimeter-bus"
|
|
42
49
|
import { routeSingleLayerWithAdaptiveExitsSteps } from "./route-single-layer-adaptive-exits"
|
|
43
50
|
import { routeSingleLayerWithPushAndShove } from "./route-single-layer-push-shove"
|
|
44
51
|
import { getRuntimeProcess } from "./runtime-process"
|
|
@@ -84,6 +91,8 @@ interface ResolvedFanoutConfig {
|
|
|
84
91
|
|
|
85
92
|
interface EvaluatedAssignment extends AssignmentAttempt {
|
|
86
93
|
blockingBusIds: string[]
|
|
94
|
+
/** The bounded peripheral strategy may finish after complete validation. */
|
|
95
|
+
stopAfterCompleteValidation?: true
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
interface GroupedBeamState {
|
|
@@ -94,6 +103,7 @@ interface GroupedBeamState {
|
|
|
94
103
|
interface MixedTerminationState {
|
|
95
104
|
plans: FanoutRoutePlan[]
|
|
96
105
|
failedBusIds: string[]
|
|
106
|
+
stopAfterCompleteValidation?: true
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
type RoutingStrategy = "default" | "group-by-layer" | "deep-first"
|
|
@@ -513,6 +523,43 @@ function createInitialLayerAssignment(params: {
|
|
|
513
523
|
) {
|
|
514
524
|
assignment[bus.busId] = sourceLayer
|
|
515
525
|
} else if (viaLayers.length > 0) {
|
|
526
|
+
// A source strictly inside a wide field can be enclosed by its routes.
|
|
527
|
+
// Boundary sources still have an outward channel, so preserve the
|
|
528
|
+
// ordinary layer preference for those buses.
|
|
529
|
+
const sourceXs = bus.connections.map(
|
|
530
|
+
(connection) => connection.sourcePoint.x,
|
|
531
|
+
)
|
|
532
|
+
const sourceYs = bus.connections.map(
|
|
533
|
+
(connection) => connection.sourcePoint.y,
|
|
534
|
+
)
|
|
535
|
+
const embeddedSingletonCount = (layer: string) =>
|
|
536
|
+
buses.filter((other) => {
|
|
537
|
+
const point = other.connections[0]?.sourcePoint
|
|
538
|
+
return (
|
|
539
|
+
other.termination.type === "boundary" &&
|
|
540
|
+
point !== undefined &&
|
|
541
|
+
point.x > Math.min(...sourceXs) + 1e-9 &&
|
|
542
|
+
point.x < Math.max(...sourceXs) - 1e-9 &&
|
|
543
|
+
point.y > Math.min(...sourceYs) + 1e-9 &&
|
|
544
|
+
point.y < Math.max(...sourceYs) - 1e-9 &&
|
|
545
|
+
getCommonExplicitExitTargetLayer(other) === layer &&
|
|
546
|
+
isDenseSingletonEmbeddedInMultiLayerWideBus({
|
|
547
|
+
singletonBus: other,
|
|
548
|
+
singletonTargetLayer: layer,
|
|
549
|
+
wideBuses: [bus],
|
|
550
|
+
})
|
|
551
|
+
)
|
|
552
|
+
}).length
|
|
553
|
+
const isolatedLayers = viaLayers.toSorted(
|
|
554
|
+
(a, b) => embeddedSingletonCount(a) - embeddedSingletonCount(b),
|
|
555
|
+
)
|
|
556
|
+
if (
|
|
557
|
+
embeddedSingletonCount(isolatedLayers[0]!) <
|
|
558
|
+
embeddedSingletonCount(viaLayers[0]!)
|
|
559
|
+
) {
|
|
560
|
+
assignment[bus.busId] = isolatedLayers[0]!
|
|
561
|
+
continue
|
|
562
|
+
}
|
|
516
563
|
if (
|
|
517
564
|
preferOrderedCoordinatedWindingLayers &&
|
|
518
565
|
busUsesCoordinatedWinding(bus)
|
|
@@ -1535,6 +1582,127 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1535
1582
|
})
|
|
1536
1583
|
}
|
|
1537
1584
|
|
|
1585
|
+
/** Reserve source escapes before joining complete buses around the package. */
|
|
1586
|
+
private *routePeripheralMixedTerminationSteps(params: {
|
|
1587
|
+
busLayerAssignments: Readonly<Record<string, string>>
|
|
1588
|
+
busesInRoutingOrder: readonly PreparedBus[]
|
|
1589
|
+
}): Generator<FanoutWorkYield, MixedTerminationState | null, unknown> {
|
|
1590
|
+
if (this.config.allowBlindAndBuriedVias) return null
|
|
1591
|
+
const bus = params.busesInRoutingOrder.find(
|
|
1592
|
+
(candidate) =>
|
|
1593
|
+
candidate.termination.type === "boundary" &&
|
|
1594
|
+
(candidate.exitEdge === "right" || candidate.exitEdge === "left") &&
|
|
1595
|
+
candidate.connections.length >= 16 &&
|
|
1596
|
+
candidate.componentObstacles.length >= 200,
|
|
1597
|
+
)
|
|
1598
|
+
if (!bus) return null
|
|
1599
|
+
const targetLayer = params.busLayerAssignments[bus.busId]
|
|
1600
|
+
if (!targetLayer) return null
|
|
1601
|
+
const targetLayerByBusId = new Map(
|
|
1602
|
+
Object.entries(params.busLayerAssignments),
|
|
1603
|
+
)
|
|
1604
|
+
this.setInProgressPlans({
|
|
1605
|
+
phase: "route-peripheral-source-escapes",
|
|
1606
|
+
plans: [],
|
|
1607
|
+
busId: bus.busId,
|
|
1608
|
+
})
|
|
1609
|
+
const sourceSteps = (
|
|
1610
|
+
bus.exitEdge === "left"
|
|
1611
|
+
? routeSplitPerimeterSourceEscapesSteps
|
|
1612
|
+
: routePeripheralSourceEscapesSteps
|
|
1613
|
+
)({
|
|
1614
|
+
...this.config,
|
|
1615
|
+
srj: this.routingSrj,
|
|
1616
|
+
buses: this.preparedBuses,
|
|
1617
|
+
bus,
|
|
1618
|
+
targetLayer,
|
|
1619
|
+
targetLayerByBusId,
|
|
1620
|
+
})
|
|
1621
|
+
let sourceResult = sourceSteps.next()
|
|
1622
|
+
while (!sourceResult.done) {
|
|
1623
|
+
yield
|
|
1624
|
+
sourceResult = sourceSteps.next()
|
|
1625
|
+
}
|
|
1626
|
+
if (!sourceResult.value) return null
|
|
1627
|
+
let source = sourceResult.value
|
|
1628
|
+
const stageParams = {
|
|
1629
|
+
...this.config,
|
|
1630
|
+
...source,
|
|
1631
|
+
srj: this.routingSrj,
|
|
1632
|
+
bus,
|
|
1633
|
+
targetLayer,
|
|
1634
|
+
}
|
|
1635
|
+
const stageSteps =
|
|
1636
|
+
"lowerConnectionIndices" in source
|
|
1637
|
+
? routeSplitPerimeterBusSteps({ ...stageParams, ...source })
|
|
1638
|
+
: routeStagedPerimeterBusSteps(stageParams)
|
|
1639
|
+
let stageResult = stageSteps.next()
|
|
1640
|
+
while (!stageResult.done) {
|
|
1641
|
+
yield
|
|
1642
|
+
stageResult = stageSteps.next()
|
|
1643
|
+
}
|
|
1644
|
+
let stagePlans = stageResult.value
|
|
1645
|
+
if (!stagePlans && "lowerConnectionIndices" in source) {
|
|
1646
|
+
const shallowSteps = routeShallowSplitPerimeterBusSteps({
|
|
1647
|
+
...stageParams,
|
|
1648
|
+
...source,
|
|
1649
|
+
buses: this.preparedBuses,
|
|
1650
|
+
})
|
|
1651
|
+
let shallowResult = shallowSteps.next()
|
|
1652
|
+
while (!shallowResult.done) {
|
|
1653
|
+
yield
|
|
1654
|
+
shallowResult = shallowSteps.next()
|
|
1655
|
+
}
|
|
1656
|
+
if (shallowResult.value) {
|
|
1657
|
+
source = shallowResult.value
|
|
1658
|
+
stagePlans = shallowResult.value.plans
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
if (!stagePlans) return null
|
|
1662
|
+
this.setInProgressPlans({
|
|
1663
|
+
phase: "route-peripheral-bus-continuations",
|
|
1664
|
+
plans: stagePlans,
|
|
1665
|
+
busId: bus.busId,
|
|
1666
|
+
})
|
|
1667
|
+
const remainingSteps = routeReservedSourceBusesSteps({
|
|
1668
|
+
...this.config,
|
|
1669
|
+
srj: this.routingSrj,
|
|
1670
|
+
buses: this.preparedBuses,
|
|
1671
|
+
sourceEscapes: source.sourceEscapes,
|
|
1672
|
+
sourceBoundary: source.sourceBoundary,
|
|
1673
|
+
initialPlans: stagePlans,
|
|
1674
|
+
targetLayerByBusId,
|
|
1675
|
+
})
|
|
1676
|
+
let remainingResult = remainingSteps.next()
|
|
1677
|
+
while (!remainingResult.done) {
|
|
1678
|
+
yield
|
|
1679
|
+
remainingResult = remainingSteps.next()
|
|
1680
|
+
}
|
|
1681
|
+
if (!remainingResult.value) return null
|
|
1682
|
+
const repairSteps = repairPeripheralBusLengthsSteps({
|
|
1683
|
+
...this.config,
|
|
1684
|
+
srj: this.routingSrj,
|
|
1685
|
+
inputSrj: this.inputSrj,
|
|
1686
|
+
plans: remainingResult.value,
|
|
1687
|
+
preparedBuses: this.preparedBuses,
|
|
1688
|
+
sharedBoundary: this.getValidationBoundary(),
|
|
1689
|
+
})
|
|
1690
|
+
let repairResult = repairSteps.next()
|
|
1691
|
+
while (!repairResult.done) {
|
|
1692
|
+
yield
|
|
1693
|
+
repairResult = repairSteps.next()
|
|
1694
|
+
}
|
|
1695
|
+
if (!repairResult.value) return null
|
|
1696
|
+
const plans = repairResult.value
|
|
1697
|
+
const output = buildOutputSimpleRouteJson({
|
|
1698
|
+
inputSrj: this.inputSrj,
|
|
1699
|
+
plans,
|
|
1700
|
+
layerNames: this.config.layerNames,
|
|
1701
|
+
})
|
|
1702
|
+
if (!this.validateCompletePlans(plans, output).valid) return null
|
|
1703
|
+
return { plans, failedBusIds: [], stopAfterCompleteValidation: true }
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1538
1706
|
/**
|
|
1539
1707
|
* Through-all source vias from a wide boundary bus can consume the only
|
|
1540
1708
|
* legal dogbone channel for nearby plane pads. Conversely, routing hundreds
|
|
@@ -5142,6 +5310,23 @@ export class FanoutSolver extends BaseSolver {
|
|
|
5142
5310
|
|
|
5143
5311
|
let mixedTerminationState: MixedTerminationState | null = null
|
|
5144
5312
|
if (!useSingleLayerPushAndShove && routingStrategy === "default") {
|
|
5313
|
+
const peripheralSolver = this.createWorkSolver(
|
|
5314
|
+
"PeripheralMixedTerminationSolver",
|
|
5315
|
+
this.routePeripheralMixedTerminationSteps({
|
|
5316
|
+
busLayerAssignments,
|
|
5317
|
+
busesInRoutingOrder,
|
|
5318
|
+
}),
|
|
5319
|
+
)
|
|
5320
|
+
mixedTerminationState = (yield {
|
|
5321
|
+
type: "subsolver",
|
|
5322
|
+
solver: peripheralSolver,
|
|
5323
|
+
}) as MixedTerminationState | null
|
|
5324
|
+
}
|
|
5325
|
+
if (
|
|
5326
|
+
!mixedTerminationState &&
|
|
5327
|
+
!useSingleLayerPushAndShove &&
|
|
5328
|
+
routingStrategy === "default"
|
|
5329
|
+
) {
|
|
5145
5330
|
const denseSolver = this.createWorkSolver(
|
|
5146
5331
|
"DenseMixedTerminationSolver",
|
|
5147
5332
|
this.routeDenseThroughAllMixedTerminationSteps({
|
|
@@ -5341,6 +5526,10 @@ export class FanoutSolver extends BaseSolver {
|
|
|
5341
5526
|
return {
|
|
5342
5527
|
summary,
|
|
5343
5528
|
plans,
|
|
5529
|
+
...(mixedTerminationState?.stopAfterCompleteValidation &&
|
|
5530
|
+
validation?.valid
|
|
5531
|
+
? { stopAfterCompleteValidation: true as const }
|
|
5532
|
+
: {}),
|
|
5344
5533
|
blockingBusIds: [...blockingBusCounts.entries()]
|
|
5345
5534
|
.toSorted(([, firstCount], [, secondCount]) => secondCount - firstCount)
|
|
5346
5535
|
.map(([busId]) => busId),
|
|
@@ -5361,7 +5550,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
5361
5550
|
if (
|
|
5362
5551
|
bestAttempt.summary.routedConnectionCount ===
|
|
5363
5552
|
this.inputSrj.connections.length &&
|
|
5364
|
-
|
|
5553
|
+
(bestAttempt.stopAfterCompleteValidation ||
|
|
5554
|
+
this.getCoordinatedAdditionalViaCount(bestAttempt.plans) === 0)
|
|
5365
5555
|
) {
|
|
5366
5556
|
return bestAttempt
|
|
5367
5557
|
}
|
|
@@ -5378,7 +5568,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
5378
5568
|
if (
|
|
5379
5569
|
bestAttempt.summary.routedConnectionCount ===
|
|
5380
5570
|
this.inputSrj.connections.length &&
|
|
5381
|
-
|
|
5571
|
+
(bestAttempt.stopAfterCompleteValidation ||
|
|
5572
|
+
this.getCoordinatedAdditionalViaCount(bestAttempt.plans) === 0)
|
|
5382
5573
|
) {
|
|
5383
5574
|
return bestAttempt
|
|
5384
5575
|
}
|
|
@@ -5932,10 +6123,11 @@ export class FanoutSolver extends BaseSolver {
|
|
|
5932
6123
|
bestScore: this.bestAttempt.summary.score,
|
|
5933
6124
|
}
|
|
5934
6125
|
if (
|
|
5935
|
-
this.
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
6126
|
+
(attempt.stopAfterCompleteValidation && this.hasCompleteBestAttempt()) ||
|
|
6127
|
+
(this.groupedBeamEvaluated &&
|
|
6128
|
+
attempt.summary.routedConnectionCount ===
|
|
6129
|
+
this.inputSrj.connections.length &&
|
|
6130
|
+
this.getCoordinatedAdditionalViaCount(this.bestAttempt.plans) === 0)
|
|
5939
6131
|
) {
|
|
5940
6132
|
this.completeBestAttemptEndpoints()
|
|
5941
6133
|
this.solved = true
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { getCornerBandSide } from "./boundary-exit"
|
|
2
|
+
import type { RouteBusParams } from "./route-bus"
|
|
3
|
+
|
|
4
|
+
/** Midpoints of unoccupied edge intervals, clipped to the bus's declared band. */
|
|
5
|
+
export function getFreeBoundaryTracks(
|
|
6
|
+
params: Pick<
|
|
7
|
+
RouteBusParams,
|
|
8
|
+
| "bus"
|
|
9
|
+
| "targetLayer"
|
|
10
|
+
| "acceptedPlans"
|
|
11
|
+
| "reservedVias"
|
|
12
|
+
| "traceWidth"
|
|
13
|
+
| "clearance"
|
|
14
|
+
>,
|
|
15
|
+
): number[] {
|
|
16
|
+
const { bus, targetLayer, traceWidth, clearance } = params
|
|
17
|
+
if (!bus.exitEdge) return []
|
|
18
|
+
const vertical = bus.exitEdge === "left" || bus.exitEdge === "right"
|
|
19
|
+
const along = vertical ? "y" : "x"
|
|
20
|
+
const across = vertical ? "x" : "y"
|
|
21
|
+
const boundary = bus.sharedBoundary
|
|
22
|
+
const edge =
|
|
23
|
+
bus.exitEdge === "left"
|
|
24
|
+
? boundary.minX
|
|
25
|
+
: bus.exitEdge === "right"
|
|
26
|
+
? boundary.maxX
|
|
27
|
+
: bus.exitEdge === "bottom"
|
|
28
|
+
? boundary.minY
|
|
29
|
+
: boundary.maxY
|
|
30
|
+
const lower = vertical ? boundary.minY : boundary.minX
|
|
31
|
+
const upper = vertical ? boundary.maxY : boundary.maxX
|
|
32
|
+
const middle = (lower + upper) / 2
|
|
33
|
+
const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
34
|
+
const minimum = (side === "maximum" ? middle : lower) + traceWidth / 2
|
|
35
|
+
const maximum = (side === "minimum" ? middle : upper) - traceWidth / 2
|
|
36
|
+
const blocked: [number, number][] = []
|
|
37
|
+
for (const plan of params.acceptedPlans) {
|
|
38
|
+
for (const segment of [
|
|
39
|
+
...plan.segments,
|
|
40
|
+
...(plan.planeEndpointSegments ?? []),
|
|
41
|
+
]) {
|
|
42
|
+
if (segment.layer !== targetLayer) continue
|
|
43
|
+
const radius = (segment.width + traceWidth) / 2 + clearance
|
|
44
|
+
const start = segment.start[across]
|
|
45
|
+
const delta = segment.end[across] - start
|
|
46
|
+
let from = 0
|
|
47
|
+
let to = 1
|
|
48
|
+
if (Math.abs(delta) < 1e-9) {
|
|
49
|
+
if (Math.abs(start - edge) > radius) continue
|
|
50
|
+
} else {
|
|
51
|
+
const first = (edge - radius - start) / delta
|
|
52
|
+
const last = (edge + radius - start) / delta
|
|
53
|
+
from = Math.max(0, Math.min(first, last))
|
|
54
|
+
to = Math.min(1, Math.max(first, last))
|
|
55
|
+
if (from > to) continue
|
|
56
|
+
}
|
|
57
|
+
const alongStart = segment.start[along]
|
|
58
|
+
const alongDelta = segment.end[along] - alongStart
|
|
59
|
+
const first = alongStart + from * alongDelta
|
|
60
|
+
const last = alongStart + to * alongDelta
|
|
61
|
+
blocked.push([
|
|
62
|
+
Math.min(first, last) - radius,
|
|
63
|
+
Math.max(first, last) + radius,
|
|
64
|
+
])
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const vias = [
|
|
68
|
+
...params.acceptedPlans.flatMap((plan) => [
|
|
69
|
+
...(plan.via ? [plan.via] : []),
|
|
70
|
+
...(plan.additionalVias ?? []),
|
|
71
|
+
...(plan.planeEndpointVia ? [plan.planeEndpointVia] : []),
|
|
72
|
+
]),
|
|
73
|
+
...(params.reservedVias ?? []).map((reserved) => reserved.via),
|
|
74
|
+
]
|
|
75
|
+
for (const via of vias) {
|
|
76
|
+
if (!via.spanLayers.includes(targetLayer)) continue
|
|
77
|
+
const radius = via.diameter / 2 + traceWidth / 2 + clearance
|
|
78
|
+
const distance = Math.abs(via.center[across] - edge)
|
|
79
|
+
if (distance >= radius) continue
|
|
80
|
+
const extent = Math.sqrt(radius * radius - distance * distance)
|
|
81
|
+
blocked.push([via.center[along] - extent, via.center[along] + extent])
|
|
82
|
+
}
|
|
83
|
+
const gaps: [number, number][] = []
|
|
84
|
+
let cursor = minimum
|
|
85
|
+
for (const [from, to] of blocked.sort((a, b) => a[0] - b[0])) {
|
|
86
|
+
if (to <= cursor || from >= maximum) continue
|
|
87
|
+
if (from > cursor) gaps.push([cursor, Math.min(from, maximum)])
|
|
88
|
+
cursor = Math.max(cursor, to)
|
|
89
|
+
if (cursor >= maximum) break
|
|
90
|
+
}
|
|
91
|
+
if (cursor < maximum) gaps.push([cursor, maximum])
|
|
92
|
+
// Edge order gives deterministic, symmetric coverage without hard-coded tracks.
|
|
93
|
+
const tracks = gaps
|
|
94
|
+
.filter(([from, to]) => to - from > 1e-6)
|
|
95
|
+
.map(([from, to]) => (from + to) / 2)
|
|
96
|
+
return tracks.length <= 32
|
|
97
|
+
? tracks
|
|
98
|
+
: Array.from(
|
|
99
|
+
{ length: 32 },
|
|
100
|
+
(_, index) => tracks[Math.round((index * (tracks.length - 1)) / 31)]!,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { distance, distancePointToSegment, segmentsAreClear } from "./geometry"
|
|
2
|
+
import {
|
|
3
|
+
getComponentDogboneViaSiteCandidates,
|
|
4
|
+
matchComponentDogboneViaSites,
|
|
5
|
+
type DogboneViaSiteGeometryRules,
|
|
6
|
+
} from "./match-component-dogbone-via-sites"
|
|
7
|
+
import type {
|
|
8
|
+
Point2D,
|
|
9
|
+
PreparedBus,
|
|
10
|
+
PreparedConnection,
|
|
11
|
+
RoutedSegment,
|
|
12
|
+
} from "./types"
|
|
13
|
+
|
|
14
|
+
const EPSILON = 1e-9
|
|
15
|
+
const TAU = Math.PI * 2
|
|
16
|
+
interface Candidate {
|
|
17
|
+
connectionIndex: number
|
|
18
|
+
point: Point2D
|
|
19
|
+
angle: number
|
|
20
|
+
sourceSegment: RoutedSegment
|
|
21
|
+
siteIndex: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Reserve ordered source vias, then assign every remaining source via together. */
|
|
25
|
+
export function matchAngularlyOrderedLocalVias(params: {
|
|
26
|
+
buses: readonly PreparedBus[]
|
|
27
|
+
busId: string
|
|
28
|
+
rules: DogboneViaSiteGeometryRules
|
|
29
|
+
maximumOrderingStates?: number
|
|
30
|
+
maximumCompleteAssignments?: number
|
|
31
|
+
}): Map<number, Point2D> | null {
|
|
32
|
+
const { buses, rules } = params
|
|
33
|
+
const bus = buses.find((b) => b.busId === params.busId)
|
|
34
|
+
if (
|
|
35
|
+
!bus ||
|
|
36
|
+
bus.termination.type !== "boundary" ||
|
|
37
|
+
!bus.exitEdge ||
|
|
38
|
+
bus.connections.length < 3
|
|
39
|
+
)
|
|
40
|
+
return null
|
|
41
|
+
const maximumOrderingStates = params.maximumOrderingStates ?? 10_000
|
|
42
|
+
const maximumCompleteAssignments = params.maximumCompleteAssignments ?? 16
|
|
43
|
+
for (const [name, value] of Object.entries({
|
|
44
|
+
maximumOrderingStates,
|
|
45
|
+
maximumCompleteAssignments,
|
|
46
|
+
})) {
|
|
47
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
48
|
+
throw new Error(`FanoutSolver: ${name} must be a positive safe integer`)
|
|
49
|
+
}
|
|
50
|
+
const center = {
|
|
51
|
+
x: (bus.componentBounds.minX + bus.componentBounds.maxX) / 2,
|
|
52
|
+
y: (bus.componentBounds.minY + bus.componentBounds.maxY) / 2,
|
|
53
|
+
}
|
|
54
|
+
const angle = (p: Point2D) => Math.atan2(p.y - center.y, p.x - center.x)
|
|
55
|
+
const targetCoordinate = (c: PreparedConnection) => {
|
|
56
|
+
const p = c.exitTargetPoint ?? c.targetPoint
|
|
57
|
+
return bus.exitEdge === "right"
|
|
58
|
+
? p.y
|
|
59
|
+
: bus.exitEdge === "top"
|
|
60
|
+
? -p.x
|
|
61
|
+
: bus.exitEdge === "left"
|
|
62
|
+
? -p.y
|
|
63
|
+
: p.x
|
|
64
|
+
}
|
|
65
|
+
const ordered = bus.connections.toSorted(
|
|
66
|
+
(a, b) =>
|
|
67
|
+
targetCoordinate(a) - targetCoordinate(b) ||
|
|
68
|
+
a.connectionIndex - b.connectionIndex,
|
|
69
|
+
)
|
|
70
|
+
const sourceAngles: number[] = []
|
|
71
|
+
for (const c of ordered) {
|
|
72
|
+
if (distance(c.sourcePoint, center) < EPSILON) return null
|
|
73
|
+
let a = angle(c.sourcePoint)
|
|
74
|
+
while (a < (sourceAngles.at(-1) ?? a) - EPSILON) a += TAU
|
|
75
|
+
sourceAngles.push(a)
|
|
76
|
+
}
|
|
77
|
+
if (sourceAngles.at(-1)! - sourceAngles[0]! >= TAU - EPSILON) return null
|
|
78
|
+
const connections = new Map(
|
|
79
|
+
buses.flatMap((b) =>
|
|
80
|
+
b.connections.map((c) => [c.connectionIndex, c] as const),
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
const orderedIndex = new Map(ordered.map((c, i) => [c.connectionIndex, i]))
|
|
84
|
+
const sites = new Map<string, number>()
|
|
85
|
+
const candidateGroups = new Map<number, Candidate[]>()
|
|
86
|
+
for (const c of connections.values())
|
|
87
|
+
candidateGroups.set(c.connectionIndex, [])
|
|
88
|
+
for (const raw of getComponentDogboneViaSiteCandidates(buses, rules)) {
|
|
89
|
+
const c = connections.get(raw.connectionIndex)!
|
|
90
|
+
const key = `${raw.point.x.toFixed(9)},${raw.point.y.toFixed(9)}`
|
|
91
|
+
if (!sites.has(key)) sites.set(key, sites.size)
|
|
92
|
+
const index = orderedIndex.get(raw.connectionIndex)
|
|
93
|
+
const sourceAngle = angle(c.sourcePoint)
|
|
94
|
+
const a =
|
|
95
|
+
index === undefined
|
|
96
|
+
? angle(raw.point)
|
|
97
|
+
: sourceAngles[index]! +
|
|
98
|
+
Math.atan2(
|
|
99
|
+
Math.sin(angle(raw.point) - sourceAngle),
|
|
100
|
+
Math.cos(angle(raw.point) - sourceAngle),
|
|
101
|
+
)
|
|
102
|
+
candidateGroups.get(raw.connectionIndex)!.push({
|
|
103
|
+
...raw,
|
|
104
|
+
angle: a,
|
|
105
|
+
siteIndex: sites.get(key)!,
|
|
106
|
+
sourceSegment: {
|
|
107
|
+
start: c.sourcePoint,
|
|
108
|
+
end: raw.point,
|
|
109
|
+
layer: c.sourceLayer,
|
|
110
|
+
width: rules.traceWidth,
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
const groups = [...candidateGroups.entries()].map(
|
|
115
|
+
([connectionIndex, candidates]) => ({ connectionIndex, candidates }),
|
|
116
|
+
)
|
|
117
|
+
if (groups.some((g) => g.candidates.length === 0)) return null
|
|
118
|
+
const domains = ordered.map((c, i) =>
|
|
119
|
+
candidateGroups
|
|
120
|
+
.get(c.connectionIndex)!
|
|
121
|
+
.toSorted(
|
|
122
|
+
(a, b) =>
|
|
123
|
+
Math.abs(a.angle - sourceAngles[i]!) -
|
|
124
|
+
Math.abs(b.angle - sourceAngles[i]!) ||
|
|
125
|
+
distance(b.point, center) - distance(a.point, center),
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
const requiredHoleSeparation = rules.viaHoleDiameter
|
|
129
|
+
? rules.viaHoleDiameter + (rules.holeToHoleClearance ?? rules.clearance)
|
|
130
|
+
: 0
|
|
131
|
+
const requiredViaToTraceSeparation =
|
|
132
|
+
rules.viaDiameter / 2 + rules.traceWidth / 2 + rules.clearance
|
|
133
|
+
const compatible = (a: Candidate, b: Candidate) => {
|
|
134
|
+
const share =
|
|
135
|
+
rules.canShareCopper?.(a.connectionIndex, b.connectionIndex) ?? false
|
|
136
|
+
const minimumViaDistance = share
|
|
137
|
+
? requiredHoleSeparation
|
|
138
|
+
: Math.max(requiredHoleSeparation, rules.viaDiameter + rules.clearance)
|
|
139
|
+
if (distance(a.point, b.point) < minimumViaDistance - EPSILON) return false
|
|
140
|
+
if (share) return true
|
|
141
|
+
return (
|
|
142
|
+
distancePointToSegment(
|
|
143
|
+
a.point,
|
|
144
|
+
b.sourceSegment.start,
|
|
145
|
+
b.sourceSegment.end,
|
|
146
|
+
) >=
|
|
147
|
+
requiredViaToTraceSeparation - EPSILON &&
|
|
148
|
+
distancePointToSegment(
|
|
149
|
+
b.point,
|
|
150
|
+
a.sourceSegment.start,
|
|
151
|
+
a.sourceSegment.end,
|
|
152
|
+
) >=
|
|
153
|
+
requiredViaToTraceSeparation - EPSILON &&
|
|
154
|
+
segmentsAreClear(a.sourceSegment, b.sourceSegment, rules.clearance)
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
const forbidden = new Map<Candidate, Set<Candidate>>()
|
|
158
|
+
for (const domain of domains)
|
|
159
|
+
for (const a of domain) {
|
|
160
|
+
const conflicts = new Set<Candidate>()
|
|
161
|
+
for (const group of groups)
|
|
162
|
+
for (const b of group.candidates) {
|
|
163
|
+
if (a.connectionIndex !== b.connectionIndex && !compatible(a, b))
|
|
164
|
+
conflicts.add(b)
|
|
165
|
+
}
|
|
166
|
+
forbidden.set(a, conflicts)
|
|
167
|
+
}
|
|
168
|
+
const chosen: Candidate[] = []
|
|
169
|
+
// A perfect matching is necessary because distinct drilled holes cannot use
|
|
170
|
+
// the same site. It is a pruning test; native matching still validates copper.
|
|
171
|
+
const canUseDistinctSiteCheck =
|
|
172
|
+
requiredHoleSeparation > EPSILON || !rules.canShareCopper
|
|
173
|
+
const remainingSitesCanMatch = () => {
|
|
174
|
+
if (!canUseDistinctSiteCheck) return true
|
|
175
|
+
const selected = new Map(
|
|
176
|
+
chosen.map((c) => [c.connectionIndex, c.siteIndex]),
|
|
177
|
+
)
|
|
178
|
+
const allowed = groups
|
|
179
|
+
.map((g) =>
|
|
180
|
+
g.candidates
|
|
181
|
+
.filter(
|
|
182
|
+
(c) =>
|
|
183
|
+
(!selected.has(c.connectionIndex) ||
|
|
184
|
+
selected.get(c.connectionIndex) === c.siteIndex) &&
|
|
185
|
+
chosen.every((a) => !forbidden.get(a)!.has(c)),
|
|
186
|
+
)
|
|
187
|
+
.map((c) => c.siteIndex),
|
|
188
|
+
)
|
|
189
|
+
.sort((a, b) => a.length - b.length)
|
|
190
|
+
if (allowed.some((d) => d.length === 0)) return false
|
|
191
|
+
const owner = new Int32Array(sites.size).fill(-1)
|
|
192
|
+
const augment = (index: number, visited: Uint8Array): boolean => {
|
|
193
|
+
for (const site of allowed[index]!) {
|
|
194
|
+
if (visited[site]) continue
|
|
195
|
+
visited[site] = 1
|
|
196
|
+
if (owner[site] === -1 || augment(owner[site]!, visited)) {
|
|
197
|
+
owner[site] = index
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
return allowed.every((_, i) => augment(i, new Uint8Array(sites.size)))
|
|
204
|
+
}
|
|
205
|
+
let states = 0
|
|
206
|
+
let completeAssignments = 0
|
|
207
|
+
let result: Map<number, Point2D> | null = null
|
|
208
|
+
const search = (index: number): boolean => {
|
|
209
|
+
if (++states > maximumOrderingStates) return false
|
|
210
|
+
if (index === domains.length) {
|
|
211
|
+
if (++completeAssignments > maximumCompleteAssignments) return false
|
|
212
|
+
const fixed = new Map(rules.fixedViaPointsByConnectionIndex)
|
|
213
|
+
for (const c of chosen) fixed.set(c.connectionIndex, c.point)
|
|
214
|
+
result = matchComponentDogboneViaSites(buses, {
|
|
215
|
+
...rules,
|
|
216
|
+
fixedViaPointsByConnectionIndex: fixed,
|
|
217
|
+
})
|
|
218
|
+
return result !== null
|
|
219
|
+
}
|
|
220
|
+
for (const candidate of domains[index]!) {
|
|
221
|
+
if (
|
|
222
|
+
candidate.angle < (chosen.at(-1)?.angle ?? -Infinity) - EPSILON ||
|
|
223
|
+
chosen.some((c) => forbidden.get(c)!.has(candidate))
|
|
224
|
+
)
|
|
225
|
+
continue
|
|
226
|
+
chosen.push(candidate)
|
|
227
|
+
if (remainingSitesCanMatch() && search(index + 1)) return true
|
|
228
|
+
chosen.pop()
|
|
229
|
+
if (
|
|
230
|
+
states > maximumOrderingStates ||
|
|
231
|
+
completeAssignments >= maximumCompleteAssignments
|
|
232
|
+
)
|
|
233
|
+
break
|
|
234
|
+
}
|
|
235
|
+
return false
|
|
236
|
+
}
|
|
237
|
+
search(0)
|
|
238
|
+
return result
|
|
239
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View horizontal fanout geometry from its opposite edge. A shared memo keeps
|
|
3
|
+
* pad identity intact: clearance checks compare a plan's sourceObstacle with
|
|
4
|
+
* the same obstacle in the transformed SRJ.
|
|
5
|
+
*/
|
|
6
|
+
export function reflectFanoutX<T>(value: T): T {
|
|
7
|
+
const memo = new Map<object, unknown>()
|
|
8
|
+
const reflect = (item: unknown): unknown => {
|
|
9
|
+
if (item === null || typeof item !== "object") return item
|
|
10
|
+
if (memo.has(item)) return memo.get(item)
|
|
11
|
+
if (item instanceof Map) {
|
|
12
|
+
const result = new Map()
|
|
13
|
+
memo.set(item, result)
|
|
14
|
+
for (const [key, entry] of item) result.set(key, reflect(entry))
|
|
15
|
+
return result
|
|
16
|
+
}
|
|
17
|
+
if (item instanceof Set) {
|
|
18
|
+
const result = new Set(item)
|
|
19
|
+
memo.set(item, result)
|
|
20
|
+
return result
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(item)) {
|
|
23
|
+
const result: unknown[] = []
|
|
24
|
+
memo.set(item, result)
|
|
25
|
+
for (const entry of item) result.push(reflect(entry))
|
|
26
|
+
return result
|
|
27
|
+
}
|
|
28
|
+
const original = item as Record<string, unknown>
|
|
29
|
+
const result: Record<string, unknown> = {}
|
|
30
|
+
memo.set(item, result)
|
|
31
|
+
for (const [key, entry] of Object.entries(original))
|
|
32
|
+
result[key] = reflect(entry)
|
|
33
|
+
if (typeof original.x === "number" && typeof original.y === "number")
|
|
34
|
+
result.x = -original.x
|
|
35
|
+
if (
|
|
36
|
+
typeof original.minX === "number" &&
|
|
37
|
+
typeof original.maxX === "number"
|
|
38
|
+
) {
|
|
39
|
+
result.minX = -original.maxX
|
|
40
|
+
result.maxX = -original.minX
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(original.xCoordinates))
|
|
43
|
+
result.xCoordinates = original.xCoordinates
|
|
44
|
+
.map((x: number) => -x)
|
|
45
|
+
.sort((a, b) => a - b)
|
|
46
|
+
if (typeof original.ccwRotationDegrees === "number")
|
|
47
|
+
result.ccwRotationDegrees = -original.ccwRotationDegrees
|
|
48
|
+
const edge = (direction: string) =>
|
|
49
|
+
direction === "left"
|
|
50
|
+
? "right"
|
|
51
|
+
: direction === "right"
|
|
52
|
+
? "left"
|
|
53
|
+
: direction
|
|
54
|
+
for (const key of ["direction", "exitEdge", "preferredExit"] as const) {
|
|
55
|
+
if (typeof original[key] === "string")
|
|
56
|
+
result[key] = original[key].split("-").map(edge).join("-")
|
|
57
|
+
}
|
|
58
|
+
if (
|
|
59
|
+
original.cornerBandSide &&
|
|
60
|
+
(original.exitEdge === "top" || original.exitEdge === "bottom")
|
|
61
|
+
)
|
|
62
|
+
result.cornerBandSide =
|
|
63
|
+
original.cornerBandSide === "minimum" ? "maximum" : "minimum"
|
|
64
|
+
return result
|
|
65
|
+
}
|
|
66
|
+
return reflect(value) as T
|
|
67
|
+
}
|