@tscircuit/fanout-solver 0.0.62 → 0.0.64
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 +71 -12
- package/lib/geometry.ts +19 -0
- package/lib/route-bus.ts +28 -2
- package/lib/route-via-minimal-winding.ts +100 -40
- package/package.json +1 -1
package/lib/fanout-solver.ts
CHANGED
|
@@ -1551,6 +1551,11 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1551
1551
|
promotedPlaneReservationBusIds?: readonly string[]
|
|
1552
1552
|
preferredBoundaryViaPoints?: ReadonlyMap<number, { x: number; y: number }>
|
|
1553
1553
|
planeReservationRetryCount?: number
|
|
1554
|
+
boundaryRecovery?: {
|
|
1555
|
+
busId: string
|
|
1556
|
+
preferOutward: boolean
|
|
1557
|
+
perpendicularSide: -1 | 1
|
|
1558
|
+
}
|
|
1554
1559
|
}): Generator<FanoutWorkYield, MixedTerminationState | null, unknown> {
|
|
1555
1560
|
if (this.config.allowBlindAndBuriedVias) return null
|
|
1556
1561
|
// An outside-package singleton escape can depend on the completed signal
|
|
@@ -1601,12 +1606,16 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1601
1606
|
)
|
|
1602
1607
|
const useConfiguredDensePlaneRouting =
|
|
1603
1608
|
configuredDensePlaneRouting || useAdaptiveDensePlaneRouting
|
|
1609
|
+
const useBoundaryRecovery = params.boundaryRecovery !== undefined
|
|
1610
|
+
const useJointPlaneRepair =
|
|
1611
|
+
useConfiguredDensePlaneRouting || useBoundaryRecovery
|
|
1604
1612
|
// A completed boundary assignment remains worth repairing jointly after
|
|
1605
1613
|
// plane reservations change; a greedy refill can discard that assignment.
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
(
|
|
1609
|
-
|
|
1614
|
+
const useJointPlaneSelection =
|
|
1615
|
+
useBoundaryRecovery ||
|
|
1616
|
+
(useAdaptiveDensePlaneRouting &&
|
|
1617
|
+
((params.planeReservationRetryCount ?? 0) === 0 ||
|
|
1618
|
+
Boolean(params.preferredBoundaryViaPoints)))
|
|
1610
1619
|
const matchLengthsAfterPlanes =
|
|
1611
1620
|
useConfiguredDensePlaneRouting &&
|
|
1612
1621
|
params.lengthMatchingStage !== "before-planes"
|
|
@@ -1852,7 +1861,9 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1852
1861
|
process.env.FANOUT_DEBUG_BOUNDARY_ORDER?.split(",") ??
|
|
1853
1862
|
(process.env.FANOUT_DEBUG_FIRST_BOUNDARY_BUS
|
|
1854
1863
|
? [process.env.FANOUT_DEBUG_FIRST_BOUNDARY_BUS]
|
|
1855
|
-
:
|
|
1864
|
+
: params.boundaryRecovery
|
|
1865
|
+
? [params.boundaryRecovery.busId]
|
|
1866
|
+
: [])
|
|
1856
1867
|
const boundaryBuses =
|
|
1857
1868
|
debugBoundaryOrder.length > 0
|
|
1858
1869
|
? initiallySortedBoundaryBuses.toSorted((first, second) => {
|
|
@@ -1914,6 +1925,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1914
1925
|
]
|
|
1915
1926
|
}
|
|
1916
1927
|
const unroutablePlaneBusIds = new Set<string>()
|
|
1928
|
+
let failedWideBoundaryBus: PreparedBus | undefined
|
|
1917
1929
|
debugDense(
|
|
1918
1930
|
"start",
|
|
1919
1931
|
boundaryBuses.map((bus) => `${bus.busId}:${bus.connections.length}`),
|
|
@@ -2015,6 +2027,16 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2015
2027
|
if (entersNeighboringSourceField)
|
|
2016
2028
|
preferBoundaryOutwardByBusId.set(bus.busId, false)
|
|
2017
2029
|
}
|
|
2030
|
+
if (params.boundaryRecovery) {
|
|
2031
|
+
preferredBoundaryPerpendicularSideByBusId.set(
|
|
2032
|
+
params.boundaryRecovery.busId,
|
|
2033
|
+
params.boundaryRecovery.perpendicularSide,
|
|
2034
|
+
)
|
|
2035
|
+
preferBoundaryOutwardByBusId.set(
|
|
2036
|
+
params.boundaryRecovery.busId,
|
|
2037
|
+
params.boundaryRecovery.preferOutward,
|
|
2038
|
+
)
|
|
2039
|
+
}
|
|
2018
2040
|
const debugFlippedBoundaryBus = process.env.FANOUT_DEBUG_FLIP_BOUNDARY_BUS
|
|
2019
2041
|
if (debugFlippedBoundaryBus) {
|
|
2020
2042
|
preferredBoundaryPerpendicularSideByBusId.set(debugFlippedBoundaryBus, -1)
|
|
@@ -2653,6 +2675,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2653
2675
|
allowBoundarySideViaFallback: bus.connections.length === 1,
|
|
2654
2676
|
preferCornerBoundaryVia: useConfiguredDensePlaneRouting,
|
|
2655
2677
|
adaptiveWindingRouteOrder,
|
|
2678
|
+
allowFixedViaReservedExitFallback: useBoundaryRecovery,
|
|
2656
2679
|
// Retain pad-aligned channels even when plane sites are reserved
|
|
2657
2680
|
// adaptively; the boundary grid can fence off a turning wide bus.
|
|
2658
2681
|
alignWindingGridToPads:
|
|
@@ -3035,6 +3058,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3035
3058
|
}
|
|
3036
3059
|
}
|
|
3037
3060
|
if (!busPlans) {
|
|
3061
|
+
if (bus.connections.length >= 8) failedWideBoundaryBus ??= bus
|
|
3038
3062
|
debugDense("route:failed", bus.busId)
|
|
3039
3063
|
return false
|
|
3040
3064
|
}
|
|
@@ -3365,6 +3389,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3365
3389
|
promotedAlternatePlaneBusIds: ReadonlySet<string> = new Set(),
|
|
3366
3390
|
): Map<number, { x: number; y: number }> | null => {
|
|
3367
3391
|
feasibleAlternatePlanePlans = []
|
|
3392
|
+
matchedPlaneBusesInRoutingOrder = null
|
|
3368
3393
|
const fixedBoundaryViaPoints = new Map(
|
|
3369
3394
|
candidatePlans.flatMap((plan) =>
|
|
3370
3395
|
plan.via
|
|
@@ -3431,11 +3456,16 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3431
3456
|
if (retainedViaPoints)
|
|
3432
3457
|
return new Map([...fixedBoundaryViaPoints, ...retainedViaPoints])
|
|
3433
3458
|
if (
|
|
3434
|
-
|
|
3459
|
+
useJointPlaneRepair ||
|
|
3435
3460
|
process.env.FANOUT_DEBUG_INCREMENTAL_PLANE_MATCH === "1"
|
|
3436
3461
|
) {
|
|
3437
3462
|
let incrementalViaPoints = new Map(fixedBoundaryViaPoints)
|
|
3438
|
-
|
|
3463
|
+
// Reservations guided the boundary search, but their copper is
|
|
3464
|
+
// still uncommitted. Recovery must choose local and longer plane
|
|
3465
|
+
// escapes together instead of locking every provisional site.
|
|
3466
|
+
const matchedPlaneBuses = useBoundaryRecovery
|
|
3467
|
+
? []
|
|
3468
|
+
: [...activeBoundaryReservationPlaneBuses]
|
|
3439
3469
|
for (const planeBus of matchedPlaneBuses) {
|
|
3440
3470
|
for (const connection of planeBus.connections) {
|
|
3441
3471
|
const reservedPoint = fixedViaPointsByConnectionIndex.get(
|
|
@@ -3581,7 +3611,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3581
3611
|
independentlyUnmatchablePlaneBuses.map((bus) => bus.busId),
|
|
3582
3612
|
)
|
|
3583
3613
|
if (
|
|
3584
|
-
!
|
|
3614
|
+
!useJointPlaneRepair &&
|
|
3585
3615
|
process.env.FANOUT_DEBUG_ROUTE_UNMATCHED_PLANES !== "1"
|
|
3586
3616
|
) {
|
|
3587
3617
|
return null
|
|
@@ -3596,7 +3626,9 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3596
3626
|
? 10_000
|
|
3597
3627
|
: useConfiguredDensePlaneRouting
|
|
3598
3628
|
? 3_000_000
|
|
3599
|
-
:
|
|
3629
|
+
: useBoundaryRecovery
|
|
3630
|
+
? 10_000
|
|
3631
|
+
: 1_000),
|
|
3600
3632
|
)
|
|
3601
3633
|
const maximumAlternatePlaneRoutes = Number(
|
|
3602
3634
|
process.env.FANOUT_DEBUG_ALTERNATE_ROUTE_COUNT ??
|
|
@@ -3774,7 +3806,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3774
3806
|
}
|
|
3775
3807
|
let alternatePlanePlans: FanoutRoutePlan[] | null
|
|
3776
3808
|
if (
|
|
3777
|
-
|
|
3809
|
+
useJointPlaneRepair ||
|
|
3778
3810
|
process.env.FANOUT_DEBUG_EXACT_COVER_ALTERNATES === "1"
|
|
3779
3811
|
) {
|
|
3780
3812
|
type IndependentPlaneRouteCandidate = {
|
|
@@ -3795,7 +3827,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3795
3827
|
})),
|
|
3796
3828
|
}),
|
|
3797
3829
|
)
|
|
3798
|
-
if (
|
|
3830
|
+
if (useJointPlaneSelection) {
|
|
3799
3831
|
const acceptedPlans = [
|
|
3800
3832
|
...candidatePlans,
|
|
3801
3833
|
...feasibleAlternatePlanePlans,
|
|
@@ -4106,7 +4138,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
4106
4138
|
)
|
|
4107
4139
|
if (!alternatePlanePlans) return null
|
|
4108
4140
|
feasibleAlternatePlanePlans = alternatePlanePlans
|
|
4109
|
-
if (
|
|
4141
|
+
if (useJointPlaneSelection) {
|
|
4110
4142
|
matchedPlaneBusesInRoutingOrder = []
|
|
4111
4143
|
return new Map([
|
|
4112
4144
|
...fixedBoundaryViaPoints,
|
|
@@ -4620,6 +4652,33 @@ export class FanoutSolver extends BaseSolver {
|
|
|
4620
4652
|
denseRoutingStrategy: "boundary-aligned",
|
|
4621
4653
|
})
|
|
4622
4654
|
if (boundaryAlignedState) return boundaryAlignedState
|
|
4655
|
+
// A later wide bus can be fenced by provisional through-vias even
|
|
4656
|
+
// though its requested exit order is routable. After both existing
|
|
4657
|
+
// grids fail, give that bus first choice of the field and retry the
|
|
4658
|
+
// four dogbone orientations. The marker bounds recursion and enables
|
|
4659
|
+
// joint selection of the remaining uncommitted plane escapes.
|
|
4660
|
+
if (!useBoundaryRecovery && failedWideBoundaryBus) {
|
|
4661
|
+
const busId = failedWideBoundaryBus.busId
|
|
4662
|
+
const outward = preferBoundaryOutwardByBusId.get(busId) ?? true
|
|
4663
|
+
const side = preferredBoundaryPerpendicularSideByBusId.get(busId) ?? 1
|
|
4664
|
+
for (const [preferOutward, perpendicularSide] of [
|
|
4665
|
+
[!outward, -side],
|
|
4666
|
+
[!outward, side],
|
|
4667
|
+
[outward, -side],
|
|
4668
|
+
[outward, side],
|
|
4669
|
+
] as const) {
|
|
4670
|
+
const recoveredState =
|
|
4671
|
+
yield* this.routeDenseThroughAllMixedTerminationSteps({
|
|
4672
|
+
...params,
|
|
4673
|
+
boundaryRecovery: {
|
|
4674
|
+
busId,
|
|
4675
|
+
preferOutward,
|
|
4676
|
+
perpendicularSide: perpendicularSide as -1 | 1,
|
|
4677
|
+
},
|
|
4678
|
+
})
|
|
4679
|
+
if (recoveredState) return recoveredState
|
|
4680
|
+
}
|
|
4681
|
+
}
|
|
4623
4682
|
}
|
|
4624
4683
|
if (
|
|
4625
4684
|
!usePadAlignedDenseRouting ||
|
package/lib/geometry.ts
CHANGED
|
@@ -190,6 +190,25 @@ export function segmentsAreClear(
|
|
|
190
190
|
): boolean {
|
|
191
191
|
if (first.layer !== second.layer) return true
|
|
192
192
|
const requiredDistance = (first.width + second.width) / 2 + clearance
|
|
193
|
+
// Axis separation is a lower bound on the distance between the segments.
|
|
194
|
+
// Leave a conservative tolerance band to the exact check below.
|
|
195
|
+
const broadPhaseDistance = requiredDistance + EPSILON
|
|
196
|
+
if (
|
|
197
|
+
Math.min(first.start.x, first.end.x) -
|
|
198
|
+
Math.max(second.start.x, second.end.x) >
|
|
199
|
+
broadPhaseDistance ||
|
|
200
|
+
Math.min(second.start.x, second.end.x) -
|
|
201
|
+
Math.max(first.start.x, first.end.x) >
|
|
202
|
+
broadPhaseDistance ||
|
|
203
|
+
Math.min(first.start.y, first.end.y) -
|
|
204
|
+
Math.max(second.start.y, second.end.y) >
|
|
205
|
+
broadPhaseDistance ||
|
|
206
|
+
Math.min(second.start.y, second.end.y) -
|
|
207
|
+
Math.max(first.start.y, first.end.y) >
|
|
208
|
+
broadPhaseDistance
|
|
209
|
+
) {
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
193
212
|
return (
|
|
194
213
|
distanceSegmentToSegment(
|
|
195
214
|
first.start,
|
package/lib/route-bus.ts
CHANGED
|
@@ -75,6 +75,8 @@ export interface RouteBusParams {
|
|
|
75
75
|
alignWindingGridToPads?: boolean
|
|
76
76
|
/** Bounds the final fixed-via winding fallback after ordered attempts. */
|
|
77
77
|
fixedViaFallbackRouteOrderAttempts?: number
|
|
78
|
+
/** Retry caller-fixed sites while preserving future exit gaps during recovery. */
|
|
79
|
+
allowFixedViaReservedExitFallback?: boolean
|
|
78
80
|
/** Skip this many otherwise-clear plane escapes when enumerating alternatives. */
|
|
79
81
|
planeCandidateSkipCount?: number
|
|
80
82
|
/** Dense corner-band phase that preserves existing lane centers when leading lanes are prepended. */
|
|
@@ -1951,6 +1953,8 @@ function planIsClearOfPlans(params: {
|
|
|
1951
1953
|
clearance,
|
|
1952
1954
|
blockingBusCounts,
|
|
1953
1955
|
} = params
|
|
1956
|
+
const planSegments = getPlanSegments(plan)
|
|
1957
|
+
const planVias = getPlanVias(plan)
|
|
1954
1958
|
for (const otherPlan of otherPlans) {
|
|
1955
1959
|
if (
|
|
1956
1960
|
allowSameNetMerges &&
|
|
@@ -1975,9 +1979,7 @@ function planIsClearOfPlans(params: {
|
|
|
1975
1979
|
(blockingBusCounts.get(otherPlan.busId) ?? 0) + 1,
|
|
1976
1980
|
)
|
|
1977
1981
|
}
|
|
1978
|
-
const planSegments = getPlanSegments(plan)
|
|
1979
1982
|
const otherSegments = getPlanSegments(otherPlan)
|
|
1980
|
-
const planVias = getPlanVias(plan)
|
|
1981
1983
|
const otherVias = getPlanVias(otherPlan)
|
|
1982
1984
|
for (const segment of planSegments) {
|
|
1983
1985
|
for (const otherSegment of otherSegments) {
|
|
@@ -2606,6 +2608,7 @@ export function* routeBusAlternativesSteps(
|
|
|
2606
2608
|
adaptiveWindingRouteOrder = false,
|
|
2607
2609
|
alignWindingGridToPads = false,
|
|
2608
2610
|
fixedViaFallbackRouteOrderAttempts = 24,
|
|
2611
|
+
allowFixedViaReservedExitFallback = false,
|
|
2609
2612
|
cornerBandTargetTrackOffset,
|
|
2610
2613
|
} = params
|
|
2611
2614
|
if (!Number.isInteger(maxAlternatives) || maxAlternatives < 1) {
|
|
@@ -2994,6 +2997,29 @@ export function* routeBusAlternativesSteps(
|
|
|
2994
2997
|
})),
|
|
2995
2998
|
).flat()
|
|
2996
2999
|
: []),
|
|
3000
|
+
// A single-layer target permutation can also let an early lane
|
|
3001
|
+
// close a future terminal's exit gap. Keep the original attempts
|
|
3002
|
+
// first, then retry the same fixed sites with every exit reserved.
|
|
3003
|
+
...(allowFixedViaReservedExitFallback &&
|
|
3004
|
+
fixedViaPointsByConnectionIndex &&
|
|
3005
|
+
!getCornerSide(bus) &&
|
|
3006
|
+
windingTargetOrderCount === 1 &&
|
|
3007
|
+
bus.connections.length > 2
|
|
3008
|
+
? [true, false].map((preferTargetDirectedLaneBias) => ({
|
|
3009
|
+
label: `fixed-vias-reserved-exits-${preferTargetDirectedLaneBias}`,
|
|
3010
|
+
useViaInPad: false,
|
|
3011
|
+
getViaHandedness: () => 0 as const,
|
|
3012
|
+
getViaPoint: (connection: PreparedConnection) =>
|
|
3013
|
+
coordinatedViaPoints.get(connection.connectionIndex)!,
|
|
3014
|
+
maximumRouteOrderAttempts: Math.min(
|
|
3015
|
+
maximumThroughAllRouteOrderAttempts,
|
|
3016
|
+
fixedViaFallbackRouteOrderAttempts,
|
|
3017
|
+
),
|
|
3018
|
+
windingOrderIndex: 0,
|
|
3019
|
+
preferTargetDirectedLaneBias,
|
|
3020
|
+
reserveTerminalExitPoints: true,
|
|
3021
|
+
}))
|
|
3022
|
+
: []),
|
|
2997
3023
|
]
|
|
2998
3024
|
: []
|
|
2999
3025
|
const planeTerminationsAlreadyOccupyTheFanout = acceptedPlans.some(
|
|
@@ -824,6 +824,13 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
824
824
|
const sharesNet = (first: string, second: string): boolean =>
|
|
825
825
|
first === second ||
|
|
826
826
|
(allowSameNetMerges && connectionsShareElectricalNet(srj, first, second))
|
|
827
|
+
const boundedBlockingSegments = blockingSegments.map((blocker) => ({
|
|
828
|
+
...blocker,
|
|
829
|
+
minX: Math.min(blocker.segment.start.x, blocker.segment.end.x),
|
|
830
|
+
maxX: Math.max(blocker.segment.start.x, blocker.segment.end.x),
|
|
831
|
+
minY: Math.min(blocker.segment.start.y, blocker.segment.end.y),
|
|
832
|
+
maxY: Math.max(blocker.segment.start.y, blocker.segment.end.y),
|
|
833
|
+
}))
|
|
827
834
|
const allBlockingVias = [...blockingVias, ...terminalVias]
|
|
828
835
|
const maximumViaToTraceDistance = allBlockingVias.reduce(
|
|
829
836
|
(maximum, { via }) =>
|
|
@@ -850,6 +857,10 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
850
857
|
}): boolean => {
|
|
851
858
|
const { segment, terminal, acceptedAttemptSegments } = params
|
|
852
859
|
const connectionName = terminal.connection.connection.name
|
|
860
|
+
const segmentMinX = Math.min(segment.start.x, segment.end.x)
|
|
861
|
+
const segmentMaxX = Math.max(segment.start.x, segment.end.x)
|
|
862
|
+
const segmentMinY = Math.min(segment.start.y, segment.end.y)
|
|
863
|
+
const segmentMaxY = Math.max(segment.start.y, segment.end.y)
|
|
853
864
|
const requiredObstacleClearance = segment.width / 2 + clearance
|
|
854
865
|
for (const obstacle of targetLayerObstacleIndex.querySegment(
|
|
855
866
|
segment,
|
|
@@ -869,8 +880,19 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
869
880
|
return false
|
|
870
881
|
}
|
|
871
882
|
}
|
|
872
|
-
for (const blocker of
|
|
883
|
+
for (const blocker of boundedBlockingSegments) {
|
|
873
884
|
if (sharesNet(connectionName, blocker.connectionName)) continue
|
|
885
|
+
const margin = (segment.width + blocker.segment.width) / 2 + clearance
|
|
886
|
+
// Keep the full clearance margin in the broad phase; the exact check
|
|
887
|
+
// retains the existing tolerance for nearby copper.
|
|
888
|
+
if (
|
|
889
|
+
segmentMaxX + margin < blocker.minX ||
|
|
890
|
+
segmentMinX - margin > blocker.maxX ||
|
|
891
|
+
segmentMaxY + margin < blocker.minY ||
|
|
892
|
+
segmentMinY - margin > blocker.maxY
|
|
893
|
+
) {
|
|
894
|
+
continue
|
|
895
|
+
}
|
|
874
896
|
if (
|
|
875
897
|
distanceSegmentToSegment(
|
|
876
898
|
segment.start,
|
|
@@ -878,7 +900,7 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
878
900
|
blocker.segment.start,
|
|
879
901
|
blocker.segment.end,
|
|
880
902
|
) <
|
|
881
|
-
|
|
903
|
+
margin - EPSILON
|
|
882
904
|
) {
|
|
883
905
|
return false
|
|
884
906
|
}
|
|
@@ -887,13 +909,13 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
887
909
|
if (sharesNet(connectionName, blocker.connectionName)) continue
|
|
888
910
|
const margin = (segment.width + blocker.segment.width) / 2 + clearance
|
|
889
911
|
if (
|
|
890
|
-
|
|
912
|
+
segmentMaxX + margin <
|
|
891
913
|
Math.min(blocker.segment.start.x, blocker.segment.end.x) ||
|
|
892
|
-
|
|
914
|
+
segmentMinX - margin >
|
|
893
915
|
Math.max(blocker.segment.start.x, blocker.segment.end.x) ||
|
|
894
|
-
|
|
916
|
+
segmentMaxY + margin <
|
|
895
917
|
Math.min(blocker.segment.start.y, blocker.segment.end.y) ||
|
|
896
|
-
|
|
918
|
+
segmentMinY - margin >
|
|
897
919
|
Math.max(blocker.segment.start.y, blocker.segment.end.y)
|
|
898
920
|
)
|
|
899
921
|
continue
|
|
@@ -917,10 +939,6 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
917
939
|
)
|
|
918
940
|
return false
|
|
919
941
|
}
|
|
920
|
-
const segmentMinX = Math.min(segment.start.x, segment.end.x)
|
|
921
|
-
const segmentMaxX = Math.max(segment.start.x, segment.end.x)
|
|
922
|
-
const segmentMinY = Math.min(segment.start.y, segment.end.y)
|
|
923
|
-
const segmentMaxY = Math.max(segment.start.y, segment.end.y)
|
|
924
942
|
for (
|
|
925
943
|
let viaIndex = getFirstViaAtOrAfterX(
|
|
926
944
|
segmentMinX - maximumViaToTraceDistance,
|
|
@@ -1151,18 +1169,33 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1151
1169
|
)
|
|
1152
1170
|
const previous = new Int32Array(stateCount).fill(-1)
|
|
1153
1171
|
const heap = new MinHeap()
|
|
1154
|
-
|
|
1172
|
+
// A node is revisited with different incoming directions. Its distance
|
|
1173
|
+
// estimate and lane penalty stay fixed throughout this terminal search.
|
|
1174
|
+
const remainingDistances = new Float64Array(nodeCount)
|
|
1175
|
+
const lanePenalties = new Float64Array(nodeCount)
|
|
1176
|
+
const targetTrack = getPerpendicularAxis(
|
|
1177
|
+
terminal.exitPoint,
|
|
1178
|
+
boundaryDirection,
|
|
1179
|
+
)
|
|
1180
|
+
for (let nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) {
|
|
1181
|
+
const point = nodes[nodeIndex]!.point
|
|
1155
1182
|
const deltaX = Math.abs(point.x - terminal.exitPoint.x)
|
|
1156
1183
|
const deltaY = Math.abs(point.y - terminal.exitPoint.y)
|
|
1157
|
-
|
|
1184
|
+
remainingDistances[nodeIndex] =
|
|
1158
1185
|
Math.max(deltaX, deltaY) + (Math.SQRT2 - 1) * Math.min(deltaX, deltaY)
|
|
1159
|
-
)
|
|
1186
|
+
const nextTrack = getPerpendicularAxis(point, boundaryDirection)
|
|
1187
|
+
lanePenalties[nodeIndex] =
|
|
1188
|
+
laneBias === 0
|
|
1189
|
+
? 0
|
|
1190
|
+
: laneBias > 0
|
|
1191
|
+
? Math.max(0, targetTrack - nextTrack) * 0.2
|
|
1192
|
+
: Math.max(0, nextTrack - targetTrack) * 0.2
|
|
1160
1193
|
}
|
|
1161
1194
|
for (const start of starts) {
|
|
1162
1195
|
const state = start.nodeIndex * 9 + 8
|
|
1163
1196
|
if (start.length >= distances[state]!) continue
|
|
1164
1197
|
distances[state] = start.length
|
|
1165
|
-
const remaining =
|
|
1198
|
+
const remaining = remainingDistances[start.nodeIndex]!
|
|
1166
1199
|
heap.push({
|
|
1167
1200
|
node: start.nodeIndex,
|
|
1168
1201
|
direction: 8,
|
|
@@ -1179,6 +1212,16 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1179
1212
|
[0, -1],
|
|
1180
1213
|
[1, -1],
|
|
1181
1214
|
] as const
|
|
1215
|
+
// Preserve the original ascending neighbor order, but do not reconsider
|
|
1216
|
+
// the five disallowed turns every time a directed state is expanded.
|
|
1217
|
+
const nextDirectionsByIncoming = Array.from({ length: 9 }, (_, incoming) =>
|
|
1218
|
+
directions.flatMap((_, directionIndex) => {
|
|
1219
|
+
const delta = Math.abs(incoming - directionIndex)
|
|
1220
|
+
return incoming === 8 || Math.min(delta, 8 - delta) <= 1
|
|
1221
|
+
? [directionIndex]
|
|
1222
|
+
: []
|
|
1223
|
+
}),
|
|
1224
|
+
)
|
|
1182
1225
|
const startsByNode = new Map<number, ConnectorCandidate[]>()
|
|
1183
1226
|
for (const start of starts) {
|
|
1184
1227
|
const values = startsByNode.get(start.nodeIndex) ?? []
|
|
@@ -1198,7 +1241,7 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1198
1241
|
const currentDistance = distances[state]!
|
|
1199
1242
|
if (
|
|
1200
1243
|
current.score >
|
|
1201
|
-
currentDistance +
|
|
1244
|
+
currentDistance + remainingDistances[current.node]! + EPSILON
|
|
1202
1245
|
)
|
|
1203
1246
|
continue
|
|
1204
1247
|
expandedStateCount++
|
|
@@ -1257,17 +1300,9 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1257
1300
|
}
|
|
1258
1301
|
}
|
|
1259
1302
|
const node = nodes[current.node]!
|
|
1260
|
-
for (
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
directionIndex++
|
|
1264
|
-
) {
|
|
1265
|
-
if (current.direction !== 8) {
|
|
1266
|
-
const rawDirectionDelta = Math.abs(current.direction - directionIndex)
|
|
1267
|
-
if (Math.min(rawDirectionDelta, 8 - rawDirectionDelta) > 1) {
|
|
1268
|
-
continue
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1303
|
+
for (const directionIndex of nextDirectionsByIncoming[
|
|
1304
|
+
current.direction
|
|
1305
|
+
]!) {
|
|
1271
1306
|
const [deltaColumn, deltaRow] = directions[directionIndex]!
|
|
1272
1307
|
const column = node.column + deltaColumn
|
|
1273
1308
|
const row = node.row + deltaRow
|
|
@@ -1278,17 +1313,7 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1278
1313
|
const nextPoint = nodes[nextNode]!.point
|
|
1279
1314
|
const addsTurn =
|
|
1280
1315
|
current.direction !== 8 && current.direction !== directionIndex
|
|
1281
|
-
const
|
|
1282
|
-
const targetTrack = getPerpendicularAxis(
|
|
1283
|
-
terminal.exitPoint,
|
|
1284
|
-
boundaryDirection,
|
|
1285
|
-
)
|
|
1286
|
-
const lanePenalty =
|
|
1287
|
-
laneBias === 0
|
|
1288
|
-
? 0
|
|
1289
|
-
: laneBias > 0
|
|
1290
|
-
? Math.max(0, targetTrack - nextTrack) * 0.2
|
|
1291
|
-
: Math.max(0, nextTrack - targetTrack) * 0.2
|
|
1316
|
+
const lanePenalty = lanePenalties[nextNode]!
|
|
1292
1317
|
const nextDistance =
|
|
1293
1318
|
currentDistance +
|
|
1294
1319
|
(deltaColumn !== 0 && deltaRow !== 0
|
|
@@ -1319,7 +1344,7 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1319
1344
|
if (edgeClearance[edgeIndex] === 2) continue
|
|
1320
1345
|
distances[nextState] = nextDistance
|
|
1321
1346
|
previous[nextState] = state
|
|
1322
|
-
const remaining =
|
|
1347
|
+
const remaining = remainingDistances[nextNode]!
|
|
1323
1348
|
heap.push({
|
|
1324
1349
|
node: nextNode,
|
|
1325
1350
|
direction: directionIndex,
|
|
@@ -1488,6 +1513,14 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1488
1513
|
|
|
1489
1514
|
const alternatives: FanoutRoutePlan[][] = []
|
|
1490
1515
|
const seenAlternativeKeys = new Set<string>()
|
|
1516
|
+
// Different complete route orders can share the same unsuccessful prefix.
|
|
1517
|
+
// Static obstacles/vias, grid and search limits belong to this invocation;
|
|
1518
|
+
// the terminal, lane bias and already accepted copper identify the rest.
|
|
1519
|
+
// A reused failure still emits its normal completion progress below.
|
|
1520
|
+
const failedSearches = new Map<
|
|
1521
|
+
string,
|
|
1522
|
+
{ expandedStateCount: number; searchBatch: number }
|
|
1523
|
+
>()
|
|
1491
1524
|
let routeOrderAttemptCount = 0
|
|
1492
1525
|
for (const routeOrder of adaptiveRouteOrders()) {
|
|
1493
1526
|
for (const laneBias of laneBiases) {
|
|
@@ -1507,13 +1540,36 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1507
1540
|
terminalIndex++
|
|
1508
1541
|
) {
|
|
1509
1542
|
const terminal = routeOrder[terminalIndex]!
|
|
1543
|
+
const failedSearchKey = JSON.stringify([
|
|
1544
|
+
terminals.indexOf(terminal),
|
|
1545
|
+
laneBias,
|
|
1546
|
+
acceptedAttemptSegments.map(({ connectionName, segment }) => [
|
|
1547
|
+
connectionName,
|
|
1548
|
+
segment.start.x,
|
|
1549
|
+
segment.start.y,
|
|
1550
|
+
segment.end.x,
|
|
1551
|
+
segment.end.y,
|
|
1552
|
+
segment.width,
|
|
1553
|
+
segment.layer,
|
|
1554
|
+
]),
|
|
1555
|
+
])
|
|
1556
|
+
const cachedFailure = failedSearches.get(failedSearchKey)
|
|
1510
1557
|
const connectionSteps = routeOneSteps({
|
|
1511
1558
|
terminal,
|
|
1512
1559
|
acceptedAttemptSegments,
|
|
1513
1560
|
laneBias,
|
|
1514
1561
|
})
|
|
1515
|
-
let connectionResult
|
|
1516
|
-
|
|
1562
|
+
let connectionResult: ReturnType<typeof connectionSteps.next> =
|
|
1563
|
+
cachedFailure === undefined
|
|
1564
|
+
? connectionSteps.next()
|
|
1565
|
+
: {
|
|
1566
|
+
done: true,
|
|
1567
|
+
value: {
|
|
1568
|
+
points: null,
|
|
1569
|
+
expandedStateCount: cachedFailure.expandedStateCount,
|
|
1570
|
+
},
|
|
1571
|
+
}
|
|
1572
|
+
let searchBatch = cachedFailure?.searchBatch ?? 0
|
|
1517
1573
|
let expandedStateCount = 0
|
|
1518
1574
|
while (!connectionResult.done) {
|
|
1519
1575
|
expandedStateCount = connectionResult.value.expandedStateCount
|
|
@@ -1560,6 +1616,10 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1560
1616
|
: {}),
|
|
1561
1617
|
}
|
|
1562
1618
|
if (!points) {
|
|
1619
|
+
failedSearches.set(failedSearchKey, {
|
|
1620
|
+
expandedStateCount: finalExpandedStateCount,
|
|
1621
|
+
searchBatch,
|
|
1622
|
+
})
|
|
1563
1623
|
if (
|
|
1564
1624
|
adaptiveRouteOrder &&
|
|
1565
1625
|
maximumRouteOrderAttempts !== undefined &&
|