@tscircuit/fanout-solver 0.0.17 → 0.0.19

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/geometry.ts CHANGED
@@ -3,12 +3,27 @@ import type { Point2D, RoutedSegment } from "./types"
3
3
 
4
4
  const EPSILON = 1e-9
5
5
 
6
- type ShapeAwareObstacle = Obstacle & { shape?: "circle" }
6
+ type ShapeAwareObstacle = Obstacle & {
7
+ shape?: "circle"
8
+ ccwRotationDegrees?: number
9
+ }
7
10
 
8
11
  function obstacleIsCircular(obstacle: Obstacle): boolean {
9
12
  return (obstacle as ShapeAwareObstacle).shape === "circle"
10
13
  }
11
14
 
15
+ function toObstacleLocalPoint(point: Point2D, obstacle: Obstacle): Point2D {
16
+ const rotationRadians =
17
+ (-((obstacle as ShapeAwareObstacle).ccwRotationDegrees ?? 0) * Math.PI) /
18
+ 180
19
+ const dx = point.x - obstacle.center.x
20
+ const dy = point.y - obstacle.center.y
21
+ return {
22
+ x: dx * Math.cos(rotationRadians) - dy * Math.sin(rotationRadians),
23
+ y: dx * Math.sin(rotationRadians) + dy * Math.cos(rotationRadians),
24
+ }
25
+ }
26
+
12
27
  export function distance(a: Point2D, b: Point2D): number {
13
28
  return Math.hypot(a.x - b.x, a.y - b.y)
14
29
  }
@@ -76,9 +91,10 @@ export function pointIsInsideObstacle(
76
91
  if (obstacleIsCircular(obstacle)) {
77
92
  return distance(point, obstacle.center) <= obstacle.width / 2 + tolerance
78
93
  }
94
+ const localPoint = toObstacleLocalPoint(point, obstacle)
79
95
  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
96
+ Math.abs(localPoint.x) <= obstacle.width / 2 + tolerance &&
97
+ Math.abs(localPoint.y) <= obstacle.height / 2 + tolerance
82
98
  )
83
99
  }
84
100
 
@@ -89,14 +105,9 @@ export function distancePointToObstacle(
89
105
  if (obstacleIsCircular(obstacle)) {
90
106
  return Math.max(0, distance(point, obstacle.center) - obstacle.width / 2)
91
107
  }
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
- )
108
+ const localPoint = toObstacleLocalPoint(point, obstacle)
109
+ const dx = Math.max(Math.abs(localPoint.x) - obstacle.width / 2, 0)
110
+ const dy = Math.max(Math.abs(localPoint.y) - obstacle.height / 2, 0)
100
111
  return Math.hypot(dx, dy)
101
112
  }
102
113
 
@@ -111,16 +122,24 @@ export function distanceSegmentToObstacle(
111
122
  obstacle.width / 2,
112
123
  )
113
124
  }
