@tscircuit/fanout-solver 0.0.37 → 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 +23 -2
- package/lib/add-via-layer-metadata.ts +48 -0
- package/lib/complete-original-endpoints.ts +30 -2
- package/lib/fanout-solver.ts +721 -33
- 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 +681 -0
- package/lib/match-component-dogbone-via-sites.ts +584 -0
- package/lib/prepare-buses.ts +26 -0
- package/lib/route-bus.ts +872 -112
- package/lib/route-via-minimal-winding.ts +390 -74
- package/lib/types.ts +39 -7
- package/lib/validate-fanout-solution.ts +62 -6
- package/lib/validate-routed-copper-drc.ts +47 -8
- package/package.json +1 -1
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SimpleRouteJson,
|
|
3
|
+
SimplifiedPcbTrace,
|
|
4
|
+
} from "@tscircuit/capacity-autorouter"
|
|
5
|
+
import {
|
|
6
|
+
distance,
|
|
7
|
+
distancePointToSegment,
|
|
8
|
+
distanceSegmentToSegment,
|
|
9
|
+
segmentsAreClear,
|
|
10
|
+
} from "./geometry"
|
|
11
|
+
import { fanoutPlansAreClear } from "./route-bus"
|
|
12
|
+
import type {
|
|
13
|
+
Bounds,
|
|
14
|
+
FanoutRoutePlan,
|
|
15
|
+
Point2D,
|
|
16
|
+
PreparedBus,
|
|
17
|
+
RoutedSegment,
|
|
18
|
+
RoutedVia,
|
|
19
|
+
} from "./types"
|
|
20
|
+
|
|
21
|
+
const EPSILON = 1e-6
|
|
22
|
+
|
|
23
|
+
function pointsMatch(first: Point2D, second: Point2D): boolean {
|
|
24
|
+
return distance(first, second) <= EPSILON
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getPlanVias(plan: FanoutRoutePlan): RoutedVia[] {
|
|
28
|
+
return [plan.via, ...(plan.additionalVias ?? [])].filter(
|
|
29
|
+
(via): via is RoutedVia => via !== undefined,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function rebuildTraceRoute(
|
|
34
|
+
plan: FanoutRoutePlan,
|
|
35
|
+
segments: readonly RoutedSegment[],
|
|
36
|
+
): SimplifiedPcbTrace["route"] | null {
|
|
37
|
+
const firstSegment = segments[0]
|
|
38
|
+
if (!firstSegment) return null
|
|
39
|
+
const firstOriginalWire = plan.trace.route.find(
|
|
40
|
+
(point) => point.route_type === "wire",
|
|
41
|
+
)
|
|
42
|
+
const lastOriginalWire = plan.trace.route.findLast(
|
|
43
|
+
(point) => point.route_type === "wire",
|
|
44
|
+
)
|
|
45
|
+
const getWireMetadata = (
|
|
46
|
+
wire: typeof firstOriginalWire,
|
|
47
|
+
): Partial<
|
|
48
|
+
Extract<SimplifiedPcbTrace["route"][number], { route_type: "wire" }>
|
|
49
|
+
> => {
|
|
50
|
+
if (wire?.route_type !== "wire") return {}
|
|
51
|
+
const metadata: Partial<
|
|
52
|
+
Extract<SimplifiedPcbTrace["route"][number], { route_type: "wire" }>
|
|
53
|
+
> = { ...wire }
|
|
54
|
+
delete metadata.route_type
|
|
55
|
+
delete metadata.x
|
|
56
|
+
delete metadata.y
|
|
57
|
+
delete metadata.width
|
|
58
|
+
delete metadata.layer
|
|
59
|
+
return metadata
|
|
60
|
+
}
|
|
61
|
+
const vias = getPlanVias(plan)
|
|
62
|
+
const startsWithSourceVia =
|
|
63
|
+
pointsMatch(firstSegment.start, plan.sourcePoint) &&
|
|
64
|
+
firstSegment.layer !== plan.sourceLayer &&
|
|
65
|
+
vias.some(
|
|
66
|
+
(via) =>
|
|
67
|
+
pointsMatch(via.center, firstSegment.start) &&
|
|
68
|
+
via.spanLayers.includes(plan.sourceLayer) &&
|
|
69
|
+
via.spanLayers.includes(firstSegment.layer),
|
|
70
|
+
)
|
|
71
|
+
const initialLayer = startsWithSourceVia
|
|
72
|
+
? plan.sourceLayer
|
|
73
|
+
: firstSegment.layer
|
|
74
|
+
const route: SimplifiedPcbTrace["route"] = [
|
|
75
|
+
{
|
|
76
|
+
...getWireMetadata(firstOriginalWire),
|
|
77
|
+
route_type: "wire",
|
|
78
|
+
...firstSegment.start,
|
|
79
|
+
width: firstSegment.width,
|
|
80
|
+
layer: initialLayer,
|
|
81
|
+
},
|
|
82
|
+
]
|
|
83
|
+
let currentPoint = firstSegment.start
|
|
84
|
+
let currentLayer = initialLayer
|
|
85
|
+
|
|
86
|
+
for (const [segmentIndex, segment] of segments.entries()) {
|
|
87
|
+
if (!pointsMatch(currentPoint, segment.start)) return null
|
|
88
|
+
if (currentLayer !== segment.layer) {
|
|
89
|
+
const transitionVia = vias.find(
|
|
90
|
+
(via) =>
|
|
91
|
+
pointsMatch(via.center, segment.start) &&
|
|
92
|
+
via.spanLayers.includes(currentLayer) &&
|
|
93
|
+
via.spanLayers.includes(segment.layer),
|
|
94
|
+
)
|
|
95
|
+
if (!transitionVia) return null
|
|
96
|
+
route.push({
|
|
97
|
+
route_type: "via",
|
|
98
|
+
...segment.start,
|
|
99
|
+
from_layer: currentLayer,
|
|
100
|
+
to_layer: segment.layer,
|
|
101
|
+
via_diameter: transitionVia.diameter,
|
|
102
|
+
via_hole_diameter: transitionVia.holeDiameter,
|
|
103
|
+
})
|
|
104
|
+
route.push({
|
|
105
|
+
route_type: "wire",
|
|
106
|
+
...segment.start,
|
|
107
|
+
width: segment.width,
|
|
108
|
+
layer: segment.layer,
|
|
109
|
+
})
|
|
110
|
+
currentLayer = segment.layer
|
|
111
|
+
}
|
|
112
|
+
route.push({
|
|
113
|
+
...(segmentIndex === segments.length - 1
|
|
114
|
+
? getWireMetadata(lastOriginalWire)
|
|
115
|
+
: {}),
|
|
116
|
+
route_type: "wire",
|
|
117
|
+
...segment.end,
|
|
118
|
+
width: segment.width,
|
|
119
|
+
layer: segment.layer,
|
|
120
|
+
})
|
|
121
|
+
currentPoint = segment.end
|
|
122
|
+
}
|
|
123
|
+
return route
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function createPlanWithSegments(
|
|
127
|
+
plan: FanoutRoutePlan,
|
|
128
|
+
segments: RoutedSegment[],
|
|
129
|
+
): FanoutRoutePlan | null {
|
|
130
|
+
const route = rebuildTraceRoute(plan, segments)
|
|
131
|
+
if (!route) return null
|
|
132
|
+
const length = [...segments, ...(plan.planeEndpointSegments ?? [])].reduce(
|
|
133
|
+
(total, segment) => total + distance(segment.start, segment.end),
|
|
134
|
+
0,
|
|
135
|
+
)
|
|
136
|
+
return {
|
|
137
|
+
...plan,
|
|
138
|
+
trace: { ...plan.trace, route },
|
|
139
|
+
segments,
|
|
140
|
+
length,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function pointIsOutsideDenseBounds(
|
|
145
|
+
point: Point2D,
|
|
146
|
+
bounds: Bounds,
|
|
147
|
+
margin: number,
|
|
148
|
+
): boolean {
|
|
149
|
+
return (
|
|
150
|
+
point.x < bounds.minX - margin ||
|
|
151
|
+
point.x > bounds.maxX + margin ||
|
|
152
|
+
point.y < bounds.minY - margin ||
|
|
153
|
+
point.y > bounds.maxY + margin
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function splitSegmentAtDenseBounds(params: {
|
|
158
|
+
segment: RoutedSegment
|
|
159
|
+
bounds: Bounds
|
|
160
|
+
margin: number
|
|
161
|
+
}): RoutedSegment[] {
|
|
162
|
+
const { segment, bounds, margin } = params
|
|
163
|
+
const expandedBounds = {
|
|
164
|
+
minX: bounds.minX - margin,
|
|
165
|
+
maxX: bounds.maxX + margin,
|
|
166
|
+
minY: bounds.minY - margin,
|
|
167
|
+
maxY: bounds.maxY + margin,
|
|
168
|
+
}
|
|
169
|
+
const deltaX = segment.end.x - segment.start.x
|
|
170
|
+
const deltaY = segment.end.y - segment.start.y
|
|
171
|
+
const splitParameters = [0, 1]
|
|
172
|
+
const addSplitParameter = (parameter: number): void => {
|
|
173
|
+
if (parameter <= EPSILON || parameter >= 1 - EPSILON) return
|
|
174
|
+
const point = {
|
|
175
|
+
x: segment.start.x + deltaX * parameter,
|
|
176
|
+
y: segment.start.y + deltaY * parameter,
|
|
177
|
+
}
|
|
178
|
+
if (
|
|
179
|
+
point.x < expandedBounds.minX - EPSILON ||
|
|
180
|
+
point.x > expandedBounds.maxX + EPSILON ||
|
|
181
|
+
point.y < expandedBounds.minY - EPSILON ||
|
|
182
|
+
point.y > expandedBounds.maxY + EPSILON
|
|
183
|
+
) {
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
splitParameters.push(parameter)
|
|
187
|
+
}
|
|
188
|
+
if (Math.abs(deltaX) > EPSILON) {
|
|
189
|
+
addSplitParameter((expandedBounds.minX - segment.start.x) / deltaX)
|
|
190
|
+
addSplitParameter((expandedBounds.maxX - segment.start.x) / deltaX)
|
|
191
|
+
}
|
|
192
|
+
if (Math.abs(deltaY) > EPSILON) {
|
|
193
|
+
addSplitParameter((expandedBounds.minY - segment.start.y) / deltaY)
|
|
194
|
+
addSplitParameter((expandedBounds.maxY - segment.start.y) / deltaY)
|
|
195
|
+
}
|
|
196
|
+
const parameters = splitParameters
|
|
197
|
+
.toSorted((first, second) => first - second)
|
|
198
|
+
.filter(
|
|
199
|
+
(parameter, index, values) =>
|
|
200
|
+
index === 0 || Math.abs(parameter - values[index - 1]!) > EPSILON,
|
|
201
|
+
)
|
|
202
|
+
return parameters.slice(1).map((endParameter, index) => {
|
|
203
|
+
const startParameter = parameters[index]!
|
|
204
|
+
return {
|
|
205
|
+
...segment,
|
|
206
|
+
start: {
|
|
207
|
+
x: segment.start.x + deltaX * startParameter,
|
|
208
|
+
y: segment.start.y + deltaY * startParameter,
|
|
209
|
+
},
|
|
210
|
+
end: {
|
|
211
|
+
x: segment.start.x + deltaX * endParameter,
|
|
212
|
+
y: segment.start.y + deltaY * endParameter,
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function getDenseCopperBounds(bus: PreparedBus): Bounds {
|
|
219
|
+
return bus.componentObstacles.reduce<Bounds>(
|
|
220
|
+
(bounds, obstacle) => ({
|
|
221
|
+
minX: Math.min(bounds.minX, obstacle.center.x - obstacle.width / 2),
|
|
222
|
+
maxX: Math.max(bounds.maxX, obstacle.center.x + obstacle.width / 2),
|
|
223
|
+
minY: Math.min(bounds.minY, obstacle.center.y - obstacle.height / 2),
|
|
224
|
+
maxY: Math.max(bounds.maxY, obstacle.center.y + obstacle.height / 2),
|
|
225
|
+
}),
|
|
226
|
+
{
|
|
227
|
+
minX: Number.POSITIVE_INFINITY,
|
|
228
|
+
maxX: Number.NEGATIVE_INFINITY,
|
|
229
|
+
minY: Number.POSITIVE_INFINITY,
|
|
230
|
+
maxY: Number.NEGATIVE_INFINITY,
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function hasNonAdjacentSelfIntersection(
|
|
236
|
+
segments: readonly RoutedSegment[],
|
|
237
|
+
): boolean {
|
|
238
|
+
for (let firstIndex = 0; firstIndex < segments.length; firstIndex++) {
|
|
239
|
+
const first = segments[firstIndex]!
|
|
240
|
+
for (
|
|
241
|
+
let secondIndex = firstIndex + 2;
|
|
242
|
+
secondIndex < segments.length;
|
|
243
|
+
secondIndex++
|
|
244
|
+
) {
|
|
245
|
+
const second = segments[secondIndex]!
|
|
246
|
+
if (first.layer !== second.layer) continue
|
|
247
|
+
if (
|
|
248
|
+
secondIndex === firstIndex + 2 &&
|
|
249
|
+
pointsMatch(first.end, second.start)
|
|
250
|
+
) {
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
if (
|
|
254
|
+
distanceSegmentToSegment(
|
|
255
|
+
first.start,
|
|
256
|
+
first.end,
|
|
257
|
+
second.start,
|
|
258
|
+
second.end,
|
|
259
|
+
) <= EPSILON
|
|
260
|
+
) {
|
|
261
|
+
return true
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return false
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function replacementCopperIsSelfClear(params: {
|
|
269
|
+
plan: FanoutRoutePlan
|
|
270
|
+
segments: readonly RoutedSegment[]
|
|
271
|
+
replacementStartIndex: number
|
|
272
|
+
replacementSegmentCount: number
|
|
273
|
+
clearance: number
|
|
274
|
+
}): boolean {
|
|
275
|
+
const {
|
|
276
|
+
plan,
|
|
277
|
+
segments,
|
|
278
|
+
replacementStartIndex,
|
|
279
|
+
replacementSegmentCount,
|
|
280
|
+
clearance,
|
|
281
|
+
} = params
|
|
282
|
+
const replacementEndIndex =
|
|
283
|
+
replacementStartIndex + replacementSegmentCount - 1
|
|
284
|
+
const vias = getPlanVias(plan)
|
|
285
|
+
const getConnectedPathDistance = (
|
|
286
|
+
firstIndex: number,
|
|
287
|
+
secondIndex: number,
|
|
288
|
+
): number => {
|
|
289
|
+
if (firstIndex === secondIndex) return 0
|
|
290
|
+
const startIndex = Math.min(firstIndex, secondIndex)
|
|
291
|
+
const endIndex = Math.max(firstIndex, secondIndex)
|
|
292
|
+
const startSegment = segments[startIndex]!
|
|
293
|
+
const endSegment = segments[endIndex]!
|
|
294
|
+
if (startSegment.layer !== endSegment.layer) {
|
|
295
|
+
return Number.POSITIVE_INFINITY
|
|
296
|
+
}
|
|
297
|
+
let currentPoint = startSegment.end
|
|
298
|
+
let pathDistance = 0
|
|
299
|
+
for (let index = startIndex + 1; index < endIndex; index++) {
|
|
300
|
+
const segment = segments[index]!
|
|
301
|
+
if (
|
|
302
|
+
segment.layer !== startSegment.layer ||
|
|
303
|
+
!pointsMatch(currentPoint, segment.start)
|
|
304
|
+
) {
|
|
305
|
+
return Number.POSITIVE_INFINITY
|
|
306
|
+
}
|
|
307
|
+
pathDistance += distance(segment.start, segment.end)
|
|
308
|
+
currentPoint = segment.end
|
|
309
|
+
}
|
|
310
|
+
return pointsMatch(currentPoint, segments[endIndex]!.start)
|
|
311
|
+
? pathDistance
|
|
312
|
+
: Number.POSITIVE_INFINITY
|
|
313
|
+
}
|
|
314
|
+
for (
|
|
315
|
+
let replacementIndex = replacementStartIndex;
|
|
316
|
+
replacementIndex <= replacementEndIndex;
|
|
317
|
+
replacementIndex++
|
|
318
|
+
) {
|
|
319
|
+
const replacement = segments[replacementIndex]!
|
|
320
|
+
for (const [otherIndex, other] of segments.entries()) {
|
|
321
|
+
const requiredCenterlineClearance =
|
|
322
|
+
replacement.width / 2 + other.width / 2 + clearance
|
|
323
|
+
if (
|
|
324
|
+
getConnectedPathDistance(replacementIndex, otherIndex) <=
|
|
325
|
+
requiredCenterlineClearance + EPSILON
|
|
326
|
+
) {
|
|
327
|
+
continue
|
|
328
|
+
}
|
|
329
|
+
if (!segmentsAreClear(replacement, other, clearance)) return false
|
|
330
|
+
}
|
|
331
|
+
for (const via of vias) {
|
|
332
|
+
if (!via.spanLayers.includes(replacement.layer)) continue
|
|
333
|
+
if (
|
|
334
|
+
pointsMatch(via.center, replacement.start) ||
|
|
335
|
+
pointsMatch(via.center, replacement.end)
|
|
336
|
+
) {
|
|
337
|
+
continue
|
|
338
|
+
}
|
|
339
|
+
if (
|
|
340
|
+
distancePointToSegment(via.center, replacement.start, replacement.end) <
|
|
341
|
+
via.diameter / 2 + replacement.width / 2 + clearance - EPSILON
|
|
342
|
+
) {
|
|
343
|
+
return false
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return true
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function pointIsInsideBounds(point: Point2D, bounds: Bounds): boolean {
|
|
351
|
+
return (
|
|
352
|
+
point.x >= bounds.minX - EPSILON &&
|
|
353
|
+
point.x <= bounds.maxX + EPSILON &&
|
|
354
|
+
point.y >= bounds.minY - EPSILON &&
|
|
355
|
+
point.y <= bounds.maxY + EPSILON
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function addScaled(point: Point2D, vector: Point2D, scale: number): Point2D {
|
|
360
|
+
return {
|
|
361
|
+
x: point.x + vector.x * scale,
|
|
362
|
+
y: point.y + vector.y * scale,
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function createMeanderPoints(params: {
|
|
367
|
+
segment: RoutedSegment
|
|
368
|
+
toothCount: number
|
|
369
|
+
targetAddedLength: number
|
|
370
|
+
pitch: number
|
|
371
|
+
placementFraction: number
|
|
372
|
+
normalSign: -1 | 1
|
|
373
|
+
}): Point2D[] | null {
|
|
374
|
+
const {
|
|
375
|
+
segment,
|
|
376
|
+
toothCount,
|
|
377
|
+
targetAddedLength,
|
|
378
|
+
pitch,
|
|
379
|
+
placementFraction,
|
|
380
|
+
normalSign,
|
|
381
|
+
} = params
|
|
382
|
+
const dx = segment.end.x - segment.start.x
|
|
383
|
+
const dy = segment.end.y - segment.start.y
|
|
384
|
+
const segmentLength = Math.hypot(dx, dy)
|
|
385
|
+
if (segmentLength <= EPSILON) return null
|
|
386
|
+
const isAxisAligned = Math.abs(dx) <= EPSILON || Math.abs(dy) <= EPSILON
|
|
387
|
+
const isFortyFiveDegree = Math.abs(Math.abs(dx) - Math.abs(dy)) <= EPSILON
|
|
388
|
+
if (!isAxisAligned && !isFortyFiveDegree) return null
|
|
389
|
+
const tangent = { x: dx / segmentLength, y: dy / segmentLength }
|
|
390
|
+
const normal = {
|
|
391
|
+
x: -tangent.y * normalSign,
|
|
392
|
+
y: tangent.x * normalSign,
|
|
393
|
+
}
|
|
394
|
+
const chamfer = Math.min(
|
|
395
|
+
pitch / 2,
|
|
396
|
+
targetAddedLength / (8 * toothCount * (Math.SQRT2 - 1)),
|
|
397
|
+
)
|
|
398
|
+
const plateau = pitch
|
|
399
|
+
const toothSpan = chamfer * 4 + plateau
|
|
400
|
+
const toothGap = pitch
|
|
401
|
+
const occupiedLength =
|
|
402
|
+
toothCount * toothSpan + Math.max(0, toothCount - 1) * toothGap
|
|
403
|
+
const minimumLead = pitch / 4
|
|
404
|
+
if (occupiedLength + minimumLead * 2 > segmentLength + EPSILON) {
|
|
405
|
+
return null
|
|
406
|
+
}
|
|
407
|
+
const minimumAddedLengthPerTooth = 4 * chamfer * (Math.SQRT2 - 1)
|
|
408
|
+
if (targetAddedLength + EPSILON < minimumAddedLengthPerTooth * toothCount) {
|
|
409
|
+
return null
|
|
410
|
+
}
|
|
411
|
+
const verticalRun =
|
|
412
|
+
(targetAddedLength / toothCount - minimumAddedLengthPerTooth) / 2
|
|
413
|
+
const movableLead = segmentLength - occupiedLength - minimumLead * 2
|
|
414
|
+
const leadingLength =
|
|
415
|
+
minimumLead + Math.max(0, movableLead) * placementFraction
|
|
416
|
+
let cursor = addScaled(segment.start, tangent, leadingLength)
|
|
417
|
+
const points: Point2D[] = [{ ...segment.start }, { ...cursor }]
|
|
418
|
+
|
|
419
|
+
for (let toothIndex = 0; toothIndex < toothCount; toothIndex++) {
|
|
420
|
+
cursor = addScaled(addScaled(cursor, tangent, chamfer), normal, chamfer)
|
|
421
|
+
points.push(cursor)
|
|
422
|
+
cursor = addScaled(cursor, normal, verticalRun)
|
|
423
|
+
points.push(cursor)
|
|
424
|
+
cursor = addScaled(addScaled(cursor, tangent, chamfer), normal, chamfer)
|
|
425
|
+
points.push(cursor)
|
|
426
|
+
cursor = addScaled(cursor, tangent, plateau)
|
|
427
|
+
points.push(cursor)
|
|
428
|
+
cursor = addScaled(addScaled(cursor, tangent, chamfer), normal, -chamfer)
|
|
429
|
+
points.push(cursor)
|
|
430
|
+
cursor = addScaled(cursor, normal, -verticalRun)
|
|
431
|
+
points.push(cursor)
|
|
432
|
+
cursor = addScaled(addScaled(cursor, tangent, chamfer), normal, -chamfer)
|
|
433
|
+
points.push(cursor)
|
|
434
|
+
if (toothIndex < toothCount - 1) {
|
|
435
|
+
cursor = addScaled(cursor, tangent, toothGap)
|
|
436
|
+
points.push(cursor)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
points.push({ ...segment.end })
|
|
440
|
+
return points.filter(
|
|
441
|
+
(point, index) => index === 0 || !pointsMatch(point, points[index - 1]!),
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function createTunedPlanCandidates(params: {
|
|
446
|
+
plan: FanoutRoutePlan
|
|
447
|
+
bus: PreparedBus
|
|
448
|
+
targetAddedLength: number
|
|
449
|
+
clearance: number
|
|
450
|
+
sharedBoundary: Bounds
|
|
451
|
+
denseBoundarySplitApplied?: boolean
|
|
452
|
+
}): FanoutRoutePlan[] {
|
|
453
|
+
const {
|
|
454
|
+
plan,
|
|
455
|
+
bus,
|
|
456
|
+
targetAddedLength,
|
|
457
|
+
clearance,
|
|
458
|
+
sharedBoundary,
|
|
459
|
+
denseBoundarySplitApplied = false,
|
|
460
|
+
} = params
|
|
461
|
+
const candidates: FanoutRoutePlan[] = []
|
|
462
|
+
const denseCopperBounds = getDenseCopperBounds(bus)
|
|
463
|
+
const denseMargin = plan.segments[0]?.width
|
|
464
|
+
? plan.segments[0].width / 2 + clearance
|
|
465
|
+
: clearance
|
|
466
|
+
const eligibleSegments = plan.segments
|
|
467
|
+
.map((segment, segmentIndex) => ({ segment, segmentIndex }))
|
|
468
|
+
.filter(({ segment }) => segment.layer === plan.targetLayer)
|
|
469
|
+
.toSorted(
|
|
470
|
+
(first, second) =>
|
|
471
|
+
distance(second.segment.start, second.segment.end) -
|
|
472
|
+
distance(first.segment.start, first.segment.end),
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
for (const { segment, segmentIndex } of eligibleSegments) {
|
|
476
|
+
const pitch = segment.width + clearance
|
|
477
|
+
const segmentLength = distance(segment.start, segment.end)
|
|
478
|
+
const maximumToothCount = Math.min(
|
|
479
|
+
12,
|
|
480
|
+
Math.max(0, Math.floor((segmentLength / pitch + 0.5) / 4)),
|
|
481
|
+
)
|
|
482
|
+
for (let toothCount = 1; toothCount <= maximumToothCount; toothCount++) {
|
|
483
|
+
for (const placementFraction of [0.5, 0, 1, 0.25, 0.75]) {
|
|
484
|
+
for (const normalSign of [1, -1] as const) {
|
|
485
|
+
const points = createMeanderPoints({
|
|
486
|
+
segment,
|
|
487
|
+
toothCount,
|
|
488
|
+
targetAddedLength,
|
|
489
|
+
pitch,
|
|
490
|
+
placementFraction,
|
|
491
|
+
normalSign,
|
|
492
|
+
})
|
|
493
|
+
if (!points) continue
|
|
494
|
+
if (
|
|
495
|
+
points.some((point) => !pointIsInsideBounds(point, sharedBoundary))
|
|
496
|
+
) {
|
|
497
|
+
continue
|
|
498
|
+
}
|
|
499
|
+
if (
|
|
500
|
+
points
|
|
501
|
+
.slice(1, -1)
|
|
502
|
+
.some(
|
|
503
|
+
(point) =>
|
|
504
|
+
!pointIsOutsideDenseBounds(
|
|
505
|
+
point,
|
|
506
|
+
denseCopperBounds,
|
|
507
|
+
denseMargin,
|
|
508
|
+
),
|
|
509
|
+
)
|
|
510
|
+
) {
|
|
511
|
+
continue
|
|
512
|
+
}
|
|
513
|
+
const replacementSegments = points.slice(1).map((end, index) => ({
|
|
514
|
+
start: points[index]!,
|
|
515
|
+
end,
|
|
516
|
+
width: segment.width,
|
|
517
|
+
layer: segment.layer,
|
|
518
|
+
}))
|
|
519
|
+
const segments = [
|
|
520
|
+
...plan.segments.slice(0, segmentIndex),
|
|
521
|
+
...replacementSegments,
|
|
522
|
+
...plan.segments.slice(segmentIndex + 1),
|
|
523
|
+
]
|
|
524
|
+
if (hasNonAdjacentSelfIntersection(segments)) continue
|
|
525
|
+
if (
|
|
526
|
+
!replacementCopperIsSelfClear({
|
|
527
|
+
plan,
|
|
528
|
+
segments,
|
|
529
|
+
replacementStartIndex: segmentIndex,
|
|
530
|
+
replacementSegmentCount: replacementSegments.length,
|
|
531
|
+
clearance,
|
|
532
|
+
})
|
|
533
|
+
) {
|
|
534
|
+
continue
|
|
535
|
+
}
|
|
536
|
+
const candidate = createPlanWithSegments(plan, segments)
|
|
537
|
+
if (candidate) candidates.push(candidate)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (denseBoundarySplitApplied) return candidates
|
|
543
|
+
const splitSegments = plan.segments.flatMap((segment) =>
|
|
544
|
+
splitSegmentAtDenseBounds({
|
|
545
|
+
segment,
|
|
546
|
+
bounds: denseCopperBounds,
|
|
547
|
+
margin: denseMargin,
|
|
548
|
+
}),
|
|
549
|
+
)
|
|
550
|
+
const splitPlan = createPlanWithSegments(plan, splitSegments)
|
|
551
|
+
if (!splitPlan) return candidates
|
|
552
|
+
const splitCandidates = createTunedPlanCandidates({
|
|
553
|
+
...params,
|
|
554
|
+
plan: splitPlan,
|
|
555
|
+
denseBoundarySplitApplied: true,
|
|
556
|
+
})
|
|
557
|
+
return [...candidates, ...splitCandidates]
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function getBusSkew(plans: readonly FanoutRoutePlan[]): number {
|
|
561
|
+
const lengths = plans.map((plan) => plan.length)
|
|
562
|
+
return Math.max(...lengths) - Math.min(...lengths)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Adds straight/45-degree meanders after the dense component escape. Matching
|
|
567
|
+
* is atomic: a constrained bus either satisfies its declared skew with the
|
|
568
|
+
* complete fanout copper still clear, or the complete assignment is rejected.
|
|
569
|
+
*/
|
|
570
|
+
export function matchBusPlanLengths(params: {
|
|
571
|
+
plans: readonly FanoutRoutePlan[]
|
|
572
|
+
preparedBuses: readonly PreparedBus[]
|
|
573
|
+
inputSrj: SimpleRouteJson
|
|
574
|
+
sharedBoundary: Bounds
|
|
575
|
+
clearance: number
|
|
576
|
+
allowBlindAndBuriedVias?: boolean
|
|
577
|
+
allowSameNetMerges?: boolean
|
|
578
|
+
}):
|
|
579
|
+
| { plans: FanoutRoutePlan[]; failedBus?: never }
|
|
580
|
+
| { plans: null; failedBus: PreparedBus } {
|
|
581
|
+
const {
|
|
582
|
+
preparedBuses,
|
|
583
|
+
inputSrj,
|
|
584
|
+
sharedBoundary,
|
|
585
|
+
clearance,
|
|
586
|
+
allowBlindAndBuriedVias = true,
|
|
587
|
+
allowSameNetMerges = false,
|
|
588
|
+
} = params
|
|
589
|
+
let matchedPlans = [...params.plans]
|
|
590
|
+
const constrainedBuses = preparedBuses.filter(
|
|
591
|
+
(bus) => bus.maxLengthSkew !== undefined && bus.connections.length > 1,
|
|
592
|
+
)
|
|
593
|
+
if (constrainedBuses.length === 0) return { plans: matchedPlans }
|
|
594
|
+
|
|
595
|
+
for (const bus of constrainedBuses) {
|
|
596
|
+
if (bus.termination.type !== "boundary") {
|
|
597
|
+
return { plans: null, failedBus: bus }
|
|
598
|
+
}
|
|
599
|
+
const maximumIterations = bus.connections.length * 2
|
|
600
|
+
for (let iteration = 0; iteration < maximumIterations; iteration++) {
|
|
601
|
+
const busPlans = matchedPlans.filter((plan) => plan.busId === bus.busId)
|
|
602
|
+
if (busPlans.length !== bus.connections.length) {
|
|
603
|
+
return { plans: null, failedBus: bus }
|
|
604
|
+
}
|
|
605
|
+
const maxLengthSkew = bus.maxLengthSkew!
|
|
606
|
+
const skew = getBusSkew(busPlans)
|
|
607
|
+
if (skew <= maxLengthSkew + EPSILON) break
|
|
608
|
+
const shortest = busPlans.toSorted(
|
|
609
|
+
(first, second) =>
|
|
610
|
+
first.length - second.length ||
|
|
611
|
+
first.connectionName.localeCompare(second.connectionName),
|
|
612
|
+
)[0]!
|
|
613
|
+
const longestLength = Math.max(...busPlans.map((plan) => plan.length))
|
|
614
|
+
const deficit = longestLength - shortest.length
|
|
615
|
+
const minimumRequiredAddition = Math.max(
|
|
616
|
+
EPSILON,
|
|
617
|
+
deficit - maxLengthSkew + EPSILON,
|
|
618
|
+
)
|
|
619
|
+
const targetAddedLengths = [
|
|
620
|
+
minimumRequiredAddition,
|
|
621
|
+
minimumRequiredAddition + maxLengthSkew * 0.1,
|
|
622
|
+
deficit - maxLengthSkew * 0.5,
|
|
623
|
+
deficit - maxLengthSkew * 0.75,
|
|
624
|
+
deficit,
|
|
625
|
+
deficit + maxLengthSkew,
|
|
626
|
+
]
|
|
627
|
+
.filter(
|
|
628
|
+
(value, index, values) =>
|
|
629
|
+
value > EPSILON &&
|
|
630
|
+
values.findIndex(
|
|
631
|
+
(candidate) => Math.abs(candidate - value) < EPSILON,
|
|
632
|
+
) === index,
|
|
633
|
+
)
|
|
634
|
+
.toSorted((first, second) => first - second)
|
|
635
|
+
let acceptedPlans: FanoutRoutePlan[] | null = null
|
|
636
|
+
for (const targetAddedLength of targetAddedLengths) {
|
|
637
|
+
const candidates = createTunedPlanCandidates({
|
|
638
|
+
plan: shortest,
|
|
639
|
+
bus,
|
|
640
|
+
targetAddedLength,
|
|
641
|
+
clearance,
|
|
642
|
+
sharedBoundary: bus.sharedBoundary,
|
|
643
|
+
})
|
|
644
|
+
for (const candidate of candidates) {
|
|
645
|
+
const nextPlans = matchedPlans.map((plan) =>
|
|
646
|
+
plan === shortest ? candidate : plan,
|
|
647
|
+
)
|
|
648
|
+
const nextBusPlans = nextPlans.filter(
|
|
649
|
+
(plan) => plan.busId === bus.busId,
|
|
650
|
+
)
|
|
651
|
+
const nextSkew = getBusSkew(nextBusPlans)
|
|
652
|
+
if (nextSkew > skew + EPSILON) continue
|
|
653
|
+
if (
|
|
654
|
+
!fanoutPlansAreClear({
|
|
655
|
+
plans: nextPlans,
|
|
656
|
+
srj: inputSrj,
|
|
657
|
+
sharedBoundary,
|
|
658
|
+
clearance,
|
|
659
|
+
allowBlindAndBuriedVias,
|
|
660
|
+
allowSameNetMerges,
|
|
661
|
+
})
|
|
662
|
+
) {
|
|
663
|
+
continue
|
|
664
|
+
}
|
|
665
|
+
acceptedPlans = nextPlans
|
|
666
|
+
break
|
|
667
|
+
}
|
|
668
|
+
if (acceptedPlans) break
|
|
669
|
+
}
|
|
670
|
+
if (!acceptedPlans) return { plans: null, failedBus: bus }
|
|
671
|
+
matchedPlans = acceptedPlans
|
|
672
|
+
}
|
|
673
|
+
const matchedBusPlans = matchedPlans.filter(
|
|
674
|
+
(plan) => plan.busId === bus.busId,
|
|
675
|
+
)
|
|
676
|
+
if (getBusSkew(matchedBusPlans) > bus.maxLengthSkew! + EPSILON) {
|
|
677
|
+
return { plans: null, failedBus: bus }
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return { plans: matchedPlans }
|
|
681
|
+
}
|