@tscircuit/fanout-solver 0.0.49 → 0.0.50
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 +1422 -139
- package/lib/match-component-dogbone-via-sites.ts +92 -14
- package/lib/route-bus.ts +101 -44
- package/lib/types.ts +11 -0
- package/package.json +1 -1
|
@@ -40,6 +40,17 @@ export interface DogboneViaSiteGeometryRules {
|
|
|
40
40
|
connectionIndex: number
|
|
41
41
|
segment: RoutedSegment
|
|
42
42
|
}[]
|
|
43
|
+
/** Routed vias that every newly assigned through-via/dogbone must clear. */
|
|
44
|
+
blockingVias?: readonly {
|
|
45
|
+
connectionIndex: number
|
|
46
|
+
center: Point2D
|
|
47
|
+
diameter: number
|
|
48
|
+
spanLayers: readonly string[]
|
|
49
|
+
}[]
|
|
50
|
+
/** Board obstacles outside the source component that dogbones must clear. */
|
|
51
|
+
additionalObstacles?: readonly Obstacle[]
|
|
52
|
+
/** Alternate horizontal plane dogbones across BGA rows. */
|
|
53
|
+
preferPlaneCheckerboardSites?: boolean
|
|
43
54
|
/** True only when the two connections are allowed to merge copper. */
|
|
44
55
|
canShareCopper?: (
|
|
45
56
|
firstConnectionIndex: number,
|
|
@@ -148,6 +159,7 @@ function getComponentMatchingInputs(
|
|
|
148
159
|
): ComponentMatchingInput[] {
|
|
149
160
|
const byComponent = new Map<string, ComponentMatchingInput>()
|
|
150
161
|
const componentByConnectionIndex = new Map<number, string>()
|
|
162
|
+
const obstacleSetByComponentId = new Map<string, Set<Obstacle>>()
|
|
151
163
|
|
|
152
164
|
for (const bus of preparedBuses) {
|
|
153
165
|
let component = byComponent.get(bus.componentId)
|
|
@@ -162,6 +174,7 @@ function getComponentMatchingInputs(
|
|
|
162
174
|
pitchY: Number.POSITIVE_INFINITY,
|
|
163
175
|
}
|
|
164
176
|
byComponent.set(bus.componentId, component)
|
|
177
|
+
obstacleSetByComponentId.set(bus.componentId, new Set())
|
|
165
178
|
}
|
|
166
179
|
|
|
167
180
|
component.xCoordinates.push(...bus.xCoordinates)
|
|
@@ -172,8 +185,10 @@ function getComponentMatchingInputs(
|
|
|
172
185
|
if (Number.isFinite(bus.pitchY) && bus.pitchY > EPSILON) {
|
|
173
186
|
component.pitchY = Math.min(component.pitchY, bus.pitchY)
|
|
174
187
|
}
|
|
188
|
+
const obstacleSet = obstacleSetByComponentId.get(bus.componentId)!
|
|
175
189
|
for (const obstacle of bus.componentObstacles) {
|
|
176
|
-
if (!
|
|
190
|
+
if (!obstacleSet.has(obstacle)) {
|
|
191
|
+
obstacleSet.add(obstacle)
|
|
177
192
|
component.obstacles.push(obstacle)
|
|
178
193
|
}
|
|
179
194
|
}
|
|
@@ -325,6 +340,9 @@ function getConnectionCandidates(params: {
|
|
|
325
340
|
}): ViaSiteCandidate[] {
|
|
326
341
|
const { connection, component, rules } = params
|
|
327
342
|
const { preparedConnection, direction } = connection
|
|
343
|
+
const obstacles = rules.additionalObstacles
|
|
344
|
+
? [...new Set([...component.obstacles, ...rules.additionalObstacles])]
|
|
345
|
+
: component.obstacles
|
|
328
346
|
const source = {
|
|
329
347
|
x: preparedConnection.sourcePoint.x,
|
|
330
348
|
y: preparedConnection.sourcePoint.y,
|
|
@@ -368,7 +386,7 @@ function getConnectionCandidates(params: {
|
|
|
368
386
|
if (
|
|
369
387
|
!viaSiteClearsObstacles({
|
|
370
388
|
point,
|
|
371
|
-
obstacles
|
|
389
|
+
obstacles,
|
|
372
390
|
viaDiameter: rules.viaDiameter,
|
|
373
391
|
clearance: rules.clearance,
|
|
374
392
|
})
|
|
@@ -385,7 +403,7 @@ function getConnectionCandidates(params: {
|
|
|
385
403
|
!sourceSegmentClearsOtherObstacles({
|
|
386
404
|
segment: sourceSegment,
|
|
387
405
|
sourceObstacle: preparedConnection.sourceObstacle,
|
|
388
|
-
obstacles
|
|
406
|
+
obstacles,
|
|
389
407
|
clearance: rules.clearance,
|
|
390
408
|
})
|
|
391
409
|
) {
|
|
@@ -423,6 +441,35 @@ function getConnectionCandidates(params: {
|
|
|
423
441
|
},
|
|
424
442
|
)
|
|
425
443
|
if (!candidateClearsRoutedCopper) continue
|
|
444
|
+
const candidateClearsRoutedVias = (rules.blockingVias ?? []).every(
|
|
445
|
+
(blocker) => {
|
|
446
|
+
if (blocker.connectionIndex === preparedConnection.connectionIndex) {
|
|
447
|
+
return true
|
|
448
|
+
}
|
|
449
|
+
if (
|
|
450
|
+
distance(point, blocker.center) <
|
|
451
|
+
(rules.viaDiameter + blocker.diameter) / 2 + rules.clearance - EPSILON
|
|
452
|
+
) {
|
|
453
|
+
return false
|
|
454
|
+
}
|
|
455
|
+
if (
|
|
456
|
+
blocker.spanLayers.includes(sourceSegment.layer) &&
|
|
457
|
+
distancePointToSegment(
|
|
458
|
+
blocker.center,
|
|
459
|
+
sourceSegment.start,
|
|
460
|
+
sourceSegment.end,
|
|
461
|
+
) <
|
|
462
|
+
blocker.diameter / 2 +
|
|
463
|
+
sourceSegment.width / 2 +
|
|
464
|
+
rules.clearance -
|
|
465
|
+
EPSILON
|
|
466
|
+
) {
|
|
467
|
+
return false
|
|
468
|
+
}
|
|
469
|
+
return true
|
|
470
|
+
},
|
|
471
|
+
)
|
|
472
|
+
if (!candidateClearsRoutedVias) continue
|
|
426
473
|
candidates.push({
|
|
427
474
|
connectionIndex: preparedConnection.connectionIndex,
|
|
428
475
|
point,
|
|
@@ -431,10 +478,30 @@ function getConnectionCandidates(params: {
|
|
|
431
478
|
})
|
|
432
479
|
}
|
|
433
480
|
|
|
481
|
+
const perpendicularCoordinates =
|
|
482
|
+
direction === "left" || direction === "right"
|
|
483
|
+
? component.yCoordinates
|
|
484
|
+
: component.xCoordinates
|
|
485
|
+
const sourcePerpendicularCoordinate =
|
|
486
|
+
direction === "left" || direction === "right" ? source.y : source.x
|
|
487
|
+
const perpendicularGridIndex = perpendicularCoordinates.findIndex(
|
|
488
|
+
(coordinate) =>
|
|
489
|
+
Math.abs(coordinate - sourcePerpendicularCoordinate) <= EPSILON,
|
|
490
|
+
)
|
|
491
|
+
const planeCheckerboardSide =
|
|
492
|
+
perpendicularGridIndex >= 0
|
|
493
|
+
? perpendicularGridIndex % 2 === 0
|
|
494
|
+
? 1
|
|
495
|
+
: -1
|
|
496
|
+
: undefined
|
|
434
497
|
const preferredPerpendicularSide =
|
|
435
498
|
connection.terminationType === "boundary"
|
|
436
499
|
? rules.preferredBoundaryPerpendicularSideByBusId?.get(connection.busId)
|
|
437
|
-
:
|
|
500
|
+
: rules.preferPlaneCheckerboardSites &&
|
|
501
|
+
connection.terminationType === "plane" &&
|
|
502
|
+
(direction === "left" || direction === "right")
|
|
503
|
+
? planeCheckerboardSide
|
|
504
|
+
: undefined
|
|
438
505
|
const preferOutward =
|
|
439
506
|
connection.terminationType === "boundary"
|
|
440
507
|
? (rules.preferBoundaryOutwardByBusId?.get(connection.busId) ?? true)
|
|
@@ -525,6 +592,25 @@ function matchComponent(params: {
|
|
|
525
592
|
}),
|
|
526
593
|
)
|
|
527
594
|
if (entries.some((entry) => entry.candidates.length === 0)) return null
|
|
595
|
+
const compatibilityCache = new Map<
|
|
596
|
+
ViaSiteCandidate,
|
|
597
|
+
Map<ViaSiteCandidate, boolean>
|
|
598
|
+
>()
|
|
599
|
+
const candidatesAreCompatible = (
|
|
600
|
+
first: ViaSiteCandidate,
|
|
601
|
+
second: ViaSiteCandidate,
|
|
602
|
+
): boolean => {
|
|
603
|
+
const cached = compatibilityCache.get(first)?.get(second)
|
|
604
|
+
if (cached !== undefined) return cached
|
|
605
|
+
const compatible = candidatesAreMutuallyClear({ first, second, rules })
|
|
606
|
+
const firstCache = compatibilityCache.get(first) ?? new Map()
|
|
607
|
+
firstCache.set(second, compatible)
|
|
608
|
+
compatibilityCache.set(first, firstCache)
|
|
609
|
+
const secondCache = compatibilityCache.get(second) ?? new Map()
|
|
610
|
+
secondCache.set(first, compatible)
|
|
611
|
+
compatibilityCache.set(second, secondCache)
|
|
612
|
+
return compatible
|
|
613
|
+
}
|
|
528
614
|
// Every solution must include each sole candidate. Seed and validate those
|
|
529
615
|
// forced choices once so recursive matching only explores genuine choices.
|
|
530
616
|
const forcedCandidates = entries.flatMap((entry) =>
|
|
@@ -542,11 +628,7 @@ function matchComponent(params: {
|
|
|
542
628
|
previousIndex++
|
|
543
629
|
) {
|
|
544
630
|
if (
|
|
545
|
-
!
|
|
546
|
-
first: candidate,
|
|
547
|
-
second: forcedCandidates[previousIndex]!,
|
|
548
|
-
rules,
|
|
549
|
-
})
|
|
631
|
+
!candidatesAreCompatible(candidate, forcedCandidates[previousIndex]!)
|
|
550
632
|
) {
|
|
551
633
|
return null
|
|
552
634
|
}
|
|
@@ -584,11 +666,7 @@ function matchComponent(params: {
|
|
|
584
666
|
): ViaSiteCandidate[] =>
|
|
585
667
|
entry.candidates.filter((candidate) =>
|
|
586
668
|
[...assignedCandidates.values()].every((assignedCandidate) =>
|
|
587
|
-
|
|
588
|
-
first: candidate,
|
|
589
|
-
second: assignedCandidate,
|
|
590
|
-
rules,
|
|
591
|
-
}),
|
|
669
|
+
candidatesAreCompatible(candidate, assignedCandidate),
|
|
592
670
|
),
|
|
593
671
|
)
|
|
594
672
|
|
package/lib/route-bus.ts
CHANGED
|
@@ -24,8 +24,8 @@ import {
|
|
|
24
24
|
obstacleSharesElectricalNet,
|
|
25
25
|
} from "./net-identity"
|
|
26
26
|
import {
|
|
27
|
-
routeViaMinimalWindingAlternativesSteps,
|
|
28
27
|
type RouteViaMinimalWindingProgress,
|
|
28
|
+
routeViaMinimalWindingAlternativesSteps,
|
|
29
29
|
type ViaMinimalWindingReservedVia,
|
|
30
30
|
} from "./route-via-minimal-winding"
|
|
31
31
|
import type {
|
|
@@ -61,6 +61,10 @@ export interface RouteBusParams {
|
|
|
61
61
|
fixedViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
|
|
62
62
|
reservedVias?: readonly ViaMinimalWindingReservedVia[]
|
|
63
63
|
viaMinimalOnly?: boolean
|
|
64
|
+
/** Bounds the final fixed-via winding fallback after ordered attempts. */
|
|
65
|
+
fixedViaFallbackRouteOrderAttempts?: number
|
|
66
|
+
/** Skip this many otherwise-clear plane escapes when enumerating alternatives. */
|
|
67
|
+
planeCandidateSkipCount?: number
|
|
64
68
|
/** Dense corner-band phase that preserves existing lane centers when leading lanes are prepended. */
|
|
65
69
|
cornerBandTargetTrackOffset?: number
|
|
66
70
|
}
|
|
@@ -1873,6 +1877,24 @@ function planIsClearOfPlans(params: {
|
|
|
1873
1877
|
return true
|
|
1874
1878
|
}
|
|
1875
1879
|
|
|
1880
|
+
export function fanoutPlansAreMutuallyClear(params: {
|
|
1881
|
+
plans: readonly FanoutRoutePlan[]
|
|
1882
|
+
srj: SimpleRouteJson
|
|
1883
|
+
clearance: number
|
|
1884
|
+
allowSameNetMerges?: boolean
|
|
1885
|
+
}): boolean {
|
|
1886
|
+
const { plans, srj, clearance, allowSameNetMerges = false } = params
|
|
1887
|
+
return plans.every((plan, index) =>
|
|
1888
|
+
planIsClearOfPlans({
|
|
1889
|
+
plan,
|
|
1890
|
+
otherPlans: plans.filter((_, otherIndex) => otherIndex !== index),
|
|
1891
|
+
srj,
|
|
1892
|
+
allowSameNetMerges,
|
|
1893
|
+
clearance,
|
|
1894
|
+
}),
|
|
1895
|
+
)
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1876
1898
|
function planIsClear(params: {
|
|
1877
1899
|
plan: FanoutRoutePlan
|
|
1878
1900
|
otherPlans: FanoutRoutePlan[]
|
|
@@ -1991,11 +2013,27 @@ function routePlaneTerminatedBus(
|
|
|
1991
2013
|
allowBlindAndBuriedVias = true,
|
|
1992
2014
|
allowSameNetMerges = false,
|
|
1993
2015
|
fixedViaPointsByConnectionIndex,
|
|
2016
|
+
planeCandidateSkipCount = 0,
|
|
1994
2017
|
} = params
|
|
1995
2018
|
const sourceObstacle = bus.connections[0]?.sourceObstacle
|
|
1996
2019
|
if (!sourceObstacle || bus.termination.type !== "plane") return null
|
|
1997
2020
|
const sourceLayer = bus.connections[0]!.sourceLayer
|
|
1998
2021
|
if (targetLayer === sourceLayer) return null
|
|
2022
|
+
let remainingPlaneCandidatesToSkip = planeCandidateSkipCount
|
|
2023
|
+
const selectClearPlanePlan = (
|
|
2024
|
+
plans: FanoutRoutePlan[],
|
|
2025
|
+
isClear: (plan: FanoutRoutePlan, index: number) => boolean,
|
|
2026
|
+
): FanoutRoutePlan | undefined => {
|
|
2027
|
+
for (const [index, plan] of plans.entries()) {
|
|
2028
|
+
if (!isClear(plan, index)) continue
|
|
2029
|
+
if (remainingPlaneCandidatesToSkip > 0) {
|
|
2030
|
+
remainingPlaneCandidatesToSkip--
|
|
2031
|
+
continue
|
|
2032
|
+
}
|
|
2033
|
+
return plan
|
|
2034
|
+
}
|
|
2035
|
+
return undefined
|
|
2036
|
+
}
|
|
1999
2037
|
|
|
2000
2038
|
if (fixedViaPointsByConnectionIndex) {
|
|
2001
2039
|
const fixedPlans: FanoutRoutePlan[] = []
|
|
@@ -2052,19 +2090,21 @@ function routePlaneTerminatedBus(
|
|
|
2052
2090
|
),
|
|
2053
2091
|
basePlan,
|
|
2054
2092
|
]
|
|
2055
|
-
const clearPlan =
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2093
|
+
const clearPlan = selectClearPlanePlan(
|
|
2094
|
+
plansToTry,
|
|
2095
|
+
(candidatePlan, candidateIndex) =>
|
|
2096
|
+
planIsClear({
|
|
2097
|
+
plan: candidatePlan,
|
|
2098
|
+
otherPlans: [...acceptedPlans, ...fixedPlans],
|
|
2099
|
+
staticClearanceCache,
|
|
2100
|
+
blockingBusCounts,
|
|
2101
|
+
cacheKey: `plane-fixed:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${candidateIndex}`,
|
|
2102
|
+
srj,
|
|
2103
|
+
sharedBoundary: bus.sharedBoundary,
|
|
2104
|
+
clearance,
|
|
2105
|
+
allowBlindAndBuriedVias,
|
|
2106
|
+
allowSameNetMerges,
|
|
2107
|
+
}),
|
|
2068
2108
|
)
|
|
2069
2109
|
if (!clearPlan) return null
|
|
2070
2110
|
fixedPlans.push(clearPlan)
|
|
@@ -2128,19 +2168,21 @@ function routePlaneTerminatedBus(
|
|
|
2128
2168
|
),
|
|
2129
2169
|
viaInPadPlan,
|
|
2130
2170
|
]
|
|
2131
|
-
const clearPlan =
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2171
|
+
const clearPlan = selectClearPlanePlan(
|
|
2172
|
+
plansToTry,
|
|
2173
|
+
(candidatePlan, candidateIndex) =>
|
|
2174
|
+
planIsClear({
|
|
2175
|
+
plan: candidatePlan,
|
|
2176
|
+
otherPlans: acceptedPlans,
|
|
2177
|
+
staticClearanceCache,
|
|
2178
|
+
blockingBusCounts,
|
|
2179
|
+
cacheKey: `plane-via-in-pad:${bus.busId}:${targetLayer}:${candidateIndex}`,
|
|
2180
|
+
srj,
|
|
2181
|
+
sharedBoundary: bus.sharedBoundary,
|
|
2182
|
+
clearance,
|
|
2183
|
+
allowBlindAndBuriedVias,
|
|
2184
|
+
allowSameNetMerges,
|
|
2185
|
+
}),
|
|
2144
2186
|
)
|
|
2145
2187
|
if (clearPlan) return [clearPlan]
|
|
2146
2188
|
}
|
|
@@ -2203,7 +2245,7 @@ function routePlaneTerminatedBus(
|
|
|
2203
2245
|
)
|
|
2204
2246
|
const perpendicularPitch = getPerpendicularPitch(directionalBus)
|
|
2205
2247
|
const sourceEscapePaths: Point2D[][] = []
|
|
2206
|
-
const maximumCandidatePaths =
|
|
2248
|
+
const maximumCandidatePaths = 128
|
|
2207
2249
|
for (
|
|
2208
2250
|
let totalSteps = 0;
|
|
2209
2251
|
totalSteps <= maximumEscapeSteps &&
|
|
@@ -2345,19 +2387,21 @@ function routePlaneTerminatedBus(
|
|
|
2345
2387
|
),
|
|
2346
2388
|
basePlan,
|
|
2347
2389
|
]
|
|
2348
|
-
plan =
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2390
|
+
plan = selectClearPlanePlan(
|
|
2391
|
+
plansToTry,
|
|
2392
|
+
(candidatePlan, candidateIndex) =>
|
|
2393
|
+
planIsClear({
|
|
2394
|
+
plan: candidatePlan,
|
|
2395
|
+
otherPlans: [...acceptedPlans, ...candidatePlans],
|
|
2396
|
+
staticClearanceCache,
|
|
2397
|
+
blockingBusCounts,
|
|
2398
|
+
cacheKey: `plane:${bus.busId}:${targetLayer}:${direction}:${preparedConnection.connectionIndex}:${viaHandedness}:${pathIndex}:${candidateIndex}`,
|
|
2399
|
+
srj,
|
|
2400
|
+
sharedBoundary: bus.sharedBoundary,
|
|
2401
|
+
clearance,
|
|
2402
|
+
allowBlindAndBuriedVias,
|
|
2403
|
+
allowSameNetMerges,
|
|
2404
|
+
}),
|
|
2361
2405
|
)
|
|
2362
2406
|
if (plan) break
|
|
2363
2407
|
}
|
|
@@ -2400,6 +2444,7 @@ export function* routeBusAlternativesSteps(
|
|
|
2400
2444
|
fixedViaPointsByConnectionIndex,
|
|
2401
2445
|
reservedVias = [],
|
|
2402
2446
|
viaMinimalOnly = false,
|
|
2447
|
+
fixedViaFallbackRouteOrderAttempts = 24,
|
|
2403
2448
|
cornerBandTargetTrackOffset,
|
|
2404
2449
|
} = params
|
|
2405
2450
|
if (!Number.isInteger(maxAlternatives) || maxAlternatives < 1) {
|
|
@@ -2417,8 +2462,20 @@ export function* routeBusAlternativesSteps(
|
|
|
2417
2462
|
return []
|
|
2418
2463
|
}
|
|
2419
2464
|
if (bus.termination.type === "plane") {
|
|
2420
|
-
const
|
|
2421
|
-
|
|
2465
|
+
const alternatives: FanoutRoutePlan[][] = []
|
|
2466
|
+
for (
|
|
2467
|
+
let planeCandidateSkipCount = 0;
|
|
2468
|
+
planeCandidateSkipCount < maxAlternatives;
|
|
2469
|
+
planeCandidateSkipCount++
|
|
2470
|
+
) {
|
|
2471
|
+
const plan = routePlaneTerminatedBus({
|
|
2472
|
+
...params,
|
|
2473
|
+
planeCandidateSkipCount,
|
|
2474
|
+
})
|
|
2475
|
+
if (!plan) break
|
|
2476
|
+
alternatives.push(plan)
|
|
2477
|
+
}
|
|
2478
|
+
return alternatives
|
|
2422
2479
|
}
|
|
2423
2480
|
const exitAxis = getExitAxis(bus)
|
|
2424
2481
|
const sourceObstacle = bus.connections[0]?.sourceObstacle
|
|
@@ -2694,7 +2751,7 @@ export function* routeBusAlternativesSteps(
|
|
|
2694
2751
|
)!,
|
|
2695
2752
|
// Preserve the existing bounded search after every inexpensive
|
|
2696
2753
|
// layer-interleave candidate has had one deterministic attempt.
|
|
2697
|
-
maximumRouteOrderAttempts:
|
|
2754
|
+
maximumRouteOrderAttempts: fixedViaFallbackRouteOrderAttempts,
|
|
2698
2755
|
windingOrderIndex: 0,
|
|
2699
2756
|
preferTargetDirectedLaneBias: true,
|
|
2700
2757
|
},
|
package/lib/types.ts
CHANGED
|
@@ -197,6 +197,17 @@ export interface FanoutSolverOptions {
|
|
|
197
197
|
allowBlindAndBuriedVias?: boolean
|
|
198
198
|
/** Allow branches belonging to the same electrical net to share copper. */
|
|
199
199
|
allowSameNetMerges?: boolean
|
|
200
|
+
/**
|
|
201
|
+
* Plane buses whose adjacent dogbone sites must be reserved before routing
|
|
202
|
+
* dense through-via boundary buses. This is an advanced deterministic hint
|
|
203
|
+
* for fields where later signal copper would otherwise consume those sites.
|
|
204
|
+
*/
|
|
205
|
+
densePlaneReservationBusIds?: readonly string[]
|
|
206
|
+
/**
|
|
207
|
+
* Plane buses that should participate in the bounded alternate-route CSP
|
|
208
|
+
* instead of using an adjacent component dogbone.
|
|
209
|
+
*/
|
|
210
|
+
denseUnrestrictedPlaneRoutingBusIds?: readonly string[]
|
|
200
211
|
singleLayerPushAndShove?: boolean
|
|
201
212
|
/**
|
|
202
213
|
* When preferred single-layer exits cannot coexist, allow a global
|