@tscircuit/fanout-solver 0.0.57 → 0.0.59
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/README.md +5 -0
- package/lib/fanout-solver.ts +84 -8
- package/lib/match-bus-lengths.ts +123 -1
- package/lib/route-bus.ts +53 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -330,6 +330,11 @@ the solver commit, dataset revision, configuration, solve totals, every sample's
|
|
|
330
330
|
status and timing, and partial routing/validation counts. Reports are saved after
|
|
331
331
|
every completed sample, including the total selected count to identify incomplete runs.
|
|
332
332
|
Timed-out workers do not retain their in-flight routing counts.
|
|
333
|
+
Every solved case also writes `benchmark-results/<sample-id>.svg`. These SVGs
|
|
334
|
+
are committed so route changes can be reviewed in Git. A run replaces the selected
|
|
335
|
+
cases' snapshots and removes their stale SVGs if they no longer solve; filtered
|
|
336
|
+
runs preserve unselected snapshots. JSON reports and captured inputs remain
|
|
337
|
+
ignored. CI includes the SVGs in its benchmark artifacts.
|
|
333
338
|
Compare reports with the same budgets to track progress. Solved means validated
|
|
334
339
|
AM62L fanout, not RAM fanout or downstream inter-chip routing. Partial, error,
|
|
335
340
|
and timeout rows are benchmark results (exit 0); invalid CLI arguments or report
|
package/lib/fanout-solver.ts
CHANGED
|
@@ -1397,9 +1397,14 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1397
1397
|
denseRoutingStrategy?: "pad-aligned" | "boundary-aligned"
|
|
1398
1398
|
lengthMatchingStage?: "before-planes" | "after-planes"
|
|
1399
1399
|
promotedPlaneReservationBusIds?: readonly string[]
|
|
1400
|
+
preferredBoundaryViaPoints?: ReadonlyMap<number, { x: number; y: number }>
|
|
1400
1401
|
planeReservationRetryCount?: number
|
|
1401
1402
|
}): Generator<FanoutWorkYield, MixedTerminationState | null, unknown> {
|
|
1402
1403
|
if (this.config.allowBlindAndBuriedVias) return null
|
|
1404
|
+
// An outside-package singleton escape can depend on the completed signal
|
|
1405
|
+
// field. Keep those sites as preferences when plane reservations change;
|
|
1406
|
+
// actual conflicts may still move.
|
|
1407
|
+
let boundaryViaPointsForRetry = params.preferredBoundaryViaPoints
|
|
1403
1408
|
const usePadAlignedDenseRouting =
|
|
1404
1409
|
params.denseRoutingStrategy !== "boundary-aligned"
|
|
1405
1410
|
const debugDense = (...values: unknown[]) => {
|
|
@@ -1444,9 +1449,12 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1444
1449
|
)
|
|
1445
1450
|
const useConfiguredDensePlaneRouting =
|
|
1446
1451
|
configuredDensePlaneRouting || useAdaptiveDensePlaneRouting
|
|
1452
|
+
// A completed boundary assignment remains worth repairing jointly after
|
|
1453
|
+
// plane reservations change; a greedy refill can discard that assignment.
|
|
1447
1454
|
const useAdaptiveJointPlaneSelection =
|
|
1448
1455
|
useAdaptiveDensePlaneRouting &&
|
|
1449
|
-
(params.planeReservationRetryCount ?? 0) === 0
|
|
1456
|
+
((params.planeReservationRetryCount ?? 0) === 0 ||
|
|
1457
|
+
Boolean(params.preferredBoundaryViaPoints))
|
|
1450
1458
|
const matchLengthsAfterPlanes =
|
|
1451
1459
|
useConfiguredDensePlaneRouting &&
|
|
1452
1460
|
params.lengthMatchingStage !== "before-planes"
|
|
@@ -1871,19 +1879,45 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1871
1879
|
)
|
|
1872
1880
|
})
|
|
1873
1881
|
: []
|
|
1882
|
+
// A corner singleton on the outward source row must leave before its
|
|
1883
|
+
// surrounding wide bus closes the shared target-layer corridor.
|
|
1874
1884
|
const throughAllLeadingSingletonBuses = hasThreeWideBoundaryBuses
|
|
1875
1885
|
? singletonBoundaryBuses.filter((singletonBus) => {
|
|
1876
1886
|
const containingWideBus = getContainingWideSourceField(singletonBus)
|
|
1877
1887
|
const singletonTargetLayer =
|
|
1878
1888
|
params.busLayerAssignments[singletonBus.busId]
|
|
1889
|
+
if (!containingWideBus || !singletonTargetLayer) return false
|
|
1879
1890
|
const containingWideLayers =
|
|
1880
|
-
containingWideBus
|
|
1881
|
-
containingWideBus
|
|
1891
|
+
containingWideBus.routableEscapeLayers ??
|
|
1892
|
+
containingWideBus.allowedLayers ??
|
|
1882
1893
|
[]
|
|
1894
|
+
const sourceAxis =
|
|
1895
|
+
singletonBus.exitEdge === "left" ||
|
|
1896
|
+
singletonBus.exitEdge === "right"
|
|
1897
|
+
? "x"
|
|
1898
|
+
: "y"
|
|
1899
|
+
const wideCoordinates = containingWideBus.connections.map(
|
|
1900
|
+
(connection) => connection.sourcePoint[sourceAxis],
|
|
1901
|
+
)
|
|
1902
|
+
const outwardSourceCoordinate =
|
|
1903
|
+
singletonBus.exitEdge === "left" ||
|
|
1904
|
+
singletonBus.exitEdge === "bottom"
|
|
1905
|
+
? Math.min(...wideCoordinates)
|
|
1906
|
+
: Math.max(...wideCoordinates)
|
|
1907
|
+
const isOnOutwardSourceEdge = singletonBus.connections.every(
|
|
1908
|
+
(connection) =>
|
|
1909
|
+
Math.abs(
|
|
1910
|
+
connection.sourcePoint[sourceAxis] - outwardSourceCoordinate,
|
|
1911
|
+
) < 1e-9,
|
|
1912
|
+
)
|
|
1883
1913
|
return Boolean(
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1914
|
+
!containingWideLayers.includes(singletonTargetLayer) ||
|
|
1915
|
+
(useAdaptiveDensePlaneRouting &&
|
|
1916
|
+
isOnOutwardSourceEdge &&
|
|
1917
|
+
getCornerBandSide(
|
|
1918
|
+
singletonBus.exitEdge,
|
|
1919
|
+
singletonBus.preferredExit,
|
|
1920
|
+
)),
|
|
1887
1921
|
)
|
|
1888
1922
|
})
|
|
1889
1923
|
: []
|
|
@@ -1994,6 +2028,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1994
2028
|
traceWidth: this.config.traceWidth,
|
|
1995
2029
|
clearance: this.config.clearance,
|
|
1996
2030
|
maximumSearchStates: 100_000,
|
|
2031
|
+
preferredViaPointsByConnectionIndex:
|
|
2032
|
+
params.preferredBoundaryViaPoints,
|
|
1997
2033
|
preferredBoundaryPerpendicularSideByBusId,
|
|
1998
2034
|
preferBoundaryOutwardByBusId,
|
|
1999
2035
|
additionalObstacles: denseAdditionalObstacles,
|
|
@@ -2012,6 +2048,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2012
2048
|
traceWidth: this.config.traceWidth,
|
|
2013
2049
|
clearance: this.config.clearance,
|
|
2014
2050
|
maximumSearchStates: 20_000,
|
|
2051
|
+
preferredViaPointsByConnectionIndex:
|
|
2052
|
+
params.preferredBoundaryViaPoints,
|
|
2015
2053
|
preferredBoundaryPerpendicularSideByBusId,
|
|
2016
2054
|
preferBoundaryOutwardByBusId,
|
|
2017
2055
|
additionalObstacles: denseAdditionalObstacles,
|
|
@@ -2070,6 +2108,8 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2070
2108
|
traceWidth: this.config.traceWidth,
|
|
2071
2109
|
clearance: this.config.clearance,
|
|
2072
2110
|
maximumSearchStates: 100_000,
|
|
2111
|
+
preferredViaPointsByConnectionIndex:
|
|
2112
|
+
params.preferredBoundaryViaPoints,
|
|
2073
2113
|
preferredBoundaryPerpendicularSideByBusId,
|
|
2074
2114
|
preferBoundaryOutwardByBusId,
|
|
2075
2115
|
fixedViaPointsByConnectionIndex: seedViaPoints,
|
|
@@ -2221,8 +2261,10 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2221
2261
|
allowBoundarySideViaFallback: bus.connections.length === 1,
|
|
2222
2262
|
preferCornerBoundaryVia: useConfiguredDensePlaneRouting,
|
|
2223
2263
|
adaptiveWindingRouteOrder,
|
|
2264
|
+
// Retain pad-aligned channels even when plane sites are reserved
|
|
2265
|
+
// adaptively; the boundary grid can fence off a turning wide bus.
|
|
2224
2266
|
alignWindingGridToPads:
|
|
2225
|
-
usePadAlignedDenseRouting && !
|
|
2267
|
+
usePadAlignedDenseRouting && !configuredDensePlaneRouting,
|
|
2226
2268
|
fixedViaFallbackRouteOrderAttempts: adaptiveWindingRouteOrder
|
|
2227
2269
|
? 60
|
|
2228
2270
|
: useConfiguredDensePlaneRouting
|
|
@@ -2407,6 +2449,31 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2407
2449
|
return false
|
|
2408
2450
|
}
|
|
2409
2451
|
matchedPlans.push(...busPlans)
|
|
2452
|
+
if (
|
|
2453
|
+
matchedPlans.length ===
|
|
2454
|
+
boundaryBuses.reduce(
|
|
2455
|
+
(sum, candidate) => sum + candidate.connections.length,
|
|
2456
|
+
0,
|
|
2457
|
+
) &&
|
|
2458
|
+
matchedPlans.some((plan) => {
|
|
2459
|
+
const bus = boundaryBuses.find(
|
|
2460
|
+
(candidate) => candidate.busId === plan.busId,
|
|
2461
|
+
)
|
|
2462
|
+
if (bus?.connections.length !== 1 || !plan.via) return false
|
|
2463
|
+
const { center } = plan.via
|
|
2464
|
+
return (
|
|
2465
|
+
center.x < bus.componentBounds.minX ||
|
|
2466
|
+
center.x > bus.componentBounds.maxX ||
|
|
2467
|
+
center.y < bus.componentBounds.minY ||
|
|
2468
|
+
center.y > bus.componentBounds.maxY
|
|
2469
|
+
)
|
|
2470
|
+
})
|
|
2471
|
+
)
|
|
2472
|
+
boundaryViaPointsForRetry = new Map(
|
|
2473
|
+
matchedPlans
|
|
2474
|
+
.filter((plan) => plan.via)
|
|
2475
|
+
.map((plan) => [plan.connectionIndex, plan.via!.center]),
|
|
2476
|
+
)
|
|
2410
2477
|
debugDense("route:complete", bus.busId, busPlans.length)
|
|
2411
2478
|
return true
|
|
2412
2479
|
}.bind(this)
|
|
@@ -2932,7 +2999,14 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2932
2999
|
let alternatePlaneSearchStates = 0
|
|
2933
3000
|
const maximumAlternatePlaneSearchStates = Number(
|
|
2934
3001
|
process.env.FANOUT_DEBUG_ALTERNATE_SEARCH_STATES ??
|
|
2935
|
-
|
|
3002
|
+
// Keep the initial search short before reserving blocked
|
|
3003
|
+
// plane sites. Later joint repairs retain the full budget.
|
|
3004
|
+
(useAdaptiveDensePlaneRouting &&
|
|
3005
|
+
(params.planeReservationRetryCount ?? 0) === 0
|
|
3006
|
+
? 10_000
|
|
3007
|
+
: useConfiguredDensePlaneRouting
|
|
3008
|
+
? 3_000_000
|
|
3009
|
+
: 1_000),
|
|
2936
3010
|
)
|
|
2937
3011
|
const maximumAlternatePlaneRoutes = Number(
|
|
2938
3012
|
process.env.FANOUT_DEBUG_ALTERNATE_ROUTE_COUNT ??
|
|
@@ -3838,6 +3912,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3838
3912
|
allowSameNetMerges: this.config.allowSameNetMerges,
|
|
3839
3913
|
allowMatchingInsideDenseBounds: true,
|
|
3840
3914
|
allowPairLaneSpreading: true,
|
|
3915
|
+
allowUnconstrainedLaneRerouting: true,
|
|
3841
3916
|
}
|
|
3842
3917
|
let matchedLengthResult = matchBusPlanLengths(lengthMatchingParams)
|
|
3843
3918
|
const shortenedBusIds = new Set<string>()
|
|
@@ -3927,6 +4002,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
3927
4002
|
debugDense("promote-reservations", refinedPromotions)
|
|
3928
4003
|
return yield* this.routeDenseThroughAllMixedTerminationSteps({
|
|
3929
4004
|
...params,
|
|
4005
|
+
preferredBoundaryViaPoints: boundaryViaPointsForRetry,
|
|
3930
4006
|
promotedPlaneReservationBusIds: [
|
|
3931
4007
|
...(params.promotedPlaneReservationBusIds ?? []),
|
|
3932
4008
|
...refinedPromotions,
|
package/lib/match-bus-lengths.ts
CHANGED
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
distanceSegmentToSegment,
|
|
9
9
|
segmentsAreClear,
|
|
10
10
|
} from "./geometry"
|
|
11
|
-
import {
|
|
11
|
+
import { getCopperLayerNames } from "./layer-names"
|
|
12
|
+
import { fanoutPlansAreClear, fanoutPlansAreMutuallyClear } from "./route-bus"
|
|
13
|
+
import { routeViaMinimalWinding } from "./route-via-minimal-winding"
|
|
12
14
|
import type {
|
|
13
15
|
Bounds,
|
|
14
16
|
FanoutRoutePlan,
|
|
@@ -647,6 +649,8 @@ export function matchBusPlanLengths(params: {
|
|
|
647
649
|
allowMatchingInsideDenseBounds?: boolean
|
|
648
650
|
/** Allow a differential pair's longer lane to move aside before tuning its mate. */
|
|
649
651
|
allowPairLaneSpreading?: boolean
|
|
652
|
+
/** Allow one unconstrained boundary lane to move around a tuning meander. */
|
|
653
|
+
allowUnconstrainedLaneRerouting?: boolean
|
|
650
654
|
/**
|
|
651
655
|
* Rejects a geometrically clear candidate when it would make a caller-owned
|
|
652
656
|
* downstream assignment (such as pending plane dogbones) infeasible.
|
|
@@ -866,6 +870,124 @@ export function matchBusPlanLengths(params: {
|
|
|
866
870
|
if (++attempts >= 4) break
|
|
867
871
|
}
|
|
868
872
|
}
|
|
873
|
+
if (!acceptedPlans && params.allowUnconstrainedLaneRerouting) {
|
|
874
|
+
// A neighboring singleton may occupy the only tuning window. Keep its
|
|
875
|
+
// source dogbone, via and boundary endpoint fixed, and reroute only its
|
|
876
|
+
// target-layer copper around a complete meander. Constrained buses and
|
|
877
|
+
// plane routes are never displaced by this bounded repair.
|
|
878
|
+
let rerouteAttempts = 0
|
|
879
|
+
candidateSearch: for (const targetAddedLength of targetAddedLengths) {
|
|
880
|
+
const candidates = createTunedPlanCandidates({
|
|
881
|
+
plan: shortest,
|
|
882
|
+
bus,
|
|
883
|
+
targetAddedLength,
|
|
884
|
+
clearance,
|
|
885
|
+
sharedBoundary: bus.sharedBoundary,
|
|
886
|
+
allowInsideDenseBounds: allowMatchingInsideDenseBounds,
|
|
887
|
+
})
|
|
888
|
+
for (const candidate of candidates) {
|
|
889
|
+
if (
|
|
890
|
+
!fanoutPlansAreClear({
|
|
891
|
+
plans: [candidate],
|
|
892
|
+
srj: inputSrj,
|
|
893
|
+
sharedBoundary,
|
|
894
|
+
clearance,
|
|
895
|
+
allowBlindAndBuriedVias,
|
|
896
|
+
allowSameNetMerges,
|
|
897
|
+
})
|
|
898
|
+
)
|
|
899
|
+
continue
|
|
900
|
+
const blockers = matchedPlans.filter(
|
|
901
|
+
(plan) =>
|
|
902
|
+
plan !== shortest &&
|
|
903
|
+
!fanoutPlansAreMutuallyClear({
|
|
904
|
+
plans: [candidate, plan],
|
|
905
|
+
srj: inputSrj,
|
|
906
|
+
clearance,
|
|
907
|
+
allowSameNetMerges,
|
|
908
|
+
}),
|
|
909
|
+
)
|
|
910
|
+
if (blockers.length !== 1) continue
|
|
911
|
+
const blocker = blockers[0]!
|
|
912
|
+
const blockerBus = preparedBuses.find(
|
|
913
|
+
(prepared) => prepared.busId === blocker.busId,
|
|
914
|
+
)
|
|
915
|
+
if (
|
|
916
|
+
!blockerBus ||
|
|
917
|
+
blockerBus.termination.type !== "boundary" ||
|
|
918
|
+
blockerBus.maxLengthSkew !== undefined ||
|
|
919
|
+
blockerBus.connections.length !== 1 ||
|
|
920
|
+
!blocker.via ||
|
|
921
|
+
blocker.additionalVias?.length ||
|
|
922
|
+
blocker.planeEndpointVia ||
|
|
923
|
+
blocker.segments.filter(
|
|
924
|
+
(segment) => segment.layer === blocker.sourceLayer,
|
|
925
|
+
).length !== 1
|
|
926
|
+
)
|
|
927
|
+
continue
|
|
928
|
+
const nextPlans = matchedPlans.map((plan) =>
|
|
929
|
+
plan === shortest ? candidate : plan,
|
|
930
|
+
)
|
|
931
|
+
for (const alignGridToPads of [true, false]) {
|
|
932
|
+
if (rerouteAttempts++ >= 4) break candidateSearch
|
|
933
|
+
const rerouted = routeViaMinimalWinding({
|
|
934
|
+
srj: inputSrj,
|
|
935
|
+
bus: blockerBus,
|
|
936
|
+
terminals: [
|
|
937
|
+
{
|
|
938
|
+
connection: blockerBus.connections[0]!,
|
|
939
|
+
viaPoint: blocker.via.center,
|
|
940
|
+
exitPoint: blocker.exitPoint,
|
|
941
|
+
},
|
|
942
|
+
],
|
|
943
|
+
targetLayer: blocker.targetLayer,
|
|
944
|
+
acceptedPlans: nextPlans.filter((plan) => plan !== blocker),
|
|
945
|
+
layerNames: getCopperLayerNames(inputSrj.layerCount),
|
|
946
|
+
traceWidth: blocker.segments[0]!.width,
|
|
947
|
+
viaDiameter: blocker.via.diameter,
|
|
948
|
+
viaHoleDiameter: blocker.via.holeDiameter,
|
|
949
|
+
clearance,
|
|
950
|
+
allowBlindAndBuriedVias,
|
|
951
|
+
allowSameNetMerges,
|
|
952
|
+
gridStepDivisor: 2,
|
|
953
|
+
alignGridToPads,
|
|
954
|
+
maximumRouteOrderAttempts: 1,
|
|
955
|
+
preferTargetDirectedLaneBias: true,
|
|
956
|
+
})?.[0]
|
|
957
|
+
if (!rerouted) continue
|
|
958
|
+
// Rebuild from the original plan to preserve its route identity,
|
|
959
|
+
// endpoint metadata and physical via span exactly.
|
|
960
|
+
const repaired = createPlanWithSegments(blocker, [
|
|
961
|
+
blocker.segments[0]!,
|
|
962
|
+
...rerouted.segments.slice(1),
|
|
963
|
+
])
|
|
964
|
+
if (!repaired) continue
|
|
965
|
+
const repairedPlans = nextPlans.map((plan) =>
|
|
966
|
+
plan === blocker ? repaired : plan,
|
|
967
|
+
)
|
|
968
|
+
if (
|
|
969
|
+
getBusSkew(
|
|
970
|
+
repairedPlans.filter((plan) => plan.busId === bus.busId),
|
|
971
|
+
) >
|
|
972
|
+
maxLengthSkew + EPSILON ||
|
|
973
|
+
!fanoutPlansAreClear({
|
|
974
|
+
plans: repairedPlans,
|
|
975
|
+
srj: inputSrj,
|
|
976
|
+
sharedBoundary,
|
|
977
|
+
clearance,
|
|
978
|
+
allowBlindAndBuriedVias,
|
|
979
|
+
allowSameNetMerges,
|
|
980
|
+
}) ||
|
|
981
|
+
(candidatePlansAreFeasible &&
|
|
982
|
+
!candidatePlansAreFeasible(repairedPlans))
|
|
983
|
+
)
|
|
984
|
+
continue
|
|
985
|
+
acceptedPlans = repairedPlans
|
|
986
|
+
break candidateSearch
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
869
991
|
if (!acceptedPlans) return { plans: null, failedBus: bus }
|
|
870
992
|
matchedPlans = acceptedPlans
|
|
871
993
|
}
|
package/lib/route-bus.ts
CHANGED
|
@@ -3060,9 +3060,9 @@ export function* routeBusAlternativesSteps(
|
|
|
3060
3060
|
// A through-via does not have to sit next to the source pad. A narrow bus
|
|
3061
3061
|
// embedded in another bus's source field can have every local dogbone site
|
|
3062
3062
|
// occupied while still having a clear source-layer escape. A pair first
|
|
3063
|
-
// relocates its two vias just outside the nearest package edge
|
|
3064
|
-
//
|
|
3065
|
-
// signal without weakening
|
|
3063
|
+
// relocates its two vias just outside the nearest package edge. A singleton
|
|
3064
|
+
// tries the existing boundary-side vias before searching outside the package.
|
|
3065
|
+
// Both forms preserve one via per signal without weakening clearance rules.
|
|
3066
3066
|
if (
|
|
3067
3067
|
alternatives.length === 0 &&
|
|
3068
3068
|
allowBoundarySideViaFallback &&
|
|
@@ -3182,6 +3182,46 @@ export function* routeBusAlternativesSteps(
|
|
|
3182
3182
|
),
|
|
3183
3183
|
)
|
|
3184
3184
|
: []
|
|
3185
|
+
const singletonMatchedVia =
|
|
3186
|
+
bus.connections.length === 1
|
|
3187
|
+
? fixedViaPointsByConnectionIndex.get(
|
|
3188
|
+
bus.connections[0]!.connectionIndex,
|
|
3189
|
+
)
|
|
3190
|
+
: undefined
|
|
3191
|
+
const packageEdgeViaCandidates = singletonMatchedVia
|
|
3192
|
+
? [
|
|
3193
|
+
{
|
|
3194
|
+
distance: sourceCenter.x - bus.componentBounds.minX,
|
|
3195
|
+
point: {
|
|
3196
|
+
x: bus.componentBounds.minX - 2 * insetStep,
|
|
3197
|
+
y: singletonMatchedVia.y,
|
|
3198
|
+
},
|
|
3199
|
+
},
|
|
3200
|
+
{
|
|
3201
|
+
distance: bus.componentBounds.maxX - sourceCenter.x,
|
|
3202
|
+
point: {
|
|
3203
|
+
x: bus.componentBounds.maxX + 2 * insetStep,
|
|
3204
|
+
y: singletonMatchedVia.y,
|
|
3205
|
+
},
|
|
3206
|
+
},
|
|
3207
|
+
{
|
|
3208
|
+
distance: sourceCenter.y - bus.componentBounds.minY,
|
|
3209
|
+
point: {
|
|
3210
|
+
x: singletonMatchedVia.x,
|
|
3211
|
+
y: bus.componentBounds.minY - 2 * insetStep,
|
|
3212
|
+
},
|
|
3213
|
+
},
|
|
3214
|
+
{
|
|
3215
|
+
distance: bus.componentBounds.maxY - sourceCenter.y,
|
|
3216
|
+
point: {
|
|
3217
|
+
x: singletonMatchedVia.x,
|
|
3218
|
+
y: bus.componentBounds.maxY + 2 * insetStep,
|
|
3219
|
+
},
|
|
3220
|
+
},
|
|
3221
|
+
]
|
|
3222
|
+
.toSorted((first, second) => first.distance - second.distance)
|
|
3223
|
+
.map(({ point }) => [point])
|
|
3224
|
+
: []
|
|
3185
3225
|
const viaCandidates = [
|
|
3186
3226
|
...displacedViaCandidates.map((points) => ({
|
|
3187
3227
|
points,
|
|
@@ -3191,6 +3231,13 @@ export function* routeBusAlternativesSteps(
|
|
|
3191
3231
|
points,
|
|
3192
3232
|
boundarySide: true,
|
|
3193
3233
|
})),
|
|
3234
|
+
// Preserve existing singleton escapes before trying a short source-layer
|
|
3235
|
+
// route beyond the package. The target layer can then wind to the exit
|
|
3236
|
+
// without a local via being fenced in by an already-routed wide bus.
|
|
3237
|
+
...packageEdgeViaCandidates.map((points) => ({
|
|
3238
|
+
points,
|
|
3239
|
+
boundarySide: false,
|
|
3240
|
+
})),
|
|
3194
3241
|
]
|
|
3195
3242
|
for (const { points: boundaryViaPoints, boundarySide } of viaCandidates) {
|
|
3196
3243
|
const boundaryVias = bus.connections.map((connection, index) => ({
|
|
@@ -3355,7 +3402,9 @@ export function* routeBusAlternativesSteps(
|
|
|
3355
3402
|
)
|
|
3356
3403
|
return {
|
|
3357
3404
|
...sourceLayerPlan,
|
|
3358
|
-
...(preferCornerBoundaryVia ||
|
|
3405
|
+
...(preferCornerBoundaryVia ||
|
|
3406
|
+
bus.connections.length > 1 ||
|
|
3407
|
+
!boundarySide
|
|
3359
3408
|
? { sourceEscapeSegmentCount: sourceLayerPlan.segments.length }
|
|
3360
3409
|
: {}),
|
|
3361
3410
|
targetLayer,
|