@tscircuit/fanout-solver 0.0.35 → 0.0.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/lib/boundary-exit.ts +52 -0
- package/lib/build-output.ts +32 -10
- package/lib/fanout-exit-position.ts +78 -0
- package/lib/fanout-solver.ts +267 -38
- package/lib/index.ts +16 -13
- package/lib/prepare-buses.ts +261 -36
- package/lib/route-bus.ts +543 -59
- package/lib/route-single-layer-adaptive-exits.ts +2 -2
- package/lib/route-single-layer-push-shove.ts +2 -1
- package/lib/route-via-minimal-winding.ts +898 -0
- package/lib/types.ts +63 -3
- package/lib/validate-fanout-solution.ts +63 -11
- package/package.json +1 -1
package/lib/types.ts
CHANGED
|
@@ -21,6 +21,35 @@ export type FanoutCorner =
|
|
|
21
21
|
|
|
22
22
|
export type FanoutBorderTarget = FanoutEdge | FanoutCorner
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* An unambiguous fanout exit position.
|
|
26
|
+
*
|
|
27
|
+
* The `*side` prefix names the physical shared-boundary edge. A corner suffix
|
|
28
|
+
* names the local escape direction and band on that edge; a `_center` suffix
|
|
29
|
+
* escapes outward through the middle of that edge. `center` leaves all three
|
|
30
|
+
* exit fields unconstrained.
|
|
31
|
+
*/
|
|
32
|
+
export type FanoutExitPosition =
|
|
33
|
+
| "topside_left"
|
|
34
|
+
| "topside_center"
|
|
35
|
+
| "topside_right"
|
|
36
|
+
| "rightside_top"
|
|
37
|
+
| "rightside_center"
|
|
38
|
+
| "rightside_bottom"
|
|
39
|
+
| "bottomside_right"
|
|
40
|
+
| "bottomside_center"
|
|
41
|
+
| "bottomside_left"
|
|
42
|
+
| "leftside_bottom"
|
|
43
|
+
| "leftside_center"
|
|
44
|
+
| "leftside_top"
|
|
45
|
+
| "center"
|
|
46
|
+
|
|
47
|
+
export interface FanoutExitPositionConfig {
|
|
48
|
+
direction?: FanoutDirection
|
|
49
|
+
preferredExit?: FanoutBorderTarget
|
|
50
|
+
exitEdge?: FanoutEdge
|
|
51
|
+
}
|
|
52
|
+
|
|
24
53
|
/**
|
|
25
54
|
* A directed region of the shared fanout boundary. Corner regions are named
|
|
26
55
|
* after the edge they belong to, so `top_left` exits through the top edge and
|
|
@@ -74,8 +103,19 @@ export type FanoutBusTermination =
|
|
|
74
103
|
export interface FanoutBusSpec extends SimpleRouteBus {
|
|
75
104
|
/** Component whose connection endpoints should be escaped. */
|
|
76
105
|
sourceComponentId?: string
|
|
106
|
+
/** Canonical local-escape, boundary-edge, and edge-band selection. */
|
|
107
|
+
exitPosition?: FanoutExitPosition
|
|
108
|
+
/** Direction used to leave the source pads locally. */
|
|
77
109
|
direction?: FanoutDirection
|
|
78
110
|
preferredExit?: FanoutBorderTarget
|
|
111
|
+
/**
|
|
112
|
+
* Physical boundary edge on which the fanout endpoints terminate.
|
|
113
|
+
*
|
|
114
|
+
* This is independent of `direction`: a bus may escape its pads `up`, use
|
|
115
|
+
* the `top-right` band, and terminate on the `right` edge. An explicit edge
|
|
116
|
+
* plus a corner-valued `preferredExit` enables the packed boundary channel.
|
|
117
|
+
*/
|
|
118
|
+
exitEdge?: FanoutEdge
|
|
79
119
|
/** Layers to which this bus is allowed to escape. */
|
|
80
120
|
allowedLayers?: readonly string[]
|
|
81
121
|
/**
|
|
@@ -84,9 +124,14 @@ export interface FanoutBusSpec extends SimpleRouteBus {
|
|
|
84
124
|
* connection's electrical endpoint in SimpleRouteJson.
|
|
85
125
|
*
|
|
86
126
|
* A caller coordinating two fanouts can pass the paired fanout's selected
|
|
87
|
-
* exit here so both sides aim toward the same track.
|
|
127
|
+
* exit here so both sides aim toward the same track. When every target also
|
|
128
|
+
* includes a layer, the solver may use the bus's other allowed layer as an
|
|
129
|
+
* internal crossover channel, then return every exit to one common bus layer
|
|
130
|
+
* in the supplied target order.
|
|
88
131
|
*/
|
|
89
|
-
connectionExitTargets?: Readonly<
|
|
132
|
+
connectionExitTargets?: Readonly<
|
|
133
|
+
Record<string, Point2D & { readonly layer?: string }>
|
|
134
|
+
>
|
|
90
135
|
/**
|
|
91
136
|
* Defaults to `{ type: "boundary" }`.
|
|
92
137
|
*
|
|
@@ -239,15 +284,23 @@ export interface PreparedConnection {
|
|
|
239
284
|
sourceObstacle: Obstacle
|
|
240
285
|
targetPoint: ConnectionPoint
|
|
241
286
|
/** Preferred downstream point used to choose the boundary exit track. */
|
|
242
|
-
exitTargetPoint?: Point2D
|
|
287
|
+
exitTargetPoint?: Point2D & { readonly layer?: string }
|
|
288
|
+
/** Whether the caller supplied the layered handoff metadata needed to coordinate winding. */
|
|
289
|
+
hasExplicitLayeredExitTarget?: boolean
|
|
243
290
|
}
|
|
244
291
|
|
|
245
292
|
export interface PreparedBus {
|
|
246
293
|
busId: string
|
|
247
294
|
direction: FanoutDirection
|
|
248
295
|
preferredExit?: FanoutBorderTarget
|
|
296
|
+
/** Explicit final boundary edge. Omitted for legacy direction-based exits. */
|
|
297
|
+
exitEdge?: FanoutEdge
|
|
298
|
+
/** Total connection count sharing this explicit edge/corner band. */
|
|
299
|
+
cornerBandConnectionCount?: number
|
|
249
300
|
/** Layers to which this bus is allowed to escape. */
|
|
250
301
|
allowedLayers?: readonly string[]
|
|
302
|
+
/** Bus layers that also survive the solver-wide escape-layer restriction. */
|
|
303
|
+
routableEscapeLayers?: readonly string[]
|
|
251
304
|
termination: FanoutBusTermination
|
|
252
305
|
connections: PreparedConnection[]
|
|
253
306
|
componentId: string
|
|
@@ -287,11 +340,18 @@ export interface FanoutRoutePlan {
|
|
|
287
340
|
targetPoint: ConnectionPoint
|
|
288
341
|
targetLayer: string
|
|
289
342
|
termination: FanoutBusTermination
|
|
343
|
+
/** Local pad escape direction. */
|
|
290
344
|
direction: FanoutDirection
|
|
345
|
+
/** Explicit physical boundary edge used by a packed corner channel. */
|
|
346
|
+
exitEdge?: FanoutEdge
|
|
347
|
+
/** Lower/left or upper/right band reserved along `exitEdge`. */
|
|
348
|
+
cornerBandSide?: "minimum" | "maximum"
|
|
291
349
|
exitPoint: Point2D
|
|
292
350
|
trace: SimplifiedPcbTrace
|
|
293
351
|
segments: RoutedSegment[]
|
|
294
352
|
via?: RoutedVia
|
|
353
|
+
/** Additional layer transitions used by an explicit winding channel. */
|
|
354
|
+
additionalVias?: RoutedVia[]
|
|
295
355
|
/** Optional capacitor-side dogbone reserved and emitted with a plane escape. */
|
|
296
356
|
planeEndpointTrace?: SimplifiedPcbTrace
|
|
297
357
|
planeEndpointSegments?: RoutedSegment[]
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
SimpleRouteJson,
|
|
4
4
|
SimplifiedPcbTrace,
|
|
5
5
|
} from "@tscircuit/capacity-autorouter"
|
|
6
|
+
import { getCornerBandSide } from "./boundary-exit"
|
|
6
7
|
import {
|
|
7
8
|
distance,
|
|
8
9
|
distancePointToObstacle,
|
|
@@ -11,13 +12,14 @@ import {
|
|
|
11
12
|
distanceSegmentToSegment,
|
|
12
13
|
segmentsAreClear,
|
|
13
14
|
} from "./geometry"
|
|
15
|
+
import { getAllRoutedTraceCopper } from "./get-routed-trace-copper"
|
|
14
16
|
import {
|
|
15
17
|
connectionsShareElectricalNet,
|
|
16
18
|
obstacleSharesElectricalNet,
|
|
17
19
|
} from "./net-identity"
|
|
18
|
-
import { getAllRoutedTraceCopper } from "./get-routed-trace-copper"
|
|
19
20
|
import type {
|
|
20
21
|
Bounds,
|
|
22
|
+
FanoutEdge,
|
|
21
23
|
FanoutRoutePlan,
|
|
22
24
|
FanoutValidationIssue,
|
|
23
25
|
FanoutValidationReport,
|
|
@@ -42,9 +44,11 @@ function getPlanSegments(plan: FanoutRoutePlan): RoutedSegment[] {
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
function getPlanVias(plan: FanoutRoutePlan) {
|
|
45
|
-
return [
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
return [
|
|
48
|
+
plan.via,
|
|
49
|
+
...(plan.additionalVias ?? []),
|
|
50
|
+
plan.planeEndpointVia,
|
|
51
|
+
].filter((via): via is NonNullable<FanoutRoutePlan["via"]> => Boolean(via))
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
function connectionPointsMatch(
|
|
@@ -73,6 +77,29 @@ function pointIsOnBoundary(point: Point2D, boundary: Bounds): boolean {
|
|
|
73
77
|
return inside && onEdge
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
function pointIsOnBoundaryEdge(
|
|
81
|
+
point: Point2D,
|
|
82
|
+
edge: FanoutEdge,
|
|
83
|
+
boundary: Bounds,
|
|
84
|
+
): boolean {
|
|
85
|
+
const inside =
|
|
86
|
+
point.x >= boundary.minX - EPSILON &&
|
|
87
|
+
point.x <= boundary.maxX + EPSILON &&
|
|
88
|
+
point.y >= boundary.minY - EPSILON &&
|
|
89
|
+
point.y <= boundary.maxY + EPSILON
|
|
90
|
+
if (!inside) return false
|
|
91
|
+
switch (edge) {
|
|
92
|
+
case "left":
|
|
93
|
+
return Math.abs(point.x - boundary.minX) <= EPSILON
|
|
94
|
+
case "right":
|
|
95
|
+
return Math.abs(point.x - boundary.maxX) <= EPSILON
|
|
96
|
+
case "top":
|
|
97
|
+
return Math.abs(point.y - boundary.maxY) <= EPSILON
|
|
98
|
+
case "bottom":
|
|
99
|
+
return Math.abs(point.y - boundary.minY) <= EPSILON
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
76
103
|
function pointIsInsideBounds(point: Point2D, bounds: Bounds): boolean {
|
|
77
104
|
return (
|
|
78
105
|
point.x >= bounds.minX - EPSILON &&
|
|
@@ -236,6 +263,31 @@ function validatePlanStructure(params: {
|
|
|
236
263
|
plan,
|
|
237
264
|
)
|
|
238
265
|
}
|
|
266
|
+
const expectedCornerBandSide = getCornerBandSide(
|
|
267
|
+
preparedBus?.exitEdge,
|
|
268
|
+
preparedBus?.preferredExit,
|
|
269
|
+
)
|
|
270
|
+
if (
|
|
271
|
+
preparedBus?.exitEdge !== plan.exitEdge ||
|
|
272
|
+
expectedCornerBandSide !== plan.cornerBandSide
|
|
273
|
+
) {
|
|
274
|
+
addIssue(
|
|
275
|
+
issues,
|
|
276
|
+
"output-exit-mismatch",
|
|
277
|
+
`Plan ${plan.connectionName} does not retain its prepared boundary edge and band`,
|
|
278
|
+
plan,
|
|
279
|
+
)
|
|
280
|
+
} else if (
|
|
281
|
+
preparedBus?.exitEdge &&
|
|
282
|
+
!pointIsOnBoundaryEdge(plan.exitPoint, preparedBus.exitEdge, sharedBoundary)
|
|
283
|
+
) {
|
|
284
|
+
addIssue(
|
|
285
|
+
issues,
|
|
286
|
+
"output-exit-mismatch",
|
|
287
|
+
`Plan ${plan.connectionName} does not terminate on its declared ${preparedBus.exitEdge} edge`,
|
|
288
|
+
plan,
|
|
289
|
+
)
|
|
290
|
+
}
|
|
239
291
|
if (plan.segments.length === 0 || plan.length <= EPSILON) {
|
|
240
292
|
addIssue(
|
|
241
293
|
issues,
|
|
@@ -291,13 +343,13 @@ function validatePlanStructure(params: {
|
|
|
291
343
|
plan,
|
|
292
344
|
)
|
|
293
345
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
) {
|
|
346
|
+
const transitionVia = getPlanVias(plan).find(
|
|
347
|
+
(via) =>
|
|
348
|
+
pointsMatch(previous.end, via.center) &&
|
|
349
|
+
via.spanLayers.includes(previous.layer) &&
|
|
350
|
+
via.spanLayers.includes(current.layer),
|
|
351
|
+
)
|
|
352
|
+
if (previous.layer !== current.layer && !transitionVia) {
|
|
301
353
|
addIssue(
|
|
302
354
|
issues,
|
|
303
355
|
"disconnected-trace",
|