@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 +21 -0
- package/README.md +366 -0
- package/lib/build-output.ts +140 -0
- package/lib/fanout-solver.ts +617 -0
- package/lib/geometry.ts +162 -0
- package/lib/index.ts +20 -0
- package/lib/layer-colors.ts +21 -0
- package/lib/layer-names.ts +137 -0
- package/lib/prepare-buses.ts +1016 -0
- package/lib/route-bus.ts +934 -0
- package/lib/route-single-layer-adaptive-exits.ts +1179 -0
- package/lib/route-single-layer-push-shove.ts +1233 -0
- package/lib/types.ts +212 -0
- package/package.json +56 -0
package/lib/geometry.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { Obstacle } from "@tscircuit/capacity-autorouter"
|
|
2
|
+
import type { Point2D, RoutedSegment } from "./types"
|
|
3
|
+
|
|
4
|
+
const EPSILON = 1e-9
|
|
5
|
+
|
|
6
|
+
type ShapeAwareObstacle = Obstacle & { shape?: "circle" }
|
|
7
|
+
|
|
8
|
+
function obstacleIsCircular(obstacle: Obstacle): boolean {
|
|
9
|
+
return (obstacle as ShapeAwareObstacle).shape === "circle"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function distance(a: Point2D, b: Point2D): number {
|
|
13
|
+
return Math.hypot(a.x - b.x, a.y - b.y)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function distancePointToSegment(
|
|
17
|
+
point: Point2D,
|
|
18
|
+
start: Point2D,
|
|
19
|
+
end: Point2D,
|
|
20
|
+
): number {
|
|
21
|
+
const dx = end.x - start.x
|
|
22
|
+
const dy = end.y - start.y
|
|
23
|
+
const lengthSquared = dx * dx + dy * dy
|
|
24
|
+
const rawT =
|
|
25
|
+
lengthSquared < EPSILON
|
|
26
|
+
? 0
|
|
27
|
+
: ((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSquared
|
|
28
|
+
const t = Math.max(0, Math.min(1, rawT))
|
|
29
|
+
return Math.hypot(point.x - (start.x + t * dx), point.y - (start.y + t * dy))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function cross(origin: Point2D, a: Point2D, b: Point2D): number {
|
|
33
|
+
return (
|
|
34
|
+
(a.x - origin.x) * (b.y - origin.y) - (a.y - origin.y) * (b.x - origin.x)
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function segmentsProperlyCross(
|
|
39
|
+
a: Point2D,
|
|
40
|
+
b: Point2D,
|
|
41
|
+
c: Point2D,
|
|
42
|
+
d: Point2D,
|
|
43
|
+
): boolean {
|
|
44
|
+
const d1 = cross(c, d, a)
|
|
45
|
+
const d2 = cross(c, d, b)
|
|
46
|
+
const d3 = cross(a, b, c)
|
|
47
|
+
const d4 = cross(a, b, d)
|
|
48
|
+
return (
|
|
49
|
+
((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
|
|
50
|
+
((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function distanceSegmentToSegment(
|
|
55
|
+
firstStart: Point2D,
|
|
56
|
+
firstEnd: Point2D,
|
|
57
|
+
secondStart: Point2D,
|
|
58
|
+
secondEnd: Point2D,
|
|
59
|
+
): number {
|
|
60
|
+
if (segmentsProperlyCross(firstStart, firstEnd, secondStart, secondEnd)) {
|
|
61
|
+
return 0
|
|
62
|
+
}
|
|
63
|
+
return Math.min(
|
|
64
|
+
distancePointToSegment(firstStart, secondStart, secondEnd),
|
|
65
|
+
distancePointToSegment(firstEnd, secondStart, secondEnd),
|
|
66
|
+
distancePointToSegment(secondStart, firstStart, firstEnd),
|
|
67
|
+
distancePointToSegment(secondEnd, firstStart, firstEnd),
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function pointIsInsideObstacle(
|
|
72
|
+
point: Point2D,
|
|
73
|
+
obstacle: Obstacle,
|
|
74
|
+
tolerance = EPSILON,
|
|
75
|
+
): boolean {
|
|
76
|
+
if (obstacleIsCircular(obstacle)) {
|
|
77
|
+
return distance(point, obstacle.center) <= obstacle.width / 2 + tolerance
|
|
78
|
+
}
|
|
79
|
+
return (
|
|
80
|
+
Math.abs(point.x - obstacle.center.x) <= obstacle.width / 2 + tolerance &&
|
|
81
|
+
Math.abs(point.y - obstacle.center.y) <= obstacle.height / 2 + tolerance
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function distancePointToObstacle(
|
|
86
|
+
point: Point2D,
|
|
87
|
+
obstacle: Obstacle,
|
|
88
|
+
): number {
|
|
89
|
+
if (obstacleIsCircular(obstacle)) {
|
|
90
|
+
return Math.max(0, distance(point, obstacle.center) - obstacle.width / 2)
|
|
91
|
+
}
|
|
92
|
+
const dx = Math.max(
|
|
93
|
+
Math.abs(point.x - obstacle.center.x) - obstacle.width / 2,
|
|
94
|
+
0,
|
|
95
|
+
)
|
|
96
|
+
const dy = Math.max(
|
|
97
|
+
Math.abs(point.y - obstacle.center.y) - obstacle.height / 2,
|
|
98
|
+
0,
|
|
99
|
+
)
|
|
100
|
+
return Math.hypot(dx, dy)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function distanceSegmentToObstacle(
|
|
104
|
+
segment: RoutedSegment,
|
|
105
|
+
obstacle: Obstacle,
|
|
106
|
+
): number {
|
|
107
|
+
if (obstacleIsCircular(obstacle)) {
|
|
108
|
+
return Math.max(
|
|
109
|
+
0,
|
|
110
|
+
distancePointToSegment(obstacle.center, segment.start, segment.end) -
|
|
111
|
+
obstacle.width / 2,
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
if (
|
|
115
|
+
pointIsInsideObstacle(segment.start, obstacle) ||
|
|
116
|
+
pointIsInsideObstacle(segment.end, obstacle)
|
|
117
|
+
) {
|
|
118
|
+
return 0
|
|
119
|
+
}
|
|
120
|
+
const minX = obstacle.center.x - obstacle.width / 2
|
|
121
|
+
const maxX = obstacle.center.x + obstacle.width / 2
|
|
122
|
+
const minY = obstacle.center.y - obstacle.height / 2
|
|
123
|
+
const maxY = obstacle.center.y + obstacle.height / 2
|
|
124
|
+
const corners = [
|
|
125
|
+
{ x: minX, y: minY },
|
|
126
|
+
{ x: maxX, y: minY },
|
|
127
|
+
{ x: maxX, y: maxY },
|
|
128
|
+
{ x: minX, y: maxY },
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
let minimumDistance = Number.POSITIVE_INFINITY
|
|
132
|
+
for (let index = 0; index < corners.length; index++) {
|
|
133
|
+
minimumDistance = Math.min(
|
|
134
|
+
minimumDistance,
|
|
135
|
+
distanceSegmentToSegment(
|
|
136
|
+
segment.start,
|
|
137
|
+
segment.end,
|
|
138
|
+
corners[index]!,
|
|
139
|
+
corners[(index + 1) % corners.length]!,
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
return minimumDistance
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function segmentsAreClear(
|
|
147
|
+
first: RoutedSegment,
|
|
148
|
+
second: RoutedSegment,
|
|
149
|
+
clearance: number,
|
|
150
|
+
): boolean {
|
|
151
|
+
if (first.layer !== second.layer) return true
|
|
152
|
+
const requiredDistance = (first.width + second.width) / 2 + clearance
|
|
153
|
+
return (
|
|
154
|
+
distanceSegmentToSegment(
|
|
155
|
+
first.start,
|
|
156
|
+
first.end,
|
|
157
|
+
second.start,
|
|
158
|
+
second.end,
|
|
159
|
+
) >=
|
|
160
|
+
requiredDistance - EPSILON
|
|
161
|
+
)
|
|
162
|
+
}
|
package/lib/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { FanoutSolver } from "./fanout-solver"
|
|
2
|
+
export { getCopperLayerColor } from "./layer-colors"
|
|
3
|
+
export { getCopperLayerNames } from "./layer-names"
|
|
4
|
+
export type {
|
|
5
|
+
Bounds,
|
|
6
|
+
FanoutAttemptSummary,
|
|
7
|
+
FanoutAvailableCornerAndSide,
|
|
8
|
+
FanoutAvailableCornerAndSideAlias,
|
|
9
|
+
FanoutAvailableCornerAndSideInput,
|
|
10
|
+
FanoutBorderDistribution,
|
|
11
|
+
FanoutBorderTarget,
|
|
12
|
+
FanoutBusSpec,
|
|
13
|
+
FanoutBusTermination,
|
|
14
|
+
FanoutCorner,
|
|
15
|
+
FanoutDirection,
|
|
16
|
+
FanoutEdge,
|
|
17
|
+
FanoutPlaneTermination,
|
|
18
|
+
FanoutSolverOptions,
|
|
19
|
+
FanoutSolverOutput,
|
|
20
|
+
} from "./types"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const COPPER_LAYER_COLORS = [
|
|
2
|
+
"#ef4444",
|
|
3
|
+
"#2563eb",
|
|
4
|
+
"#16a34a",
|
|
5
|
+
"#9333ea",
|
|
6
|
+
"#f59e0b",
|
|
7
|
+
"#0891b2",
|
|
8
|
+
"#db2777",
|
|
9
|
+
"#65a30d",
|
|
10
|
+
"#4f46e5",
|
|
11
|
+
"#ea580c",
|
|
12
|
+
] as const
|
|
13
|
+
|
|
14
|
+
export function getCopperLayerColor(layerIndex: number): string {
|
|
15
|
+
if (!Number.isInteger(layerIndex) || layerIndex < 0) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`FanoutSolver: copper layer index must be a non-negative integer, received ${layerIndex}`,
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
return COPPER_LAYER_COLORS[layerIndex % COPPER_LAYER_COLORS.length]!
|
|
21
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export function getCopperLayerNames(layerCount: number): string[] {
|
|
2
|
+
if (!Number.isInteger(layerCount) || layerCount < 1) {
|
|
3
|
+
throw new Error(
|
|
4
|
+
`FanoutSolver: layerCount must be a positive integer, received ${layerCount}`,
|
|
5
|
+
)
|
|
6
|
+
}
|
|
7
|
+
if (layerCount === 1) return ["top"]
|
|
8
|
+
if (layerCount === 2) return ["top", "bottom"]
|
|
9
|
+
|
|
10
|
+
return [
|
|
11
|
+
"top",
|
|
12
|
+
...Array.from(
|
|
13
|
+
{ length: layerCount - 2 },
|
|
14
|
+
(_, index) => `inner${index + 1}`,
|
|
15
|
+
),
|
|
16
|
+
"bottom",
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getLayerSpan(
|
|
21
|
+
fromLayer: string,
|
|
22
|
+
toLayer: string,
|
|
23
|
+
layerNames: string[],
|
|
24
|
+
): string[] {
|
|
25
|
+
const fromIndex = layerNames.indexOf(fromLayer)
|
|
26
|
+
const toIndex = layerNames.indexOf(toLayer)
|
|
27
|
+
if (fromIndex < 0 || toIndex < 0) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`FanoutSolver: cannot build via span from "${fromLayer}" to "${toLayer}"`,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
const firstIndex = Math.min(fromIndex, toIndex)
|
|
33
|
+
const lastIndex = Math.max(fromIndex, toIndex)
|
|
34
|
+
return layerNames.slice(firstIndex, lastIndex + 1)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function generateLayerAssignments(params: {
|
|
38
|
+
busIds: string[]
|
|
39
|
+
layers: string[]
|
|
40
|
+
maxAssignments: number
|
|
41
|
+
}): Array<Readonly<Record<string, string>>> {
|
|
42
|
+
const { busIds, layers, maxAssignments } = params
|
|
43
|
+
if (layers.length === 0) {
|
|
44
|
+
throw new Error("FanoutSolver: no escape layers are available")
|
|
45
|
+
}
|
|
46
|
+
if (!Number.isInteger(maxAssignments) || maxAssignments < 1) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`FanoutSolver: maxLayerCombinations must be positive, received ${maxAssignments}`,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const rawCombinationCount = layers.length ** busIds.length
|
|
53
|
+
const combinationCount = Math.min(
|
|
54
|
+
maxAssignments,
|
|
55
|
+
Number.isFinite(rawCombinationCount) ? rawCombinationCount : maxAssignments,
|
|
56
|
+
)
|
|
57
|
+
const assignments: Array<Readonly<Record<string, string>>> = []
|
|
58
|
+
const seenAssignments = new Set<string>()
|
|
59
|
+
|
|
60
|
+
function addAssignment(layerIndexes: number[]): void {
|
|
61
|
+
const assignment: Record<string, string> = {}
|
|
62
|
+
for (let busIndex = 0; busIndex < busIds.length; busIndex++) {
|
|
63
|
+
assignment[busIds[busIndex]!] = layers[layerIndexes[busIndex]!]!
|
|
64
|
+
}
|
|
65
|
+
const key = JSON.stringify(assignment)
|
|
66
|
+
if (seenAssignments.has(key)) return
|
|
67
|
+
seenAssignments.add(key)
|
|
68
|
+
assignments.push(assignment)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (rawCombinationCount <= maxAssignments) {
|
|
72
|
+
for (let ordinal = 0; ordinal < combinationCount; ordinal++) {
|
|
73
|
+
const layerIndexes: number[] = []
|
|
74
|
+
let remaining = ordinal
|
|
75
|
+
for (let busIndex = 0; busIndex < busIds.length; busIndex++) {
|
|
76
|
+
const digit = remaining % layers.length
|
|
77
|
+
remaining = Math.floor(remaining / layers.length)
|
|
78
|
+
layerIndexes.push((digit + busIndex) % layers.length)
|
|
79
|
+
}
|
|
80
|
+
addAssignment(layerIndexes)
|
|
81
|
+
}
|
|
82
|
+
return assignments
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function mix32(value: number): number {
|
|
86
|
+
let mixed = value | 0
|
|
87
|
+
mixed = Math.imul(mixed ^ (mixed >>> 16), 0x21f0aaad)
|
|
88
|
+
mixed = Math.imul(mixed ^ (mixed >>> 15), 0x735a2d97)
|
|
89
|
+
return (mixed ^ (mixed >>> 15)) >>> 0
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const balancedLayerIndexes = busIds.map(
|
|
93
|
+
(_, busIndex) => busIndex % layers.length,
|
|
94
|
+
)
|
|
95
|
+
addAssignment(balancedLayerIndexes)
|
|
96
|
+
for (
|
|
97
|
+
let globalShift = 1;
|
|
98
|
+
globalShift < layers.length && assignments.length < combinationCount;
|
|
99
|
+
globalShift++
|
|
100
|
+
) {
|
|
101
|
+
addAssignment(
|
|
102
|
+
balancedLayerIndexes.map(
|
|
103
|
+
(layerIndex) => (layerIndex + globalShift) % layers.length,
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
for (
|
|
108
|
+
let busIndex = 0;
|
|
109
|
+
busIndex < busIds.length && assignments.length < combinationCount;
|
|
110
|
+
busIndex++
|
|
111
|
+
) {
|
|
112
|
+
for (
|
|
113
|
+
let shift = 1;
|
|
114
|
+
shift < layers.length && assignments.length < combinationCount;
|
|
115
|
+
shift++
|
|
116
|
+
) {
|
|
117
|
+
const layerIndexes = [...balancedLayerIndexes]
|
|
118
|
+
layerIndexes[busIndex] =
|
|
119
|
+
(balancedLayerIndexes[busIndex]! + shift) % layers.length
|
|
120
|
+
addAssignment(layerIndexes)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (
|
|
124
|
+
let seed = 1;
|
|
125
|
+
assignments.length < combinationCount && seed < combinationCount * 20;
|
|
126
|
+
seed++
|
|
127
|
+
) {
|
|
128
|
+
addAssignment(
|
|
129
|
+
busIds.map(
|
|
130
|
+
(_, busIndex) =>
|
|
131
|
+
mix32(seed * 0x9e3779b1 + busIndex * 0x85ebca6b) % layers.length,
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return assignments
|
|
137
|
+
}
|