125
+ const localStart = toObstacleLocalPoint(segment.start, obstacle)
126
+ const localEnd = toObstacleLocalPoint(segment.end, obstacle)
127
+ if (
128
+ Math.abs(localStart.x) <= obstacle.width / 2 + EPSILON &&
129
+ Math.abs(localStart.y) <= obstacle.height / 2 + EPSILON
130
+ ) {
131
+ return 0
132
+ }
114
133
  if (
115
- pointIsInsideObstacle(segment.start, obstacle) ||
116
- pointIsInsideObstacle(segment.end, obstacle)
134
+ Math.abs(localEnd.x) <= obstacle.width / 2 + EPSILON &&
135
+ Math.abs(localEnd.y) <= obstacle.height / 2 + EPSILON
117
136
  ) {
118
137
  return 0
119
138
  }
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
139
+ const minX = -obstacle.width / 2
140
+ const maxX = obstacle.width / 2
141
+ const minY = -obstacle.height / 2
142
+ const maxY = obstacle.height / 2
124
143
  const corners = [
125
144
  { x: minX, y: minY },
126
145
  { x: maxX, y: minY },
@@ -133,8 +152,8 @@ export function distanceSegmentToObstacle(
133
152
  minimumDistance = Math.min(
134
153
  minimumDistance,
135
154
  distanceSegmentToSegment(
136
- segment.start,
137
- segment.end,
155
+ localStart,
156
+ localEnd,
138
157
  corners[index]!,
139
158
  corners[(index + 1) % corners.length]!,
140
159
  ),
package/lib/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { FanoutSolver } from "./fanout-solver"
2
2
  export { getCopperLayerColor } from "./layer-colors"
3
3
  export { getCopperLayerNames } from "./layer-names"
4
+ export { validateFanoutSolution } from "./validate-fanout-solution"
4
5
  export type {
5
6
  Bounds,
6
7
  FanoutAttemptSummary,
@@ -15,6 +16,10 @@ export type {
15
16
  FanoutDirection,
16
17
  FanoutEdge,
17
18
  FanoutPlaneTermination,
19
+ FanoutRoutePlan,
18
20
  FanoutSolverOptions,
19
21
  FanoutSolverOutput,
22
+ FanoutValidationIssue,
23
+ FanoutValidationReport,
24
+ PreparedBus,
20
25
  } from "./types"
@@ -37,9 +37,10 @@ export function getLayerSpan(
37
37
  export function generateLayerAssignments(params: {
38
38
  busIds: string[]
39
39
  layers: string[]
40
+ layersByBusId?: Readonly<Record<string, readonly string[]>>
40
41
  maxAssignments: number
41
42
  }): Array<Readonly<Record<string, string>>> {
42
- const { busIds, layers, maxAssignments } = params
43
+ const { busIds, layers, layersByBusId, maxAssignments } = params
43
44
  if (layers.length === 0) {
44
45
  throw new Error("FanoutSolver: no escape layers are available")
45
46
  }
@@ -49,7 +50,22 @@ export function generateLayerAssignments(params: {
49
50
  )
50
51
  }
51
52
 
52
- const rawCombinationCount = layers.length ** busIds.length
53
+ const availableLayersByBus = busIds.map(
54
+ (busId) => layersByBusId?.[busId] ?? layers,
55
+ )
56
+ const busWithoutLayersIndex = availableLayersByBus.findIndex(
57
+ (availableLayers) => availableLayers.length === 0,
58
+ )
59
+ if (busWithoutLayersIndex >= 0) {
60
+ throw new Error(
61
+ `FanoutSolver: no escape layers are available for bus "${busIds[busWithoutLayersIndex]}"`,
62
+ )
63
+ }
64
+
65
+ const rawCombinationCount = availableLayersByBus.reduce(
66
+ (count, availableLayers) => count * availableLayers.length,
67
+ 1,
68
+ )
53
69
  const combinationCount = Math.min(
54
70
  maxAssignments,
55
71
  Number.isFinite(rawCombinationCount) ? rawCombinationCount : maxAssignments,
@@ -60,7 +76,8 @@ export function generateLayerAssignments(params: {
60
76
  function addAssignment(layerIndexes: number[]): void {
61
77
  const assignment: Record<string, string> = {}
62
78
  for (let busIndex = 0; busIndex < busIds.length; busIndex++) {
63
- assignment[busIds[busIndex]!] = layers[layerIndexes[busIndex]!]!
79
+ assignment[busIds[busIndex]!] =
80
+ availableLayersByBus[busIndex]![layerIndexes[busIndex]!]!
64
81
  }
65
82
  const key = JSON.stringify(assignment)
66
83
  if (seenAssignments.has(key)) return
@@ -73,9 +90,10 @@ export function generateLayerAssignments(params: {
73
90
  const layerIndexes: number[] = []
74
91
  let remaining = ordinal
75
92
  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)
93
+ const layerCount = availableLayersByBus[busIndex]!.length
94
+ const digit = remaining % layerCount
95
+ remaining = Math.floor(remaining / layerCount)
96
+ layerIndexes.push((digit + busIndex) % layerCount)
79
97
  }
80
98
  addAssignment(layerIndexes)
81
99
  }
@@ -90,17 +108,22 @@ export function generateLayerAssignments(params: {
90
108
  }
91
109
 
92
110
  const balancedLayerIndexes = busIds.map(
93
- (_, busIndex) => busIndex % layers.length,
111
+ (_, busIndex) => busIndex % availableLayersByBus[busIndex]!.length,
94
112
  )
95
113
  addAssignment(balancedLayerIndexes)
114
+ const maximumAvailableLayerCount = Math.max(
115
+ ...availableLayersByBus.map((availableLayers) => availableLayers.length),
116
+ )
96
117
  for (
97
118
  let globalShift = 1;
98
- globalShift < layers.length && assignments.length < combinationCount;
119
+ globalShift < maximumAvailableLayerCount &&
120
+ assignments.length < combinationCount;
99
121
  globalShift++
100
122
  ) {
101
123
  addAssignment(
102
124
  balancedLayerIndexes.map(
103
- (layerIndex) => (layerIndex + globalShift) % layers.length,
125
+ (layerIndex, busIndex) =>
126
+ (layerIndex + globalShift) % availableLayersByBus[busIndex]!.length,
104
127
  ),
105
128
  )
106
129
  }
@@ -111,12 +134,14 @@ export function generateLayerAssignments(params: {
111
134
  ) {
112
135
  for (
113
136
  let shift = 1;
114
- shift < layers.length && assignments.length < combinationCount;
137
+ shift < availableLayersByBus[busIndex]!.length &&
138
+ assignments.length < combinationCount;
115
139
  shift++
116
140
  ) {
117
141
  const layerIndexes = [...balancedLayerIndexes]
118
142
  layerIndexes[busIndex] =
119
- (balancedLayerIndexes[busIndex]! + shift) % layers.length
143
+ (balancedLayerIndexes[busIndex]! + shift) %
144
+ availableLayersByBus[busIndex]!.length
120
145
  addAssignment(layerIndexes)
121
146
  }
122
147
  }
@@ -128,7 +153,8 @@ export function generateLayerAssignments(params: {
128
153
  addAssignment(
129
154
  busIds.map(
130
155
  (_, busIndex) =>
131
- mix32(seed * 0x9e3779b1 + busIndex * 0x85ebca6b) % layers.length,
156
+ mix32(seed * 0x9e3779b1 + busIndex * 0x85ebca6b) %
157
+ availableLayersByBus[busIndex]!.length,
132
158
  ),
133
159
  )
134
160
  }
@@ -0,0 +1,163 @@
1
+ import type {
2
+ Obstacle,
3
+ SimpleRouteConnection,
4
+ SimpleRouteJson,
5
+ } from "@tscircuit/capacity-autorouter"
6
+
7
+ interface ElectricalNetIdentity {
8
+ connectionNetKeys: Map<string, string>
9
+ tokenNetKeys: Map<string, Set<string>>
10
+ }
11
+
12
+ const identityCache = new WeakMap<SimpleRouteJson, ElectricalNetIdentity>()
13
+
14
+ export function getConnectionNetKey(connection: SimpleRouteConnection): string {
15
+ return (
16
+ connection.netConnectionName ??
17
+ connection.rootConnectionName ??
18
+ connection.name
19
+ )
20
+ }
21
+
22
+ function addTokenNet(
23
+ tokenNetKeys: Map<string, Set<string>>,
24
+ token: string | undefined,
25
+ netKey: string,
26
+ ): boolean {
27
+ if (!token) return false
28
+ const keys = tokenNetKeys.get(token) ?? new Set<string>()
29
+ const sizeBefore = keys.size
30
+ keys.add(netKey)
31
+ tokenNetKeys.set(token, keys)
32
+ return keys.size !== sizeBefore
33
+ }
34
+
35
+ function getKnownNetKeys(
36
+ tokenNetKeys: Map<string, Set<string>>,
37
+ tokens: readonly string[],
38
+ ): Set<string> {
39
+ const keys = new Set<string>()
40
+ for (const token of tokens) {
41
+ for (const key of tokenNetKeys.get(token) ?? []) keys.add(key)
42
+ }
43
+ return keys
44
+ }
45
+
46
+ function createElectricalNetIdentity(
47
+ srj: SimpleRouteJson,
48
+ ): ElectricalNetIdentity {
49
+ const connectionNetKeys = new Map<string, string>()
50
+ const tokenNetKeys = new Map<string, Set<string>>()
51
+
52
+ for (const connection of srj.connections) {
53
+ const netKey = getConnectionNetKey(connection)
54
+ connectionNetKeys.set(connection.name, netKey)
55
+ addTokenNet(tokenNetKeys, connection.name, netKey)
56
+ addTokenNet(tokenNetKeys, connection.rootConnectionName, netKey)
57
+ addTokenNet(tokenNetKeys, connection.netConnectionName, netKey)
58
+ for (const point of connection.pointsToConnect) {
59
+ addTokenNet(tokenNetKeys, point.pointId, netKey)
60
+ addTokenNet(tokenNetKeys, point.pcb_port_id, netKey)
61
+ }
62
+ }
63
+
64
+ for (const trace of srj.traces ?? []) {
65
+ const netKey = trace.connection_name
66
+ ? connectionNetKeys.get(trace.connection_name)
67
+ : undefined
68
+ if (!netKey) continue
69
+ addTokenNet(tokenNetKeys, trace.pcb_trace_id, netKey)
70
+ for (const token of trace.connectsTo ?? []) {
71
+ addTokenNet(tokenNetKeys, token, netKey)
72
+ }
73
+ }
74
+
75
+ // Obstacle metadata often contains both a connection id and a lower-level
76
+ // connectivity id. Propagate the known net across that metadata so another
77
+ // pad that only names the connectivity id is still recognized as same-net.
78
+ for (let pass = 0; pass < 2; pass++) {
79
+ let changed = false
80
+ for (const obstacle of srj.obstacles) {
81
+ const netKeys = getKnownNetKeys(tokenNetKeys, obstacle.connectedTo)
82
+ if (netKeys.size !== 1) continue
83
+ const netKey = [...netKeys][0]!
84
+ for (const token of obstacle.connectedTo) {
85
+ changed = addTokenNet(tokenNetKeys, token, netKey) || changed
86
+ }
87
+ }
88
+ if (!changed) break
89
+ }
90
+
91
+ const parentByNetKey = new Map<string, string>()
92
+ const findRoot = (netKey: string): string => {
93
+ const parent = parentByNetKey.get(netKey) ?? netKey
94
+ parentByNetKey.set(netKey, parent)
95
+ if (parent === netKey) return netKey
96
+ const root = findRoot(parent)
97
+ parentByNetKey.set(netKey, root)
98
+ return root
99
+ }
100
+ const union = (first: string, second: string): void => {
101
+ const firstRoot = findRoot(first)
102
+ const secondRoot = findRoot(second)
103
+ if (firstRoot !== secondRoot) parentByNetKey.set(secondRoot, firstRoot)
104
+ }
105
+ for (const netKeys of tokenNetKeys.values()) {
106
+ const [firstNetKey, ...otherNetKeys] = [...netKeys]
107
+ if (!firstNetKey) continue
108
+ for (const otherNetKey of otherNetKeys) union(firstNetKey, otherNetKey)
109
+ }
110
+ for (const connectedTokens of [
111
+ ...srj.obstacles.map((obstacle) => obstacle.connectedTo),
112
+ ...(srj.traces ?? []).map((trace) => trace.connectsTo ?? []),
113
+ ]) {
114
+ const [firstNetKey, ...otherNetKeys] = [
115
+ ...getKnownNetKeys(tokenNetKeys, connectedTokens),
116
+ ]
117
+ if (!firstNetKey) continue
118
+ for (const otherNetKey of otherNetKeys) union(firstNetKey, otherNetKey)
119
+ }
120
+ for (const [connectionName, netKey] of connectionNetKeys) {
121
+ connectionNetKeys.set(connectionName, findRoot(netKey))
122
+ }
123
+ for (const [token, netKeys] of tokenNetKeys) {
124
+ tokenNetKeys.set(
125
+ token,
126
+ new Set([...netKeys].map((netKey) => findRoot(netKey))),
127
+ )
128
+ }
129
+
130
+ return { connectionNetKeys, tokenNetKeys }
131
+ }
132
+
133
+ function getElectricalNetIdentity(srj: SimpleRouteJson): ElectricalNetIdentity {
134
+ const cached = identityCache.get(srj)
135
+ if (cached) return cached
136
+ const identity = createElectricalNetIdentity(srj)
137
+ identityCache.set(srj, identity)
138
+ return identity
139
+ }
140
+
141
+ export function connectionsShareElectricalNet(
142
+ srj: SimpleRouteJson,
143
+ firstConnectionName: string,
144
+ secondConnectionName: string,
145
+ ): boolean {
146
+ const identity = getElectricalNetIdentity(srj)
147
+ const firstNet = identity.connectionNetKeys.get(firstConnectionName)
148
+ const secondNet = identity.connectionNetKeys.get(secondConnectionName)
149
+ return firstNet !== undefined && firstNet === secondNet
150
+ }
151
+
152
+ export function obstacleSharesElectricalNet(
153
+ srj: SimpleRouteJson,
154
+ obstacle: Obstacle,
155
+ connectionName: string,
156
+ ): boolean {
157
+ const identity = getElectricalNetIdentity(srj)
158
+ const connectionNet = identity.connectionNetKeys.get(connectionName)
159
+ if (!connectionNet) return false
160
+ return obstacle.connectedTo.some((token) =>
161
+ identity.tokenNetKeys.get(token)?.has(connectionNet),
162
+ )
163
+ }