@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 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
+ ![clad1 RP2040 fanout reproduction](docs/images/dataset06/sample001.png)
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
+ }