@tscircuit/fanout-solver 0.0.53 → 0.0.55
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 +147 -25
- package/lib/match-bus-lengths.ts +106 -0
- package/lib/route-bus.ts +87 -26
- package/lib/route-via-minimal-winding.ts +22 -15
- package/lib/runtime-process.ts +10 -0
- package/lib/shorten-bus-plans.ts +75 -0
- package/lib/types.ts +2 -0
- package/package.json +1 -1
package/lib/fanout-solver.ts
CHANGED
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
} from "./route-bus"
|
|
34
34
|
import { routeSingleLayerWithAdaptiveExitsSteps } from "./route-single-layer-adaptive-exits"
|
|
35
35
|
import { routeSingleLayerWithPushAndShove } from "./route-single-layer-push-shove"
|
|
36
|
+
import { getRuntimeProcess } from "./runtime-process"
|
|
37
|
+
import { shortenBusPlans } from "./shorten-bus-plans"
|
|
36
38
|
import type {
|
|
37
39
|
AssignmentAttempt,
|
|
38
40
|
Bounds,
|
|
@@ -48,6 +50,11 @@ import type {
|
|
|
48
50
|
import { validateFanoutSolution } from "./validate-fanout-solution"
|
|
49
51
|
import { visualizeSimpleRouteJson } from "./visualize-simple-route-json"
|
|
50
52
|
|
|
53
|
+
// Browser Web Workers do not expose Node's `process` global. Keep a local,
|
|
54
|
+
// browser-safe adapter so the optional debug environment flags still work in
|
|
55
|
+
// Node and Bun without crashing browser-based routing.
|
|
56
|
+
const process = getRuntimeProcess(globalThis)
|
|
57
|
+
|
|
51
58
|
interface ResolvedFanoutConfig {
|
|
52
59
|
traceWidth: number
|
|
53
60
|
viaDiameter: number
|
|
@@ -1380,8 +1387,12 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1380
1387
|
private *routeDenseThroughAllMixedTerminationSteps(params: {
|
|
1381
1388
|
busLayerAssignments: Readonly<Record<string, string>>
|
|
1382
1389
|
busesInRoutingOrder: readonly PreparedBus[]
|
|
1390
|
+
denseRoutingStrategy?: "pad-aligned" | "boundary-aligned"
|
|
1391
|
+
lengthMatchingStage?: "before-planes" | "after-planes"
|
|
1383
1392
|
}): Generator<FanoutWorkYield, MixedTerminationState | null, unknown> {
|
|
1384
1393
|
if (this.config.allowBlindAndBuriedVias) return null
|
|
1394
|
+
const usePadAlignedDenseRouting =
|
|
1395
|
+
params.denseRoutingStrategy !== "boundary-aligned"
|
|
1385
1396
|
const debugDense = (...values: unknown[]) => {
|
|
1386
1397
|
if (process.env.FANOUT_DEBUG_DENSE === "1") {
|
|
1387
1398
|
if (
|
|
@@ -1414,6 +1425,9 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1414
1425
|
const useConfiguredDensePlaneRouting =
|
|
1415
1426
|
this.config.densePlaneReservationBusIds.length > 0 ||
|
|
1416
1427
|
this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0
|
|
1428
|
+
const matchLengthsAfterPlanes =
|
|
1429
|
+
useConfiguredDensePlaneRouting &&
|
|
1430
|
+
params.lengthMatchingStage !== "before-planes"
|
|
1417
1431
|
const useJointBoundaryViaReservation = shouldUseJointBoundaryViaReservation(
|
|
1418
1432
|
unsortedBoundaryBuses.map((bus) => bus.connections.length),
|
|
1419
1433
|
)
|
|
@@ -1902,6 +1916,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1902
1916
|
...singletonDeferralCandidates.filter((bus) => {
|
|
1903
1917
|
const containingBus = getContainingWideSourceField(bus)
|
|
1904
1918
|
const sharesContainingBusLayer =
|
|
1919
|
+
usePadAlignedDenseRouting &&
|
|
1905
1920
|
!useConfiguredDensePlaneRouting &&
|
|
1906
1921
|
containingBus &&
|
|
1907
1922
|
params.busLayerAssignments[containingBus.busId] ===
|
|
@@ -2160,9 +2175,11 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2160
2175
|
fixedViaPointsByConnectionIndex,
|
|
2161
2176
|
reservedVias: getReservedVias(bus),
|
|
2162
2177
|
viaMinimalOnly: process.env.FANOUT_DEBUG_ALLOW_EXTRA_VIAS !== "1",
|
|
2163
|
-
allowBoundarySideViaFallback:
|
|
2178
|
+
allowBoundarySideViaFallback: true,
|
|
2179
|
+
preferCornerBoundaryVia: useConfiguredDensePlaneRouting,
|
|
2164
2180
|
adaptiveWindingRouteOrder,
|
|
2165
|
-
alignWindingGridToPads:
|
|
2181
|
+
alignWindingGridToPads:
|
|
2182
|
+
usePadAlignedDenseRouting && !useConfiguredDensePlaneRouting,
|
|
2166
2183
|
fixedViaFallbackRouteOrderAttempts: adaptiveWindingRouteOrder
|
|
2167
2184
|
? 60
|
|
2168
2185
|
: useConfiguredDensePlaneRouting
|
|
@@ -2218,7 +2235,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2218
2235
|
if (!busPlans && preferSingleLayerWinding) {
|
|
2219
2236
|
busPlans = (yield* routeAlternatives(routeParams, 1))[0]
|
|
2220
2237
|
}
|
|
2221
|
-
if (!busPlans
|
|
2238
|
+
if (!busPlans) {
|
|
2222
2239
|
const originalPoints = fixedViaPointsByConnectionIndex
|
|
2223
2240
|
const originalOutward =
|
|
2224
2241
|
preferBoundaryOutwardByBusId.get(bus.busId) ?? true
|
|
@@ -2292,6 +2309,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2292
2309
|
...routeParams,
|
|
2293
2310
|
fixedViaPointsByConnectionIndex: rematchedPoints,
|
|
2294
2311
|
reservedVias: getReservedVias(bus),
|
|
2312
|
+
alignWindingGridToPads: useConfiguredDensePlaneRouting,
|
|
2295
2313
|
fixedViaFallbackRouteOrderAttempts: 3,
|
|
2296
2314
|
},
|
|
2297
2315
|
1,
|
|
@@ -2313,7 +2331,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2313
2331
|
// Only pay for additional A* variants when the first topology is so
|
|
2314
2332
|
// skewed that compact meanders are unlikely to absorb the deficit.
|
|
2315
2333
|
// This keeps already-near-matched buses on the single-attempt path.
|
|
2316
|
-
if (needsRouteDiversity) {
|
|
2334
|
+
if (needsRouteDiversity && !matchLengthsAfterPlanes) {
|
|
2317
2335
|
busPlans = (yield* routeAlternatives(routeParams, 3)).toSorted(
|
|
2318
2336
|
(first, second) => {
|
|
2319
2337
|
const firstLengths = first.map((plan) => plan.length)
|
|
@@ -2630,6 +2648,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2630
2648
|
let feasibleAlternatePlanePlans: FanoutRoutePlan[] = []
|
|
2631
2649
|
const matchViaPointsAroundPlans = (
|
|
2632
2650
|
candidatePlans: readonly FanoutRoutePlan[],
|
|
2651
|
+
promotedAlternatePlaneBusIds: ReadonlySet<string> = new Set(),
|
|
2633
2652
|
): Map<number, { x: number; y: number }> | null => {
|
|
2634
2653
|
feasibleAlternatePlanePlans = []
|
|
2635
2654
|
const fixedBoundaryViaPoints = new Map(
|
|
@@ -2645,10 +2664,18 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2645
2664
|
segment,
|
|
2646
2665
|
})),
|
|
2647
2666
|
)
|
|
2648
|
-
const
|
|
2667
|
+
const preserveBoundaryCopper =
|
|
2668
|
+
!useConfiguredDensePlaneRouting ||
|
|
2669
|
+
candidatePlans.some(
|
|
2670
|
+
(plan) =>
|
|
2671
|
+
plan.segments.filter(
|
|
2672
|
+
(segment) => segment.layer === plan.sourceLayer,
|
|
2673
|
+
).length > 1,
|
|
2674
|
+
)
|
|
2675
|
+
const boundaryBusesToMatch = !preserveBoundaryCopper
|
|
2649
2676
|
? boundaryBuses
|
|
2650
2677
|
: []
|
|
2651
|
-
const blockingVias =
|
|
2678
|
+
const blockingVias = !preserveBoundaryCopper
|
|
2652
2679
|
? []
|
|
2653
2680
|
: candidatePlans.flatMap((plan) =>
|
|
2654
2681
|
[
|
|
@@ -2667,7 +2694,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2667
2694
|
// Completed boundary paths can reach their via with several source-
|
|
2668
2695
|
// layer segments. Treat their actual copper as fixed obstacles instead
|
|
2669
2696
|
// of reinterpreting each as a straight pad-to-via dogbone.
|
|
2670
|
-
const retainedViaPoints =
|
|
2697
|
+
const retainedViaPoints = !preserveBoundaryCopper
|
|
2671
2698
|
? null
|
|
2672
2699
|
: matchComponentDogboneViaSites(planeBuses, {
|
|
2673
2700
|
viaDiameter: this.config.viaDiameter,
|
|
@@ -2775,6 +2802,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2775
2802
|
)
|
|
2776
2803
|
: []
|
|
2777
2804
|
const additionalAlternatePlaneBusIds = new Set([
|
|
2805
|
+
...promotedAlternatePlaneBusIds,
|
|
2778
2806
|
...this.config.denseUnrestrictedPlaneRoutingBusIds,
|
|
2779
2807
|
...(process.env.FANOUT_DEBUG_ADDITIONAL_ALTERNATE_PLANE_BUS_IDS?.split(
|
|
2780
2808
|
",",
|
|
@@ -3345,7 +3373,16 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3345
3373
|
)[0]
|
|
3346
3374
|
if (!promotedPlans) {
|
|
3347
3375
|
debugDense("plane-route:promote-failed", planeBus.busId)
|
|
3348
|
-
|
|
3376
|
+
// Let these newly blocked drops participate in the joint
|
|
3377
|
+
// choice. Each retry adds previously excluded plane buses,
|
|
3378
|
+
// so the recursion is bounded by the number of plane buses.
|
|
3379
|
+
return matchViaPointsAroundPlans(
|
|
3380
|
+
candidatePlans,
|
|
3381
|
+
new Set([
|
|
3382
|
+
...promotedAlternatePlaneBusIds,
|
|
3383
|
+
...zeroCandidatePlaneBuses.map((bus) => bus.busId),
|
|
3384
|
+
]),
|
|
3385
|
+
)
|
|
3349
3386
|
}
|
|
3350
3387
|
feasibleAlternatePlanePlans.push(...promotedPlans)
|
|
3351
3388
|
}
|
|
@@ -3466,22 +3503,25 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3466
3503
|
: null
|
|
3467
3504
|
}
|
|
3468
3505
|
debugDense("length-match:start", matchedPlans.length)
|
|
3469
|
-
const matchedLengthResult =
|
|
3470
|
-
plans: matchedPlans
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3506
|
+
const matchedLengthResult = matchLengthsAfterPlanes
|
|
3507
|
+
? { plans: matchedPlans }
|
|
3508
|
+
: matchBusPlanLengths({
|
|
3509
|
+
plans: matchedPlans,
|
|
3510
|
+
preparedBuses: this.preparedBuses,
|
|
3511
|
+
inputSrj: this.inputSrj,
|
|
3512
|
+
sharedBoundary: this.getValidationBoundary(),
|
|
3513
|
+
clearance: this.config.clearance,
|
|
3514
|
+
allowBlindAndBuriedVias: false,
|
|
3515
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
3516
|
+
allowMatchingInsideDenseBounds: true,
|
|
3517
|
+
candidatePlansAreFeasible: (candidatePlans) => {
|
|
3518
|
+
const candidateViaPoints =
|
|
3519
|
+
matchViaPointsAroundPlans(candidatePlans)
|
|
3520
|
+
if (!candidateViaPoints) return false
|
|
3521
|
+
feasibleViaPoints = candidateViaPoints
|
|
3522
|
+
return true
|
|
3523
|
+
},
|
|
3524
|
+
})
|
|
3485
3525
|
debugDense(
|
|
3486
3526
|
"length-match:complete",
|
|
3487
3527
|
matchedLengthResult.plans?.length ?? "failed",
|
|
@@ -3565,6 +3605,65 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3565
3605
|
yield
|
|
3566
3606
|
}
|
|
3567
3607
|
}
|
|
3608
|
+
if (matchedRoutingSucceeded && matchLengthsAfterPlanes) {
|
|
3609
|
+
// With the configured plane escape strategy, route those dogbones
|
|
3610
|
+
// before tuning. Length matching can then check the actual complete
|
|
3611
|
+
// copper instead of repeatedly searching for a new plane assignment
|
|
3612
|
+
// for every prospective meander.
|
|
3613
|
+
const lengthMatchingParams = {
|
|
3614
|
+
plans: matchedPlans,
|
|
3615
|
+
preparedBuses: this.preparedBuses,
|
|
3616
|
+
inputSrj: this.inputSrj,
|
|
3617
|
+
sharedBoundary: this.getValidationBoundary(),
|
|
3618
|
+
clearance: this.config.clearance,
|
|
3619
|
+
allowBlindAndBuriedVias: false,
|
|
3620
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
3621
|
+
allowMatchingInsideDenseBounds: true,
|
|
3622
|
+
allowPairLaneSpreading: true,
|
|
3623
|
+
}
|
|
3624
|
+
let matchedLengthResult = matchBusPlanLengths(lengthMatchingParams)
|
|
3625
|
+
const shortenedBusIds = new Set<string>()
|
|
3626
|
+
while (
|
|
3627
|
+
!matchedLengthResult.plans &&
|
|
3628
|
+
matchedLengthResult.failedBus &&
|
|
3629
|
+
!shortenedBusIds.has(matchedLengthResult.failedBus.busId)
|
|
3630
|
+
) {
|
|
3631
|
+
const bus = matchedLengthResult.failedBus
|
|
3632
|
+
shortenedBusIds.add(bus.busId)
|
|
3633
|
+
const shortened = shortenBusPlans({
|
|
3634
|
+
plans: matchedPlans,
|
|
3635
|
+
bus,
|
|
3636
|
+
srj: this.inputSrj,
|
|
3637
|
+
sharedBoundary: this.getValidationBoundary(),
|
|
3638
|
+
layerNames: this.config.layerNames,
|
|
3639
|
+
traceWidth: this.config.traceWidth,
|
|
3640
|
+
viaDiameter: this.config.viaDiameter,
|
|
3641
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
3642
|
+
clearance: this.config.clearance,
|
|
3643
|
+
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
3644
|
+
})
|
|
3645
|
+
if (shortened.every((plan, index) => plan === matchedPlans[index]))
|
|
3646
|
+
break
|
|
3647
|
+
matchedPlans = shortened
|
|
3648
|
+
matchedLengthResult = matchBusPlanLengths({
|
|
3649
|
+
...lengthMatchingParams,
|
|
3650
|
+
plans: matchedPlans,
|
|
3651
|
+
})
|
|
3652
|
+
}
|
|
3653
|
+
if (matchedLengthResult.plans) {
|
|
3654
|
+
matchedPlans = matchedLengthResult.plans
|
|
3655
|
+
} else {
|
|
3656
|
+
matchedRoutingSucceeded = false
|
|
3657
|
+
}
|
|
3658
|
+
this.setInProgressPlans({
|
|
3659
|
+
phase: "match-dense-complete-lengths",
|
|
3660
|
+
plans: matchedPlans,
|
|
3661
|
+
strategy: "default",
|
|
3662
|
+
unitIndex: ++denseWorkUnitIndex,
|
|
3663
|
+
unitCount: denseWorkUnitCount,
|
|
3664
|
+
})
|
|
3665
|
+
yield
|
|
3666
|
+
}
|
|
3568
3667
|
const densePlansAreClear =
|
|
3569
3668
|
matchedRoutingSucceeded &&
|
|
3570
3669
|
fanoutPlansAreClear({
|
|
@@ -3586,7 +3685,30 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3586
3685
|
}
|
|
3587
3686
|
}
|
|
3588
3687
|
|
|
3589
|
-
if (
|
|
3688
|
+
if (matchLengthsAfterPlanes) {
|
|
3689
|
+
return yield* this.routeDenseThroughAllMixedTerminationSteps({
|
|
3690
|
+
...params,
|
|
3691
|
+
lengthMatchingStage: "before-planes",
|
|
3692
|
+
})
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
// Pad-aligned windings can fence off a same-layer singleton even when
|
|
3696
|
+
// every wide bus routes successfully. Before widening the search, retry
|
|
3697
|
+
// the coordinated reservation with a boundary-aligned grid and let those
|
|
3698
|
+
// singleton sites remain provisional until surrounding copper is fixed.
|
|
3699
|
+
if (usePadAlignedDenseRouting && !useConfiguredDensePlaneRouting) {
|
|
3700
|
+
const boundaryAlignedState =
|
|
3701
|
+
yield* this.routeDenseThroughAllMixedTerminationSteps({
|
|
3702
|
+
...params,
|
|
3703
|
+
denseRoutingStrategy: "boundary-aligned",
|
|
3704
|
+
})
|
|
3705
|
+
if (boundaryAlignedState) return boundaryAlignedState
|
|
3706
|
+
}
|
|
3707
|
+
if (
|
|
3708
|
+
!usePadAlignedDenseRouting ||
|
|
3709
|
+
process.env.FANOUT_DEBUG_DENSE_ONLY === "1"
|
|
3710
|
+
)
|
|
3711
|
+
return null
|
|
3590
3712
|
|
|
3591
3713
|
const maximumStates = 8
|
|
3592
3714
|
const getBoundaryStates = (
|
package/lib/match-bus-lengths.ts
CHANGED
|
@@ -565,6 +565,67 @@ function getBusSkew(plans: readonly FanoutRoutePlan[]): number {
|
|
|
565
565
|
return Math.max(...lengths) - Math.min(...lengths)
|
|
566
566
|
}
|
|
567
567
|
|
|
568
|
+
function* createSpreadLaneCandidates(
|
|
569
|
+
plan: FanoutRoutePlan,
|
|
570
|
+
clearance: number,
|
|
571
|
+
): Generator<FanoutRoutePlan> {
|
|
572
|
+
for (const { segment, index } of plan.segments
|
|
573
|
+
.map((segment, index) => ({ segment, index }))
|
|
574
|
+
.filter(({ segment }) => segment.layer === plan.targetLayer)
|
|
575
|
+
.toSorted(
|
|
576
|
+
(a, b) =>
|
|
577
|
+
distance(b.segment.start, b.segment.end) -
|
|
578
|
+
distance(a.segment.start, a.segment.end),
|
|
579
|
+
)) {
|
|
580
|
+
const length = distance(segment.start, segment.end)
|
|
581
|
+
if (length <= EPSILON) continue
|
|
582
|
+
const dx = Math.abs(segment.end.x - segment.start.x)
|
|
583
|
+
const dy = Math.abs(segment.end.y - segment.start.y)
|
|
584
|
+
if (dx > EPSILON && dy > EPSILON && Math.abs(dx - dy) > EPSILON) continue
|
|
585
|
+
const tangent = {
|
|
586
|
+
x: (segment.end.x - segment.start.x) / length,
|
|
587
|
+
y: (segment.end.y - segment.start.y) / length,
|
|
588
|
+
}
|
|
589
|
+
const pitch = segment.width + clearance
|
|
590
|
+
for (const multiple of [2, 3, 4, 6]) {
|
|
591
|
+
const offset = pitch * multiple
|
|
592
|
+
if (length < 2 * offset + pitch) continue
|
|
593
|
+
for (const sign of [1, -1]) {
|
|
594
|
+
const normal = { x: -tangent.y * sign, y: tangent.x * sign }
|
|
595
|
+
const points = [
|
|
596
|
+
segment.start,
|
|
597
|
+
addScaled(addScaled(segment.start, tangent, offset), normal, offset),
|
|
598
|
+
addScaled(addScaled(segment.end, tangent, -offset), normal, offset),
|
|
599
|
+
segment.end,
|
|
600
|
+
]
|
|
601
|
+
const replacement = points.slice(1).map((end, i) => ({
|
|
602
|
+
...segment,
|
|
603
|
+
start: points[i]!,
|
|
604
|
+
end,
|
|
605
|
+
}))
|
|
606
|
+
const segments = [
|
|
607
|
+
...plan.segments.slice(0, index),
|
|
608
|
+
...replacement,
|
|
609
|
+
...plan.segments.slice(index + 1),
|
|
610
|
+
]
|
|
611
|
+
if (hasNonAdjacentSelfIntersection(segments)) continue
|
|
612
|
+
if (
|
|
613
|
+
!replacementCopperIsSelfClear({
|
|
614
|
+
plan,
|
|
615
|
+
segments,
|
|
616
|
+
replacementStartIndex: index,
|
|
617
|
+
replacementSegmentCount: replacement.length,
|
|
618
|
+
clearance,
|
|
619
|
+
})
|
|
620
|
+
)
|
|
621
|
+
continue
|
|
622
|
+
const candidate = createPlanWithSegments(plan, segments)
|
|
623
|
+
if (candidate) yield candidate
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
568
629
|
/**
|
|
569
630
|
* Adds straight/45-degree meanders after the dense component escape. Matching
|
|
570
631
|
* is atomic: a constrained bus either satisfies its declared skew with the
|
|
@@ -584,6 +645,8 @@ export function matchBusPlanLengths(params: {
|
|
|
584
645
|
* revalidated and whose remaining dogbone capacity is checked atomically.
|
|
585
646
|
*/
|
|
586
647
|
allowMatchingInsideDenseBounds?: boolean
|
|
648
|
+
/** Allow a differential pair's longer lane to move aside before tuning its mate. */
|
|
649
|
+
allowPairLaneSpreading?: boolean
|
|
587
650
|
/**
|
|
588
651
|
* Rejects a geometrically clear candidate when it would make a caller-owned
|
|
589
652
|
* downstream assignment (such as pending plane dogbones) infeasible.
|
|
@@ -760,6 +823,49 @@ export function matchBusPlanLengths(params: {
|
|
|
760
823
|
}
|
|
761
824
|
if (acceptedPlans) break
|
|
762
825
|
}
|
|
826
|
+
if (
|
|
827
|
+
!acceptedPlans &&
|
|
828
|
+
params.allowPairLaneSpreading &&
|
|
829
|
+
bus.connections.length === 2
|
|
830
|
+
) {
|
|
831
|
+
// A tightly packed pair may leave no space to lengthen the inner lane.
|
|
832
|
+
// Move the outer lane, then retune the complete pair atomically. Only
|
|
833
|
+
// four geometrically clear placements may start another matching pass.
|
|
834
|
+
const longer = busPlans.find((plan) => plan !== shortest)!
|
|
835
|
+
let attempts = 0
|
|
836
|
+
for (const candidate of createSpreadLaneCandidates(longer, clearance)) {
|
|
837
|
+
const nextPlans = matchedPlans.map((plan) =>
|
|
838
|
+
plan === longer ? candidate : plan,
|
|
839
|
+
)
|
|
840
|
+
if (
|
|
841
|
+
!fanoutPlansAreClear({
|
|
842
|
+
plans: nextPlans,
|
|
843
|
+
srj: inputSrj,
|
|
844
|
+
sharedBoundary,
|
|
845
|
+
clearance,
|
|
846
|
+
allowBlindAndBuriedVias,
|
|
847
|
+
allowSameNetMerges,
|
|
848
|
+
})
|
|
849
|
+
)
|
|
850
|
+
continue
|
|
851
|
+
if (
|
|
852
|
+
candidatePlansAreFeasible &&
|
|
853
|
+
!candidatePlansAreFeasible(nextPlans)
|
|
854
|
+
)
|
|
855
|
+
continue
|
|
856
|
+
const result = matchBusPlanLengths({
|
|
857
|
+
...params,
|
|
858
|
+
plans: nextPlans,
|
|
859
|
+
preparedBuses: [bus],
|
|
860
|
+
allowPairLaneSpreading: false,
|
|
861
|
+
})
|
|
862
|
+
if (result.plans) {
|
|
863
|
+
acceptedPlans = result.plans
|
|
864
|
+
break
|
|
865
|
+
}
|
|
866
|
+
if (++attempts >= 4) break
|
|
867
|
+
}
|
|
868
|
+
}
|
|
763
869
|
if (!acceptedPlans) return { plans: null, failedBus: bus }
|
|
764
870
|
matchedPlans = acceptedPlans
|
|
765
871
|
}
|
package/lib/route-bus.ts
CHANGED
|
@@ -64,6 +64,8 @@ export interface RouteBusParams {
|
|
|
64
64
|
viaMinimalOnly?: boolean
|
|
65
65
|
/** Permit a singleton to move its provisional via near the boundary. */
|
|
66
66
|
allowBoundarySideViaFallback?: boolean
|
|
67
|
+
/** Reserve corner tuning space when selecting a boundary-side via. */
|
|
68
|
+
preferCornerBoundaryVia?: boolean
|
|
67
69
|
/** Retry blocked winding terminals ahead of already-routed terminals. */
|
|
68
70
|
adaptiveWindingRouteOrder?: boolean
|
|
69
71
|
/** Preserve pad-lattice channels in the automatic dense routing path. */
|
|
@@ -1611,8 +1613,10 @@ function segmentIsClearOfObstacles(params: {
|
|
|
1611
1613
|
) {
|
|
1612
1614
|
continue
|
|
1613
1615
|
}
|
|
1616
|
+
// A winding escape can turn more than once while leaving its own pad.
|
|
1614
1617
|
if (
|
|
1615
|
-
segmentIndex
|
|
1618
|
+
segmentIndex >= 0 &&
|
|
1619
|
+
segmentIndex < (plan.sourceEscapeSegmentCount ?? 1) &&
|
|
1616
1620
|
obstacle === plan.sourceObstacle &&
|
|
1617
1621
|
segment.layer === plan.sourceLayer
|
|
1618
1622
|
) {
|
|
@@ -2003,7 +2007,9 @@ export function fanoutPlansAreClear(params: {
|
|
|
2003
2007
|
}
|
|
2004
2008
|
|
|
2005
2009
|
function routePlaneTerminatedBus(
|
|
2006
|
-
params: RouteBusParams
|
|
2010
|
+
params: RouteBusParams & {
|
|
2011
|
+
collectAlternative?: (plan: FanoutRoutePlan) => boolean
|
|
2012
|
+
},
|
|
2007
2013
|
): FanoutRoutePlan[] | null {
|
|
2008
2014
|
const {
|
|
2009
2015
|
srj,
|
|
@@ -2037,6 +2043,8 @@ function routePlaneTerminatedBus(
|
|
|
2037
2043
|
remainingPlaneCandidatesToSkip--
|
|
2038
2044
|
continue
|
|
2039
2045
|
}
|
|
2046
|
+
if (params.collectAlternative && !params.collectAlternative(plan))
|
|
2047
|
+
continue
|
|
2040
2048
|
return plan
|
|
2041
2049
|
}
|
|
2042
2050
|
return undefined
|
|
@@ -2452,6 +2460,7 @@ export function* routeBusAlternativesSteps(
|
|
|
2452
2460
|
reservedVias = [],
|
|
2453
2461
|
viaMinimalOnly = false,
|
|
2454
2462
|
allowBoundarySideViaFallback = false,
|
|
2463
|
+
preferCornerBoundaryVia = false,
|
|
2455
2464
|
adaptiveWindingRouteOrder = false,
|
|
2456
2465
|
alignWindingGridToPads = false,
|
|
2457
2466
|
fixedViaFallbackRouteOrderAttempts = 24,
|
|
@@ -2473,6 +2482,19 @@ export function* routeBusAlternativesSteps(
|
|
|
2473
2482
|
}
|
|
2474
2483
|
if (bus.termination.type === "plane") {
|
|
2475
2484
|
const alternatives: FanoutRoutePlan[][] = []
|
|
2485
|
+
if (bus.connections.length === 1 && maxAlternatives > 1) {
|
|
2486
|
+
// Enumerate once instead of rebuilding and skipping every earlier route
|
|
2487
|
+
// for each successive alternative. Keep the original candidate order.
|
|
2488
|
+
routePlaneTerminatedBus({
|
|
2489
|
+
...params,
|
|
2490
|
+
planeCandidateSkipCount: 0,
|
|
2491
|
+
collectAlternative: (plan) => {
|
|
2492
|
+
alternatives.push([plan])
|
|
2493
|
+
return alternatives.length >= maxAlternatives
|
|
2494
|
+
},
|
|
2495
|
+
})
|
|
2496
|
+
return alternatives
|
|
2497
|
+
}
|
|
2476
2498
|
for (
|
|
2477
2499
|
let planeCandidateSkipCount = 0;
|
|
2478
2500
|
planeCandidateSkipCount < maxAlternatives;
|
|
@@ -2801,14 +2823,19 @@ export function* routeBusAlternativesSteps(
|
|
|
2801
2823
|
acceptedBoundaryPlansExist && !allowBlindAndBuriedVias
|
|
2802
2824
|
? [...mixedDogboneTerminalPatterns, ...uniformDogboneTerminalPatterns]
|
|
2803
2825
|
: [...uniformDogboneTerminalPatterns, ...mixedDogboneTerminalPatterns]
|
|
2826
|
+
const unreservedTerminalPatterns: CoordinatedTerminalPattern[] =
|
|
2827
|
+
canUseViaInPadTerminals
|
|
2828
|
+
? planeTerminationsAlreadyOccupyTheFanout
|
|
2829
|
+
? [viaInPadTerminalPattern, ...dogboneTerminalPatterns]
|
|
2830
|
+
: [...dogboneTerminalPatterns, viaInPadTerminalPattern]
|
|
2831
|
+
: dogboneTerminalPatterns
|
|
2832
|
+
// Automatically matched sites are candidates, not caller reservations.
|
|
2833
|
+
// Keep the ordinary dogbone patterns available before adding crossover
|
|
2834
|
+
// vias when that first site assignment cannot route the complete bus.
|
|
2804
2835
|
const terminalPatterns: CoordinatedTerminalPattern[] =
|
|
2805
|
-
|
|
2836
|
+
fixedViaPointsByConnectionIndex
|
|
2806
2837
|
? fixedViaTerminalPatterns
|
|
2807
|
-
:
|
|
2808
|
-
? planeTerminationsAlreadyOccupyTheFanout
|
|
2809
|
-
? [viaInPadTerminalPattern, ...dogboneTerminalPatterns]
|
|
2810
|
-
: [...dogboneTerminalPatterns, viaInPadTerminalPattern]
|
|
2811
|
-
: dogboneTerminalPatterns
|
|
2838
|
+
: [...fixedViaTerminalPatterns, ...unreservedTerminalPatterns]
|
|
2812
2839
|
const seenTerminalSignatures = new Set<string>()
|
|
2813
2840
|
for (const terminalPattern of terminalPatterns) {
|
|
2814
2841
|
const terminals = bus.connections.map((preparedConnection) => {
|
|
@@ -2857,12 +2884,24 @@ export function* routeBusAlternativesSteps(
|
|
|
2857
2884
|
),
|
|
2858
2885
|
}
|
|
2859
2886
|
})
|
|
2887
|
+
const alignGridToPads =
|
|
2888
|
+
alignWindingGridToPads ||
|
|
2889
|
+
Boolean(terminalPattern.getViaPoint && !fixedViaPointsByConnectionIndex)
|
|
2890
|
+
const gridStepDivisor =
|
|
2891
|
+
terminalPattern.getViaPoint &&
|
|
2892
|
+
Math.min(bus.pitchX, bus.pitchY) -
|
|
2893
|
+
2 * (viaDiameter / 2 + traceWidth / 2 + clearance) <
|
|
2894
|
+
traceWidth + clearance
|
|
2895
|
+
? 2
|
|
2896
|
+
: 1
|
|
2860
2897
|
const terminalSignature = `${terminals
|
|
2861
2898
|
.map(
|
|
2862
2899
|
(terminal) =>
|
|
2863
2900
|
`${terminal.connection.connectionIndex}:${terminal.viaPoint.x}:${terminal.viaPoint.y}:${terminal.exitPoint.x}:${terminal.exitPoint.y}`,
|
|
2864
2901
|
)
|
|
2865
|
-
.join(
|
|
2902
|
+
.join(
|
|
2903
|
+
"|",
|
|
2904
|
+
)}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}:${gridStepDivisor}:${alignGridToPads}`
|
|
2866
2905
|
if (seenTerminalSignatures.has(terminalSignature)) continue
|
|
2867
2906
|
seenTerminalSignatures.add(terminalSignature)
|
|
2868
2907
|
const windingSteps = routeViaMinimalWindingAlternativesSteps(
|
|
@@ -2881,17 +2920,9 @@ export function* routeBusAlternativesSteps(
|
|
|
2881
2920
|
allowSameNetMerges,
|
|
2882
2921
|
maximumRouteOrderAttempts: terminalPattern.maximumRouteOrderAttempts,
|
|
2883
2922
|
adaptiveRouteOrder: adaptiveWindingRouteOrder,
|
|
2884
|
-
alignGridToPads
|
|
2885
|
-
alignWindingGridToPads ||
|
|
2886
|
-
Boolean(coordinatedViaPoints && !fixedViaPointsByConnectionIndex),
|
|
2923
|
+
alignGridToPads,
|
|
2887
2924
|
reservedVias,
|
|
2888
|
-
gridStepDivisor
|
|
2889
|
-
coordinatedViaPoints &&
|
|
2890
|
-
Math.min(bus.pitchX, bus.pitchY) -
|
|
2891
|
-
2 * (viaDiameter / 2 + traceWidth / 2 + clearance) <
|
|
2892
|
-
traceWidth + clearance
|
|
2893
|
-
? 2
|
|
2894
|
-
: 1,
|
|
2925
|
+
gridStepDivisor,
|
|
2895
2926
|
preferTargetDirectedLaneBias:
|
|
2896
2927
|
terminalPattern.preferTargetDirectedLaneBias,
|
|
2897
2928
|
},
|
|
@@ -2962,11 +2993,24 @@ export function* routeBusAlternativesSteps(
|
|
|
2962
2993
|
const preparedConnection = bus.connections[0]!
|
|
2963
2994
|
const boundaryDirection = getDirectionForExitEdge(bus.exitEdge)
|
|
2964
2995
|
const boundaryExitAxis = getExitAxis(bus, boundaryDirection)
|
|
2965
|
-
const finalTrack =
|
|
2966
|
-
bus
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2996
|
+
const finalTrack =
|
|
2997
|
+
preferCornerBoundaryVia && getCornerSide(bus)
|
|
2998
|
+
? getCornerTargetTrack({
|
|
2999
|
+
bus,
|
|
3000
|
+
connection: preparedConnection,
|
|
3001
|
+
cornerExitLaneOffset: cornerLaneOffsets.exit,
|
|
3002
|
+
traceWidth,
|
|
3003
|
+
viaDiameter,
|
|
3004
|
+
clearance,
|
|
3005
|
+
layerNames,
|
|
3006
|
+
targetLayer,
|
|
3007
|
+
cornerBandTargetTrackOffset,
|
|
3008
|
+
})
|
|
3009
|
+
: getBoundaryTargetTrack({
|
|
3010
|
+
bus,
|
|
3011
|
+
connection: preparedConnection,
|
|
3012
|
+
boundaryDirection,
|
|
3013
|
+
})
|
|
2970
3014
|
const finalExitPoint = makePoint(
|
|
2971
3015
|
boundaryExitAxis,
|
|
2972
3016
|
finalTrack,
|
|
@@ -2992,13 +3036,27 @@ export function* routeBusAlternativesSteps(
|
|
|
2992
3036
|
viaDiameter / 2 + clearance,
|
|
2993
3037
|
Math.min(bus.pitchX, bus.pitchY) / 2,
|
|
2994
3038
|
)
|
|
2995
|
-
|
|
3039
|
+
const cornerSide = preferCornerBoundaryVia ? getCornerSide(bus) : undefined
|
|
3040
|
+
const boundaryViaCandidates = [1, 2, 3, 4, 5].flatMap((multiple) => {
|
|
2996
3041
|
const inset = multiple * insetStep
|
|
2997
|
-
const
|
|
3042
|
+
const straight = makePoint(
|
|
2998
3043
|
boundaryExitAxis - directionSign(boundaryDirection) * inset,
|
|
2999
3044
|
finalTrack,
|
|
3000
3045
|
boundaryDirection,
|
|
3001
3046
|
)
|
|
3047
|
+
if (!cornerSide) return [straight]
|
|
3048
|
+
// Approach corner exits diagonally to leave the adjacent pair's tuning
|
|
3049
|
+
// lane free of the through-via barrel. Retain the straight fallback.
|
|
3050
|
+
return [
|
|
3051
|
+
makePoint(
|
|
3052
|
+
boundaryExitAxis - directionSign(boundaryDirection) * inset,
|
|
3053
|
+
finalTrack + (cornerSide === "maximum" ? inset : -inset),
|
|
3054
|
+
boundaryDirection,
|
|
3055
|
+
),
|
|
3056
|
+
straight,
|
|
3057
|
+
]
|
|
3058
|
+
})
|
|
3059
|
+
for (const boundaryViaPoint of boundaryViaCandidates) {
|
|
3002
3060
|
const sourceLayerSteps = routeViaMinimalWindingAlternativesSteps(
|
|
3003
3061
|
{
|
|
3004
3062
|
srj: sourceLayerSrj,
|
|
@@ -3065,6 +3123,9 @@ export function* routeBusAlternativesSteps(
|
|
|
3065
3123
|
const plans: FanoutRoutePlan[] = [
|
|
3066
3124
|
{
|
|
3067
3125
|
...sourceLayerPlan,
|
|
3126
|
+
...(preferCornerBoundaryVia
|
|
3127
|
+
? { sourceEscapeSegmentCount: sourceLayerPlan.segments.length }
|
|
3128
|
+
: {}),
|
|
3068
3129
|
targetLayer,
|
|
3069
3130
|
exitPoint: finalExitPoint,
|
|
3070
3131
|
via,
|
|
@@ -1075,6 +1075,10 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1075
1075
|
endByNode.set(end.nodeIndex, values)
|
|
1076
1076
|
}
|
|
1077
1077
|
const stateCount = nodeCount * 9
|
|
1078
|
+
// A* visits a grid edge with several incoming directions. Copper does not
|
|
1079
|
+
// change while routing this terminal, so check each edge only once. Keep
|
|
1080
|
+
// this cache local: later terminals and route-order attempts add blockers.
|
|
1081
|
+
const edgeClearance = new Uint8Array(nodeCount * 8)
|
|
1078
1082
|
const distances = new Float64Array(stateCount).fill(
|
|
1079
1083
|
Number.POSITIVE_INFINITY,
|
|
1080
1084
|
)
|
|
@@ -1205,21 +1209,6 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1205
1209
|
}
|
|
1206
1210
|
const nextNode = row * columnCount + column
|
|
1207
1211
|
const nextPoint = nodes[nextNode]!.point
|
|
1208
|
-
const segment: RoutedSegment = {
|
|
1209
|
-
start: node.point,
|
|
1210
|
-
end: nextPoint,
|
|
1211
|
-
width: traceWidth,
|
|
1212
|
-
layer: targetLayer,
|
|
1213
|
-
}
|
|
1214
|
-
if (
|
|
1215
|
-
!segmentIsClear({
|
|
1216
|
-
segment,
|
|
1217
|
-
terminal,
|
|
1218
|
-
acceptedAttemptSegments,
|
|
1219
|
-
})
|
|
1220
|
-
) {
|
|
1221
|
-
continue
|
|
1222
|
-
}
|
|
1223
1212
|
const addsTurn =
|
|
1224
1213
|
current.direction !== 8 && current.direction !== directionIndex
|
|
1225
1214
|
const nextTrack = getPerpendicularAxis(nextPoint, boundaryDirection)
|
|
@@ -1242,6 +1231,24 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1242
1231
|
lanePenalty
|
|
1243
1232
|
const nextState = nextNode * 9 + directionIndex
|
|
1244
1233
|
if (nextDistance >= distances[nextState]! - EPSILON) continue
|
|
1234
|
+
const edgeIndex = current.node * 8 + directionIndex
|
|
1235
|
+
if (edgeClearance[edgeIndex] === 0) {
|
|
1236
|
+
const clear = segmentIsClear({
|
|
1237
|
+
segment: {
|
|
1238
|
+
start: node.point,
|
|
1239
|
+
end: nextPoint,
|
|
1240
|
+
width: traceWidth,
|
|
1241
|
+
layer: targetLayer,
|
|
1242
|
+
},
|
|
1243
|
+
terminal,
|
|
1244
|
+
acceptedAttemptSegments,
|
|
1245
|
+
})
|
|
1246
|
+
edgeClearance[edgeIndex] = clear ? 1 : 2
|
|
1247
|
+
edgeClearance[nextNode * 8 + ((directionIndex + 4) % 8)] = clear
|
|
1248
|
+
? 1
|
|
1249
|
+
: 2
|
|
1250
|
+
}
|
|
1251
|
+
if (edgeClearance[edgeIndex] === 2) continue
|
|
1245
1252
|
distances[nextState] = nextDistance
|
|
1246
1253
|
previous[nextState] = state
|
|
1247
1254
|
const remaining = heuristic(nextPoint)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter"
|
|
2
|
+
import { fanoutPlansAreClear } from "./route-bus"
|
|
3
|
+
import { routeViaMinimalWinding } from "./route-via-minimal-winding"
|
|
4
|
+
import type { Bounds, FanoutRoutePlan, PreparedBus } from "./types"
|
|
5
|
+
|
|
6
|
+
/** Revisit excessive winding detours after provisional reservations are gone. */
|
|
7
|
+
export function shortenBusPlans(params: {
|
|
8
|
+
plans: readonly FanoutRoutePlan[]
|
|
9
|
+
bus: PreparedBus
|
|
10
|
+
srj: SimpleRouteJson
|
|
11
|
+
sharedBoundary: Bounds
|
|
12
|
+
layerNames: string[]
|
|
13
|
+
traceWidth: number
|
|
14
|
+
viaDiameter: number
|
|
15
|
+
viaHoleDiameter: number
|
|
16
|
+
clearance: number
|
|
17
|
+
allowSameNetMerges: boolean
|
|
18
|
+
}): FanoutRoutePlan[] {
|
|
19
|
+
const { bus } = params
|
|
20
|
+
let plans = [...params.plans]
|
|
21
|
+
if (bus.maxLengthSkew === undefined) return plans
|
|
22
|
+
const busPlans = plans
|
|
23
|
+
.filter((plan) => plan.busId === bus.busId)
|
|
24
|
+
.toSorted((first, second) => second.length - first.length)
|
|
25
|
+
for (const plan of busPlans) {
|
|
26
|
+
const minimumLength = Math.min(
|
|
27
|
+
...plans.filter((p) => p.busId === bus.busId).map((p) => p.length),
|
|
28
|
+
)
|
|
29
|
+
if (plan.length - minimumLength <= bus.maxLengthSkew) continue
|
|
30
|
+
// Retain the original source dogbone, through-via and boundary endpoint.
|
|
31
|
+
if (
|
|
32
|
+
!plan.via ||
|
|
33
|
+
plan.additionalVias?.length ||
|
|
34
|
+
plan.planeEndpointVia ||
|
|
35
|
+
plan.segments.filter((segment) => segment.layer === plan.sourceLayer)
|
|
36
|
+
.length !== 1
|
|
37
|
+
)
|
|
38
|
+
continue
|
|
39
|
+
const connection = bus.connections.find(
|
|
40
|
+
(c) => c.connectionIndex === plan.connectionIndex,
|
|
41
|
+
)!
|
|
42
|
+
for (const alignGridToPads of [true, false]) {
|
|
43
|
+
const candidate = routeViaMinimalWinding({
|
|
44
|
+
...params,
|
|
45
|
+
bus: {
|
|
46
|
+
...bus,
|
|
47
|
+
connections: [connection],
|
|
48
|
+
routableEscapeLayers: [plan.targetLayer],
|
|
49
|
+
},
|
|
50
|
+
terminals: [
|
|
51
|
+
{ connection, viaPoint: plan.via.center, exitPoint: plan.exitPoint },
|
|
52
|
+
],
|
|
53
|
+
targetLayer: plan.targetLayer,
|
|
54
|
+
acceptedPlans: plans.filter((p) => p !== plan),
|
|
55
|
+
allowBlindAndBuriedVias: false,
|
|
56
|
+
gridStepDivisor: 2,
|
|
57
|
+
alignGridToPads,
|
|
58
|
+
maximumRouteOrderAttempts: 1,
|
|
59
|
+
})?.[0]
|
|
60
|
+
if (!candidate || candidate.length >= plan.length - 1e-6) continue
|
|
61
|
+
const nextPlans = plans.map((p) => (p === plan ? candidate : p))
|
|
62
|
+
if (
|
|
63
|
+
!fanoutPlansAreClear({
|
|
64
|
+
...params,
|
|
65
|
+
plans: nextPlans,
|
|
66
|
+
allowBlindAndBuriedVias: false,
|
|
67
|
+
})
|
|
68
|
+
)
|
|
69
|
+
continue
|
|
70
|
+
plans = nextPlans
|
|
71
|
+
break
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return plans
|
|
75
|
+
}
|
package/lib/types.ts
CHANGED
|
@@ -388,6 +388,8 @@ export interface FanoutRoutePlan {
|
|
|
388
388
|
exitPoint: Point2D
|
|
389
389
|
trace: FanoutSimplifiedPcbTrace
|
|
390
390
|
segments: RoutedSegment[]
|
|
391
|
+
/** Validated winding segments that leave this route's own source pad. */
|
|
392
|
+
sourceEscapeSegmentCount?: number
|
|
391
393
|
via?: RoutedVia
|
|
392
394
|
/** Additional layer transitions used by an explicit winding channel. */
|
|
393
395
|
additionalVias?: RoutedVia[]
|