@tscircuit/fanout-solver 0.0.63 → 0.0.65
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 +269 -18
- package/lib/get-free-boundary-tracks.ts +102 -0
- package/lib/match-angularly-ordered-local-vias.ts +239 -0
- package/lib/reflect-fanout-x.ts +67 -0
- package/lib/repair-peripheral-bus-lengths.ts +167 -0
- package/lib/route-adaptive-left-crossbar-bus.ts +425 -0
- package/lib/route-bottom-crossbar-bus.ts +559 -0
- package/lib/route-bus.ts +44 -2
- package/lib/route-left-crossbar-bus.ts +399 -0
- package/lib/route-opposite-bottom-crossbar-bus.ts +70 -0
- package/lib/route-peripheral-source-escapes.ts +488 -0
- package/lib/route-reserved-narrow-buses.ts +430 -0
- package/lib/route-reserved-source-buses.ts +243 -0
- package/lib/route-shallow-split-perimeter-bus.ts +268 -0
- package/lib/route-split-perimeter-bus.ts +397 -0
- package/lib/route-split-perimeter-source-escapes.ts +579 -0
- package/lib/route-staged-perimeter-bus.ts +257 -0
- package/lib/route-via-minimal-winding.ts +78 -30
- package/package.json +1 -1
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { distance, distancePointToSegment, segmentsAreClear } from "./geometry"
|
|
2
|
+
import {
|
|
3
|
+
getComponentDogboneViaSiteCandidates,
|
|
4
|
+
matchComponentDogboneViaSites,
|
|
5
|
+
type DogboneViaSiteGeometryRules,
|
|
6
|
+
} from "./match-component-dogbone-via-sites"
|
|
7
|
+
import type {
|
|
8
|
+
Point2D,
|
|
9
|
+
PreparedBus,
|
|
10
|
+
PreparedConnection,
|
|
11
|
+
RoutedSegment,
|
|
12
|
+
} from "./types"
|
|
13
|
+
|
|
14
|
+
const EPSILON = 1e-9
|
|
15
|
+
const TAU = Math.PI * 2
|
|
16
|
+
interface Candidate {
|
|
17
|
+
connectionIndex: number
|
|
18
|
+
point: Point2D
|
|
19
|
+
angle: number
|
|
20
|
+
sourceSegment: RoutedSegment
|
|
21
|
+
siteIndex: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Reserve ordered source vias, then assign every remaining source via together. */
|
|
25
|
+
export function matchAngularlyOrderedLocalVias(params: {
|
|
26
|
+
buses: readonly PreparedBus[]
|
|
27
|
+
busId: string
|
|
28
|
+
rules: DogboneViaSiteGeometryRules
|
|
29
|
+
maximumOrderingStates?: number
|
|
30
|
+
maximumCompleteAssignments?: number
|
|
31
|
+
}): Map<number, Point2D> | null {
|
|
32
|
+
const { buses, rules } = params
|
|
33
|
+
const bus = buses.find((b) => b.busId === params.busId)
|
|
34
|
+
if (
|
|
35
|
+
!bus ||
|
|
36
|
+
bus.termination.type !== "boundary" ||
|
|
37
|
+
!bus.exitEdge ||
|
|
38
|
+
bus.connections.length < 3
|
|
39
|
+
)
|
|
40
|
+
return null
|
|
41
|
+
const maximumOrderingStates = params.maximumOrderingStates ?? 10_000
|
|
42
|
+
const maximumCompleteAssignments = params.maximumCompleteAssignments ?? 16
|
|
43
|
+
for (const [name, value] of Object.entries({
|
|
44
|
+
maximumOrderingStates,
|
|
45
|
+
maximumCompleteAssignments,
|
|
46
|
+
})) {
|
|
47
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
48
|
+
throw new Error(`FanoutSolver: ${name} must be a positive safe integer`)
|
|
49
|
+
}
|
|
50
|
+
const center = {
|
|
51
|
+
x: (bus.componentBounds.minX + bus.componentBounds.maxX) / 2,
|
|
52
|
+
y: (bus.componentBounds.minY + bus.componentBounds.maxY) / 2,
|
|
53
|
+
}
|
|
54
|
+
const angle = (p: Point2D) => Math.atan2(p.y - center.y, p.x - center.x)
|
|
55
|
+
const targetCoordinate = (c: PreparedConnection) => {
|
|
56
|
+
const p = c.exitTargetPoint ?? c.targetPoint
|
|
57
|
+
return bus.exitEdge === "right"
|
|
58
|
+
? p.y
|
|
59
|
+
: bus.exitEdge === "top"
|
|
60
|
+
? -p.x
|
|
61
|
+
: bus.exitEdge === "left"
|
|
62
|
+
? -p.y
|
|
63
|
+
: p.x
|
|
64
|
+
}
|
|
65
|
+
const ordered = bus.connections.toSorted(
|
|
66
|
+
(a, b) =>
|
|
67
|
+
targetCoordinate(a) - targetCoordinate(b) ||
|
|
68
|
+
a.connectionIndex - b.connectionIndex,
|
|
69
|
+
)
|
|
70
|
+
const sourceAngles: number[] = []
|
|
71
|
+
for (const c of ordered) {
|
|
72
|
+
if (distance(c.sourcePoint, center) < EPSILON) return null
|
|
73
|
+
let a = angle(c.sourcePoint)
|
|
74
|
+
while (a < (sourceAngles.at(-1) ?? a) - EPSILON) a += TAU
|
|
75
|
+
sourceAngles.push(a)
|
|
76
|
+
}
|
|
77
|
+
if (sourceAngles.at(-1)! - sourceAngles[0]! >= TAU - EPSILON) return null
|
|
78
|
+
const connections = new Map(
|
|
79
|
+
buses.flatMap((b) =>
|
|
80
|
+
b.connections.map((c) => [c.connectionIndex, c] as const),
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
const orderedIndex = new Map(ordered.map((c, i) => [c.connectionIndex, i]))
|
|
84
|
+
const sites = new Map<string, number>()
|
|
85
|
+
const candidateGroups = new Map<number, Candidate[]>()
|
|
86
|
+
for (const c of connections.values())
|
|
87
|
+
candidateGroups.set(c.connectionIndex, [])
|
|
88
|
+
for (const raw of getComponentDogboneViaSiteCandidates(buses, rules)) {
|
|
89
|
+
const c = connections.get(raw.connectionIndex)!
|
|
90
|
+
const key = `${raw.point.x.toFixed(9)},${raw.point.y.toFixed(9)}`
|
|
91
|
+
if (!sites.has(key)) sites.set(key, sites.size)
|
|
92
|
+
const index = orderedIndex.get(raw.connectionIndex)
|
|
93
|
+
const sourceAngle = angle(c.sourcePoint)
|
|
94
|
+
const a =
|
|
95
|
+
index === undefined
|
|
96
|
+
? angle(raw.point)
|
|
97
|
+
: sourceAngles[index]! +
|
|
98
|
+
Math.atan2(
|
|
99
|
+
Math.sin(angle(raw.point) - sourceAngle),
|
|
100
|
+
Math.cos(angle(raw.point) - sourceAngle),
|
|
101
|
+
)
|
|
102
|
+
candidateGroups.get(raw.connectionIndex)!.push({
|
|
103
|
+
...raw,
|
|
104
|
+
angle: a,
|
|
105
|
+
siteIndex: sites.get(key)!,
|
|
106
|
+
sourceSegment: {
|
|
107
|
+
start: c.sourcePoint,
|
|
108
|
+
end: raw.point,
|
|
109
|
+
layer: c.sourceLayer,
|
|
110
|
+
width: rules.traceWidth,
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
const groups = [...candidateGroups.entries()].map(
|
|
115
|
+
([connectionIndex, candidates]) => ({ connectionIndex, candidates }),
|
|
116
|
+
)
|
|
117
|
+
if (groups.some((g) => g.candidates.length === 0)) return null
|
|
118
|
+
const domains = ordered.map((c, i) =>
|
|
119
|
+
candidateGroups
|
|
120
|
+
.get(c.connectionIndex)!
|
|
121
|
+
.toSorted(
|
|
122
|
+
(a, b) =>
|
|
123
|
+
Math.abs(a.angle - sourceAngles[i]!) -
|
|
124
|
+
Math.abs(b.angle - sourceAngles[i]!) ||
|
|
125
|
+
distance(b.point, center) - distance(a.point, center),
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
const requiredHoleSeparation = rules.viaHoleDiameter
|
|
129
|
+
? rules.viaHoleDiameter + (rules.holeToHoleClearance ?? rules.clearance)
|
|
130
|
+
: 0
|
|
131
|
+
const requiredViaToTraceSeparation =
|
|
132
|
+
rules.viaDiameter / 2 + rules.traceWidth / 2 + rules.clearance
|
|
133
|
+
const compatible = (a: Candidate, b: Candidate) => {
|
|
134
|
+
const share =
|
|
135
|
+
rules.canShareCopper?.(a.connectionIndex, b.connectionIndex) ?? false
|
|
136
|
+
const minimumViaDistance = share
|
|
137
|
+
? requiredHoleSeparation
|
|
138
|
+
: Math.max(requiredHoleSeparation, rules.viaDiameter + rules.clearance)
|
|
139
|
+
if (distance(a.point, b.point) < minimumViaDistance - EPSILON) return false
|
|
140
|
+
if (share) return true
|
|
141
|
+
return (
|
|
142
|
+
distancePointToSegment(
|
|
143
|
+
a.point,
|
|
144
|
+
b.sourceSegment.start,
|
|
145
|
+
b.sourceSegment.end,
|
|
146
|
+
) >=
|
|
147
|
+
requiredViaToTraceSeparation - EPSILON &&
|
|
148
|
+
distancePointToSegment(
|
|
149
|
+
b.point,
|
|
150
|
+
a.sourceSegment.start,
|
|
151
|
+
a.sourceSegment.end,
|
|
152
|
+
) >=
|
|
153
|
+
requiredViaToTraceSeparation - EPSILON &&
|
|
154
|
+
segmentsAreClear(a.sourceSegment, b.sourceSegment, rules.clearance)
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
const forbidden = new Map<Candidate, Set<Candidate>>()
|
|
158
|
+
for (const domain of domains)
|
|
159
|
+
for (const a of domain) {
|
|
160
|
+
const conflicts = new Set<Candidate>()
|
|
161
|
+
for (const group of groups)
|
|
162
|
+
for (const b of group.candidates) {
|
|
163
|
+
if (a.connectionIndex !== b.connectionIndex && !compatible(a, b))
|
|
164
|
+
conflicts.add(b)
|
|
165
|
+
}
|
|
166
|
+
forbidden.set(a, conflicts)
|
|
167
|
+
}
|
|
168
|
+
const chosen: Candidate[] = []
|
|
169
|
+
// A perfect matching is necessary because distinct drilled holes cannot use
|
|
170
|
+
// the same site. It is a pruning test; native matching still validates copper.
|
|
171
|
+
const canUseDistinctSiteCheck =
|
|
172
|
+
requiredHoleSeparation > EPSILON || !rules.canShareCopper
|
|
173
|
+
const remainingSitesCanMatch = () => {
|
|
174
|
+
if (!canUseDistinctSiteCheck) return true
|
|
175
|
+
const selected = new Map(
|
|
176
|
+
chosen.map((c) => [c.connectionIndex, c.siteIndex]),
|
|
177
|
+
)
|
|
178
|
+
const allowed = groups
|
|
179
|
+
.map((g) =>
|
|
180
|
+
g.candidates
|
|
181
|
+
.filter(
|
|
182
|
+
(c) =>
|
|
183
|
+
(!selected.has(c.connectionIndex) ||
|
|
184
|
+
selected.get(c.connectionIndex) === c.siteIndex) &&
|
|
185
|
+
chosen.every((a) => !forbidden.get(a)!.has(c)),
|
|
186
|
+
)
|
|
187
|
+
.map((c) => c.siteIndex),
|
|
188
|
+
)
|
|
189
|
+
.sort((a, b) => a.length - b.length)
|
|
190
|
+
if (allowed.some((d) => d.length === 0)) return false
|
|
191
|
+
const owner = new Int32Array(sites.size).fill(-1)
|
|
192
|
+
const augment = (index: number, visited: Uint8Array): boolean => {
|
|
193
|
+
for (const site of allowed[index]!) {
|
|
194
|
+
if (visited[site]) continue
|
|
195
|
+
visited[site] = 1
|
|
196
|
+
if (owner[site] === -1 || augment(owner[site]!, visited)) {
|
|
197
|
+
owner[site] = index
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
return allowed.every((_, i) => augment(i, new Uint8Array(sites.size)))
|
|
204
|
+
}
|
|
205
|
+
let states = 0
|
|
206
|
+
let completeAssignments = 0
|
|
207
|
+
let result: Map<number, Point2D> | null = null
|
|
208
|
+
const search = (index: number): boolean => {
|
|
209
|
+
if (++states > maximumOrderingStates) return false
|
|
210
|
+
if (index === domains.length) {
|
|
211
|
+
if (++completeAssignments > maximumCompleteAssignments) return false
|
|
212
|
+
const fixed = new Map(rules.fixedViaPointsByConnectionIndex)
|
|
213
|
+
for (const c of chosen) fixed.set(c.connectionIndex, c.point)
|
|
214
|
+
result = matchComponentDogboneViaSites(buses, {
|
|
215
|
+
...rules,
|
|
216
|
+
fixedViaPointsByConnectionIndex: fixed,
|
|
217
|
+
})
|
|
218
|
+
return result !== null
|
|
219
|
+
}
|
|
220
|
+
for (const candidate of domains[index]!) {
|
|
221
|
+
if (
|
|
222
|
+
candidate.angle < (chosen.at(-1)?.angle ?? -Infinity) - EPSILON ||
|
|
223
|
+
chosen.some((c) => forbidden.get(c)!.has(candidate))
|
|
224
|
+
)
|
|
225
|
+
continue
|
|
226
|
+
chosen.push(candidate)
|
|
227
|
+
if (remainingSitesCanMatch() && search(index + 1)) return true
|
|
228
|
+
chosen.pop()
|
|
229
|
+
if (
|
|
230
|
+
states > maximumOrderingStates ||
|
|
231
|
+
completeAssignments >= maximumCompleteAssignments
|
|
232
|
+
)
|
|
233
|
+
break
|
|
234
|
+
}
|
|
235
|
+
return false
|
|
236
|
+
}
|
|
237
|
+
search(0)
|
|
238
|
+
return result
|
|
239
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View horizontal fanout geometry from its opposite edge. A shared memo keeps
|
|
3
|
+
* pad identity intact: clearance checks compare a plan's sourceObstacle with
|
|
4
|
+
* the same obstacle in the transformed SRJ.
|
|
5
|
+
*/
|
|
6
|
+
export function reflectFanoutX<T>(value: T): T {
|
|
7
|
+
const memo = new Map<object, unknown>()
|
|
8
|
+
const reflect = (item: unknown): unknown => {
|
|
9
|
+
if (item === null || typeof item !== "object") return item
|
|
10
|
+
if (memo.has(item)) return memo.get(item)
|
|
11
|
+
if (item instanceof Map) {
|
|
12
|
+
const result = new Map()
|
|
13
|
+
memo.set(item, result)
|
|
14
|
+
for (const [key, entry] of item) result.set(key, reflect(entry))
|
|
15
|
+
return result
|
|
16
|
+
}
|
|
17
|
+
if (item instanceof Set) {
|
|
18
|
+
const result = new Set(item)
|
|
19
|
+
memo.set(item, result)
|
|
20
|
+
return result
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(item)) {
|
|
23
|
+
const result: unknown[] = []
|
|
24
|
+
memo.set(item, result)
|
|
25
|
+
for (const entry of item) result.push(reflect(entry))
|
|
26
|
+
return result
|
|
27
|
+
}
|
|
28
|
+
const original = item as Record<string, unknown>
|
|
29
|
+
const result: Record<string, unknown> = {}
|
|
30
|
+
memo.set(item, result)
|
|
31
|
+
for (const [key, entry] of Object.entries(original))
|
|
32
|
+
result[key] = reflect(entry)
|
|
33
|
+
if (typeof original.x === "number" && typeof original.y === "number")
|
|
34
|
+
result.x = -original.x
|
|
35
|
+
if (
|
|
36
|
+
typeof original.minX === "number" &&
|
|
37
|
+
typeof original.maxX === "number"
|
|
38
|
+
) {
|
|
39
|
+
result.minX = -original.maxX
|
|
40
|
+
result.maxX = -original.minX
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(original.xCoordinates))
|
|
43
|
+
result.xCoordinates = original.xCoordinates
|
|
44
|
+
.map((x: number) => -x)
|
|
45
|
+
.sort((a, b) => a - b)
|
|
46
|
+
if (typeof original.ccwRotationDegrees === "number")
|
|
47
|
+
result.ccwRotationDegrees = -original.ccwRotationDegrees
|
|
48
|
+
const edge = (direction: string) =>
|
|
49
|
+
direction === "left"
|
|
50
|
+
? "right"
|
|
51
|
+
: direction === "right"
|
|
52
|
+
? "left"
|
|
53
|
+
: direction
|
|
54
|
+
for (const key of ["direction", "exitEdge", "preferredExit"] as const) {
|
|
55
|
+
if (typeof original[key] === "string")
|
|
56
|
+
result[key] = original[key].split("-").map(edge).join("-")
|
|
57
|
+
}
|
|
58
|
+
if (
|
|
59
|
+
original.cornerBandSide &&
|
|
60
|
+
(original.exitEdge === "top" || original.exitEdge === "bottom")
|
|
61
|
+
)
|
|
62
|
+
result.cornerBandSide =
|
|
63
|
+
original.cornerBandSide === "minimum" ? "maximum" : "minimum"
|
|
64
|
+
return result
|
|
65
|
+
}
|
|
66
|
+
return reflect(value) as T
|
|
67
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter"
|
|
2
|
+
import { getCornerBandSide } from "./boundary-exit"
|
|
3
|
+
import { matchBusPlanLengths } from "./match-bus-lengths"
|
|
4
|
+
import type { RouteBusParams } from "./route-bus"
|
|
5
|
+
import {
|
|
6
|
+
routeViaMinimalWindingAlternativesSteps,
|
|
7
|
+
type RouteViaMinimalWindingProgress,
|
|
8
|
+
} from "./route-via-minimal-winding"
|
|
9
|
+
import type { Bounds, FanoutRoutePlan, PreparedBus } from "./types"
|
|
10
|
+
|
|
11
|
+
interface Params
|
|
12
|
+
extends Omit<RouteBusParams, "bus" | "targetLayer" | "acceptedPlans"> {
|
|
13
|
+
inputSrj: SimpleRouteJson
|
|
14
|
+
sharedBoundary: Bounds
|
|
15
|
+
preparedBuses: readonly PreparedBus[]
|
|
16
|
+
plans: readonly FanoutRoutePlan[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Move a pair within its boundary band when its first route leaves no tuning room. */
|
|
20
|
+
export function* repairPeripheralBusLengthsSteps(
|
|
21
|
+
params: Params,
|
|
22
|
+
): Generator<RouteViaMinimalWindingProgress, FanoutRoutePlan[] | null, void> {
|
|
23
|
+
const match = (
|
|
24
|
+
plans: readonly FanoutRoutePlan[],
|
|
25
|
+
buses = params.preparedBuses,
|
|
26
|
+
) =>
|
|
27
|
+
matchBusPlanLengths({
|
|
28
|
+
plans,
|
|
29
|
+
preparedBuses: buses,
|
|
30
|
+
inputSrj: params.inputSrj,
|
|
31
|
+
sharedBoundary: params.sharedBoundary,
|
|
32
|
+
clearance: params.clearance,
|
|
33
|
+
allowBlindAndBuriedVias: params.allowBlindAndBuriedVias,
|
|
34
|
+
allowSameNetMerges: params.allowSameNetMerges,
|
|
35
|
+
})
|
|
36
|
+
const skew = (plans: readonly FanoutRoutePlan[]) =>
|
|
37
|
+
Math.max(...plans.map((plan) => plan.length)) -
|
|
38
|
+
Math.min(...plans.map((plan) => plan.length))
|
|
39
|
+
const repaired = new Set<string>()
|
|
40
|
+
let current = [...params.plans]
|
|
41
|
+
while (repaired.size < 3) {
|
|
42
|
+
const matched = match(current)
|
|
43
|
+
if (matched.plans) return matched.plans
|
|
44
|
+
const bus = matched.failedBus
|
|
45
|
+
if (
|
|
46
|
+
repaired.has(bus.busId) ||
|
|
47
|
+
bus.connections.length !== 2 ||
|
|
48
|
+
bus.termination.type !== "boundary" ||
|
|
49
|
+
!bus.exitEdge
|
|
50
|
+
)
|
|
51
|
+
return null
|
|
52
|
+
repaired.add(bus.busId)
|
|
53
|
+
const own = current.filter((plan) => plan.busId === bus.busId)
|
|
54
|
+
if (
|
|
55
|
+
own.length !== 2 ||
|
|
56
|
+
own.some((plan) => !plan.via || plan.additionalVias?.length) ||
|
|
57
|
+
own[0]!.targetLayer !== own[1]!.targetLayer
|
|
58
|
+
)
|
|
59
|
+
return null
|
|
60
|
+
const accepted = current.filter((plan) => plan.busId !== bus.busId)
|
|
61
|
+
const sourceEscapePaths = new Map(
|
|
62
|
+
own.map((plan) => [
|
|
63
|
+
plan.connectionIndex,
|
|
64
|
+
[
|
|
65
|
+
plan.sourcePoint,
|
|
66
|
+
...plan.segments
|
|
67
|
+
.slice(0, plan.sourceEscapeSegmentCount ?? 1)
|
|
68
|
+
.map((segment) => segment.end),
|
|
69
|
+
],
|
|
70
|
+
]),
|
|
71
|
+
)
|
|
72
|
+
const horizontal = bus.exitEdge === "left" || bus.exitEdge === "right"
|
|
73
|
+
const axis = horizontal ? "y" : "x"
|
|
74
|
+
const minimum = horizontal
|
|
75
|
+
? params.sharedBoundary.minY
|
|
76
|
+
: params.sharedBoundary.minX
|
|
77
|
+
const maximum = horizontal
|
|
78
|
+
? params.sharedBoundary.maxY
|
|
79
|
+
: params.sharedBoundary.maxX
|
|
80
|
+
const middle = (minimum + maximum) / 2
|
|
81
|
+
const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
82
|
+
const pitch = params.traceWidth + params.clearance
|
|
83
|
+
const initialSkew = skew(own)
|
|
84
|
+
const candidates: FanoutRoutePlan[][] = []
|
|
85
|
+
let replacement: FanoutRoutePlan[] | null = null
|
|
86
|
+
search: for (const multiplier of [0, 1, -1, 2, -2, 3, -3, 4, -4]) {
|
|
87
|
+
const offset = multiplier * pitch
|
|
88
|
+
if (
|
|
89
|
+
own.some((plan) => {
|
|
90
|
+
const track = plan.exitPoint[axis] + offset
|
|
91
|
+
return (
|
|
92
|
+
track < minimum + params.traceWidth / 2 ||
|
|
93
|
+
track > maximum - params.traceWidth / 2 ||
|
|
94
|
+
(side === "minimum" && track >= middle) ||
|
|
95
|
+
(side === "maximum" && track <= middle)
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
continue
|
|
100
|
+
for (const laneBias of [0, -1, 1] as const) {
|
|
101
|
+
for (const routeOrder of [
|
|
102
|
+
[0, 1],
|
|
103
|
+
[1, 0],
|
|
104
|
+
]) {
|
|
105
|
+
const alternatives = yield* routeViaMinimalWindingAlternativesSteps(
|
|
106
|
+
{
|
|
107
|
+
...params,
|
|
108
|
+
bus,
|
|
109
|
+
targetLayer: own[0]!.targetLayer,
|
|
110
|
+
acceptedPlans: accepted,
|
|
111
|
+
terminals: bus.connections.map((connection) => {
|
|
112
|
+
const original = own.find(
|
|
113
|
+
(plan) => plan.connectionIndex === connection.connectionIndex,
|
|
114
|
+
)!
|
|
115
|
+
return {
|
|
116
|
+
connection,
|
|
117
|
+
viaPoint: original.via!.center,
|
|
118
|
+
exitPoint: {
|
|
119
|
+
...original.exitPoint,
|
|
120
|
+
[axis]: original.exitPoint[axis] + offset,
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
}),
|
|
124
|
+
sourceEscapePaths,
|
|
125
|
+
reservedVias: undefined,
|
|
126
|
+
gridStep: pitch / 2,
|
|
127
|
+
gridStepDivisor: 2,
|
|
128
|
+
alignGridToPads: true,
|
|
129
|
+
maximumRouteOrderAttempts: 1,
|
|
130
|
+
routeOrder,
|
|
131
|
+
laneBias,
|
|
132
|
+
},
|
|
133
|
+
1,
|
|
134
|
+
false,
|
|
135
|
+
)
|
|
136
|
+
if (!alternatives.length || skew(alternatives[0]!) >= initialSkew)
|
|
137
|
+
continue
|
|
138
|
+
const candidate = alternatives[0]!.map((plan) => ({
|
|
139
|
+
...plan,
|
|
140
|
+
cornerBandSide: side,
|
|
141
|
+
}))
|
|
142
|
+
candidates.push(candidate)
|
|
143
|
+
// A small remaining deficit is inexpensive to tune. Larger deficits
|
|
144
|
+
// are attempted below after collecting the best available geometry.
|
|
145
|
+
if (skew(candidate) > 2 * bus.maxLengthSkew!) continue
|
|
146
|
+
const tuned = match([...accepted, ...candidate], [bus])
|
|
147
|
+
if (!tuned.plans) continue
|
|
148
|
+
replacement = tuned.plans
|
|
149
|
+
break search
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!replacement) {
|
|
154
|
+
for (const candidate of candidates
|
|
155
|
+
.toSorted((a, b) => skew(a) - skew(b))
|
|
156
|
+
.slice(0, 3)) {
|
|
157
|
+
const tuned = match([...accepted, ...candidate], [bus])
|
|
158
|
+
if (!tuned.plans) continue
|
|
159
|
+
replacement = tuned.plans
|
|
160
|
+
break
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!replacement) return null
|
|
164
|
+
current = replacement
|
|
165
|
+
}
|
|
166
|
+
return match(current).plans
|
|
167
|
+
}
|