@tscircuit/fanout-solver 0.0.10
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/LICENSE +21 -0
- package/README.md +366 -0
- package/lib/build-output.ts +140 -0
- package/lib/fanout-solver.ts +617 -0
- package/lib/geometry.ts +162 -0
- package/lib/index.ts +20 -0
- package/lib/layer-colors.ts +21 -0
- package/lib/layer-names.ts +137 -0
- package/lib/prepare-buses.ts +1016 -0
- package/lib/route-bus.ts +934 -0
- package/lib/route-single-layer-adaptive-exits.ts +1179 -0
- package/lib/route-single-layer-push-shove.ts +1233 -0
- package/lib/types.ts +212 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tscircuit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
# @tscircuit/fanout-solver
|
|
2
|
+
|
|
3
|
+
BGA fanout preprocessor for
|
|
4
|
+
[`SimpleRouteJson`](https://github.com/tscircuit/tscircuit-autorouter).
|
|
5
|
+
|
|
6
|
+
`FanoutSolver` routes every connected package pad to one shared breakout boundary
|
|
7
|
+
before the general-purpose autorouter runs. It can put a small escape via in a
|
|
8
|
+
pad-to-pad channel or move an oversized via diagonally into the interstice
|
|
9
|
+
between four pad corners. It routes every member of a bus in the same direction
|
|
10
|
+
and treats each bus-layer decision atomically.
|
|
11
|
+
|
|
12
|
+
## Behavior
|
|
13
|
+
|
|
14
|
+
- Uses `SimpleRouteJson.buses` when present. It also understands a point
|
|
15
|
+
`busId` and names such as `BUS_DDR_01`.
|
|
16
|
+
- Detects rectangular pad footprints through obstacle `componentId` metadata,
|
|
17
|
+
including perimeter packages and two-pad passives.
|
|
18
|
+
- Handles multiple mixed footprints inside one shared breakout boundary.
|
|
19
|
+
- Routes perimeter and inner-matrix pads; the benchmark connects every pad.
|
|
20
|
+
- Uses `sharedBoundary` as the common exit rectangle. Without one, it infers a
|
|
21
|
+
shared rectangle around the source footprints selected for the buses, without
|
|
22
|
+
expanding that boundary to include destination footprints.
|
|
23
|
+
- Ends every fanout trace exactly on its selected `sharedBoundary` edge. Border
|
|
24
|
+
distribution and lane spreading happen inside the rectangle, never after the
|
|
25
|
+
trace has crossed the boundary.
|
|
26
|
+
- Infers one outward direction per bus from the bus endpoints, or accepts an
|
|
27
|
+
explicit direction override.
|
|
28
|
+
- Accepts an additive `preferredExit` bus field for a particular edge
|
|
29
|
+
(`left`, `right`, `top`, or `bottom`) or corner (`top-left`, `top-right`,
|
|
30
|
+
`bottom-left`, or `bottom-right`). A corner chooses a compatible adjacent
|
|
31
|
+
edge and reserves the bus at that end of the border.
|
|
32
|
+
- `availableCornersAndSides` can restrict every boundary-terminated bus to
|
|
33
|
+
named regions of the shared boundary. For example,
|
|
34
|
+
`['top_left', 'top_middle', 'top_right']` allows only top-edge exits;
|
|
35
|
+
`top` is an alias for `top_middle` (with matching aliases for the other
|
|
36
|
+
edges).
|
|
37
|
+
- `borderDistribution: "even"` uses outward-only shoves to equalize
|
|
38
|
+
under-filled gaps across the occupied border interval while preserving bus
|
|
39
|
+
order, existing wider corridors, and trace/clearance pitch. The default
|
|
40
|
+
`"preserve"` mode stays source-aligned.
|
|
41
|
+
- Supports balanced nearest-edge partitioning for package breakouts. Ties
|
|
42
|
+
alternate instead of favoring one axis; square grids distribute equally
|
|
43
|
+
across north, south, east, and west.
|
|
44
|
+
- Enumerates combinations of the copper layers implied by `layerCount`.
|
|
45
|
+
- Prefers depth-cycled layer assignments: matching north/south (or east/west)
|
|
46
|
+
bus depths share a layer, and deeper pairs cycle through every available
|
|
47
|
+
escape layer. This forces a small stackup to reuse routing channels.
|
|
48
|
+
- Keeps outward-edge buses on their source layer when possible. If a bus needs
|
|
49
|
+
a via, every connection in that bus receives one and moves to the same
|
|
50
|
+
assigned layer; mixed via use within a bus is never committed.
|
|
51
|
+
- Accepts a bus-level `termination` target. The default
|
|
52
|
+
`{ type: "boundary" }` preserves the ordinary breakout contract, while
|
|
53
|
+
`{ type: "plane", layer: "inner1" }` escapes each source pad to a legal local
|
|
54
|
+
via and considers the connection complete on that plane instead of extending
|
|
55
|
+
it to the shared boundary.
|
|
56
|
+
- Uses a straight pad-pair escape when the via fits. Otherwise it uses a 45°
|
|
57
|
+
four-pad interstitial escape and nested side bands that spread deeper
|
|
58
|
+
two-layer buses around already-routed outer buses.
|
|
59
|
+
- `compactBusTracks` bends each bus into a trace/clearance-pitch routing
|
|
60
|
+
envelope, so a wide pad row does not consume a disproportionately wide
|
|
61
|
+
breakout corridor.
|
|
62
|
+
- Chamfers orthogonal routing corners into 45° segments before validating and
|
|
63
|
+
emitting the fanout.
|
|
64
|
+
- Verifies pad, via, trace, and already-routed fanout clearance.
|
|
65
|
+
- Treats an obstacle whose `connectedTo` list contains the connection name as
|
|
66
|
+
electrically connected copper rather than a foreign keepout.
|
|
67
|
+
- Emits supplied fanout traces, via obstacles, and moved breakout endpoints in a
|
|
68
|
+
new `SimpleRouteJson`. The returned problem is ready for a downstream
|
|
69
|
+
autorouter to finish.
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
This repository uses tscircuit's source-first GitHub package convention:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
bun add https://github.com/tscircuit/fanout-solver
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { CapacityMeshSolver } from "@tscircuit/capacity-autorouter"
|
|
83
|
+
import { FanoutSolver } from "@tscircuit/fanout-solver"
|
|
84
|
+
|
|
85
|
+
const fanoutSolver = new FanoutSolver(simpleRouteJson, {
|
|
86
|
+
maxLayerCombinations: 256,
|
|
87
|
+
sharedBoundary: {
|
|
88
|
+
minX: -25,
|
|
89
|
+
maxX: 25,
|
|
90
|
+
minY: -25,
|
|
91
|
+
maxY: 25,
|
|
92
|
+
},
|
|
93
|
+
componentBounds: {
|
|
94
|
+
"bga-01": { minX: -4.7, maxX: 4.7, minY: -4.7, maxY: 4.7 },
|
|
95
|
+
},
|
|
96
|
+
busDirections: {
|
|
97
|
+
ddr: "right",
|
|
98
|
+
},
|
|
99
|
+
busExitPreferences: {
|
|
100
|
+
clocks: "top-right",
|
|
101
|
+
},
|
|
102
|
+
availableCornersAndSides: ["top_left", "top", "top_right"],
|
|
103
|
+
borderDistribution: "even",
|
|
104
|
+
compactBusTracks: true,
|
|
105
|
+
buses: [
|
|
106
|
+
{
|
|
107
|
+
busId: "ground",
|
|
108
|
+
connectionNames: ["VSS_A1", "VSS_A2"],
|
|
109
|
+
direction: "right",
|
|
110
|
+
termination: { type: "plane", layer: "inner1" },
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
})
|
|
114
|
+
fanoutSolver.solve()
|
|
115
|
+
|
|
116
|
+
if (fanoutSolver.failed) {
|
|
117
|
+
throw new Error(fanoutSolver.error ?? "Fanout failed")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const autorouter = new CapacityMeshSolver(
|
|
121
|
+
fanoutSolver.getOutputSimpleRouteJson(),
|
|
122
|
+
)
|
|
123
|
+
autorouter.solve()
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The canonical bus input is the current `SimpleRouteJson` bus structure:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
{
|
|
130
|
+
buses: [
|
|
131
|
+
{
|
|
132
|
+
busId: "ddr",
|
|
133
|
+
connectionNames: ["BUS_DDR_01", "BUS_DDR_02", "BUS_DDR_03"],
|
|
134
|
+
preferredExit: "right",
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`preferredExit` is an optional fanout extension to `SimpleRouteBus`; omitting it
|
|
141
|
+
leaves ordinary `SimpleRouteJson` behavior unchanged. All listed connections
|
|
142
|
+
receive the same escape direction and target layer. If one connection cannot be
|
|
143
|
+
routed cleanly, the solver rejects that bus for the current layer assignment and
|
|
144
|
+
tries another combination. `busExitPreferences` provides the same override
|
|
145
|
+
without modifying the input object.
|
|
146
|
+
|
|
147
|
+
`availableCornersAndSides` is a solver-wide hard constraint. Its directed
|
|
148
|
+
corner names distinguish the two edges meeting at a corner: `top_left` exits
|
|
149
|
+
through the top edge, while `left_top` exits through the left edge. The complete
|
|
150
|
+
set is `top_left`, `top_middle`, `top_right`, `right_top`, `right_middle`,
|
|
151
|
+
`right_bottom`, `bottom_right`, `bottom_middle`, `bottom_left`, `left_bottom`,
|
|
152
|
+
`left_middle`, and `left_top`. `top`, `right`, `bottom`, and `left` alias the
|
|
153
|
+
corresponding middle region. An empty list is invalid; omit the option to allow
|
|
154
|
+
all edges.
|
|
155
|
+
|
|
156
|
+
`termination` is another additive extension:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
type FanoutBusTermination =
|
|
160
|
+
| { type: "boundary" }
|
|
161
|
+
| { type: "plane"; layer: string }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
A plane-targeted connection may contain only its package-pad source point. The
|
|
165
|
+
solver creates the local dogbone and via, records it in `planeTerminations`, and
|
|
166
|
+
removes the completed connection from the returned downstream
|
|
167
|
+
`SimpleRouteJson`. Plane layers are fixed targets and are not included in the
|
|
168
|
+
bus-layer combination search.
|
|
169
|
+
|
|
170
|
+
## Output contract
|
|
171
|
+
|
|
172
|
+
`getOutput()` returns:
|
|
173
|
+
|
|
174
|
+
- `simpleRouteJson`: the downstream routing problem with fanout prefixes
|
|
175
|
+
- `fanoutTraces`: the newly supplied pad-to-breakout traces
|
|
176
|
+
- `planeTerminations`: the completed local-via connection, layer, and via data
|
|
177
|
+
- `busLayerAssignments`: the selected layer for every bus
|
|
178
|
+
- `busDirections`: the direction shared by each bus
|
|
179
|
+
- `attempts`: score and success metadata for every tried layer combination
|
|
180
|
+
|
|
181
|
+
## Dataset 01
|
|
182
|
+
|
|
183
|
+
`datasets/dataset01.ts` contains five deterministic footprinter samples. The
|
|
184
|
+
samples contain exactly one through five BGA footprints, and every sample is
|
|
185
|
+
solved as one `SimpleRouteJson`.
|
|
186
|
+
|
|
187
|
+
| Sample | Footprints | Pads | Connections |
|
|
188
|
+
| ----------- | ---------: | ---: | ----------: |
|
|
189
|
+
| `sample001` | 1 | 64 | 64 |
|
|
190
|
+
| `sample002` | 2 | 100 | 100 |
|
|
191
|
+
| `sample003` | 3 | 136 | 136 |
|
|
192
|
+
| `sample004` | 4 | 200 | 200 |
|
|
193
|
+
| `sample005` | 5 | 236 | 236 |
|
|
194
|
+
|
|
195
|
+
The Cosmos debugger provides Previous/Next controls and direct sample tabs. It
|
|
196
|
+
also stores the selected dataset and sample in the `dataset` and `sample` URL
|
|
197
|
+
parameters. Each sample has one shared boundary around all of its footprints,
|
|
198
|
+
and component bounds come from the exact footprinter-generated copper pad
|
|
199
|
+
extents.
|
|
200
|
+
|
|
201
|
+
## Dataset 02
|
|
202
|
+
|
|
203
|
+
`datasets/dataset02.ts` is the four-layer BGA400 stress benchmark. It routes
|
|
204
|
+
every ball in the exact footprinter string
|
|
205
|
+
`bga400_grid20x20_p0.8mm_pad0.3mm_circularpads`; the debugger includes that
|
|
206
|
+
string in both the visible sample heading and browser title. The sample uses
|
|
207
|
+
[JLCPCB's published 0.10/0.10 mm trace and spacing capability](https://jlcpcb.com/capabilities/pcb-capabilities/),
|
|
208
|
+
0.10 mm pad/copper clearance, and standard 0.25/0.15 mm vias.
|
|
209
|
+
|
|
210
|
+
Exactly 100 balls and ten buses escape through each package edge. The four
|
|
211
|
+
perimeter buses (76 balls) stay via-free on top. The remaining 36 buses use 324
|
|
212
|
+
bus-atomic vias. Matching opposite-edge depths cycle through `inner1`,
|
|
213
|
+
`inner2`, and `bottom`, so every escape layer carries twelve buses instead of
|
|
214
|
+
receiving one easy depth band.
|
|
215
|
+
|
|
216
|
+
The earlier 0.4 mm corner-interstitial case remains a dedicated regression
|
|
217
|
+
test. It cannot honestly route the full BGA400 on four layers with the retained
|
|
218
|
+
0.10 mm rules: adjacent 0.15 mm via centers are 0.40 mm apart, but a crossing
|
|
219
|
+
0.10 mm trace needs 0.45 mm center-to-center capacity after both clearances are
|
|
220
|
+
included. The repeated via row is therefore a physical copper wall. The
|
|
221
|
+
0.8 mm BGA400 leaves real reusable channels while still forcing a four-layer
|
|
222
|
+
solution.
|
|
223
|
+
|
|
224
|
+
| Sample | Footprints | Pads | Buses | Vias | Layers |
|
|
225
|
+
| ----------- | ---------: | ---: | ----: | ---: | -----: |
|
|
226
|
+
| `sample001` | 1 | 400 | 40 | 324 | 4 |
|
|
227
|
+
|
|
228
|
+
Every bus exits the shared component boundary and uses only straight or 45°
|
|
229
|
+
segments. Top, inner1, inner2, and bottom traces use distinct red, blue, green,
|
|
230
|
+
and purple colors in Cosmos and the verification PNG.
|
|
231
|
+
|
|
232
|
+
## Dataset 03
|
|
233
|
+
|
|
234
|
+
`datasets/dataset03.ts` contains four two-layer mixed-footprint samples. Every
|
|
235
|
+
sample uses the exact footprinter strings `qfn50_p0.4mm`, `res0603`, and
|
|
236
|
+
`cap0603`, for 54 routed pads across three footprints and one shared boundary.
|
|
237
|
+
The QFN rotates through 0°, 90°, 180°, and 270° while the two 0603 packages
|
|
238
|
+
alternate between tangential and radial placement.
|
|
239
|
+
|
|
240
|
+
The close tangential samples deliberately block two opposite top-layer QFN
|
|
241
|
+
escape corridors. The solver moves each obstructed QFN side as one atomic bus
|
|
242
|
+
to bottom while keeping the surrounding 0603 terminals independently routable.
|
|
243
|
+
The radial samples offset the passives toward package corners to create
|
|
244
|
+
asymmetric channels without relaxing JLCPCB's 0.10 mm copper clearance or
|
|
245
|
+
standard 0.25/0.15 mm via constraints.
|
|
246
|
+
|
|
247
|
+
| Sample | Footprints | Pads | Buses | Layers |
|
|
248
|
+
| ----------- | ---------: | ---: | ----: | -----: |
|
|
249
|
+
| `sample001` | 3 | 54 | 8 | 2 |
|
|
250
|
+
| `sample002` | 3 | 54 | 8 | 2 |
|
|
251
|
+
| `sample003` | 3 | 54 | 8 | 2 |
|
|
252
|
+
| `sample004` | 3 | 54 | 8 | 2 |
|
|
253
|
+
|
|
254
|
+
## Dataset 04
|
|
255
|
+
|
|
256
|
+
`datasets/dataset04.ts` contains five top-copper-only package-plus-decoupling
|
|
257
|
+
stress cases. Every sample places exactly eight `cap0603` footprints at the
|
|
258
|
+
cardinal and diagonal positions around one central package, then uses
|
|
259
|
+
push-and-shove bends to move complete ordered bundles through one shared
|
|
260
|
+
boundary. The `"even"` border-distribution option makes the exit lanes consume
|
|
261
|
+
their available border interval consistently, and the four diagonal capacitor
|
|
262
|
+
pairs explicitly request their matching corners. No Dataset 04 route contains
|
|
263
|
+
a via or a non-top-layer wire.
|
|
264
|
+
|
|
265
|
+
The BGA cases use the exact footprinter strings
|
|
266
|
+
`bga16_grid4x4_p0.8mm_pad0.3mm_circularpads`,
|
|
267
|
+
`bga25_grid5x5_p1.75mm_pad0.3mm_circularpads`,
|
|
268
|
+
`bga36_grid6x6_p1.5mm_pad0.3mm_circularpads`, and
|
|
269
|
+
`bga64_grid8x8_p1.5mm_pad0.3mm_circularpads`. Every inner ball is connected,
|
|
270
|
+
grid-line buses remain atomic, and a sweep-line channel router pushes already
|
|
271
|
+
allocated traces when a new pad row needs corridor capacity.
|
|
272
|
+
|
|
273
|
+
The final sample uses the RP2040-class footprinter string
|
|
274
|
+
`qfn56_w7.8_h7.8_p0.4mm_pw0.23mm_pl0.8mm_thermalpad3.2x3.2_startingpin(topside,rightpin)_ccw`.
|
|
275
|
+
Those compensated footprinter dimensions reproduce
|
|
276
|
+
[Raspberry Pi's reference land pattern](https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf)
|
|
277
|
+
exactly: perimeter centers at ±3.4 mm, 0.4 mm pitch, 0.8×0.23 mm pads, and a
|
|
278
|
+
3.2×3.2 mm exposed pad, with no overlapping copper.
|
|
279
|
+
It routes all 56 perimeter pins, the exposed thermal pad, and all 16 capacitor
|
|
280
|
+
pads. The thermal-pad trace leaves on a 45° diagonal through a package corner,
|
|
281
|
+
centered between the outermost pads on its two adjacent edges. That diagonal
|
|
282
|
+
channel clears both rectangular pad corners under the same 0.10 mm trace and
|
|
283
|
+
0.10 mm edge-clearance rules.
|
|
284
|
+
|
|
285
|
+
All five regressions independently verify JLCPCB's 0.10 mm trace width and
|
|
286
|
+
0.10 mm copper-clearance rules, top-only routing, shared-boundary exits, inner
|
|
287
|
+
BGA pad coverage, ordered push-and-shove bends, and the absence of 90° corners.
|
|
288
|
+
|
|
289
|
+
| Sample | Footprints | Routed pads | Buses | Vias | Layers |
|
|
290
|
+
| ----------- | ---------: | ----------: | ----: | ---: | -----: |
|
|
291
|
+
| `sample001` | 9 | 32 | 24 | 0 | 1 |
|
|
292
|
+
| `sample002` | 9 | 41 | 25 | 0 | 1 |
|
|
293
|
+
| `sample003` | 9 | 52 | 28 | 0 | 1 |
|
|
294
|
+
| `sample004` | 9 | 80 | 32 | 0 | 1 |
|
|
295
|
+
| `sample005` | 9 | 73 | 73 | 0 | 1 |
|
|
296
|
+
|
|
297
|
+
## Dataset 05
|
|
298
|
+
|
|
299
|
+
`datasets/dataset05.ts` uses the attached Rockchip RK3588 V1.1 ball-assignment
|
|
300
|
+
data as its checked-in source of truth. It preserves the exact 34×34 published
|
|
301
|
+
orientation, all 1,088 populated ball coordinates and names, and all 68
|
|
302
|
+
unpopulated positions. Every generated connection and pad obstacle carries its
|
|
303
|
+
complete source assignment as `rk3588BallAssignment` metadata.
|
|
304
|
+
|
|
305
|
+
The six-layer sample dedicates `inner1` to the 422 ground balls and `inner2` to
|
|
306
|
+
the 167 power balls. Those 589 connections end at unique 0.25/0.15 mm local
|
|
307
|
+
dogbone vias. The remaining 499 signal balls are divided into short,
|
|
308
|
+
direction-consistent geometric buses and escape to the shared boundary on
|
|
309
|
+
`top`, `inner3`, `inner4`, and `bottom`. All copper uses the same 0.10 mm trace
|
|
310
|
+
and clearance values as the JLCPCB regressions.
|
|
311
|
+
|
|
312
|
+
| Sample | Package | Balls | Plane terminations | Boundary signals | Layers |
|
|
313
|
+
| ----------- | ------------ | ----: | -----------------: | ---------------: | -----: |
|
|
314
|
+
| `sample001` | FCBGA1088L | 1088 | 589 | 499 | 6 |
|
|
315
|
+
|
|
316
|
+
## Dataset 06
|
|
317
|
+
|
|
318
|
+
`datasets/dataset06.ts` preserves the 132-connection, 265-obstacle,
|
|
319
|
+
single-layer mixed-footprint input from the clad1 RP2040 board:
|
|
320
|
+
|
|
321
|
+

|
|
322
|
+
|
|
323
|
+
The original reproduction failed for three independent reasons:
|
|
324
|
+
|
|
325
|
+
- several serialized `source_trace_*` names belong to the same canonical
|
|
326
|
+
`connectivity_net…`, so treating those names as foreign nets creates false
|
|
327
|
+
clearance conflicts;
|
|
328
|
+
- the RP2040 exposed pad is enclosed and must merge into a same-net perimeter
|
|
329
|
+
pad before following that pad's escape;
|
|
330
|
+
- all 132 outward-facing exit preferences cannot coexist without crossings on
|
|
331
|
+
one layer.
|
|
332
|
+
|
|
333
|
+
Dataset 06 enables `singleLayerAdaptiveExits`. After the preferred
|
|
334
|
+
push-and-shove attempt fails, this pass reserves short pad-escape stubs, routes
|
|
335
|
+
the remaining physical terminals together, locally merges the few
|
|
336
|
+
multi-terminal pads that do not need independent channels, and chooses
|
|
337
|
+
alternate sides of the same shared boundary. The result routes all 132
|
|
338
|
+
connections on top copper with the configured 0.1 mm trace width and 0.1 mm
|
|
339
|
+
clearance.
|
|
340
|
+
|
|
341
|
+
## Development
|
|
342
|
+
|
|
343
|
+
```sh
|
|
344
|
+
bun install
|
|
345
|
+
bun run typecheck
|
|
346
|
+
bun test
|
|
347
|
+
bun run benchmark
|
|
348
|
+
bun run render:dataset
|
|
349
|
+
bun run start
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The benchmark runs every sample in all datasets and reports footprint, pad,
|
|
353
|
+
connection, routing, and layer-assignment metrics. `bun run start` opens the
|
|
354
|
+
datasets in the standard tscircuit solver debugger. `bun run
|
|
355
|
+
render:dataset` writes `graphics-debug` PNGs under one subdirectory per dataset,
|
|
356
|
+
with a red shared boundary, gray component courtyards, and green fanout-exit
|
|
357
|
+
markers. Pass an output directory and dataset id to render one dataset, for
|
|
358
|
+
example `bun scripts/render-dataset-pngs.ts docs/images dataset06`. Failed
|
|
359
|
+
regressions render their best partial attempt with a visible `INCOMPLETE`
|
|
360
|
+
label.
|
|
361
|
+
|
|
362
|
+
## Scope
|
|
363
|
+
|
|
364
|
+
This package owns the BGA pad-to-breakout prefix. It does not replace the
|
|
365
|
+
board-level autorouter, length-match buses, or route arbitrary obstacles between
|
|
366
|
+
the breakout boundary and the final destination.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { Obstacle, SimpleRouteJson } from "@tscircuit/capacity-autorouter"
|
|
2
|
+
import type { FanoutRoutePlan } from "./types"
|
|
3
|
+
|
|
4
|
+
function createViaObstacle(
|
|
5
|
+
plan: FanoutRoutePlan,
|
|
6
|
+
layerNames: string[],
|
|
7
|
+
): Obstacle | null {
|
|
8
|
+
if (!plan.via) return null
|
|
9
|
+
const zLayers = plan.via.spanLayers.map((layer) => {
|
|
10
|
+
const layerIndex = layerNames.indexOf(layer)
|
|
11
|
+
if (layerIndex < 0) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`FanoutSolver: via for "${plan.connectionName}" uses unknown layer "${layer}"`,
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
return layerIndex
|
|
17
|
+
})
|
|
18
|
+
return {
|
|
19
|
+
obstacleId: `fanout-via:${plan.connectionName}`,
|
|
20
|
+
type: "rect",
|
|
21
|
+
center: plan.via.center,
|
|
22
|
+
width: plan.via.diameter,
|
|
23
|
+
height: plan.via.diameter,
|
|
24
|
+
layers: plan.via.spanLayers,
|
|
25
|
+
zLayers,
|
|
26
|
+
__zLayers: zLayers,
|
|
27
|
+
connectedTo: [plan.connectionName, plan.trace.pcb_trace_id],
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildOutputSimpleRouteJson(params: {
|
|
32
|
+
inputSrj: SimpleRouteJson
|
|
33
|
+
plans: FanoutRoutePlan[]
|
|
34
|
+
layerNames: string[]
|
|
35
|
+
}): SimpleRouteJson {
|
|
36
|
+
const { inputSrj, plans, layerNames } = params
|
|
37
|
+
const outputConnections = inputSrj.connections.map((connection) => ({
|
|
38
|
+
...connection,
|
|
39
|
+
pointsToConnect: connection.pointsToConnect.map((point) => ({ ...point })),
|
|
40
|
+
}))
|
|
41
|
+
const viaObstacles: Obstacle[] = []
|
|
42
|
+
const planeTerminatedConnectionNames = new Set<string>()
|
|
43
|
+
|
|
44
|
+
for (const plan of plans) {
|
|
45
|
+
const connection = outputConnections[plan.connectionIndex]
|
|
46
|
+
if (!connection) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`FanoutSolver: output connection index ${plan.connectionIndex} is missing`,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
connection.pointsToConnect[plan.sourcePointIndex] = {
|
|
52
|
+
x: plan.exitPoint.x,
|
|
53
|
+
y: plan.exitPoint.y,
|
|
54
|
+
layer: plan.targetLayer,
|
|
55
|
+
pointId:
|
|
56
|
+
plan.termination.type === "plane"
|
|
57
|
+
? `fanout-plane:${plan.connectionName}`
|
|
58
|
+
: `fanout-exit:${plan.connectionName}`,
|
|
59
|
+
}
|
|
60
|
+
if (plan.termination.type === "plane") {
|
|
61
|
+
planeTerminatedConnectionNames.add(plan.connectionName)
|
|
62
|
+
}
|
|
63
|
+
const viaObstacle = createViaObstacle(plan, layerNames)
|
|
64
|
+
if (viaObstacle) viaObstacles.push(viaObstacle)
|
|
65
|
+
}
|
|
66
|
+
const coordinateRoutePoints = plans.flatMap((plan) =>
|
|
67
|
+
plan.trace.route.filter(
|
|
68
|
+
(
|
|
69
|
+
routePoint,
|
|
70
|
+
): routePoint is Extract<typeof routePoint, { x: number; y: number }> =>
|
|
71
|
+
"x" in routePoint && "y" in routePoint,
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
const boundsMargin = Math.max(
|
|
75
|
+
inputSrj.defaultObstacleMargin ?? 0,
|
|
76
|
+
inputSrj.minTraceWidth,
|
|
77
|
+
)
|
|
78
|
+
const outputBounds =
|
|
79
|
+
coordinateRoutePoints.length === 0
|
|
80
|
+
? { ...inputSrj.bounds }
|
|
81
|
+
: {
|
|
82
|
+
minX: Math.min(
|
|
83
|
+
inputSrj.bounds.minX,
|
|
84
|
+
...coordinateRoutePoints.map(
|
|
85
|
+
(routePoint) => routePoint.x - boundsMargin,
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
maxX: Math.max(
|
|
89
|
+
inputSrj.bounds.maxX,
|
|
90
|
+
...coordinateRoutePoints.map(
|
|
91
|
+
(routePoint) => routePoint.x + boundsMargin,
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
minY: Math.min(
|
|
95
|
+
inputSrj.bounds.minY,
|
|
96
|
+
...coordinateRoutePoints.map(
|
|
97
|
+
(routePoint) => routePoint.y - boundsMargin,
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
maxY: Math.max(
|
|
101
|
+
inputSrj.bounds.maxY,
|
|
102
|
+
...coordinateRoutePoints.map(
|
|
103
|
+
(routePoint) => routePoint.y + boundsMargin,
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
...inputSrj,
|
|
110
|
+
bounds: outputBounds,
|
|
111
|
+
connections: outputConnections.filter(
|
|
112
|
+
(connection) => !planeTerminatedConnectionNames.has(connection.name),
|
|
113
|
+
),
|
|
114
|
+
buses: inputSrj.buses
|
|
115
|
+
?.map((bus) => ({
|
|
116
|
+
...bus,
|
|
117
|
+
connectionNames: bus.connectionNames.filter(
|
|
118
|
+
(connectionName) =>
|
|
119
|
+
!planeTerminatedConnectionNames.has(connectionName),
|
|
120
|
+
),
|
|
121
|
+
}))
|
|
122
|
+
.filter((bus) => bus.connectionNames.length > 0),
|
|
123
|
+
obstacles: [
|
|
124
|
+
...inputSrj.obstacles.map((obstacle) => ({
|
|
125
|
+
...obstacle,
|
|
126
|
+
center: { ...obstacle.center },
|
|
127
|
+
layers: [...obstacle.layers],
|
|
128
|
+
connectedTo: [...obstacle.connectedTo],
|
|
129
|
+
})),
|
|
130
|
+
...viaObstacles,
|
|
131
|
+
],
|
|
132
|
+
traces: [
|
|
133
|
+
...(inputSrj.traces ?? []).map((trace) => ({
|
|
134
|
+
...trace,
|
|
135
|
+
route: trace.route.map((routePoint) => ({ ...routePoint })),
|
|
136
|
+
})),
|
|
137
|
+
...plans.map((plan) => plan.trace),
|
|
138
|
+
],
|
|
139
|
+
}
|
|
140
|
+
}
|