@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.
@@ -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
+ }