@tscircuit/fanout-solver 0.0.35 → 0.0.36
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/fanout-exit-position.ts +78 -0
- package/lib/fanout-solver.ts +53 -8
- package/lib/index.ts +16 -13
- package/lib/prepare-buses.ts +255 -34
- package/lib/route-bus.ts +199 -12
- package/lib/route-single-layer-adaptive-exits.ts +2 -2
- package/lib/route-single-layer-push-shove.ts +2 -1
- package/lib/types.ts +49 -0
- package/lib/validate-fanout-solution.ts +51 -1
- package/package.json +1 -1
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
|
+
}
|
|
@@ -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
|
+
}
|
package/lib/fanout-solver.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter"
|
|
2
2
|
import { BaseSolver } from "@tscircuit/solver-utils"
|
|
3
3
|
import type { GraphicsObject } from "graphics-debug"
|
|
4
|
+
import { getCornerBandSide } from "./boundary-exit"
|
|
4
5
|
import { buildOutputSimpleRouteJson } from "./build-output"
|
|
5
6
|
import {
|
|
6
|
-
completeOriginalEndpoints,
|
|
7
7
|
type CompleteOriginalEndpointsResult,
|
|
8
|
+
completeOriginalEndpoints,
|
|
8
9
|
} from "./complete-original-endpoints"
|
|
9
10
|
import { generateLayerAssignments, getCopperLayerNames } from "./layer-names"
|
|
10
11
|
import {
|
|
@@ -12,14 +13,12 @@ import {
|
|
|
12
13
|
resolveAvailableBoundaryRegions,
|
|
13
14
|
} from "./prepare-buses"
|
|
14
15
|
import {
|
|
16
|
+
type RouteBusStaticClearanceCache,
|
|
15
17
|
routeBus,
|
|
16
18
|
routeBusAlternatives,
|
|
17
|
-
type RouteBusStaticClearanceCache,
|
|
18
19
|
} from "./route-bus"
|
|
19
20
|
import { routeSingleLayerWithAdaptiveExits } from "./route-single-layer-adaptive-exits"
|
|
20
21
|
import { routeSingleLayerWithPushAndShove } from "./route-single-layer-push-shove"
|
|
21
|
-
import { validateFanoutSolution } from "./validate-fanout-solution"
|
|
22
|
-
import { visualizeSimpleRouteJson } from "./visualize-simple-route-json"
|
|
23
22
|
import type {
|
|
24
23
|
AssignmentAttempt,
|
|
25
24
|
Bounds,
|
|
@@ -30,6 +29,8 @@ import type {
|
|
|
30
29
|
FanoutSolverOutput,
|
|
31
30
|
PreparedBus,
|
|
32
31
|
} from "./types"
|
|
32
|
+
import { validateFanoutSolution } from "./validate-fanout-solution"
|
|
33
|
+
import { visualizeSimpleRouteJson } from "./visualize-simple-route-json"
|
|
33
34
|
|
|
34
35
|
interface ResolvedFanoutConfig {
|
|
35
36
|
traceWidth: number
|
|
@@ -146,6 +147,45 @@ function resolveConfig(
|
|
|
146
147
|
}
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
function validateCornerBandCapacities(
|
|
151
|
+
buses: readonly PreparedBus[],
|
|
152
|
+
config: ResolvedFanoutConfig,
|
|
153
|
+
): void {
|
|
154
|
+
const checkedBands = new Set<string>()
|
|
155
|
+
const exitPitch = Math.max(
|
|
156
|
+
config.traceWidth + config.clearance,
|
|
157
|
+
config.viaDiameter + config.clearance,
|
|
158
|
+
)
|
|
159
|
+
// Keep the block clear of the physical end of the edge and leave one
|
|
160
|
+
// unoccupied via-pitch between the minimum and maximum quarter bands.
|
|
161
|
+
const endInset = Math.max(
|
|
162
|
+
config.viaDiameter / 2 + config.clearance,
|
|
163
|
+
exitPitch,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
for (const bus of buses) {
|
|
167
|
+
const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
168
|
+
if (!bus.exitEdge || !side) continue
|
|
169
|
+
const bandKey = `${bus.exitEdge}:${side}`
|
|
170
|
+
if (checkedBands.has(bandKey)) continue
|
|
171
|
+
checkedBands.add(bandKey)
|
|
172
|
+
|
|
173
|
+
const edgeLength =
|
|
174
|
+
bus.exitEdge === "left" || bus.exitEdge === "right"
|
|
175
|
+
? bus.sharedBoundary.maxY - bus.sharedBoundary.minY
|
|
176
|
+
: bus.sharedBoundary.maxX - bus.sharedBoundary.minX
|
|
177
|
+
const connectionCount =
|
|
178
|
+
bus.cornerBandConnectionCount ?? bus.connections.length
|
|
179
|
+
const halfTrackSpan = ((connectionCount - 1) * exitPitch) / 2
|
|
180
|
+
const availableHalfTrackSpan = edgeLength / 4 - endInset
|
|
181
|
+
if (halfTrackSpan > availableHalfTrackSpan + 1e-6) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`FanoutSolver: ${side} band on the ${bus.exitEdge} edge cannot fit ${connectionCount} via-safe exits`,
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
149
189
|
function assignmentLoadPenalty(
|
|
150
190
|
assignment: Readonly<Record<string, string>>,
|
|
151
191
|
buses: readonly PreparedBus[],
|
|
@@ -411,6 +451,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
411
451
|
}
|
|
412
452
|
this.config = resolveConfig(inputSrj, options)
|
|
413
453
|
this.preparedBuses = prepareFanoutBuses(this.routingSrj, options)
|
|
454
|
+
validateCornerBandCapacities(this.preparedBuses, this.config)
|
|
414
455
|
for (const bus of this.preparedBuses) {
|
|
415
456
|
for (const allowedLayer of bus.allowedLayers ?? []) {
|
|
416
457
|
if (!this.config.layerNames.includes(allowedLayer)) {
|
|
@@ -566,7 +607,13 @@ export class FanoutSolver extends BaseSolver {
|
|
|
566
607
|
let failedBusIds: string[] = []
|
|
567
608
|
let blockingBusCounts = new Map<string, number>()
|
|
568
609
|
const isSingleLayerFanout = this.config.escapeLayers.length === 1
|
|
569
|
-
|
|
610
|
+
const useSingleLayerPushAndShove =
|
|
611
|
+
isSingleLayerFanout &&
|
|
612
|
+
this.config.singleLayerPushAndShove &&
|
|
613
|
+
!this.preparedBuses.some(
|
|
614
|
+
(bus) => bus.exitEdge && bus.preferredExit?.includes("-"),
|
|
615
|
+
)
|
|
616
|
+
if (useSingleLayerPushAndShove) {
|
|
570
617
|
const singleLayerParams = {
|
|
571
618
|
srj: this.routingSrj,
|
|
572
619
|
buses: this.preparedBuses,
|
|
@@ -609,9 +656,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
609
656
|
)
|
|
610
657
|
|
|
611
658
|
let routingPrefixKey = `${routingStrategy}|`
|
|
612
|
-
for (const bus of
|
|
613
|
-
? []
|
|
614
|
-
: busesInRoutingOrder) {
|
|
659
|
+
for (const bus of useSingleLayerPushAndShove ? [] : busesInRoutingOrder) {
|
|
615
660
|
const targetLayer = busLayerAssignments[bus.busId]
|
|
616
661
|
if (!targetLayer) {
|
|
617
662
|
throw new Error(
|
package/lib/index.ts
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
export { FanoutSolver } from "./fanout-solver"
|
|
2
1
|
export { completeOriginalEndpoints } from "./complete-original-endpoints"
|
|
2
|
+
export { getFanoutExitPositionConfig } from "./fanout-exit-position"
|
|
3
|
+
export { FanoutSolver } from "./fanout-solver"
|
|
3
4
|
export { getCopperLayerColor } from "./layer-colors"
|
|
4
5
|
export { getCopperLayerNames } from "./layer-names"
|
|
5
|
-
export { validateOriginalEndpointConnectivity } from "./validate-original-endpoint-connectivity"
|
|
6
|
-
export { validateRoutedCopperDrc } from "./validate-routed-copper-drc"
|
|
7
|
-
export { validateFanoutSolution } from "./validate-fanout-solution"
|
|
8
|
-
export type {
|
|
9
|
-
OriginalEndpointConnectivityIssue,
|
|
10
|
-
OriginalEndpointConnectivityReport,
|
|
11
|
-
} from "./validate-original-endpoint-connectivity"
|
|
12
|
-
export type {
|
|
13
|
-
RoutedCopperDrcIssue,
|
|
14
|
-
RoutedCopperDrcIssueCode,
|
|
15
|
-
RoutedCopperDrcReport,
|
|
16
|
-
} from "./validate-routed-copper-drc"
|
|
17
6
|
export type {
|
|
18
7
|
Bounds,
|
|
19
8
|
FanoutAttemptSummary,
|
|
@@ -30,6 +19,8 @@ export type {
|
|
|
30
19
|
FanoutDownstreamRouterOptions,
|
|
31
20
|
FanoutEdge,
|
|
32
21
|
FanoutEndpointCompletionReport,
|
|
22
|
+
FanoutExitPosition,
|
|
23
|
+
FanoutExitPositionConfig,
|
|
33
24
|
FanoutPlaneConnectivity,
|
|
34
25
|
FanoutPlaneTermination,
|
|
35
26
|
FanoutRoutePlan,
|
|
@@ -41,3 +32,15 @@ export type {
|
|
|
41
32
|
PreparedBus,
|
|
42
33
|
SimpleRouteJsonWithFanoutPlanes,
|
|
43
34
|
} from "./types"
|
|
35
|
+
export { validateFanoutSolution } from "./validate-fanout-solution"
|
|
36
|
+
export type {
|
|
37
|
+
OriginalEndpointConnectivityIssue,
|
|
38
|
+
OriginalEndpointConnectivityReport,
|
|
39
|
+
} from "./validate-original-endpoint-connectivity"
|
|
40
|
+
export { validateOriginalEndpointConnectivity } from "./validate-original-endpoint-connectivity"
|
|
41
|
+
export type {
|
|
42
|
+
RoutedCopperDrcIssue,
|
|
43
|
+
RoutedCopperDrcIssueCode,
|
|
44
|
+
RoutedCopperDrcReport,
|
|
45
|
+
} from "./validate-routed-copper-drc"
|
|
46
|
+
export { validateRoutedCopperDrc } from "./validate-routed-copper-drc"
|
package/lib/prepare-buses.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type {
|
|
|
4
4
|
SimpleRouteConnection,
|
|
5
5
|
SimpleRouteJson,
|
|
6
6
|
} from "@tscircuit/capacity-autorouter"
|
|
7
|
+
import { borderTargetIncludesEdge, getCornerBandSide } from "./boundary-exit"
|
|
8
|
+
import { getFanoutExitPositionConfig } from "./fanout-exit-position"
|
|
7
9
|
import { distance, pointIsInsideObstacle } from "./geometry"
|
|
8
10
|
import type {
|
|
9
11
|
Bounds,
|
|
@@ -12,6 +14,9 @@ import type {
|
|
|
12
14
|
FanoutBusSpec,
|
|
13
15
|
FanoutBusTermination,
|
|
14
16
|
FanoutDirection,
|
|
17
|
+
FanoutEdge,
|
|
18
|
+
FanoutExitPosition,
|
|
19
|
+
FanoutExitPositionConfig,
|
|
15
20
|
FanoutSolverOptions,
|
|
16
21
|
PreparedBus,
|
|
17
22
|
PreparedConnection,
|
|
@@ -20,6 +25,8 @@ import type {
|
|
|
20
25
|
export interface AvailableBoundaryRegion {
|
|
21
26
|
direction: FanoutDirection
|
|
22
27
|
preferredExit: FanoutBorderTarget
|
|
28
|
+
/** Physical shared-boundary edge selected by the first region token. */
|
|
29
|
+
exitEdge: FanoutEdge
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
const FANOUT_BORDER_TARGETS = new Set<FanoutBorderTarget>([
|
|
@@ -33,72 +40,90 @@ const FANOUT_BORDER_TARGETS = new Set<FanoutBorderTarget>([
|
|
|
33
40
|
"bottom-right",
|
|
34
41
|
])
|
|
35
42
|
|
|
43
|
+
const FANOUT_EDGES = new Set<FanoutEdge>(["left", "right", "top", "bottom"])
|
|
44
|
+
|
|
36
45
|
const AVAILABLE_BOUNDARY_REGIONS: Readonly<
|
|
37
46
|
Record<FanoutAvailableCornerAndSideInput, AvailableBoundaryRegion>
|
|
38
47
|
> = {
|
|
39
48
|
top_left: {
|
|
40
49
|
direction: "up",
|
|
41
50
|
preferredExit: "top-left",
|
|
51
|
+
exitEdge: "top",
|
|
42
52
|
},
|
|
43
53
|
top_middle: {
|
|
44
54
|
direction: "up",
|
|
45
55
|
preferredExit: "top",
|
|
56
|
+
exitEdge: "top",
|
|
46
57
|
},
|
|
47
58
|
top_right: {
|
|
48
59
|
direction: "up",
|
|
49
60
|
preferredExit: "top-right",
|
|
61
|
+
exitEdge: "top",
|
|
50
62
|
},
|
|
51
63
|
right_top: {
|
|
52
64
|
direction: "right",
|
|
53
65
|
preferredExit: "top-right",
|
|
66
|
+
exitEdge: "right",
|
|
54
67
|
},
|
|
55
68
|
right_middle: {
|
|
56
69
|
direction: "right",
|
|
57
70
|
preferredExit: "right",
|
|
71
|
+
exitEdge: "right",
|
|
58
72
|
},
|
|
59
73
|
right_bottom: {
|
|
60
74
|
direction: "right",
|
|
61
75
|
preferredExit: "bottom-right",
|
|
76
|
+
exitEdge: "right",
|
|
62
77
|
},
|
|
63
78
|
bottom_right: {
|
|
64
79
|
direction: "down",
|
|
65
80
|
preferredExit: "bottom-right",
|
|
81
|
+
exitEdge: "bottom",
|
|
66
82
|
},
|
|
67
83
|
bottom_middle: {
|
|
68
84
|
direction: "down",
|
|
69
85
|
preferredExit: "bottom",
|
|
86
|
+
exitEdge: "bottom",
|
|
70
87
|
},
|
|
71
88
|
bottom_left: {
|
|
72
89
|
direction: "down",
|
|
73
90
|
preferredExit: "bottom-left",
|
|
91
|
+
exitEdge: "bottom",
|
|
74
92
|
},
|
|
75
93
|
left_bottom: {
|
|
76
94
|
direction: "left",
|
|
77
95
|
preferredExit: "bottom-left",
|
|
96
|
+
exitEdge: "left",
|
|
78
97
|
},
|
|
79
98
|
left_middle: {
|
|
80
99
|
direction: "left",
|
|
81
100
|
preferredExit: "left",
|
|
101
|
+
exitEdge: "left",
|
|
82
102
|
},
|
|
83
103
|
left_top: {
|
|
84
104
|
direction: "left",
|
|
85
105
|
preferredExit: "top-left",
|
|
106
|
+
exitEdge: "left",
|
|
86
107
|
},
|
|
87
108
|
top: {
|
|
88
109
|
direction: "up",
|
|
89
110
|
preferredExit: "top",
|
|
111
|
+
exitEdge: "top",
|
|
90
112
|
},
|
|
91
113
|
right: {
|
|
92
114
|
direction: "right",
|
|
93
115
|
preferredExit: "right",
|
|
116
|
+
exitEdge: "right",
|
|
94
117
|
},
|
|
95
118
|
bottom: {
|
|
96
119
|
direction: "down",
|
|
97
120
|
preferredExit: "bottom",
|
|
121
|
+
exitEdge: "bottom",
|
|
98
122
|
},
|
|
99
123
|
left: {
|
|
100
124
|
direction: "left",
|
|
101
125
|
preferredExit: "left",
|
|
126
|
+
exitEdge: "left",
|
|
102
127
|
},
|
|
103
128
|
}
|
|
104
129
|
|
|
@@ -407,6 +432,134 @@ function resolvePreferredExit(
|
|
|
407
432
|
return value
|
|
408
433
|
}
|
|
409
434
|
|
|
435
|
+
function resolveExitEdge(
|
|
436
|
+
busId: string,
|
|
437
|
+
value: FanoutEdge | undefined,
|
|
438
|
+
): FanoutEdge | undefined {
|
|
439
|
+
if (value === undefined) return undefined
|
|
440
|
+
if (!FANOUT_EDGES.has(value)) {
|
|
441
|
+
throw new Error(
|
|
442
|
+
`FanoutSolver: bus "${busId}" has invalid exitEdge "${value}"`,
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
return value
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function resolveExitPosition(
|
|
449
|
+
busId: string,
|
|
450
|
+
value: FanoutExitPosition | undefined,
|
|
451
|
+
): Readonly<FanoutExitPositionConfig> | undefined {
|
|
452
|
+
if (value === undefined) return undefined
|
|
453
|
+
try {
|
|
454
|
+
return getFanoutExitPositionConfig(value)
|
|
455
|
+
} catch {
|
|
456
|
+
throw new Error(
|
|
457
|
+
`FanoutSolver: bus "${busId}" has invalid exitPosition "${value}"`,
|
|
458
|
+
)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function assertExitPositionFieldMatches<T extends string>(params: {
|
|
463
|
+
busId: string
|
|
464
|
+
exitPosition: FanoutExitPosition
|
|
465
|
+
fieldName: "direction" | "preferredExit" | "exitEdge"
|
|
466
|
+
expected: T | undefined
|
|
467
|
+
actual: T | undefined
|
|
468
|
+
sourceName: string
|
|
469
|
+
}): void {
|
|
470
|
+
const { busId, exitPosition, fieldName, expected, actual, sourceName } =
|
|
471
|
+
params
|
|
472
|
+
if (actual === undefined || actual === expected) return
|
|
473
|
+
throw new Error(
|
|
474
|
+
`FanoutSolver: bus "${busId}" exitPosition "${exitPosition}" conflicts with ${sourceName} ${fieldName} "${actual}"`,
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function resolveBusExitFields(params: {
|
|
479
|
+
busId: string
|
|
480
|
+
requestedBus: FanoutBusSpec
|
|
481
|
+
options: FanoutSolverOptions
|
|
482
|
+
}): Pick<
|
|
483
|
+
FanoutBusSpec,
|
|
484
|
+
"exitPosition" | "direction" | "preferredExit" | "exitEdge"
|
|
485
|
+
> {
|
|
486
|
+
const { busId, requestedBus, options } = params
|
|
487
|
+
const exitPosition = requestedBus.exitPosition
|
|
488
|
+
const exitPositionConfig = resolveExitPosition(busId, exitPosition)
|
|
489
|
+
const busPreferredExit = resolvePreferredExit(
|
|
490
|
+
busId,
|
|
491
|
+
requestedBus.preferredExit,
|
|
492
|
+
)
|
|
493
|
+
const optionPreferredExit = resolvePreferredExit(
|
|
494
|
+
busId,
|
|
495
|
+
options.busExitPreferences?.[busId],
|
|
496
|
+
)
|
|
497
|
+
const busExitEdge = resolveExitEdge(busId, requestedBus.exitEdge)
|
|
498
|
+
|
|
499
|
+
if (exitPositionConfig && exitPosition) {
|
|
500
|
+
for (const [actual, sourceName] of [
|
|
501
|
+
[requestedBus.direction, "bus"],
|
|
502
|
+
[options.busDirections?.[busId], "busDirections"],
|
|
503
|
+
] as const) {
|
|
504
|
+
assertExitPositionFieldMatches({
|
|
505
|
+
busId,
|
|
506
|
+
exitPosition,
|
|
507
|
+
fieldName: "direction",
|
|
508
|
+
expected: exitPositionConfig.direction,
|
|
509
|
+
actual,
|
|
510
|
+
sourceName,
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
for (const [actual, sourceName] of [
|
|
514
|
+
[busPreferredExit, "bus"],
|
|
515
|
+
[optionPreferredExit, "busExitPreferences"],
|
|
516
|
+
] as const) {
|
|
517
|
+
assertExitPositionFieldMatches({
|
|
518
|
+
busId,
|
|
519
|
+
exitPosition,
|
|
520
|
+
fieldName: "preferredExit",
|
|
521
|
+
expected: exitPositionConfig.preferredExit,
|
|
522
|
+
actual,
|
|
523
|
+
sourceName,
|
|
524
|
+
})
|
|
525
|
+
}
|
|
526
|
+
assertExitPositionFieldMatches({
|
|
527
|
+
busId,
|
|
528
|
+
exitPosition,
|
|
529
|
+
fieldName: "exitEdge",
|
|
530
|
+
expected: exitPositionConfig.exitEdge,
|
|
531
|
+
actual: busExitEdge,
|
|
532
|
+
sourceName: "bus",
|
|
533
|
+
})
|
|
534
|
+
return {
|
|
535
|
+
exitPosition,
|
|
536
|
+
...(exitPositionConfig.direction
|
|
537
|
+
? { direction: exitPositionConfig.direction }
|
|
538
|
+
: {}),
|
|
539
|
+
...(exitPositionConfig.preferredExit
|
|
540
|
+
? { preferredExit: exitPositionConfig.preferredExit }
|
|
541
|
+
: {}),
|
|
542
|
+
...(exitPositionConfig.exitEdge
|
|
543
|
+
? { exitEdge: exitPositionConfig.exitEdge }
|
|
544
|
+
: {}),
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const direction =
|
|
549
|
+
options.busDirections?.[busId] ??
|
|
550
|
+
requestedBus.direction ??
|
|
551
|
+
options.defaultDirection
|
|
552
|
+
const preferredExit = resolvePreferredExit(
|
|
553
|
+
busId,
|
|
554
|
+
optionPreferredExit ?? busPreferredExit ?? options.defaultPreferredExit,
|
|
555
|
+
)
|
|
556
|
+
return {
|
|
557
|
+
...(direction ? { direction } : {}),
|
|
558
|
+
...(preferredExit ? { preferredExit } : {}),
|
|
559
|
+
...(busExitEdge ? { exitEdge: busExitEdge } : {}),
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
410
563
|
function resolveAllowedLayers(
|
|
411
564
|
busId: string,
|
|
412
565
|
allowedLayers: readonly string[] | undefined,
|
|
@@ -446,7 +599,7 @@ export function resolveAvailableBoundaryRegions(
|
|
|
446
599
|
`FanoutSolver: invalid availableCornersAndSides value "${input}"`,
|
|
447
600
|
)
|
|
448
601
|
}
|
|
449
|
-
const key = `${region.direction}:${region.preferredExit}`
|
|
602
|
+
const key = `${region.exitEdge}:${region.direction}:${region.preferredExit}`
|
|
450
603
|
if (seen.has(key)) continue
|
|
451
604
|
seen.add(key)
|
|
452
605
|
regions.push(region)
|
|
@@ -505,17 +658,19 @@ function resolveBusSpecs(
|
|
|
505
658
|
requestedBus.busId,
|
|
506
659
|
(requestedBus as FanoutBusSpec).termination,
|
|
507
660
|
)
|
|
508
|
-
const
|
|
509
|
-
requestedBus.busId,
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
)
|
|
661
|
+
const resolvedExitFields = resolveBusExitFields({
|
|
662
|
+
busId: requestedBus.busId,
|
|
663
|
+
requestedBus: requestedBus as FanoutBusSpec,
|
|
664
|
+
options,
|
|
665
|
+
})
|
|
514
666
|
const allowedLayers = resolveAllowedLayers(
|
|
515
667
|
requestedBus.busId,
|
|
516
668
|
(requestedBus as FanoutBusSpec).allowedLayers,
|
|
517
669
|
)
|
|
518
|
-
if (
|
|
670
|
+
if (
|
|
671
|
+
termination.type === "plane" &&
|
|
672
|
+
resolvedExitFields.preferredExit !== undefined
|
|
673
|
+
) {
|
|
519
674
|
throw new Error(
|
|
520
675
|
`FanoutSolver: plane-terminated bus "${requestedBus.busId}" cannot also specify preferredExit`,
|
|
521
676
|
)
|
|
@@ -525,11 +680,7 @@ function resolveBusSpecs(
|
|
|
525
680
|
sourceComponentId:
|
|
526
681
|
(requestedBus as FanoutBusSpec).sourceComponentId ??
|
|
527
682
|
options.sourceComponentId,
|
|
528
|
-
|
|
529
|
-
options.busDirections?.[requestedBus.busId] ??
|
|
530
|
-
(requestedBus as FanoutBusSpec).direction ??
|
|
531
|
-
options.defaultDirection,
|
|
532
|
-
preferredExit,
|
|
683
|
+
...resolvedExitFields,
|
|
533
684
|
...(allowedLayers === undefined ? {} : { allowedLayers }),
|
|
534
685
|
termination,
|
|
535
686
|
})
|
|
@@ -540,26 +691,26 @@ function resolveBusSpecs(
|
|
|
540
691
|
const inferredBusId = inferBusId(connection)
|
|
541
692
|
if (inferredBusId) {
|
|
542
693
|
const existing = specsById.get(inferredBusId)
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
...
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
694
|
+
if (existing) {
|
|
695
|
+
specsById.set(inferredBusId, {
|
|
696
|
+
...existing,
|
|
697
|
+
connectionNames: [...existing.connectionNames, connection.name],
|
|
698
|
+
})
|
|
699
|
+
} else {
|
|
700
|
+
specsById.set(inferredBusId, {
|
|
701
|
+
busId: inferredBusId,
|
|
702
|
+
connectionNames: [connection.name],
|
|
703
|
+
direction:
|
|
704
|
+
options.busDirections?.[inferredBusId] ?? options.defaultDirection,
|
|
705
|
+
sourceComponentId: options.sourceComponentId,
|
|
706
|
+
preferredExit: resolvePreferredExit(
|
|
707
|
+
inferredBusId,
|
|
708
|
+
options.busExitPreferences?.[inferredBusId] ??
|
|
709
|
+
options.defaultPreferredExit,
|
|
710
|
+
),
|
|
711
|
+
termination: { type: "boundary" },
|
|
712
|
+
})
|
|
713
|
+
}
|
|
563
714
|
} else {
|
|
564
715
|
const singletonBusId = `connection:${connection.name}`
|
|
565
716
|
specsById.set(singletonBusId, {
|
|
@@ -892,6 +1043,27 @@ function resolveAvailableBusExit(params: {
|
|
|
892
1043
|
)[0]!
|
|
893
1044
|
}
|
|
894
1045
|
|
|
1046
|
+
function validateExplicitExitAvailability(params: {
|
|
1047
|
+
busId: string
|
|
1048
|
+
exitEdge: FanoutEdge
|
|
1049
|
+
preferredExit: FanoutBorderTarget
|
|
1050
|
+
availableRegions: readonly AvailableBoundaryRegion[]
|
|
1051
|
+
}): void {
|
|
1052
|
+
const { busId, exitEdge, preferredExit, availableRegions } = params
|
|
1053
|
+
const requestedBandSide = getCornerBandSide(exitEdge, preferredExit)
|
|
1054
|
+
const hasCompatibleRegion = availableRegions.some(
|
|
1055
|
+
(region) =>
|
|
1056
|
+
region.exitEdge === exitEdge &&
|
|
1057
|
+
getCornerBandSide(region.exitEdge, region.preferredExit) ===
|
|
1058
|
+
requestedBandSide,
|
|
1059
|
+
)
|
|
1060
|
+
if (!hasCompatibleRegion) {
|
|
1061
|
+
throw new Error(
|
|
1062
|
+
`FanoutSolver: bus "${busId}" cannot use its requested exit with availableCornersAndSides`,
|
|
1063
|
+
)
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
895
1067
|
function resolveBusDirection(params: {
|
|
896
1068
|
busId: string
|
|
897
1069
|
explicitDirection?: FanoutDirection
|
|
@@ -1031,6 +1203,20 @@ export function prepareFanoutBuses(
|
|
|
1031
1203
|
sourceGrid,
|
|
1032
1204
|
preparedConnections,
|
|
1033
1205
|
} of resolvedBusInputs) {
|
|
1206
|
+
if (busSpec.exitEdge && !busSpec.preferredExit) {
|
|
1207
|
+
throw new Error(
|
|
1208
|
+
`FanoutSolver: bus "${busSpec.busId}" exitEdge requires preferredExit`,
|
|
1209
|
+
)
|
|
1210
|
+
}
|
|
1211
|
+
if (
|
|
1212
|
+
busSpec.exitEdge &&
|
|
1213
|
+
busSpec.preferredExit &&
|
|
1214
|
+
!borderTargetIncludesEdge(busSpec.preferredExit, busSpec.exitEdge)
|
|
1215
|
+
) {
|
|
1216
|
+
throw new Error(
|
|
1217
|
+
`FanoutSolver: bus "${busSpec.busId}" exitEdge "${busSpec.exitEdge}" is incompatible with preferredExit "${busSpec.preferredExit}"`,
|
|
1218
|
+
)
|
|
1219
|
+
}
|
|
1034
1220
|
const resolvedExit = resolveBusDirection({
|
|
1035
1221
|
busId: busSpec.busId,
|
|
1036
1222
|
explicitDirection:
|
|
@@ -1039,12 +1225,29 @@ export function prepareFanoutBuses(
|
|
|
1039
1225
|
connections: preparedConnections,
|
|
1040
1226
|
sharedBoundary,
|
|
1041
1227
|
availableRegions:
|
|
1042
|
-
busSpec.termination?.type === "plane"
|
|
1228
|
+
busSpec.termination?.type === "plane" || busSpec.exitEdge
|
|
1229
|
+
? undefined
|
|
1230
|
+
: availableRegions,
|
|
1043
1231
|
})
|
|
1232
|
+
if (
|
|
1233
|
+
busSpec.termination?.type !== "plane" &&
|
|
1234
|
+
busSpec.exitEdge &&
|
|
1235
|
+
resolvedExit.preferredExit &&
|
|
1236
|
+
availableRegions
|
|
1237
|
+
) {
|
|
1238
|
+
validateExplicitExitAvailability({
|
|
1239
|
+
busId: busSpec.busId,
|
|
1240
|
+
exitEdge: busSpec.exitEdge,
|
|
1241
|
+
preferredExit: resolvedExit.preferredExit,
|
|
1242
|
+
availableRegions,
|
|
1243
|
+
})
|
|
1244
|
+
}
|
|
1044
1245
|
buses.push({
|
|
1045
1246
|
busId: busSpec.busId,
|
|
1046
1247
|
direction: resolvedExit.direction,
|
|
1047
1248
|
preferredExit: resolvedExit.preferredExit,
|
|
1249
|
+
...(busSpec.exitEdge ? { exitEdge: busSpec.exitEdge } : {}),
|
|
1250
|
+
cornerBandConnectionCount: 0,
|
|
1048
1251
|
allowedLayers: busSpec.allowedLayers,
|
|
1049
1252
|
termination: busSpec.termination ?? { type: "boundary" },
|
|
1050
1253
|
connections: preparedConnections,
|
|
@@ -1059,5 +1262,23 @@ export function prepareFanoutBuses(
|
|
|
1059
1262
|
})
|
|
1060
1263
|
}
|
|
1061
1264
|
|
|
1265
|
+
const cornerBandConnectionCounts = new Map<string, number>()
|
|
1266
|
+
for (const bus of buses) {
|
|
1267
|
+
const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
1268
|
+
if (!bus.exitEdge || !side) continue
|
|
1269
|
+
const key = `${bus.exitEdge}:${side}`
|
|
1270
|
+
cornerBandConnectionCounts.set(
|
|
1271
|
+
key,
|
|
1272
|
+
(cornerBandConnectionCounts.get(key) ?? 0) + bus.connections.length,
|
|
1273
|
+
)
|
|
1274
|
+
}
|
|
1275
|
+
for (const bus of buses) {
|
|
1276
|
+
const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
1277
|
+
if (!bus.exitEdge || !side) continue
|
|
1278
|
+
bus.cornerBandConnectionCount =
|
|
1279
|
+
cornerBandConnectionCounts.get(`${bus.exitEdge}:${side}`) ??
|
|
1280
|
+
bus.connections.length
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1062
1283
|
return buses
|
|
1063
1284
|
}
|
package/lib/route-bus.ts
CHANGED
|
@@ -3,6 +3,12 @@ import type {
|
|
|
3
3
|
SimpleRouteJson,
|
|
4
4
|
SimplifiedPcbTrace,
|
|
5
5
|
} from "@tscircuit/capacity-autorouter"
|
|
6
|
+
import {
|
|
7
|
+
getCornerBandSide,
|
|
8
|
+
getDirectionForExitEdge,
|
|
9
|
+
getExitEdgeForDirection,
|
|
10
|
+
} from "./boundary-exit"
|
|
11
|
+
import { createFanoutOutputIds } from "./fanout-output-ids"
|
|
6
12
|
import {
|
|
7
13
|
distance,
|
|
8
14
|
distancePointToObstacle,
|
|
@@ -10,7 +16,6 @@ import {
|
|
|
10
16
|
distanceSegmentToObstacle,
|
|
11
17
|
segmentsAreClear,
|
|
12
18
|
} from "./geometry"
|
|
13
|
-
import { createFanoutOutputIds } from "./fanout-output-ids"
|
|
14
19
|
import { getAllRoutedTraceCopper } from "./get-routed-trace-copper"
|
|
15
20
|
import { getLayerSpan } from "./layer-names"
|
|
16
21
|
import {
|
|
@@ -72,6 +77,92 @@ function getPerpendicularAxis(
|
|
|
72
77
|
return isHorizontal(direction) ? point.y : point.x
|
|
73
78
|
}
|
|
74
79
|
|
|
80
|
+
function getCornerSide(bus: PreparedBus): "minimum" | "maximum" | undefined {
|
|
81
|
+
return getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getLocalCornerSide(
|
|
85
|
+
bus: PreparedBus,
|
|
86
|
+
): "minimum" | "maximum" | undefined {
|
|
87
|
+
return getCornerBandSide(
|
|
88
|
+
getExitEdgeForDirection(bus.direction),
|
|
89
|
+
bus.preferredExit,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getCornerTargetTrack(params: {
|
|
94
|
+
bus: PreparedBus
|
|
95
|
+
connection: PreparedConnection
|
|
96
|
+
cornerExitLaneOffset: number
|
|
97
|
+
traceWidth: number
|
|
98
|
+
viaDiameter: number
|
|
99
|
+
clearance: number
|
|
100
|
+
}): number {
|
|
101
|
+
const {
|
|
102
|
+
bus,
|
|
103
|
+
connection,
|
|
104
|
+
cornerExitLaneOffset,
|
|
105
|
+
traceWidth,
|
|
106
|
+
viaDiameter,
|
|
107
|
+
clearance,
|
|
108
|
+
} = params
|
|
109
|
+
const side = getCornerSide(bus)
|
|
110
|
+
if (!side || !bus.exitEdge) {
|
|
111
|
+
return getPerpendicularAxis(
|
|
112
|
+
connection.exitTargetPoint ?? connection.targetPoint,
|
|
113
|
+
bus.direction,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
const boundaryDirection = getDirectionForExitEdge(bus.exitEdge)
|
|
117
|
+
const boundaryMinimum = isHorizontal(boundaryDirection)
|
|
118
|
+
? bus.sharedBoundary.minY
|
|
119
|
+
: bus.sharedBoundary.minX
|
|
120
|
+
const boundaryMaximum = isHorizontal(boundaryDirection)
|
|
121
|
+
? bus.sharedBoundary.maxY
|
|
122
|
+
: bus.sharedBoundary.maxX
|
|
123
|
+
const pitch = Math.max(traceWidth + clearance, viaDiameter + clearance)
|
|
124
|
+
const bandCenter =
|
|
125
|
+
boundaryMinimum +
|
|
126
|
+
(boundaryMaximum - boundaryMinimum) * (side === "minimum" ? 0.25 : 0.75)
|
|
127
|
+
const bandConnectionCount = Math.max(
|
|
128
|
+
bus.connections.length,
|
|
129
|
+
bus.cornerBandConnectionCount ?? bus.connections.length,
|
|
130
|
+
)
|
|
131
|
+
const firstTrack = bandCenter - ((bandConnectionCount - 1) * pitch) / 2
|
|
132
|
+
const rank = getConnectionRank(bus, connection)
|
|
133
|
+
const globalSlot = cornerExitLaneOffset + rank
|
|
134
|
+
const reverseSlotOrder =
|
|
135
|
+
(side === "maximum") === directionSign(boundaryDirection) > 0
|
|
136
|
+
const orientedSlot = reverseSlotOrder
|
|
137
|
+
? bandConnectionCount - 1 - globalSlot
|
|
138
|
+
: globalSlot
|
|
139
|
+
return firstTrack + orientedSlot * pitch
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getCornerLaneOffsets(
|
|
143
|
+
bus: PreparedBus,
|
|
144
|
+
acceptedPlans: readonly FanoutRoutePlan[],
|
|
145
|
+
): { exit: number; localChannel: number; boundaryChannel: number } {
|
|
146
|
+
const side = getCornerSide(bus)
|
|
147
|
+
if (!side || !bus.exitEdge) {
|
|
148
|
+
return { exit: 0, localChannel: 0, boundaryChannel: 0 }
|
|
149
|
+
}
|
|
150
|
+
const cornerPlans = acceptedPlans.filter(
|
|
151
|
+
(plan) => plan.exitEdge && plan.cornerBandSide !== undefined,
|
|
152
|
+
)
|
|
153
|
+
const plansOnExitEdge = cornerPlans.filter(
|
|
154
|
+
(plan) => plan.exitEdge === bus.exitEdge && plan.cornerBandSide === side,
|
|
155
|
+
)
|
|
156
|
+
const plansOnLocalEdge = cornerPlans.filter(
|
|
157
|
+
(plan) => plan.direction === bus.direction,
|
|
158
|
+
)
|
|
159
|
+
return {
|
|
160
|
+
exit: plansOnExitEdge.length,
|
|
161
|
+
localChannel: plansOnLocalEdge.length,
|
|
162
|
+
boundaryChannel: plansOnExitEdge.length,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
75
166
|
function makePoint(
|
|
76
167
|
axis: number,
|
|
77
168
|
perpendicularAxis: number,
|
|
@@ -82,8 +173,11 @@ function makePoint(
|
|
|
82
173
|
: { x: perpendicularAxis, y: axis }
|
|
83
174
|
}
|
|
84
175
|
|
|
85
|
-
function getExitAxis(
|
|
86
|
-
|
|
176
|
+
function getExitAxis(
|
|
177
|
+
bus: PreparedBus,
|
|
178
|
+
direction: FanoutDirection = bus.direction,
|
|
179
|
+
): number {
|
|
180
|
+
switch (direction) {
|
|
87
181
|
case "right":
|
|
88
182
|
return bus.sharedBoundary.maxX
|
|
89
183
|
case "left":
|
|
@@ -296,10 +390,12 @@ function getPreferredTrack(params: {
|
|
|
296
390
|
connection: PreparedConnection
|
|
297
391
|
traceWidth: number
|
|
298
392
|
}): number {
|
|
299
|
-
const preferredTrack =
|
|
300
|
-
params.connection.
|
|
301
|
-
|
|
302
|
-
|
|
393
|
+
const preferredTrack = getCornerSide(params.bus)
|
|
394
|
+
? getPerpendicularAxis(params.connection.sourcePoint, params.bus.direction)
|
|
395
|
+
: getPerpendicularAxis(
|
|
396
|
+
params.connection.exitTargetPoint ?? params.connection.targetPoint,
|
|
397
|
+
params.bus.direction,
|
|
398
|
+
)
|
|
303
399
|
const boundaryMinimum = isHorizontal(params.bus.direction)
|
|
304
400
|
? params.bus.sharedBoundary.minY
|
|
305
401
|
: params.bus.sharedBoundary.minX
|
|
@@ -494,6 +590,9 @@ function buildPlan(params: {
|
|
|
494
590
|
viaHandedness: ViaHandedness
|
|
495
591
|
interstitialEscape: boolean
|
|
496
592
|
spreadLaneIndex: number
|
|
593
|
+
cornerExitLaneOffset: number
|
|
594
|
+
cornerLocalChannelLaneOffset: number
|
|
595
|
+
cornerBoundaryChannelLaneOffset: number
|
|
497
596
|
clearance: number
|
|
498
597
|
terminateAtVia: boolean
|
|
499
598
|
}): FanoutRoutePlan {
|
|
@@ -510,6 +609,9 @@ function buildPlan(params: {
|
|
|
510
609
|
viaHandedness,
|
|
511
610
|
interstitialEscape,
|
|
512
611
|
spreadLaneIndex,
|
|
612
|
+
cornerExitLaneOffset,
|
|
613
|
+
cornerLocalChannelLaneOffset,
|
|
614
|
+
cornerBoundaryChannelLaneOffset,
|
|
513
615
|
clearance,
|
|
514
616
|
terminateAtVia,
|
|
515
617
|
} = params
|
|
@@ -567,9 +669,70 @@ function buildPlan(params: {
|
|
|
567
669
|
? getAxis(spreadPoint, bus.direction)
|
|
568
670
|
: viaAxis + sign * Math.abs(track - viaPerpendicularAxis)
|
|
569
671
|
const doglegPoint = makePoint(targetLayerDoglegAxis, track, bus.direction)
|
|
672
|
+
const cornerSide = getCornerSide(bus)
|
|
673
|
+
const boundaryDirection = bus.exitEdge
|
|
674
|
+
? getDirectionForExitEdge(bus.exitEdge)
|
|
675
|
+
: bus.direction
|
|
676
|
+
const boundarySign = directionSign(boundaryDirection)
|
|
677
|
+
const boundaryExitAxis = getExitAxis(bus, boundaryDirection)
|
|
678
|
+
const cornerTrack =
|
|
679
|
+
cornerSide && bus.exitEdge
|
|
680
|
+
? getCornerTargetTrack({
|
|
681
|
+
bus,
|
|
682
|
+
connection: preparedConnection,
|
|
683
|
+
cornerExitLaneOffset,
|
|
684
|
+
traceWidth,
|
|
685
|
+
viaDiameter,
|
|
686
|
+
clearance,
|
|
687
|
+
})
|
|
688
|
+
: track
|
|
689
|
+
const connectionRank = getConnectionRank(bus, preparedConnection)
|
|
690
|
+
const localCornerSide = getLocalCornerSide(bus)
|
|
691
|
+
const localChannelLaneIndex =
|
|
692
|
+
localCornerSide === "maximum"
|
|
693
|
+
? cornerLocalChannelLaneOffset + connectionRank
|
|
694
|
+
: cornerLocalChannelLaneOffset +
|
|
695
|
+
bus.connections.length -
|
|
696
|
+
1 -
|
|
697
|
+
connectionRank
|
|
698
|
+
const globalBoundarySlot = cornerBoundaryChannelLaneOffset + connectionRank
|
|
699
|
+
const boundaryBandConnectionCount = Math.max(
|
|
700
|
+
bus.connections.length,
|
|
701
|
+
bus.cornerBandConnectionCount ?? bus.connections.length,
|
|
702
|
+
)
|
|
703
|
+
const boundaryChannelLaneIndex =
|
|
704
|
+
boundarySign > 0
|
|
705
|
+
? globalBoundarySlot
|
|
706
|
+
: boundaryBandConnectionCount - 1 - globalBoundarySlot
|
|
707
|
+
const channelInset = (laneIndex: number) =>
|
|
708
|
+
viaDiameter / 2 +
|
|
709
|
+
traceWidth / 2 +
|
|
710
|
+
clearance +
|
|
711
|
+
laneIndex * (traceWidth + clearance)
|
|
712
|
+
const localChannelAxis =
|
|
713
|
+
getExitAxis(bus, bus.direction) - sign * channelInset(localChannelLaneIndex)
|
|
714
|
+
const boundaryChannelAxis =
|
|
715
|
+
boundaryExitAxis - boundarySign * channelInset(boundaryChannelLaneIndex)
|
|
716
|
+
const localChannelSourcePoint = makePoint(
|
|
717
|
+
localChannelAxis,
|
|
718
|
+
track,
|
|
719
|
+
bus.direction,
|
|
720
|
+
)
|
|
721
|
+
const localChannelTargetPoint = makePoint(
|
|
722
|
+
boundaryChannelAxis,
|
|
723
|
+
localChannelAxis,
|
|
724
|
+
boundaryDirection,
|
|
725
|
+
)
|
|
726
|
+
const boundaryChannelTargetPoint = makePoint(
|
|
727
|
+
boundaryChannelAxis,
|
|
728
|
+
cornerTrack,
|
|
729
|
+
boundaryDirection,
|
|
730
|
+
)
|
|
570
731
|
const exitPoint = terminateAtVia
|
|
571
732
|
? viaPoint
|
|
572
|
-
:
|
|
733
|
+
: cornerSide
|
|
734
|
+
? makePoint(boundaryExitAxis, cornerTrack, boundaryDirection)
|
|
735
|
+
: makePoint(exitAxis, track, bus.direction)
|
|
573
736
|
const segments: RoutedSegment[] = []
|
|
574
737
|
const route: SimplifiedPcbTrace["route"] = []
|
|
575
738
|
|
|
@@ -631,12 +794,27 @@ function buildPlan(params: {
|
|
|
631
794
|
|
|
632
795
|
const targetLayerPoints = terminateAtVia
|
|
633
796
|
? [viaPoint]
|
|
634
|
-
:
|
|
797
|
+
: cornerSide
|
|
635
798
|
? chamferOrthogonalPolyline(
|
|
636
|
-
[
|
|
799
|
+
[
|
|
800
|
+
viaPoint,
|
|
801
|
+
...(useNestedSpread ? [spreadPoint] : []),
|
|
802
|
+
doglegPoint,
|
|
803
|
+
localChannelSourcePoint,
|
|
804
|
+
...(isHorizontal(bus.direction) !== isHorizontal(boundaryDirection)
|
|
805
|
+
? [localChannelTargetPoint]
|
|
806
|
+
: []),
|
|
807
|
+
boundaryChannelTargetPoint,
|
|
808
|
+
exitPoint,
|
|
809
|
+
],
|
|
637
810
|
Math.max(traceWidth + clearance, traceWidth * 2),
|
|
638
811
|
)
|
|
639
|
-
:
|
|
812
|
+
: useNestedSpread
|
|
813
|
+
? chamferOrthogonalPolyline(
|
|
814
|
+
[viaPoint, spreadPoint, doglegPoint, exitPoint],
|
|
815
|
+
Math.max(traceWidth + clearance, traceWidth * 2),
|
|
816
|
+
)
|
|
817
|
+
: [viaPoint, doglegPoint, exitPoint]
|
|
640
818
|
for (let index = 1; index < targetLayerPoints.length; index++) {
|
|
641
819
|
const previousPoint = targetLayerPoints[index - 1]!
|
|
642
820
|
const nextPoint = targetLayerPoints[index]!
|
|
@@ -666,6 +844,8 @@ function buildPlan(params: {
|
|
|
666
844
|
targetLayer,
|
|
667
845
|
termination: bus.termination,
|
|
668
846
|
direction: bus.direction,
|
|
847
|
+
...(bus.exitEdge ? { exitEdge: bus.exitEdge } : {}),
|
|
848
|
+
...(cornerSide ? { cornerBandSide: cornerSide } : {}),
|
|
669
849
|
exitPoint,
|
|
670
850
|
trace: {
|
|
671
851
|
type: "pcb_trace",
|
|
@@ -1314,6 +1494,9 @@ function routePlaneTerminatedBus(
|
|
|
1314
1494
|
viaHandedness,
|
|
1315
1495
|
interstitialEscape: !pairChannelFitsVia,
|
|
1316
1496
|
spreadLaneIndex: 0,
|
|
1497
|
+
cornerExitLaneOffset: 0,
|
|
1498
|
+
cornerLocalChannelLaneOffset: 0,
|
|
1499
|
+
cornerBoundaryChannelLaneOffset: 0,
|
|
1317
1500
|
clearance,
|
|
1318
1501
|
terminateAtVia: true,
|
|
1319
1502
|
})
|
|
@@ -1415,6 +1598,7 @@ export function routeBusAlternatives(
|
|
|
1415
1598
|
|
|
1416
1599
|
const alternatives: FanoutRoutePlan[][] = []
|
|
1417
1600
|
const seenAlternativeKeys = new Set<string>()
|
|
1601
|
+
const cornerLaneOffsets = getCornerLaneOffsets(bus, acceptedPlans)
|
|
1418
1602
|
|
|
1419
1603
|
const addAlternative = (plans: FanoutRoutePlan[]): void => {
|
|
1420
1604
|
const key = plans
|
|
@@ -1501,6 +1685,9 @@ export function routeBusAlternatives(
|
|
|
1501
1685
|
connectionRank,
|
|
1502
1686
|
bus.connections.length - connectionRank - 1,
|
|
1503
1687
|
),
|
|
1688
|
+
cornerExitLaneOffset: cornerLaneOffsets.exit,
|
|
1689
|
+
cornerLocalChannelLaneOffset: cornerLaneOffsets.localChannel,
|
|
1690
|
+
cornerBoundaryChannelLaneOffset: cornerLaneOffsets.boundaryChannel,
|
|
1504
1691
|
clearance,
|
|
1505
1692
|
terminateAtVia: false,
|
|
1506
1693
|
})
|
|
@@ -1510,7 +1697,7 @@ export function routeBusAlternatives(
|
|
|
1510
1697
|
otherPlans: [...acceptedPlans, ...candidatePlans],
|
|
1511
1698
|
staticClearanceCache,
|
|
1512
1699
|
blockingBusCounts,
|
|
1513
|
-
cacheKey: `boundary:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${viaHandedness}:${trackIndex}`,
|
|
1700
|
+
cacheKey: `boundary:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${viaHandedness}:${trackIndex}:${bus.exitEdge ?? "legacy"}:${cornerLaneOffsets.exit}:${cornerLaneOffsets.localChannel}:${cornerLaneOffsets.boundaryChannel}`,
|
|
1514
1701
|
srj,
|
|
1515
1702
|
sharedBoundary: bus.sharedBoundary,
|
|
1516
1703
|
clearance,
|
|
@@ -3,13 +3,13 @@ import type {
|
|
|
3
3
|
SimpleRouteJson,
|
|
4
4
|
SimplifiedPcbTrace,
|
|
5
5
|
} from "@tscircuit/capacity-autorouter"
|
|
6
|
+
import { createFanoutOutputIds } from "./fanout-output-ids"
|
|
6
7
|
import {
|
|
7
8
|
distance,
|
|
8
9
|
distancePointToObstacle,
|
|
9
10
|
distanceSegmentToObstacle,
|
|
10
11
|
distanceSegmentToSegment,
|
|
11
12
|
} from "./geometry"
|
|
12
|
-
import { createFanoutOutputIds } from "./fanout-output-ids"
|
|
13
13
|
import { type AvailableBoundaryRegion, getRegionAnchor } from "./prepare-buses"
|
|
14
14
|
import type {
|
|
15
15
|
FanoutDirection,
|
|
@@ -729,7 +729,6 @@ function routeDirectionGroup(params: {
|
|
|
729
729
|
if (achievedFlow !== terminals.length) {
|
|
730
730
|
if (FANOUT_FLOW_DEBUG_ENABLED) {
|
|
731
731
|
const unmatchedConnections = terminals.flatMap((terminal, index) => {
|
|
732
|
-
const terminalNode = terminalStart + index
|
|
733
732
|
return terminalWasMatched(index)
|
|
734
733
|
? []
|
|
735
734
|
: [terminal.item.connection.connection.name]
|
|
@@ -832,6 +831,7 @@ function buildPlan(route: FlowRoute, traceWidth: number): FanoutRoutePlan {
|
|
|
832
831
|
targetLayer: "top",
|
|
833
832
|
termination: item.bus.termination,
|
|
834
833
|
direction: item.bus.direction,
|
|
834
|
+
...(item.bus.exitEdge ? { exitEdge: item.bus.exitEdge } : {}),
|
|
835
835
|
exitPoint: points.at(-1)!,
|
|
836
836
|
trace: {
|
|
837
837
|
type: "pcb_trace",
|
|
@@ -3,12 +3,12 @@ import type {
|
|
|
3
3
|
SimpleRouteJson,
|
|
4
4
|
SimplifiedPcbTrace,
|
|
5
5
|
} from "@tscircuit/capacity-autorouter"
|
|
6
|
+
import { createFanoutOutputIds } from "./fanout-output-ids"
|
|
6
7
|
import {
|
|
7
8
|
distance,
|
|
8
9
|
distanceSegmentToObstacle,
|
|
9
10
|
distanceSegmentToSegment,
|
|
10
11
|
} from "./geometry"
|
|
11
|
-
import { createFanoutOutputIds } from "./fanout-output-ids"
|
|
12
12
|
import type {
|
|
13
13
|
FanoutBorderDistribution,
|
|
14
14
|
FanoutCorner,
|
|
@@ -768,6 +768,7 @@ function buildPlan(path: RoutedPath): FanoutRoutePlan {
|
|
|
768
768
|
targetLayer: "top",
|
|
769
769
|
termination: item.bus.termination,
|
|
770
770
|
direction: item.direction,
|
|
771
|
+
...(item.bus.exitEdge ? { exitEdge: item.bus.exitEdge } : {}),
|
|
771
772
|
exitPoint: points.at(-1)!,
|
|
772
773
|
trace: {
|
|
773
774
|
type: "pcb_trace",
|
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
|
/**
|
|
@@ -246,6 +286,10 @@ export interface PreparedBus {
|
|
|
246
286
|
busId: string
|
|
247
287
|
direction: FanoutDirection
|
|
248
288
|
preferredExit?: FanoutBorderTarget
|
|
289
|
+
/** Explicit final boundary edge. Omitted for legacy direction-based exits. */
|
|
290
|
+
exitEdge?: FanoutEdge
|
|
291
|
+
/** Total connection count sharing this explicit edge/corner band. */
|
|
292
|
+
cornerBandConnectionCount?: number
|
|
249
293
|
/** Layers to which this bus is allowed to escape. */
|
|
250
294
|
allowedLayers?: readonly string[]
|
|
251
295
|
termination: FanoutBusTermination
|
|
@@ -287,7 +331,12 @@ export interface FanoutRoutePlan {
|
|
|
287
331
|
targetPoint: ConnectionPoint
|
|
288
332
|
targetLayer: string
|
|
289
333
|
termination: FanoutBusTermination
|
|
334
|
+
/** Local pad escape direction. */
|
|
290
335
|
direction: FanoutDirection
|
|
336
|
+
/** Explicit physical boundary edge used by a packed corner channel. */
|
|
337
|
+
exitEdge?: FanoutEdge
|
|
338
|
+
/** Lower/left or upper/right band reserved along `exitEdge`. */
|
|
339
|
+
cornerBandSide?: "minimum" | "maximum"
|
|
291
340
|
exitPoint: Point2D
|
|
292
341
|
trace: SimplifiedPcbTrace
|
|
293
342
|
segments: 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,
|
|
@@ -73,6 +75,29 @@ function pointIsOnBoundary(point: Point2D, boundary: Bounds): boolean {
|
|
|
73
75
|
return inside && onEdge
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
function pointIsOnBoundaryEdge(
|
|
79
|
+
point: Point2D,
|
|
80
|
+
edge: FanoutEdge,
|
|
81
|
+
boundary: Bounds,
|
|
82
|
+
): boolean {
|
|
83
|
+
const inside =
|
|
84
|
+
point.x >= boundary.minX - EPSILON &&
|
|
85
|
+
point.x <= boundary.maxX + EPSILON &&
|
|
86
|
+
point.y >= boundary.minY - EPSILON &&
|
|
87
|
+
point.y <= boundary.maxY + EPSILON
|
|
88
|
+
if (!inside) return false
|
|
89
|
+
switch (edge) {
|
|
90
|
+
case "left":
|
|
91
|
+
return Math.abs(point.x - boundary.minX) <= EPSILON
|
|
92
|
+
case "right":
|
|
93
|
+
return Math.abs(point.x - boundary.maxX) <= EPSILON
|
|
94
|
+
case "top":
|
|
95
|
+
return Math.abs(point.y - boundary.maxY) <= EPSILON
|
|
96
|
+
case "bottom":
|
|
97
|
+
return Math.abs(point.y - boundary.minY) <= EPSILON
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
76
101
|
function pointIsInsideBounds(point: Point2D, bounds: Bounds): boolean {
|
|
77
102
|
return (
|
|
78
103
|
point.x >= bounds.minX - EPSILON &&
|
|
@@ -236,6 +261,31 @@ function validatePlanStructure(params: {
|
|
|
236
261
|
plan,
|
|
237
262
|
)
|
|
238
263
|
}
|
|
264
|
+
const expectedCornerBandSide = getCornerBandSide(
|
|
265
|
+
preparedBus?.exitEdge,
|
|
266
|
+
preparedBus?.preferredExit,
|
|
267
|
+
)
|
|
268
|
+
if (
|
|
269
|
+
preparedBus?.exitEdge !== plan.exitEdge ||
|
|
270
|
+
expectedCornerBandSide !== plan.cornerBandSide
|
|
271
|
+
) {
|
|
272
|
+
addIssue(
|
|
273
|
+
issues,
|
|
274
|
+
"output-exit-mismatch",
|
|
275
|
+
`Plan ${plan.connectionName} does not retain its prepared boundary edge and band`,
|
|
276
|
+
plan,
|
|
277
|
+
)
|
|
278
|
+
} else if (
|
|
279
|
+
preparedBus?.exitEdge &&
|
|
280
|
+
!pointIsOnBoundaryEdge(plan.exitPoint, preparedBus.exitEdge, sharedBoundary)
|
|
281
|
+
) {
|
|
282
|
+
addIssue(
|
|
283
|
+
issues,
|
|
284
|
+
"output-exit-mismatch",
|
|
285
|
+
`Plan ${plan.connectionName} does not terminate on its declared ${preparedBus.exitEdge} edge`,
|
|
286
|
+
plan,
|
|
287
|
+
)
|
|
288
|
+
}
|
|
239
289
|
if (plan.segments.length === 0 || plan.length <= EPSILON) {
|
|
240
290
|
addIssue(
|
|
241
291
|
issues,
|