@tscircuit/fanout-solver 0.0.38 → 0.0.40
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 +7 -0
- package/lib/add-via-layer-metadata.ts +48 -0
- package/lib/complete-original-endpoints.ts +30 -2
- package/lib/fanout-solver.ts +905 -14
- package/lib/geometry.ts +21 -0
- package/lib/get-routed-trace-copper.ts +15 -6
- package/lib/index.ts +3 -0
- package/lib/layer-names.ts +40 -0
- package/lib/match-bus-lengths.ts +237 -34
- package/lib/match-component-dogbone-via-sites.ts +655 -0
- package/lib/route-bus.ts +1026 -130
- package/lib/route-via-minimal-winding.ts +423 -76
- package/lib/types.ts +35 -7
- package/lib/validate-fanout-solution.ts +34 -6
- package/lib/validate-routed-copper-drc.ts +47 -8
- package/package.json +1 -1
package/lib/geometry.ts
CHANGED
|
@@ -98,6 +98,27 @@ export function pointIsInsideObstacle(
|
|
|
98
98
|
)
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
export function circleFitsInsideObstacle(params: {
|
|
102
|
+
center: Point2D
|
|
103
|
+
diameter: number
|
|
104
|
+
obstacle: Obstacle
|
|
105
|
+
tolerance?: number
|
|
106
|
+
}): boolean {
|
|
107
|
+
const { center, diameter, obstacle, tolerance = EPSILON } = params
|
|
108
|
+
const radius = diameter / 2
|
|
109
|
+
if (obstacleIsCircular(obstacle)) {
|
|
110
|
+
return (
|
|
111
|
+
distance(center, obstacle.center) + radius <=
|
|
112
|
+
obstacle.width / 2 + tolerance
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
const localCenter = toObstacleLocalPoint(center, obstacle)
|
|
116
|
+
return (
|
|
117
|
+
Math.abs(localCenter.x) + radius <= obstacle.width / 2 + tolerance &&
|
|
118
|
+
Math.abs(localCenter.y) + radius <= obstacle.height / 2 + tolerance
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
101
122
|
export function distancePointToObstacle(
|
|
102
123
|
point: Point2D,
|
|
103
124
|
obstacle: Obstacle,
|
|
@@ -2,7 +2,7 @@ import type {
|
|
|
2
2
|
SimpleRouteJson,
|
|
3
3
|
SimplifiedPcbTrace,
|
|
4
4
|
} from "@tscircuit/capacity-autorouter"
|
|
5
|
-
import { getCopperLayerNames,
|
|
5
|
+
import { getCopperLayerNames, getRouteViaSpanLayers } from "./layer-names"
|
|
6
6
|
import type { Point2D, RoutedSegment } from "./types"
|
|
7
7
|
|
|
8
8
|
export interface RoutedTraceVia {
|
|
@@ -21,6 +21,7 @@ export interface RoutedTraceCopper {
|
|
|
21
21
|
export function getRoutedTraceCopper(
|
|
22
22
|
srj: SimpleRouteJson,
|
|
23
23
|
trace: SimplifiedPcbTrace,
|
|
24
|
+
allowBlindAndBuriedVias = true,
|
|
24
25
|
): RoutedTraceCopper {
|
|
25
26
|
const layerNames = getCopperLayerNames(srj.layerCount)
|
|
26
27
|
const segments: RoutedSegment[] = []
|
|
@@ -39,11 +40,16 @@ export function getRoutedTraceCopper(
|
|
|
39
40
|
srj.min_via_pad_diameter ??
|
|
40
41
|
srj.minViaDiameter ??
|
|
41
42
|
srj.minTraceWidth,
|
|
42
|
-
spanLayers:
|
|
43
|
-
routePoint.from_layer,
|
|
44
|
-
routePoint.to_layer,
|
|
43
|
+
spanLayers: getRouteViaSpanLayers({
|
|
44
|
+
fromLayer: routePoint.from_layer,
|
|
45
|
+
toLayer: routePoint.to_layer,
|
|
46
|
+
layers:
|
|
47
|
+
"layers" in routePoint && Array.isArray(routePoint.layers)
|
|
48
|
+
? (routePoint.layers as string[])
|
|
49
|
+
: undefined,
|
|
45
50
|
layerNames,
|
|
46
|
-
|
|
51
|
+
allowBlindAndBuriedVias,
|
|
52
|
+
}),
|
|
47
53
|
})
|
|
48
54
|
previousWire = undefined
|
|
49
55
|
continue
|
|
@@ -78,6 +84,9 @@ export function getRoutedTraceCopper(
|
|
|
78
84
|
|
|
79
85
|
export function getAllRoutedTraceCopper(
|
|
80
86
|
srj: SimpleRouteJson,
|
|
87
|
+
allowBlindAndBuriedVias = true,
|
|
81
88
|
): RoutedTraceCopper[] {
|
|
82
|
-
return (srj.traces ?? []).map((trace) =>
|
|
89
|
+
return (srj.traces ?? []).map((trace) =>
|
|
90
|
+
getRoutedTraceCopper(srj, trace, allowBlindAndBuriedVias),
|
|
91
|
+
)
|
|
83
92
|
}
|
package/lib/index.ts
CHANGED
|
@@ -24,10 +24,13 @@ export type {
|
|
|
24
24
|
FanoutPlaneConnectivity,
|
|
25
25
|
FanoutPlaneTermination,
|
|
26
26
|
FanoutRoutePlan,
|
|
27
|
+
FanoutRoutePoint,
|
|
28
|
+
FanoutSimplifiedPcbTrace,
|
|
27
29
|
FanoutSolverOptions,
|
|
28
30
|
FanoutSolverOutput,
|
|
29
31
|
FanoutValidationIssue,
|
|
30
32
|
FanoutValidationReport,
|
|
33
|
+
FanoutViaRoutePoint,
|
|
31
34
|
Point2D,
|
|
32
35
|
PreparedBus,
|
|
33
36
|
SimpleRouteJsonWithFanoutPlanes,
|
package/lib/layer-names.ts
CHANGED
|
@@ -34,6 +34,46 @@ export function getLayerSpan(
|
|
|
34
34
|
return layerNames.slice(firstIndex, lastIndex + 1)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export function getViaSpanLayers(params: {
|
|
38
|
+
fromLayer: string
|
|
39
|
+
toLayer: string
|
|
40
|
+
layerNames: string[]
|
|
41
|
+
allowBlindAndBuriedVias: boolean
|
|
42
|
+
}): string[] {
|
|
43
|
+
const { fromLayer, toLayer, layerNames, allowBlindAndBuriedVias } = params
|
|
44
|
+
const logicalSpan = getLayerSpan(fromLayer, toLayer, layerNames)
|
|
45
|
+
return allowBlindAndBuriedVias ? logicalSpan : [...layerNames]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getRouteViaSpanLayers(params: {
|
|
49
|
+
fromLayer: string
|
|
50
|
+
toLayer: string
|
|
51
|
+
layers?: readonly string[]
|
|
52
|
+
layerNames: string[]
|
|
53
|
+
allowBlindAndBuriedVias: boolean
|
|
54
|
+
}): string[] {
|
|
55
|
+
const { fromLayer, toLayer, layers, layerNames, allowBlindAndBuriedVias } =
|
|
56
|
+
params
|
|
57
|
+
const configuredSpan = getViaSpanLayers({
|
|
58
|
+
fromLayer,
|
|
59
|
+
toLayer,
|
|
60
|
+
layerNames,
|
|
61
|
+
allowBlindAndBuriedVias,
|
|
62
|
+
})
|
|
63
|
+
if (!layers) return configuredSpan
|
|
64
|
+
if (
|
|
65
|
+
layers.length === 0 ||
|
|
66
|
+
!layers.includes(fromLayer) ||
|
|
67
|
+
!layers.includes(toLayer) ||
|
|
68
|
+
layers.some((layer) => !layerNames.includes(layer))
|
|
69
|
+
) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`FanoutSolver: route via from "${fromLayer}" to "${toLayer}" has an invalid physical layer span`,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
return [...layers]
|
|
75
|
+
}
|
|
76
|
+
|
|
37
77
|
export function generateLayerAssignments(params: {
|
|
38
78
|
busIds: string[]
|
|
39
79
|
layers: string[]
|
package/lib/match-bus-lengths.ts
CHANGED
|
@@ -42,21 +42,46 @@ function rebuildTraceRoute(
|
|
|
42
42
|
const lastOriginalWire = plan.trace.route.findLast(
|
|
43
43
|
(point) => point.route_type === "wire",
|
|
44
44
|
)
|
|
45
|
+
const getWireMetadata = (
|
|
46
|
+
wire: typeof firstOriginalWire,
|
|
47
|
+
): Partial<
|
|
48
|
+
Extract<SimplifiedPcbTrace["route"][number], { route_type: "wire" }>
|
|
49
|
+
> => {
|
|
50
|
+
if (wire?.route_type !== "wire") return {}
|
|
51
|
+
const metadata: Partial<
|
|
52
|
+
Extract<SimplifiedPcbTrace["route"][number], { route_type: "wire" }>
|
|
53
|
+
> = { ...wire }
|
|
54
|
+
delete metadata.route_type
|
|
55
|
+
delete metadata.x
|
|
56
|
+
delete metadata.y
|
|
57
|
+
delete metadata.width
|
|
58
|
+
delete metadata.layer
|
|
59
|
+
return metadata
|
|
60
|
+
}
|
|
61
|
+
const vias = getPlanVias(plan)
|
|
62
|
+
const startsWithSourceVia =
|
|
63
|
+
pointsMatch(firstSegment.start, plan.sourcePoint) &&
|
|
64
|
+
firstSegment.layer !== plan.sourceLayer &&
|
|
65
|
+
vias.some(
|
|
66
|
+
(via) =>
|
|
67
|
+
pointsMatch(via.center, firstSegment.start) &&
|
|
68
|
+
via.spanLayers.includes(plan.sourceLayer) &&
|
|
69
|
+
via.spanLayers.includes(firstSegment.layer),
|
|
70
|
+
)
|
|
71
|
+
const initialLayer = startsWithSourceVia
|
|
72
|
+
? plan.sourceLayer
|
|
73
|
+
: firstSegment.layer
|
|
45
74
|
const route: SimplifiedPcbTrace["route"] = [
|
|
46
75
|
{
|
|
76
|
+
...getWireMetadata(firstOriginalWire),
|
|
47
77
|
route_type: "wire",
|
|
48
78
|
...firstSegment.start,
|
|
49
79
|
width: firstSegment.width,
|
|
50
|
-
layer:
|
|
51
|
-
...(firstOriginalWire?.route_type === "wire" &&
|
|
52
|
-
firstOriginalWire.start_pcb_port_id
|
|
53
|
-
? { start_pcb_port_id: firstOriginalWire.start_pcb_port_id }
|
|
54
|
-
: {}),
|
|
80
|
+
layer: initialLayer,
|
|
55
81
|
},
|
|
56
82
|
]
|
|
57
83
|
let currentPoint = firstSegment.start
|
|
58
|
-
let currentLayer =
|
|
59
|
-
const vias = getPlanVias(plan)
|
|
84
|
+
let currentLayer = initialLayer
|
|
60
85
|
|
|
61
86
|
for (const [segmentIndex, segment] of segments.entries()) {
|
|
62
87
|
if (!pointsMatch(currentPoint, segment.start)) return null
|
|
@@ -85,15 +110,13 @@ function rebuildTraceRoute(
|
|
|
85
110
|
currentLayer = segment.layer
|
|
86
111
|
}
|
|
87
112
|
route.push({
|
|
113
|
+
...(segmentIndex === segments.length - 1
|
|
114
|
+
? getWireMetadata(lastOriginalWire)
|
|
115
|
+
: {}),
|
|
88
116
|
route_type: "wire",
|
|
89
117
|
...segment.end,
|
|
90
118
|
width: segment.width,
|
|
91
119
|
layer: segment.layer,
|
|
92
|
-
...(segmentIndex === segments.length - 1 &&
|
|
93
|
-
lastOriginalWire?.route_type === "wire" &&
|
|
94
|
-
lastOriginalWire.end_pcb_port_id
|
|
95
|
-
? { end_pcb_port_id: lastOriginalWire.end_pcb_port_id }
|
|
96
|
-
: {}),
|
|
97
120
|
})
|
|
98
121
|
currentPoint = segment.end
|
|
99
122
|
}
|
|
@@ -131,6 +154,67 @@ function pointIsOutsideDenseBounds(
|
|
|
131
154
|
)
|
|
132
155
|
}
|
|
133
156
|
|
|
157
|
+
function splitSegmentAtDenseBounds(params: {
|
|
158
|
+
segment: RoutedSegment
|
|
159
|
+
bounds: Bounds
|
|
160
|
+
margin: number
|
|
161
|
+
}): RoutedSegment[] {
|
|
162
|
+
const { segment, bounds, margin } = params
|
|
163
|
+
const expandedBounds = {
|
|
164
|
+
minX: bounds.minX - margin,
|
|
165
|
+
maxX: bounds.maxX + margin,
|
|
166
|
+
minY: bounds.minY - margin,
|
|
167
|
+
maxY: bounds.maxY + margin,
|
|
168
|
+
}
|
|
169
|
+
const deltaX = segment.end.x - segment.start.x
|
|
170
|
+
const deltaY = segment.end.y - segment.start.y
|
|
171
|
+
const splitParameters = [0, 1]
|
|
172
|
+
const addSplitParameter = (parameter: number): void => {
|
|
173
|
+
if (parameter <= EPSILON || parameter >= 1 - EPSILON) return
|
|
174
|
+
const point = {
|
|
175
|
+
x: segment.start.x + deltaX * parameter,
|
|
176
|
+
y: segment.start.y + deltaY * parameter,
|
|
177
|
+
}
|
|
178
|
+
if (
|
|
179
|
+
point.x < expandedBounds.minX - EPSILON ||
|
|
180
|
+
point.x > expandedBounds.maxX + EPSILON ||
|
|
181
|
+
point.y < expandedBounds.minY - EPSILON ||
|
|
182
|
+
point.y > expandedBounds.maxY + EPSILON
|
|
183
|
+
) {
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
splitParameters.push(parameter)
|
|
187
|
+
}
|
|
188
|
+
if (Math.abs(deltaX) > EPSILON) {
|
|
189
|
+
addSplitParameter((expandedBounds.minX - segment.start.x) / deltaX)
|
|
190
|
+
addSplitParameter((expandedBounds.maxX - segment.start.x) / deltaX)
|
|
191
|
+
}
|
|
192
|
+
if (Math.abs(deltaY) > EPSILON) {
|
|
193
|
+
addSplitParameter((expandedBounds.minY - segment.start.y) / deltaY)
|
|
194
|
+
addSplitParameter((expandedBounds.maxY - segment.start.y) / deltaY)
|
|
195
|
+
}
|
|
196
|
+
const parameters = splitParameters
|
|
197
|
+
.toSorted((first, second) => first - second)
|
|
198
|
+
.filter(
|
|
199
|
+
(parameter, index, values) =>
|
|
200
|
+
index === 0 || Math.abs(parameter - values[index - 1]!) > EPSILON,
|
|
201
|
+
)
|
|
202
|
+
return parameters.slice(1).map((endParameter, index) => {
|
|
203
|
+
const startParameter = parameters[index]!
|
|
204
|
+
return {
|
|
205
|
+
...segment,
|
|
206
|
+
start: {
|
|
207
|
+
x: segment.start.x + deltaX * startParameter,
|
|
208
|
+
y: segment.start.y + deltaY * startParameter,
|
|
209
|
+
},
|
|
210
|
+
end: {
|
|
211
|
+
x: segment.start.x + deltaX * endParameter,
|
|
212
|
+
y: segment.start.y + deltaY * endParameter,
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
134
218
|
function getDenseCopperBounds(bus: PreparedBus): Bounds {
|
|
135
219
|
return bus.componentObstacles.reduce<Bounds>(
|
|
136
220
|
(bounds, obstacle) => ({
|
|
@@ -364,8 +448,18 @@ function createTunedPlanCandidates(params: {
|
|
|
364
448
|
targetAddedLength: number
|
|
365
449
|
clearance: number
|
|
366
450
|
sharedBoundary: Bounds
|
|
451
|
+
allowInsideDenseBounds?: boolean
|
|
452
|
+
denseBoundarySplitApplied?: boolean
|
|
367
453
|
}): FanoutRoutePlan[] {
|
|
368
|
-
const {
|
|
454
|
+
const {
|
|
455
|
+
plan,
|
|
456
|
+
bus,
|
|
457
|
+
targetAddedLength,
|
|
458
|
+
clearance,
|
|
459
|
+
sharedBoundary,
|
|
460
|
+
allowInsideDenseBounds = false,
|
|
461
|
+
denseBoundarySplitApplied = false,
|
|
462
|
+
} = params
|
|
369
463
|
const candidates: FanoutRoutePlan[] = []
|
|
370
464
|
const denseCopperBounds = getDenseCopperBounds(bus)
|
|
371
465
|
const denseMargin = plan.segments[0]?.width
|
|
@@ -405,6 +499,7 @@ function createTunedPlanCandidates(params: {
|
|
|
405
499
|
continue
|
|
406
500
|
}
|
|
407
501
|
if (
|
|
502
|
+
!allowInsideDenseBounds &&
|
|
408
503
|
points
|
|
409
504
|
.slice(1, -1)
|
|
410
505
|
.some(
|
|
@@ -447,7 +542,22 @@ function createTunedPlanCandidates(params: {
|
|
|
447
542
|
}
|
|
448
543
|
}
|
|
449
544
|
}
|
|
450
|
-
return candidates
|
|
545
|
+
if (denseBoundarySplitApplied) return candidates
|
|
546
|
+
const splitSegments = plan.segments.flatMap((segment) =>
|
|
547
|
+
splitSegmentAtDenseBounds({
|
|
548
|
+
segment,
|
|
549
|
+
bounds: denseCopperBounds,
|
|
550
|
+
margin: denseMargin,
|
|
551
|
+
}),
|
|
552
|
+
)
|
|
553
|
+
const splitPlan = createPlanWithSegments(plan, splitSegments)
|
|
554
|
+
if (!splitPlan) return candidates
|
|
555
|
+
const splitCandidates = createTunedPlanCandidates({
|
|
556
|
+
...params,
|
|
557
|
+
plan: splitPlan,
|
|
558
|
+
denseBoundarySplitApplied: true,
|
|
559
|
+
})
|
|
560
|
+
return [...candidates, ...splitCandidates]
|
|
451
561
|
}
|
|
452
562
|
|
|
453
563
|
function getBusSkew(plans: readonly FanoutRoutePlan[]): number {
|
|
@@ -466,7 +576,19 @@ export function matchBusPlanLengths(params: {
|
|
|
466
576
|
inputSrj: SimpleRouteJson
|
|
467
577
|
sharedBoundary: Bounds
|
|
468
578
|
clearance: number
|
|
579
|
+
allowBlindAndBuriedVias?: boolean
|
|
469
580
|
allowSameNetMerges?: boolean
|
|
581
|
+
/**
|
|
582
|
+
* Allows tuning on target-layer copper inside the component pad envelope.
|
|
583
|
+
* Intended only for coordinated dense routing whose complete copper is
|
|
584
|
+
* revalidated and whose remaining dogbone capacity is checked atomically.
|
|
585
|
+
*/
|
|
586
|
+
allowMatchingInsideDenseBounds?: boolean
|
|
587
|
+
/**
|
|
588
|
+
* Rejects a geometrically clear candidate when it would make a caller-owned
|
|
589
|
+
* downstream assignment (such as pending plane dogbones) infeasible.
|
|
590
|
+
*/
|
|
591
|
+
candidatePlansAreFeasible?: (plans: readonly FanoutRoutePlan[]) => boolean
|
|
470
592
|
}):
|
|
471
593
|
| { plans: FanoutRoutePlan[]; failedBus?: never }
|
|
472
594
|
| { plans: null; failedBus: PreparedBus } {
|
|
@@ -475,7 +597,10 @@ export function matchBusPlanLengths(params: {
|
|
|
475
597
|
inputSrj,
|
|
476
598
|
sharedBoundary,
|
|
477
599
|
clearance,
|
|
600
|
+
allowBlindAndBuriedVias = true,
|
|
478
601
|
allowSameNetMerges = false,
|
|
602
|
+
allowMatchingInsideDenseBounds = false,
|
|
603
|
+
candidatePlansAreFeasible,
|
|
479
604
|
} = params
|
|
480
605
|
let matchedPlans = [...params.plans]
|
|
481
606
|
const constrainedBuses = preparedBuses.filter(
|
|
@@ -524,6 +649,95 @@ export function matchBusPlanLengths(params: {
|
|
|
524
649
|
)
|
|
525
650
|
.toSorted((first, second) => first - second)
|
|
526
651
|
let acceptedPlans: FanoutRoutePlan[] | null = null
|
|
652
|
+
const acceptCandidate = (
|
|
653
|
+
candidate: FanoutRoutePlan,
|
|
654
|
+
): FanoutRoutePlan[] | null => {
|
|
655
|
+
const nextPlans = matchedPlans.map((plan) =>
|
|
656
|
+
plan === shortest ? candidate : plan,
|
|
657
|
+
)
|
|
658
|
+
const nextBusPlans = nextPlans.filter(
|
|
659
|
+
(plan) => plan.busId === bus.busId,
|
|
660
|
+
)
|
|
661
|
+
if (getBusSkew(nextBusPlans) > skew + EPSILON) return null
|
|
662
|
+
if (
|
|
663
|
+
!fanoutPlansAreClear({
|
|
664
|
+
plans: nextPlans,
|
|
665
|
+
srj: inputSrj,
|
|
666
|
+
sharedBoundary,
|
|
667
|
+
clearance,
|
|
668
|
+
allowBlindAndBuriedVias,
|
|
669
|
+
allowSameNetMerges,
|
|
670
|
+
})
|
|
671
|
+
) {
|
|
672
|
+
return null
|
|
673
|
+
}
|
|
674
|
+
if (
|
|
675
|
+
candidatePlansAreFeasible &&
|
|
676
|
+
!candidatePlansAreFeasible(nextPlans)
|
|
677
|
+
) {
|
|
678
|
+
return null
|
|
679
|
+
}
|
|
680
|
+
return nextPlans
|
|
681
|
+
}
|
|
682
|
+
const findMultiSpanCandidate = (
|
|
683
|
+
targetAddedLength: number,
|
|
684
|
+
): FanoutRoutePlan[] | null => {
|
|
685
|
+
const maximumSearchStates = 320
|
|
686
|
+
const maximumCandidatesPerState = 48
|
|
687
|
+
let searchedStateCount = 0
|
|
688
|
+
const sampleCandidates = (
|
|
689
|
+
candidates: readonly FanoutRoutePlan[],
|
|
690
|
+
): FanoutRoutePlan[] => {
|
|
691
|
+
if (candidates.length <= maximumCandidatesPerState) {
|
|
692
|
+
return [...candidates]
|
|
693
|
+
}
|
|
694
|
+
return Array.from(
|
|
695
|
+
{ length: maximumCandidatesPerState },
|
|
696
|
+
(_, sampleIndex) =>
|
|
697
|
+
candidates[
|
|
698
|
+
Math.floor(
|
|
699
|
+
(sampleIndex * candidates.length) / maximumCandidatesPerState,
|
|
700
|
+
)
|
|
701
|
+
]!,
|
|
702
|
+
)
|
|
703
|
+
}
|
|
704
|
+
const search = (
|
|
705
|
+
currentPlan: FanoutRoutePlan,
|
|
706
|
+
stagesRemaining: number,
|
|
707
|
+
): FanoutRoutePlan[] | null => {
|
|
708
|
+
const addedLength = currentPlan.length - shortest.length
|
|
709
|
+
const remainingAddition = targetAddedLength - addedLength
|
|
710
|
+
if (remainingAddition <= EPSILON) {
|
|
711
|
+
return acceptCandidate(currentPlan)
|
|
712
|
+
}
|
|
713
|
+
if (stagesRemaining <= 0) return null
|
|
714
|
+
const stageAddedLength = remainingAddition / stagesRemaining
|
|
715
|
+
const candidates = sampleCandidates(
|
|
716
|
+
createTunedPlanCandidates({
|
|
717
|
+
plan: currentPlan,
|
|
718
|
+
bus,
|
|
719
|
+
targetAddedLength: stageAddedLength,
|
|
720
|
+
clearance,
|
|
721
|
+
sharedBoundary: bus.sharedBoundary,
|
|
722
|
+
allowInsideDenseBounds: allowMatchingInsideDenseBounds,
|
|
723
|
+
}),
|
|
724
|
+
)
|
|
725
|
+
for (const candidate of candidates) {
|
|
726
|
+
searchedStateCount++
|
|
727
|
+
if (searchedStateCount > maximumSearchStates) return null
|
|
728
|
+
if (!acceptCandidate(candidate)) continue
|
|
729
|
+
const result = search(candidate, stagesRemaining - 1)
|
|
730
|
+
if (result) return result
|
|
731
|
+
}
|
|
732
|
+
return null
|
|
733
|
+
}
|
|
734
|
+
for (let stageCount = 2; stageCount <= 8; stageCount++) {
|
|
735
|
+
const result = search(shortest, stageCount)
|
|
736
|
+
if (result) return result
|
|
737
|
+
if (searchedStateCount > maximumSearchStates) break
|
|
738
|
+
}
|
|
739
|
+
return null
|
|
740
|
+
}
|
|
527
741
|
for (const targetAddedLength of targetAddedLengths) {
|
|
528
742
|
const candidates = createTunedPlanCandidates({
|
|
529
743
|
plan: shortest,
|
|
@@ -531,30 +745,19 @@ export function matchBusPlanLengths(params: {
|
|
|
531
745
|
targetAddedLength,
|
|
532
746
|
clearance,
|
|
533
747
|
sharedBoundary: bus.sharedBoundary,
|
|
748
|
+
allowInsideDenseBounds: allowMatchingInsideDenseBounds,
|
|
534
749
|
})
|
|
535
750
|
for (const candidate of candidates) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
)
|
|
539
|
-
const nextBusPlans = nextPlans.filter(
|
|
540
|
-
(plan) => plan.busId === bus.busId,
|
|
541
|
-
)
|
|
542
|
-
const nextSkew = getBusSkew(nextBusPlans)
|
|
543
|
-
if (nextSkew > skew + EPSILON) continue
|
|
544
|
-
if (
|
|
545
|
-
!fanoutPlansAreClear({
|
|
546
|
-
plans: nextPlans,
|
|
547
|
-
srj: inputSrj,
|
|
548
|
-
sharedBoundary,
|
|
549
|
-
clearance,
|
|
550
|
-
allowSameNetMerges,
|
|
551
|
-
})
|
|
552
|
-
) {
|
|
553
|
-
continue
|
|
554
|
-
}
|
|
555
|
-
acceptedPlans = nextPlans
|
|
751
|
+
acceptedPlans = acceptCandidate(candidate)
|
|
752
|
+
if (!acceptedPlans) continue
|
|
556
753
|
break
|
|
557
754
|
}
|
|
755
|
+
if (
|
|
756
|
+
!acceptedPlans &&
|
|
757
|
+
Math.abs(targetAddedLength - minimumRequiredAddition) <= EPSILON
|
|
758
|
+
) {
|
|
759
|
+
acceptedPlans = findMultiSpanCandidate(targetAddedLength)
|
|
760
|
+
}
|
|
558
761
|
if (acceptedPlans) break
|
|
559
762
|
}
|
|
560
763
|
if (!acceptedPlans) return { plans: null, failedBus: bus }
|