@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 CHANGED
@@ -29,6 +29,10 @@ and treats each bus-layer decision atomically.
29
29
  (`left`, `right`, `top`, or `bottom`) or corner (`top-left`, `top-right`,
30
30
  `bottom-left`, or `bottom-right`). A corner chooses a compatible adjacent
31
31
  edge and reserves the bus at that end of the border.
32
+ - Accepts an unambiguous `exitPosition` bus field when the local pad escape and
33
+ final boundary edge differ. For example, `rightside_top` escapes locally
34
+ upward into the upper band and terminates on the right boundary, while
35
+ `topside_right` escapes locally rightward and terminates on the top boundary.
32
36
  - `availableCornersAndSides` can restrict every boundary-terminated bus to
33
37
  named regions of the shared boundary. For example,
34
38
  `['top_left', 'top_middle', 'top_right']` allows only top-edge exits;
@@ -161,6 +165,16 @@ const autorouter = new CapacityMeshSolver(
161
165
  autorouter.solve()
162
166
  ```
163
167
 
168
+ Canonical exit positions are edge-first: `topside_left`, `topside_center`,
169
+ `topside_right`, `rightside_top`, `rightside_center`, `rightside_bottom`,
170
+ `bottomside_right`, `bottomside_center`, `bottomside_left`, `leftside_bottom`,
171
+ `leftside_center`, `leftside_top`, and `center`. They normalize atomically into
172
+ the local `direction`, boundary-band `preferredExit`, and physical `exitEdge`;
173
+ conflicting bus-level legacy fields are rejected. Existing buses that omit
174
+ `exitPosition` retain their previous behavior. Hosts can import
175
+ `getFanoutExitPositionConfig` to inspect the same normalized tuple without
176
+ duplicating this mapping.
177
+
164
178
  The downstream callback is optional. It lets the application choose its
165
179
  board-level router while keeping `@tscircuit/fanout-solver` free of a runtime
166
180
  autorouter import. Returned traces are still accepted only after the fanout
@@ -0,0 +1,52 @@
1
+ import type { FanoutBorderTarget, FanoutDirection, FanoutEdge } from "./types"
2
+
3
+ export function getExitEdgeForDirection(
4
+ direction: FanoutDirection,
5
+ ): FanoutEdge {
6
+ switch (direction) {
7
+ case "left":
8
+ return "left"
9
+ case "right":
10
+ return "right"
11
+ case "up":
12
+ return "top"
13
+ case "down":
14
+ return "bottom"
15
+ }
16
+ }
17
+
18
+ export function getDirectionForExitEdge(exitEdge: FanoutEdge): FanoutDirection {
19
+ switch (exitEdge) {
20
+ case "left":
21
+ return "left"
22
+ case "right":
23
+ return "right"
24
+ case "top":
25
+ return "up"
26
+ case "bottom":
27
+ return "down"
28
+ }
29
+ }
30
+
31
+ export function borderTargetIncludesEdge(
32
+ preferredExit: FanoutBorderTarget,
33
+ exitEdge: FanoutEdge,
34
+ ): boolean {
35
+ return preferredExit === exitEdge || preferredExit.includes(exitEdge)
36
+ }
37
+
38
+ /**
39
+ * Returns the lower/left or upper/right band selected along an explicit edge.
40
+ * Edge-center targets do not select a corner band.
41
+ */
42
+ export function getCornerBandSide(
43
+ exitEdge: FanoutEdge | undefined,
44
+ preferredExit: FanoutBorderTarget | undefined,
45
+ ): "minimum" | "maximum" | undefined {
46
+ if (!exitEdge || !preferredExit?.includes("-")) return undefined
47
+ if (!borderTargetIncludesEdge(preferredExit, exitEdge)) return undefined
48
+ if (exitEdge === "left" || exitEdge === "right") {
49
+ return preferredExit.startsWith("top-") ? "maximum" : "minimum"
50
+ }
51
+ return preferredExit.endsWith("-right") ? "maximum" : "minimum"
52
+ }
@@ -5,10 +5,9 @@ import type { FanoutRoutePlan, SimpleRouteJsonWithFanoutPlanes } from "./types"
5
5
  function createViaObstacle(
6
6
  plan: FanoutRoutePlan,
7
7
  layerNames: string[],
8
- endpoint = false,
8
+ via: NonNullable<FanoutRoutePlan["via"]>,
9
+ viaIndex: number | "endpoint",
9
10
  ): Obstacle | null {
10
- const via = endpoint ? plan.planeEndpointVia : plan.via
11
- if (!via) return null
12
11
  const zLayers = via.spanLayers.map((layer) => {
13
12
  const layerIndex = layerNames.indexOf(layer)
14
13
  if (layerIndex < 0) {
@@ -20,9 +19,12 @@ function createViaObstacle(
20
19
  })
21
20
  const outputIds = createFanoutOutputIds(plan)
22
21
  return {
23
- obstacleId: endpoint
24
- ? outputIds.planeEndpointViaObstacleId
25
- : outputIds.viaObstacleId,
22
+ obstacleId:
23
+ viaIndex === "endpoint"
24
+ ? outputIds.planeEndpointViaObstacleId
25
+ : viaIndex === 0
26
+ ? outputIds.viaObstacleId
27
+ : `${outputIds.viaObstacleId}:${viaIndex}`,
26
28
  type: "rect",
27
29
  center: via.center,
28
30
  width: via.diameter,
@@ -67,10 +69,30 @@ export function buildOutputSimpleRouteJson(params: {
67
69
  if (plan.termination.type === "plane") {
68
70
  planeTerminatedConnectionNames.add(plan.connectionName)
69
71
  }
70
- const viaObstacle = createViaObstacle(plan, layerNames)
71
- if (viaObstacle) viaObstacles.push(viaObstacle)
72
- const endpointViaObstacle = createViaObstacle(plan, layerNames, true)
73
- if (endpointViaObstacle) viaObstacles.push(endpointViaObstacle)
72
+ if (plan.via) {
73
+ const viaObstacle = createViaObstacle(plan, layerNames, plan.via, 0)
74
+ if (viaObstacle) viaObstacles.push(viaObstacle)
75
+ }
76
+ for (const [additionalViaIndex, via] of (
77
+ plan.additionalVias ?? []
78
+ ).entries()) {
79
+ const viaObstacle = createViaObstacle(
80
+ plan,
81
+ layerNames,
82
+ via,
83
+ additionalViaIndex + 1,
84
+ )
85
+ if (viaObstacle) viaObstacles.push(viaObstacle)
86
+ }
87
+ if (plan.planeEndpointVia) {
88
+ const endpointViaObstacle = createViaObstacle(
89
+ plan,
90
+ layerNames,
91
+ plan.planeEndpointVia,
92
+ "endpoint",
93
+ )
94
+ if (endpointViaObstacle) viaObstacles.push(endpointViaObstacle)
95
+ }
74
96
  }
75
97
  const planTraces = plans.flatMap((plan) => [
76
98
  plan.trace,
@@ -0,0 +1,78 @@
1
+ import type { FanoutExitPosition, FanoutExitPositionConfig } from "./types"
2
+
3
+ const FANOUT_EXIT_POSITION_CONFIGS: Readonly<
4
+ Record<FanoutExitPosition, Readonly<FanoutExitPositionConfig>>
5
+ > = {
6
+ topside_left: {
7
+ direction: "left",
8
+ preferredExit: "top-left",
9
+ exitEdge: "top",
10
+ },
11
+ topside_center: {
12
+ direction: "up",
13
+ preferredExit: "top",
14
+ exitEdge: "top",
15
+ },
16
+ topside_right: {
17
+ direction: "right",
18
+ preferredExit: "top-right",
19
+ exitEdge: "top",
20
+ },
21
+ rightside_top: {
22
+ direction: "up",
23
+ preferredExit: "top-right",
24
+ exitEdge: "right",
25
+ },
26
+ rightside_center: {
27
+ direction: "right",
28
+ preferredExit: "right",
29
+ exitEdge: "right",
30
+ },
31
+ rightside_bottom: {
32
+ direction: "down",
33
+ preferredExit: "bottom-right",
34
+ exitEdge: "right",
35
+ },
36
+ bottomside_right: {
37
+ direction: "right",
38
+ preferredExit: "bottom-right",
39
+ exitEdge: "bottom",
40
+ },
41
+ bottomside_center: {
42
+ direction: "down",
43
+ preferredExit: "bottom",
44
+ exitEdge: "bottom",
45
+ },
46
+ bottomside_left: {
47
+ direction: "left",
48
+ preferredExit: "bottom-left",
49
+ exitEdge: "bottom",
50
+ },
51
+ leftside_bottom: {
52
+ direction: "down",
53
+ preferredExit: "bottom-left",
54
+ exitEdge: "left",
55
+ },
56
+ leftside_center: {
57
+ direction: "left",
58
+ preferredExit: "left",
59
+ exitEdge: "left",
60
+ },
61
+ leftside_top: {
62
+ direction: "up",
63
+ preferredExit: "top-left",
64
+ exitEdge: "left",
65
+ },
66
+ center: {},
67
+ }
68
+
69
+ /** Resolves a canonical exit position into the solver's orthogonal fields. */
70
+ export function getFanoutExitPositionConfig(
71
+ exitPosition: FanoutExitPosition,
72
+ ): Readonly<FanoutExitPositionConfig> {
73
+ const config = FANOUT_EXIT_POSITION_CONFIGS[exitPosition]
74
+ if (!config) {
75
+ throw new Error(`Invalid fanout exit position "${exitPosition}"`)
76
+ }
77
+ return config
78
+ }