@tscircuit/fanout-solver 0.0.38 → 0.0.39
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 +7 -0
- package/lib/add-via-layer-metadata.ts +48 -0
- package/lib/complete-original-endpoints.ts +30 -2
- package/lib/fanout-solver.ts +636 -14
- package/lib/geometry.ts +21 -0
- package/lib/get-routed-trace-copper.ts +15 -6
- package/lib/index.ts +3 -0
- package/lib/layer-names.ts +40 -0
- package/lib/match-bus-lengths.ts +124 -14
- package/lib/match-component-dogbone-via-sites.ts +584 -0
- package/lib/route-bus.ts +872 -112
- package/lib/route-via-minimal-winding.ts +390 -74
- package/lib/types.ts +35 -7
- package/lib/validate-fanout-solution.ts +34 -6
- package/lib/validate-routed-copper-drc.ts +47 -8
- package/package.json +1 -1
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import type { Obstacle } from "@tscircuit/capacity-autorouter"
|
|
2
|
+
import {
|
|
3
|
+
distance,
|
|
4
|
+
distancePointToObstacle,
|
|
5
|
+
distancePointToSegment,
|
|
6
|
+
distanceSegmentToObstacle,
|
|
7
|
+
segmentsAreClear,
|
|
8
|
+
} from "./geometry"
|
|
9
|
+
import type {
|
|
10
|
+
FanoutDirection,
|
|
11
|
+
Point2D,
|
|
12
|
+
PreparedBus,
|
|
13
|
+
PreparedConnection,
|
|
14
|
+
RoutedSegment,
|
|
15
|
+
} from "./types"
|
|
16
|
+
|
|
17
|
+
const EPSILON = 1e-9
|
|
18
|
+
const DEFAULT_MAXIMUM_SEARCH_STATES = 100_000
|
|
19
|
+
|
|
20
|
+
export interface DogboneViaSiteGeometryRules {
|
|
21
|
+
viaDiameter: number
|
|
22
|
+
viaHoleDiameter?: number
|
|
23
|
+
traceWidth: number
|
|
24
|
+
clearance: number
|
|
25
|
+
/** Defaults to `clearance` when a hole diameter is supplied. */
|
|
26
|
+
holeToHoleClearance?: number
|
|
27
|
+
/** Bounds the deterministic backtracking search across all components. */
|
|
28
|
+
maximumSearchStates?: number
|
|
29
|
+
/**
|
|
30
|
+
* Optional bounded-search preference for boundary-bus dogbones. The sign
|
|
31
|
+
* refers to the axis perpendicular to each bus's local escape direction.
|
|
32
|
+
*/
|
|
33
|
+
preferredBoundaryPerpendicularSideByBusId?: ReadonlyMap<string, -1 | 1>
|
|
34
|
+
/** Prefer the local outward or inward half-pitch row for a boundary bus. */
|
|
35
|
+
preferBoundaryOutwardByBusId?: ReadonlyMap<string, boolean>
|
|
36
|
+
/** True only when the two connections are allowed to merge copper. */
|
|
37
|
+
canShareCopper?: (
|
|
38
|
+
firstConnectionIndex: number,
|
|
39
|
+
secondConnectionIndex: number,
|
|
40
|
+
) => boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface ComponentConnection {
|
|
44
|
+
preparedConnection: PreparedConnection
|
|
45
|
+
busId: string
|
|
46
|
+
direction: FanoutDirection
|
|
47
|
+
terminationType: PreparedBus["termination"]["type"]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ComponentMatchingInput {
|
|
51
|
+
componentId: string
|
|
52
|
+
connections: ComponentConnection[]
|
|
53
|
+
obstacles: Obstacle[]
|
|
54
|
+
xCoordinates: number[]
|
|
55
|
+
yCoordinates: number[]
|
|
56
|
+
pitchX: number
|
|
57
|
+
pitchY: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface ViaSiteCandidate {
|
|
61
|
+
connectionIndex: number
|
|
62
|
+
point: Point2D
|
|
63
|
+
sourceSegment: RoutedSegment
|
|
64
|
+
outwardRank: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface ConnectionCandidates {
|
|
68
|
+
connection: ComponentConnection
|
|
69
|
+
candidates: ViaSiteCandidate[]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function assertGeometryRules(rules: DogboneViaSiteGeometryRules): number {
|
|
73
|
+
for (const [name, value] of [
|
|
74
|
+
["viaDiameter", rules.viaDiameter],
|
|
75
|
+
["traceWidth", rules.traceWidth],
|
|
76
|
+
] as const) {
|
|
77
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`FanoutSolver: dogbone ${name} must be a positive finite number, received ${value}`,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!Number.isFinite(rules.clearance) || rules.clearance < 0) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`FanoutSolver: dogbone clearance must be a non-negative finite number, received ${rules.clearance}`,
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
if (
|
|
89
|
+
rules.viaHoleDiameter !== undefined &&
|
|
90
|
+
(!Number.isFinite(rules.viaHoleDiameter) || rules.viaHoleDiameter <= 0)
|
|
91
|
+
) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`FanoutSolver: dogbone viaHoleDiameter must be a positive finite number, received ${rules.viaHoleDiameter}`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
if (
|
|
97
|
+
rules.holeToHoleClearance !== undefined &&
|
|
98
|
+
(!Number.isFinite(rules.holeToHoleClearance) ||
|
|
99
|
+
rules.holeToHoleClearance < 0)
|
|
100
|
+
) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`FanoutSolver: dogbone holeToHoleClearance must be a non-negative finite number, received ${rules.holeToHoleClearance}`,
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
rules.holeToHoleClearance !== undefined &&
|
|
107
|
+
rules.viaHoleDiameter === undefined
|
|
108
|
+
) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
"FanoutSolver: dogbone holeToHoleClearance requires viaHoleDiameter",
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
const maximumSearchStates =
|
|
114
|
+
rules.maximumSearchStates ?? DEFAULT_MAXIMUM_SEARCH_STATES
|
|
115
|
+
if (!Number.isInteger(maximumSearchStates) || maximumSearchStates < 1) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`FanoutSolver: dogbone maximumSearchStates must be a positive integer, received ${maximumSearchStates}`,
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
return maximumSearchStates
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function uniqueSortedCoordinates(values: readonly number[]): number[] {
|
|
124
|
+
const result: number[] = []
|
|
125
|
+
for (const value of values.toSorted((first, second) => first - second)) {
|
|
126
|
+
if (!Number.isFinite(value)) continue
|
|
127
|
+
if (result.length === 0 || Math.abs(result.at(-1)! - value) > EPSILON) {
|
|
128
|
+
result.push(value)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return result
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getComponentMatchingInputs(
|
|
135
|
+
preparedBuses: readonly PreparedBus[],
|
|
136
|
+
): ComponentMatchingInput[] {
|
|
137
|
+
const byComponent = new Map<string, ComponentMatchingInput>()
|
|
138
|
+
const componentByConnectionIndex = new Map<number, string>()
|
|
139
|
+
|
|
140
|
+
for (const bus of preparedBuses) {
|
|
141
|
+
let component = byComponent.get(bus.componentId)
|
|
142
|
+
if (!component) {
|
|
143
|
+
component = {
|
|
144
|
+
componentId: bus.componentId,
|
|
145
|
+
connections: [],
|
|
146
|
+
obstacles: [],
|
|
147
|
+
xCoordinates: [],
|
|
148
|
+
yCoordinates: [],
|
|
149
|
+
pitchX: Number.POSITIVE_INFINITY,
|
|
150
|
+
pitchY: Number.POSITIVE_INFINITY,
|
|
151
|
+
}
|
|
152
|
+
byComponent.set(bus.componentId, component)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
component.xCoordinates.push(...bus.xCoordinates)
|
|
156
|
+
component.yCoordinates.push(...bus.yCoordinates)
|
|
157
|
+
if (Number.isFinite(bus.pitchX) && bus.pitchX > EPSILON) {
|
|
158
|
+
component.pitchX = Math.min(component.pitchX, bus.pitchX)
|
|
159
|
+
}
|
|
160
|
+
if (Number.isFinite(bus.pitchY) && bus.pitchY > EPSILON) {
|
|
161
|
+
component.pitchY = Math.min(component.pitchY, bus.pitchY)
|
|
162
|
+
}
|
|
163
|
+
for (const obstacle of bus.componentObstacles) {
|
|
164
|
+
if (!component.obstacles.includes(obstacle)) {
|
|
165
|
+
component.obstacles.push(obstacle)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const preparedConnection of bus.connections) {
|
|
169
|
+
const existingComponent = componentByConnectionIndex.get(
|
|
170
|
+
preparedConnection.connectionIndex,
|
|
171
|
+
)
|
|
172
|
+
if (existingComponent !== undefined) {
|
|
173
|
+
if (existingComponent !== bus.componentId) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`FanoutSolver: connection index ${preparedConnection.connectionIndex} belongs to multiple components`,
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
componentByConnectionIndex.set(
|
|
181
|
+
preparedConnection.connectionIndex,
|
|
182
|
+
bus.componentId,
|
|
183
|
+
)
|
|
184
|
+
component.connections.push({
|
|
185
|
+
preparedConnection,
|
|
186
|
+
busId: bus.busId,
|
|
187
|
+
direction: bus.direction,
|
|
188
|
+
terminationType: bus.termination.type,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return [...byComponent.values()]
|
|
194
|
+
.map((component) => ({
|
|
195
|
+
...component,
|
|
196
|
+
connections: component.connections.toSorted(
|
|
197
|
+
(first, second) =>
|
|
198
|
+
first.preparedConnection.connectionIndex -
|
|
199
|
+
second.preparedConnection.connectionIndex,
|
|
200
|
+
),
|
|
201
|
+
xCoordinates: uniqueSortedCoordinates(component.xCoordinates),
|
|
202
|
+
yCoordinates: uniqueSortedCoordinates(component.yCoordinates),
|
|
203
|
+
}))
|
|
204
|
+
.toSorted((first, second) =>
|
|
205
|
+
first.componentId.localeCompare(second.componentId),
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getInterstitialCoordinates(params: {
|
|
210
|
+
coordinates: readonly number[]
|
|
211
|
+
pitch: number
|
|
212
|
+
}): number[] {
|
|
213
|
+
const { coordinates, pitch } = params
|
|
214
|
+
if (coordinates.length === 0 || !Number.isFinite(pitch) || pitch <= EPSILON) {
|
|
215
|
+
return []
|
|
216
|
+
}
|
|
217
|
+
const interstitialCoordinates = [coordinates[0]! - pitch / 2]
|
|
218
|
+
for (let index = 1; index < coordinates.length; index++) {
|
|
219
|
+
interstitialCoordinates.push(
|
|
220
|
+
(coordinates[index - 1]! + coordinates[index]!) / 2,
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
interstitialCoordinates.push(coordinates.at(-1)! + pitch / 2)
|
|
224
|
+
return uniqueSortedCoordinates(interstitialCoordinates)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function getAdjacentInterstitialCoordinates(params: {
|
|
228
|
+
sourceCoordinate: number
|
|
229
|
+
coordinates: readonly number[]
|
|
230
|
+
pitch: number
|
|
231
|
+
}): number[] {
|
|
232
|
+
const { sourceCoordinate, coordinates, pitch } = params
|
|
233
|
+
const interstitialCoordinates = getInterstitialCoordinates({
|
|
234
|
+
coordinates,
|
|
235
|
+
pitch,
|
|
236
|
+
})
|
|
237
|
+
const before = interstitialCoordinates
|
|
238
|
+
.filter((coordinate) => coordinate < sourceCoordinate - EPSILON)
|
|
239
|
+
.at(-1)
|
|
240
|
+
const after = interstitialCoordinates.find(
|
|
241
|
+
(coordinate) => coordinate > sourceCoordinate + EPSILON,
|
|
242
|
+
)
|
|
243
|
+
return [before, after].filter(
|
|
244
|
+
(coordinate): coordinate is number => coordinate !== undefined,
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function directSegmentIsStraightOr45(start: Point2D, end: Point2D): boolean {
|
|
249
|
+
const absoluteX = Math.abs(end.x - start.x)
|
|
250
|
+
const absoluteY = Math.abs(end.y - start.y)
|
|
251
|
+
return (
|
|
252
|
+
absoluteX <= EPSILON ||
|
|
253
|
+
absoluteY <= EPSILON ||
|
|
254
|
+
Math.abs(absoluteX - absoluteY) <= EPSILON
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function getOutwardRank(params: {
|
|
259
|
+
source: Point2D
|
|
260
|
+
site: Point2D
|
|
261
|
+
direction: FanoutDirection
|
|
262
|
+
}): number {
|
|
263
|
+
const { source, site, direction } = params
|
|
264
|
+
const outwardDisplacement =
|
|
265
|
+
direction === "right"
|
|
266
|
+
? site.x - source.x
|
|
267
|
+
: direction === "left"
|
|
268
|
+
? source.x - site.x
|
|
269
|
+
: direction === "up"
|
|
270
|
+
? site.y - source.y
|
|
271
|
+
: source.y - site.y
|
|
272
|
+
return outwardDisplacement > EPSILON
|
|
273
|
+
? 0
|
|
274
|
+
: Math.abs(outwardDisplacement) <= EPSILON
|
|
275
|
+
? 1
|
|
276
|
+
: 2
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function viaSiteClearsObstacles(params: {
|
|
280
|
+
point: Point2D
|
|
281
|
+
obstacles: readonly Obstacle[]
|
|
282
|
+
viaDiameter: number
|
|
283
|
+
clearance: number
|
|
284
|
+
}): boolean {
|
|
285
|
+
const { point, obstacles, viaDiameter, clearance } = params
|
|
286
|
+
const requiredClearance = viaDiameter / 2 + clearance
|
|
287
|
+
return obstacles.every(
|
|
288
|
+
(obstacle) =>
|
|
289
|
+
distancePointToObstacle(point, obstacle) >= requiredClearance - EPSILON,
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function sourceSegmentClearsOtherObstacles(params: {
|
|
294
|
+
segment: RoutedSegment
|
|
295
|
+
sourceObstacle: Obstacle
|
|
296
|
+
obstacles: readonly Obstacle[]
|
|
297
|
+
clearance: number
|
|
298
|
+
}): boolean {
|
|
299
|
+
const { segment, sourceObstacle, obstacles, clearance } = params
|
|
300
|
+
const requiredClearance = segment.width / 2 + clearance
|
|
301
|
+
return obstacles.every(
|
|
302
|
+
(obstacle) =>
|
|
303
|
+
obstacle === sourceObstacle ||
|
|
304
|
+
distanceSegmentToObstacle(segment, obstacle) >=
|
|
305
|
+
requiredClearance - EPSILON,
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function getConnectionCandidates(params: {
|
|
310
|
+
connection: ComponentConnection
|
|
311
|
+
component: ComponentMatchingInput
|
|
312
|
+
rules: DogboneViaSiteGeometryRules
|
|
313
|
+
}): ViaSiteCandidate[] {
|
|
314
|
+
const { connection, component, rules } = params
|
|
315
|
+
const { preparedConnection, direction } = connection
|
|
316
|
+
const source = {
|
|
317
|
+
x: preparedConnection.sourcePoint.x,
|
|
318
|
+
y: preparedConnection.sourcePoint.y,
|
|
319
|
+
}
|
|
320
|
+
const adjacentX = getAdjacentInterstitialCoordinates({
|
|
321
|
+
sourceCoordinate: source.x,
|
|
322
|
+
coordinates: component.xCoordinates,
|
|
323
|
+
pitch: component.pitchX,
|
|
324
|
+
})
|
|
325
|
+
const adjacentY = getAdjacentInterstitialCoordinates({
|
|
326
|
+
sourceCoordinate: source.y,
|
|
327
|
+
coordinates: component.yCoordinates,
|
|
328
|
+
pitch: component.pitchY,
|
|
329
|
+
})
|
|
330
|
+
const rawPoints: Point2D[] = [
|
|
331
|
+
...adjacentX.map((x) => ({ x, y: source.y })),
|
|
332
|
+
...adjacentY.map((y) => ({ x: source.x, y })),
|
|
333
|
+
...adjacentX.flatMap((x) => adjacentY.map((y) => ({ x, y }))),
|
|
334
|
+
]
|
|
335
|
+
const uniquePoints: Point2D[] = []
|
|
336
|
+
for (const point of rawPoints) {
|
|
337
|
+
if (
|
|
338
|
+
!uniquePoints.some((candidate) => distance(candidate, point) <= EPSILON)
|
|
339
|
+
) {
|
|
340
|
+
uniquePoints.push(point)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const candidates: ViaSiteCandidate[] = []
|
|
345
|
+
for (const point of uniquePoints) {
|
|
346
|
+
if (
|
|
347
|
+
connection.terminationType === "plane" &&
|
|
348
|
+
!directSegmentIsStraightOr45(source, point)
|
|
349
|
+
) {
|
|
350
|
+
continue
|
|
351
|
+
}
|
|
352
|
+
if (
|
|
353
|
+
!viaSiteClearsObstacles({
|
|
354
|
+
point,
|
|
355
|
+
obstacles: component.obstacles,
|
|
356
|
+
viaDiameter: rules.viaDiameter,
|
|
357
|
+
clearance: rules.clearance,
|
|
358
|
+
})
|
|
359
|
+
) {
|
|
360
|
+
continue
|
|
361
|
+
}
|
|
362
|
+
const sourceSegment: RoutedSegment = {
|
|
363
|
+
start: source,
|
|
364
|
+
end: point,
|
|
365
|
+
width: rules.traceWidth,
|
|
366
|
+
layer: preparedConnection.sourceLayer,
|
|
367
|
+
}
|
|
368
|
+
if (
|
|
369
|
+
!sourceSegmentClearsOtherObstacles({
|
|
370
|
+
segment: sourceSegment,
|
|
371
|
+
sourceObstacle: preparedConnection.sourceObstacle,
|
|
372
|
+
obstacles: component.obstacles,
|
|
373
|
+
clearance: rules.clearance,
|
|
374
|
+
})
|
|
375
|
+
) {
|
|
376
|
+
continue
|
|
377
|
+
}
|
|
378
|
+
candidates.push({
|
|
379
|
+
connectionIndex: preparedConnection.connectionIndex,
|
|
380
|
+
point,
|
|
381
|
+
sourceSegment,
|
|
382
|
+
outwardRank: getOutwardRank({ source, site: point, direction }),
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const preferredPerpendicularSide =
|
|
387
|
+
connection.terminationType === "boundary"
|
|
388
|
+
? rules.preferredBoundaryPerpendicularSideByBusId?.get(connection.busId)
|
|
389
|
+
: undefined
|
|
390
|
+
const preferOutward =
|
|
391
|
+
connection.terminationType === "boundary"
|
|
392
|
+
? (rules.preferBoundaryOutwardByBusId?.get(connection.busId) ?? true)
|
|
393
|
+
: true
|
|
394
|
+
const getPerpendicularPreferenceRank = (
|
|
395
|
+
candidate: ViaSiteCandidate,
|
|
396
|
+
): number => {
|
|
397
|
+
if (preferredPerpendicularSide === undefined) return 0
|
|
398
|
+
const displacement =
|
|
399
|
+
direction === "left" || direction === "right"
|
|
400
|
+
? candidate.point.y - source.y
|
|
401
|
+
: candidate.point.x - source.x
|
|
402
|
+
return displacement * preferredPerpendicularSide > EPSILON
|
|
403
|
+
? 0
|
|
404
|
+
: Math.abs(displacement) <= EPSILON
|
|
405
|
+
? 1
|
|
406
|
+
: 2
|
|
407
|
+
}
|
|
408
|
+
return candidates.toSorted(
|
|
409
|
+
(first, second) =>
|
|
410
|
+
(preferOutward
|
|
411
|
+
? first.outwardRank - second.outwardRank
|
|
412
|
+
: second.outwardRank - first.outwardRank) ||
|
|
413
|
+
getPerpendicularPreferenceRank(first) -
|
|
414
|
+
getPerpendicularPreferenceRank(second) ||
|
|
415
|
+
distance(source, first.point) - distance(source, second.point) ||
|
|
416
|
+
first.point.x - second.point.x ||
|
|
417
|
+
first.point.y - second.point.y,
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function candidatesAreMutuallyClear(params: {
|
|
422
|
+
first: ViaSiteCandidate
|
|
423
|
+
second: ViaSiteCandidate
|
|
424
|
+
rules: DogboneViaSiteGeometryRules
|
|
425
|
+
}): boolean {
|
|
426
|
+
const { first, second, rules } = params
|
|
427
|
+
const canShareCopper =
|
|
428
|
+
rules.canShareCopper?.(first.connectionIndex, second.connectionIndex) ??
|
|
429
|
+
false
|
|
430
|
+
const requiredHoleSeparation = rules.viaHoleDiameter
|
|
431
|
+
? rules.viaHoleDiameter + (rules.holeToHoleClearance ?? rules.clearance)
|
|
432
|
+
: 0
|
|
433
|
+
const requiredViaSeparation = canShareCopper
|
|
434
|
+
? requiredHoleSeparation
|
|
435
|
+
: Math.max(rules.viaDiameter + rules.clearance, requiredHoleSeparation)
|
|
436
|
+
if (distance(first.point, second.point) < requiredViaSeparation - EPSILON) {
|
|
437
|
+
return false
|
|
438
|
+
}
|
|
439
|
+
const requiredViaToTraceClearance =
|
|
440
|
+
rules.viaDiameter / 2 + rules.traceWidth / 2 + rules.clearance
|
|
441
|
+
if (!canShareCopper) {
|
|
442
|
+
if (
|
|
443
|
+
distancePointToSegment(
|
|
444
|
+
first.point,
|
|
445
|
+
second.sourceSegment.start,
|
|
446
|
+
second.sourceSegment.end,
|
|
447
|
+
) <
|
|
448
|
+
requiredViaToTraceClearance - EPSILON ||
|
|
449
|
+
distancePointToSegment(
|
|
450
|
+
second.point,
|
|
451
|
+
first.sourceSegment.start,
|
|
452
|
+
first.sourceSegment.end,
|
|
453
|
+
) <
|
|
454
|
+
requiredViaToTraceClearance - EPSILON
|
|
455
|
+
) {
|
|
456
|
+
return false
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (canShareCopper) return true
|
|
460
|
+
return segmentsAreClear(
|
|
461
|
+
first.sourceSegment,
|
|
462
|
+
second.sourceSegment,
|
|
463
|
+
rules.clearance,
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function matchComponent(params: {
|
|
468
|
+
component: ComponentMatchingInput
|
|
469
|
+
rules: DogboneViaSiteGeometryRules
|
|
470
|
+
consumeSearchState: () => boolean
|
|
471
|
+
}): Map<number, Point2D> | null {
|
|
472
|
+
const { component, rules, consumeSearchState } = params
|
|
473
|
+
const entries: ConnectionCandidates[] = component.connections.map(
|
|
474
|
+
(connection) => ({
|
|
475
|
+
connection,
|
|
476
|
+
candidates: getConnectionCandidates({ connection, component, rules }),
|
|
477
|
+
}),
|
|
478
|
+
)
|
|
479
|
+
if (entries.some((entry) => entry.candidates.length === 0)) return null
|
|
480
|
+
|
|
481
|
+
const assignedCandidates = new Map<number, ViaSiteCandidate>()
|
|
482
|
+
const remaining = new Set(
|
|
483
|
+
entries.map((entry) => entry.connection.preparedConnection.connectionIndex),
|
|
484
|
+
)
|
|
485
|
+
const entryByConnectionIndex = new Map(
|
|
486
|
+
entries.map((entry) => [
|
|
487
|
+
entry.connection.preparedConnection.connectionIndex,
|
|
488
|
+
entry,
|
|
489
|
+
]),
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
const getViableCandidates = (
|
|
493
|
+
entry: ConnectionCandidates,
|
|
494
|
+
): ViaSiteCandidate[] =>
|
|
495
|
+
entry.candidates.filter((candidate) =>
|
|
496
|
+
[...assignedCandidates.values()].every((assignedCandidate) =>
|
|
497
|
+
candidatesAreMutuallyClear({
|
|
498
|
+
first: candidate,
|
|
499
|
+
second: assignedCandidate,
|
|
500
|
+
rules,
|
|
501
|
+
}),
|
|
502
|
+
),
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
const augmentMatching = (): boolean => {
|
|
506
|
+
if (!consumeSearchState()) return false
|
|
507
|
+
if (remaining.size === 0) return true
|
|
508
|
+
|
|
509
|
+
let selectedEntry: ConnectionCandidates | undefined
|
|
510
|
+
let selectedCandidates: ViaSiteCandidate[] = []
|
|
511
|
+
for (const connectionIndex of [...remaining].toSorted(
|
|
512
|
+
(first, second) => first - second,
|
|
513
|
+
)) {
|
|
514
|
+
const entry = entryByConnectionIndex.get(connectionIndex)!
|
|
515
|
+
const viableCandidates = getViableCandidates(entry)
|
|
516
|
+
if (viableCandidates.length === 0) return false
|
|
517
|
+
if (
|
|
518
|
+
!selectedEntry ||
|
|
519
|
+
viableCandidates.length < selectedCandidates.length ||
|
|
520
|
+
(viableCandidates.length === selectedCandidates.length &&
|
|
521
|
+
connectionIndex <
|
|
522
|
+
selectedEntry.connection.preparedConnection.connectionIndex)
|
|
523
|
+
) {
|
|
524
|
+
selectedEntry = entry
|
|
525
|
+
selectedCandidates = viableCandidates
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const connectionIndex =
|
|
530
|
+
selectedEntry!.connection.preparedConnection.connectionIndex
|
|
531
|
+
remaining.delete(connectionIndex)
|
|
532
|
+
for (const candidate of selectedCandidates) {
|
|
533
|
+
assignedCandidates.set(connectionIndex, candidate)
|
|
534
|
+
if (augmentMatching()) return true
|
|
535
|
+
assignedCandidates.delete(connectionIndex)
|
|
536
|
+
}
|
|
537
|
+
remaining.add(connectionIndex)
|
|
538
|
+
return false
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (!augmentMatching()) return null
|
|
542
|
+
return new Map(
|
|
543
|
+
[...assignedCandidates.entries()]
|
|
544
|
+
.toSorted(([first], [second]) => first - second)
|
|
545
|
+
.map(([connectionIndex, candidate]) => [
|
|
546
|
+
connectionIndex,
|
|
547
|
+
{ ...candidate.point },
|
|
548
|
+
]),
|
|
549
|
+
)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Matches every prepared connection to a legal adjacent dogbone via site.
|
|
554
|
+
*
|
|
555
|
+
* Candidate sites are derived from the component pad-center grid: midpoint
|
|
556
|
+
* gaps plus one half-pitch perimeter coordinate on each side. The matcher
|
|
557
|
+
* never infers component or connection metadata from identifiers.
|
|
558
|
+
*/
|
|
559
|
+
export function matchComponentDogboneViaSites(
|
|
560
|
+
preparedBuses: readonly PreparedBus[],
|
|
561
|
+
rules: DogboneViaSiteGeometryRules,
|
|
562
|
+
): Map<number, Point2D> | null {
|
|
563
|
+
const maximumSearchStates = assertGeometryRules(rules)
|
|
564
|
+
if (preparedBuses.length === 0) return new Map()
|
|
565
|
+
|
|
566
|
+
let consumedSearchStates = 0
|
|
567
|
+
const consumeSearchState = (): boolean => {
|
|
568
|
+
consumedSearchStates++
|
|
569
|
+
return consumedSearchStates <= maximumSearchStates
|
|
570
|
+
}
|
|
571
|
+
const result = new Map<number, Point2D>()
|
|
572
|
+
for (const component of getComponentMatchingInputs(preparedBuses)) {
|
|
573
|
+
const componentResult = matchComponent({
|
|
574
|
+
component,
|
|
575
|
+
rules,
|
|
576
|
+
consumeSearchState,
|
|
577
|
+
})
|
|
578
|
+
if (!componentResult) return null
|
|
579
|
+
for (const [connectionIndex, point] of componentResult) {
|
|
580
|
+
result.set(connectionIndex, point)
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return result
|
|
584
|
+
}
|