@tscircuit/fanout-solver 0.0.64 → 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 +198 -6
- 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 +18 -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,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
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import type { Obstacle } from "@tscircuit/capacity-autorouter"
|
|
2
|
+
import { routeSingleLayerWithAdaptiveExitsSteps } from "./route-single-layer-adaptive-exits"
|
|
3
|
+
import { buildViaMinimalWindingPlan } from "./route-via-minimal-winding"
|
|
4
|
+
import { getCornerBandSide } from "./boundary-exit"
|
|
5
|
+
import {
|
|
6
|
+
distance,
|
|
7
|
+
distancePointToSegment,
|
|
8
|
+
distanceSegmentToSegment,
|
|
9
|
+
} from "./geometry"
|
|
10
|
+
import { getViaSpanLayers } from "./layer-names"
|
|
11
|
+
import {
|
|
12
|
+
fanoutPlansAreClear,
|
|
13
|
+
getCornerTargetTrack,
|
|
14
|
+
type RouteBusParams,
|
|
15
|
+
} from "./route-bus"
|
|
16
|
+
import type { PeripheralSourceEscape } from "./route-peripheral-source-escapes"
|
|
17
|
+
import {
|
|
18
|
+
routeViaMinimalWindingAlternativesSteps,
|
|
19
|
+
type RouteViaMinimalWindingProgress,
|
|
20
|
+
} from "./route-via-minimal-winding"
|
|
21
|
+
import type {
|
|
22
|
+
Bounds,
|
|
23
|
+
FanoutRoutePlan,
|
|
24
|
+
Point2D,
|
|
25
|
+
RoutedSegment,
|
|
26
|
+
RoutedVia,
|
|
27
|
+
} from "./types"
|
|
28
|
+
|
|
29
|
+
/** Choose unordered left source exits, then reconnect their original target order through a two-layer crossbar. */
|
|
30
|
+
export function* routeAdaptiveLeftCrossbarBusSteps(
|
|
31
|
+
params: RouteBusParams & {
|
|
32
|
+
sourceEscapes: readonly PeripheralSourceEscape[]
|
|
33
|
+
sourceBoundary: Bounds
|
|
34
|
+
},
|
|
35
|
+
): Generator<RouteViaMinimalWindingProgress, FanoutRoutePlan[] | null, void> {
|
|
36
|
+
const {
|
|
37
|
+
bus,
|
|
38
|
+
sourceBoundary,
|
|
39
|
+
sourceEscapes,
|
|
40
|
+
traceWidth: w,
|
|
41
|
+
clearance: c,
|
|
42
|
+
targetLayer,
|
|
43
|
+
} = params
|
|
44
|
+
const cornerSide = getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
45
|
+
if (
|
|
46
|
+
params.allowBlindAndBuriedVias ||
|
|
47
|
+
bus.exitEdge !== "right" ||
|
|
48
|
+
(cornerSide !== undefined && cornerSide !== "minimum") ||
|
|
49
|
+
bus.connections.length < 3
|
|
50
|
+
)
|
|
51
|
+
return null
|
|
52
|
+
const allowedLayers = bus.routableEscapeLayers ?? bus.allowedLayers ?? []
|
|
53
|
+
const crossoverLayer = allowedLayers.find((layer) => layer !== targetLayer)
|
|
54
|
+
if (
|
|
55
|
+
!crossoverLayer ||
|
|
56
|
+
!allowedLayers.includes(targetLayer) ||
|
|
57
|
+
bus.connections.some((connection) => connection.sourceLayer === targetLayer)
|
|
58
|
+
)
|
|
59
|
+
return null
|
|
60
|
+
const byIndex = new Map(
|
|
61
|
+
sourceEscapes.map((source) => [source.connectionIndex, source]),
|
|
62
|
+
)
|
|
63
|
+
const pitch = w + c,
|
|
64
|
+
padPitch = Math.min(bus.pitchX, bus.pitchY),
|
|
65
|
+
viaPitch = params.viaDiameter + c
|
|
66
|
+
const firstColumn =
|
|
67
|
+
bus.sharedBoundary.minX + Math.max(padPitch / 2, params.viaDiameter / 2 + c)
|
|
68
|
+
if (
|
|
69
|
+
firstColumn + (bus.connections.length - 1) * viaPitch >=
|
|
70
|
+
sourceBoundary.minX
|
|
71
|
+
)
|
|
72
|
+
return null
|
|
73
|
+
const sameCornerCount = params.acceptedPlans.filter(
|
|
74
|
+
(plan) =>
|
|
75
|
+
plan.exitEdge === bus.exitEdge && plan.cornerBandSide === "minimum",
|
|
76
|
+
).length
|
|
77
|
+
const terminals = bus.connections.map((connection) => {
|
|
78
|
+
const source = byIndex.get(connection.connectionIndex)
|
|
79
|
+
if (!source)
|
|
80
|
+
throw new Error(
|
|
81
|
+
`FanoutSolver: missing crossbar source escape for ${connection.connection.name}`,
|
|
82
|
+
)
|
|
83
|
+
return {
|
|
84
|
+
connection,
|
|
85
|
+
viaPoint: source.via.center,
|
|
86
|
+
exitPoint: {
|
|
87
|
+
x: bus.sharedBoundary.maxX,
|
|
88
|
+
y: cornerSide
|
|
89
|
+
? getCornerTargetTrack({
|
|
90
|
+
...params,
|
|
91
|
+
connection,
|
|
92
|
+
cornerExitLaneOffset: sameCornerCount,
|
|
93
|
+
})
|
|
94
|
+
: getCornerTargetTrack({
|
|
95
|
+
...params,
|
|
96
|
+
bus: { ...bus, preferredExit: "bottom-right" },
|
|
97
|
+
connection,
|
|
98
|
+
cornerExitLaneOffset: sameCornerCount,
|
|
99
|
+
windingOrderIndex: 0,
|
|
100
|
+
}),
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
const targetOrdered = terminals.toSorted(
|
|
105
|
+
(a, b) => a.exitPoint.y - b.exitPoint.y,
|
|
106
|
+
)
|
|
107
|
+
if (!cornerSide)
|
|
108
|
+
for (const [rank, terminal] of targetOrdered.entries())
|
|
109
|
+
terminal.exitPoint.y = sourceBoundary.minY + padPitch + rank * viaPitch
|
|
110
|
+
const crossoverTrackByIndex = new Map(
|
|
111
|
+
targetOrdered.map((terminal) => [
|
|
112
|
+
terminal.connection.connectionIndex,
|
|
113
|
+
terminal.exitPoint.y,
|
|
114
|
+
]),
|
|
115
|
+
)
|
|
116
|
+
const maxPort = Math.min(
|
|
117
|
+
sourceBoundary.maxY - pitch,
|
|
118
|
+
Math.max(...terminals.map((t) => t.viaPoint.y)) + 2 * padPitch,
|
|
119
|
+
)
|
|
120
|
+
const minPort = Math.max(
|
|
121
|
+
sourceBoundary.minY + pitch,
|
|
122
|
+
Math.min(...terminals.map((t) => t.viaPoint.y)) - 2 * padPitch,
|
|
123
|
+
...(cornerSide
|
|
124
|
+
? []
|
|
125
|
+
: [
|
|
126
|
+
Math.max(...crossoverTrackByIndex.values()) +
|
|
127
|
+
params.viaDiameter / 2 +
|
|
128
|
+
w / 2 +
|
|
129
|
+
c +
|
|
130
|
+
pitch,
|
|
131
|
+
]),
|
|
132
|
+
)
|
|
133
|
+
if (
|
|
134
|
+
maxPort - minPort < (terminals.length - 1) * viaPitch ||
|
|
135
|
+
minPort <=
|
|
136
|
+
Math.max(...crossoverTrackByIndex.values()) +
|
|
137
|
+
params.viaDiameter / 2 +
|
|
138
|
+
w / 2 +
|
|
139
|
+
c
|
|
140
|
+
)
|
|
141
|
+
return null
|
|
142
|
+
const sourcePaths = new Map(
|
|
143
|
+
sourceEscapes.map((source) => [
|
|
144
|
+
source.connectionIndex,
|
|
145
|
+
[source.segments[0]!.start, ...source.segments.map((s) => s.end)],
|
|
146
|
+
]),
|
|
147
|
+
)
|
|
148
|
+
const obstacles: Obstacle[] = [
|
|
149
|
+
...params.srj.obstacles
|
|
150
|
+
.filter((o) => o.layers.includes(targetLayer))
|
|
151
|
+
.map((o) => ({ ...o, layers: ["top"] })),
|
|
152
|
+
...sourceEscapes.map((source) => ({
|
|
153
|
+
type: "rect" as const,
|
|
154
|
+
shape: "circle",
|
|
155
|
+
center: source.via.center,
|
|
156
|
+
width: source.via.diameter,
|
|
157
|
+
height: source.via.diameter,
|
|
158
|
+
layers: ["top"],
|
|
159
|
+
connectedTo: [source.connectionName],
|
|
160
|
+
})),
|
|
161
|
+
]
|
|
162
|
+
for (let y = sourceBoundary.minY; y <= sourceBoundary.maxY; y += pitch)
|
|
163
|
+
if (y < minPort || y > maxPort)
|
|
164
|
+
obstacles.push({
|
|
165
|
+
type: "rect",
|
|
166
|
+
shape: "circle",
|
|
167
|
+
center: { x: sourceBoundary.minX, y },
|
|
168
|
+
width: w / 1000,
|
|
169
|
+
height: w / 1000,
|
|
170
|
+
layers: ["top"],
|
|
171
|
+
connectedTo: ["blocked-port"],
|
|
172
|
+
} as Obstacle)
|
|
173
|
+
const flowBuses = terminals.map((terminal, i) => ({
|
|
174
|
+
...bus,
|
|
175
|
+
busId: bus.busId + ":" + i,
|
|
176
|
+
sharedBoundary: sourceBoundary,
|
|
177
|
+
preferredExit: undefined,
|
|
178
|
+
exitEdge: undefined,
|
|
179
|
+
connections: [
|
|
180
|
+
{
|
|
181
|
+
...terminal.connection,
|
|
182
|
+
sourcePoint: { ...terminal.viaPoint, layer: "top" },
|
|
183
|
+
sourceLayer: "top",
|
|
184
|
+
sourceObstacle: obstacles.find(
|
|
185
|
+
(o) =>
|
|
186
|
+
o.connectedTo.includes(terminal.connection.connection.name) &&
|
|
187
|
+
distance(o.center, terminal.viaPoint) < 1e-7,
|
|
188
|
+
)!,
|
|
189
|
+
exitTargetPoint: undefined,
|
|
190
|
+
hasExplicitLayeredExitTarget: false,
|
|
191
|
+
hasExplicitExitTarget: false,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
}))
|
|
195
|
+
const flow = routeSingleLayerWithAdaptiveExitsSteps({
|
|
196
|
+
srj: { ...params.srj, obstacles },
|
|
197
|
+
buses: flowBuses,
|
|
198
|
+
traceWidth: w,
|
|
199
|
+
clearance: c,
|
|
200
|
+
availableBoundaryRegions: [
|
|
201
|
+
{ exitEdge: "left", direction: "left", preferredExit: "left" },
|
|
202
|
+
],
|
|
203
|
+
})
|
|
204
|
+
let flowStep = flow.next()
|
|
205
|
+
while (!flowStep.done) {
|
|
206
|
+
yield {
|
|
207
|
+
phase: "route-connection",
|
|
208
|
+
routeOrderAttempt: 0,
|
|
209
|
+
connectionIndex: 0,
|
|
210
|
+
connectionCount: terminals.length,
|
|
211
|
+
connectionName: terminals[0]!.connection.connection.name,
|
|
212
|
+
searchBatch: 0,
|
|
213
|
+
expandedStateCount: 0,
|
|
214
|
+
connectionComplete: false,
|
|
215
|
+
}
|
|
216
|
+
flowStep = flow.next()
|
|
217
|
+
}
|
|
218
|
+
const stage: FanoutRoutePlan[] | undefined =
|
|
219
|
+
flowStep.value?.length === terminals.length
|
|
220
|
+
? flowStep.value.map((plan) => {
|
|
221
|
+
const terminal = terminals.find(
|
|
222
|
+
(t) => t.connection.connectionIndex === plan.connectionIndex,
|
|
223
|
+
)!
|
|
224
|
+
return buildViaMinimalWindingPlan({
|
|
225
|
+
...params,
|
|
226
|
+
terminal: { ...terminal, exitPoint: plan.exitPoint },
|
|
227
|
+
sourceEscapePoints: sourcePaths.get(plan.connectionIndex),
|
|
228
|
+
targetLayerPoints: [
|
|
229
|
+
plan.segments[0]!.start,
|
|
230
|
+
...plan.segments.map((s) => s.end),
|
|
231
|
+
],
|
|
232
|
+
allowBlindAndBuriedVias: false,
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
: undefined
|
|
236
|
+
if (!stage) return null
|
|
237
|
+
const sorted = stage.toSorted((a, b) =>
|
|
238
|
+
cornerSide
|
|
239
|
+
? a.exitPoint.y - b.exitPoint.y
|
|
240
|
+
: terminals.find(
|
|
241
|
+
(t) => t.connection.connectionIndex === a.connectionIndex,
|
|
242
|
+
)!.exitPoint.y -
|
|
243
|
+
terminals.find(
|
|
244
|
+
(t) => t.connection.connectionIndex === b.connectionIndex,
|
|
245
|
+
)!.exitPoint.y,
|
|
246
|
+
)
|
|
247
|
+
const via = (
|
|
248
|
+
point: Point2D,
|
|
249
|
+
fromLayer: string,
|
|
250
|
+
toLayer: string,
|
|
251
|
+
): RoutedVia => ({
|
|
252
|
+
center: point,
|
|
253
|
+
diameter: params.viaDiameter,
|
|
254
|
+
holeDiameter: params.viaHoleDiameter,
|
|
255
|
+
fromLayer,
|
|
256
|
+
toLayer,
|
|
257
|
+
spanLayers: getViaSpanLayers({
|
|
258
|
+
fromLayer,
|
|
259
|
+
toLayer,
|
|
260
|
+
layerNames: params.layerNames,
|
|
261
|
+
allowBlindAndBuriedVias: false,
|
|
262
|
+
}),
|
|
263
|
+
})
|
|
264
|
+
const prefixByIndex = new Map<number, FanoutRoutePlan>()
|
|
265
|
+
for (const [rank, plan] of sorted.entries()) {
|
|
266
|
+
const last = plan.segments.at(-1)!,
|
|
267
|
+
dx = last.end.x - last.start.x,
|
|
268
|
+
dy = last.end.y - last.start.y
|
|
269
|
+
if (dx >= 0 || Math.abs(dy) > -dx + 1e-7) return null
|
|
270
|
+
const original = terminals.find(
|
|
271
|
+
(t) => t.connection.connectionIndex === plan.connectionIndex,
|
|
272
|
+
)!
|
|
273
|
+
const column = firstColumn + rank * viaPitch,
|
|
274
|
+
first = { x: column, y: plan.exitPoint.y },
|
|
275
|
+
second = {
|
|
276
|
+
x: column,
|
|
277
|
+
y: crossoverTrackByIndex.get(plan.connectionIndex)!,
|
|
278
|
+
}
|
|
279
|
+
const additions: RoutedSegment[] = [
|
|
280
|
+
{ start: plan.exitPoint, end: first, width: w, layer: targetLayer },
|
|
281
|
+
{ start: first, end: second, width: w, layer: crossoverLayer },
|
|
282
|
+
]
|
|
283
|
+
prefixByIndex.set(plan.connectionIndex, {
|
|
284
|
+
...plan,
|
|
285
|
+
segments: [...plan.segments, ...additions],
|
|
286
|
+
additionalVias: [
|
|
287
|
+
via(first, targetLayer, crossoverLayer),
|
|
288
|
+
via(second, crossoverLayer, targetLayer),
|
|
289
|
+
],
|
|
290
|
+
direction: bus.direction,
|
|
291
|
+
exitEdge: bus.exitEdge,
|
|
292
|
+
cornerBandSide: getCornerBandSide(bus.exitEdge, bus.preferredExit),
|
|
293
|
+
exitPoint: original.exitPoint,
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
const prefixes = [...prefixByIndex.values()]
|
|
297
|
+
const continuationConnections = terminals.map((t) => ({
|
|
298
|
+
...t.connection,
|
|
299
|
+
sourcePoint: {
|
|
300
|
+
...prefixByIndex.get(t.connection.connectionIndex)!.additionalVias![1]!
|
|
301
|
+
.center,
|
|
302
|
+
layer: targetLayer,
|
|
303
|
+
},
|
|
304
|
+
sourceLayer: targetLayer,
|
|
305
|
+
}))
|
|
306
|
+
const tailParams = {
|
|
307
|
+
...params,
|
|
308
|
+
bus: { ...bus, connections: continuationConnections },
|
|
309
|
+
terminals: continuationConnections.map((connection, i) => ({
|
|
310
|
+
connection,
|
|
311
|
+
viaPoint: connection.sourcePoint,
|
|
312
|
+
exitPoint: terminals[i]!.exitPoint,
|
|
313
|
+
})),
|
|
314
|
+
acceptedPlans: [...params.acceptedPlans, ...prefixes],
|
|
315
|
+
reservedVias: [
|
|
316
|
+
...sourceEscapes.map((source) => ({
|
|
317
|
+
connectionName: source.connectionName,
|
|
318
|
+
via: source.via,
|
|
319
|
+
})),
|
|
320
|
+
...prefixes.flatMap((plan) =>
|
|
321
|
+
plan.additionalVias!.map((via) => ({
|
|
322
|
+
connectionName: plan.connectionName,
|
|
323
|
+
via,
|
|
324
|
+
})),
|
|
325
|
+
),
|
|
326
|
+
],
|
|
327
|
+
allowSourceLayerRouting: true,
|
|
328
|
+
sourceEscapePaths: undefined,
|
|
329
|
+
gridStep: (pitch * 3) / 8,
|
|
330
|
+
gridStepDivisor: 2 as const,
|
|
331
|
+
alignGridToPads: true,
|
|
332
|
+
maximumRouteOrderAttempts: 6,
|
|
333
|
+
routeOrder: terminals
|
|
334
|
+
.map((_, i) => i)
|
|
335
|
+
.sort((a, b) => terminals[a]!.exitPoint.y - terminals[b]!.exitPoint.y),
|
|
336
|
+
}
|
|
337
|
+
const continuations = yield* routeViaMinimalWindingAlternativesSteps(
|
|
338
|
+
tailParams,
|
|
339
|
+
1,
|
|
340
|
+
false,
|
|
341
|
+
)
|
|
342
|
+
if (!continuations.length) return null
|
|
343
|
+
const plans = stage.map((original) => {
|
|
344
|
+
const prefix = prefixByIndex.get(original.connectionIndex)!,
|
|
345
|
+
tail = continuations[0]!.find(
|
|
346
|
+
(plan) => plan.connectionIndex === original.connectionIndex,
|
|
347
|
+
)!,
|
|
348
|
+
[first, second] = prefix.additionalVias!
|
|
349
|
+
const wire = (point: Point2D, layer: string) => ({
|
|
350
|
+
route_type: "wire" as const,
|
|
351
|
+
...point,
|
|
352
|
+
width: w,
|
|
353
|
+
layer,
|
|
354
|
+
})
|
|
355
|
+
const viaPoint = (via: RoutedVia) => ({
|
|
356
|
+
route_type: "via" as const,
|
|
357
|
+
...via.center,
|
|
358
|
+
from_layer: via.fromLayer,
|
|
359
|
+
to_layer: via.toLayer,
|
|
360
|
+
via_diameter: via.diameter,
|
|
361
|
+
via_hole_diameter: via.holeDiameter,
|
|
362
|
+
})
|
|
363
|
+
const segments = [...prefix.segments, ...tail.segments]
|
|
364
|
+
return {
|
|
365
|
+
...prefix,
|
|
366
|
+
segments,
|
|
367
|
+
trace: {
|
|
368
|
+
...original.trace,
|
|
369
|
+
route: [
|
|
370
|
+
...original.trace.route,
|
|
371
|
+
wire(first!.center, targetLayer),
|
|
372
|
+
viaPoint(first!),
|
|
373
|
+
wire(first!.center, crossoverLayer),
|
|
374
|
+
wire(second!.center, crossoverLayer),
|
|
375
|
+
viaPoint(second!),
|
|
376
|
+
...tail.trace.route,
|
|
377
|
+
],
|
|
378
|
+
},
|
|
379
|
+
length: segments.reduce((sum, s) => sum + distance(s.start, s.end), 0),
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
if (
|
|
383
|
+
!fanoutPlansAreClear({
|
|
384
|
+
...params,
|
|
385
|
+
plans: [...params.acceptedPlans, ...plans],
|
|
386
|
+
sharedBoundary: bus.sharedBoundary,
|
|
387
|
+
})
|
|
388
|
+
)
|
|
389
|
+
return null
|
|
390
|
+
for (const plan of plans)
|
|
391
|
+
for (const source of sourceEscapes) {
|
|
392
|
+
if (plan.connectionIndex === source.connectionIndex) continue
|
|
393
|
+
for (const s of plan.segments) {
|
|
394
|
+
if (
|
|
395
|
+
source.via.spanLayers.includes(s.layer) &&
|
|
396
|
+
distancePointToSegment(source.via.center, s.start, s.end) <
|
|
397
|
+
source.via.diameter / 2 + w / 2 + c - 1e-7
|
|
398
|
+
)
|
|
399
|
+
return null
|
|
400
|
+
for (const other of source.segments)
|
|
401
|
+
if (
|
|
402
|
+
other.layer === s.layer &&
|
|
403
|
+
distanceSegmentToSegment(s.start, s.end, other.start, other.end) <
|
|
404
|
+
w + c - 1e-7
|
|
405
|
+
)
|
|
406
|
+
return null
|
|
407
|
+
}
|
|
408
|
+
for (const added of plan.additionalVias!) {
|
|
409
|
+
if (
|
|
410
|
+
distance(added.center, source.via.center) <
|
|
411
|
+
params.viaDiameter + c - 1e-7
|
|
412
|
+
)
|
|
413
|
+
return null
|
|
414
|
+
for (const s of source.segments)
|
|
415
|
+
if (
|
|
416
|
+
added.spanLayers.includes(s.layer) &&
|
|
417
|
+
distancePointToSegment(added.center, s.start, s.end) <
|
|
418
|
+
params.viaDiameter / 2 + w / 2 + c - 1e-7
|
|
419
|
+
)
|
|
420
|
+
return null
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// The caller length-matches all complete buses before final validation.
|
|
424
|
+
return plans
|
|
425
|
+
}
|