@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/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<Record<string, Point2D>>
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 [plan.via, plan.planeEndpointVia].filter(
46
- (via): via is NonNullable<FanoutRoutePlan["via"]> => Boolean(via),
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
- if (
295
- previous.layer !== current.layer &&
296
- (!plan.via ||
297
- !pointsMatch(previous.end, plan.via.center) ||
298
- !plan.via.spanLayers.includes(previous.layer) ||
299
- !plan.via.spanLayers.includes(current.layer))
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",