@tscircuit/fanout-solver 0.0.36 → 0.0.38
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 +16 -2
- package/lib/build-output.ts +32 -10
- package/lib/fanout-solver.ts +294 -44
- package/lib/match-bus-lengths.ts +571 -0
- package/lib/prepare-buses.ts +32 -2
- package/lib/route-bus.ts +378 -81
- package/lib/route-via-minimal-winding.ts +898 -0
- package/lib/types.ts +18 -3
- package/lib/validate-fanout-solution.ts +40 -10
- package/package.json +1 -1
|
@@ -0,0 +1,571 @@
|
|
|
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 route: SimplifiedPcbTrace["route"] = [
|
|
46
|
+
{
|
|
47
|
+
route_type: "wire",
|
|
48
|
+
...firstSegment.start,
|
|
49
|
+
width: firstSegment.width,
|
|
50
|
+
layer: firstSegment.layer,
|
|
51
|
+
...(firstOriginalWire?.route_type === "wire" &&
|
|
52
|
+
firstOriginalWire.start_pcb_port_id
|
|
53
|
+
? { start_pcb_port_id: firstOriginalWire.start_pcb_port_id }
|
|
54
|
+
: {}),
|
|
55
|
+
},
|
|
56
|
+
]
|
|
57
|
+
let currentPoint = firstSegment.start
|
|
58
|
+
let currentLayer = firstSegment.layer
|
|
59
|
+
const vias = getPlanVias(plan)
|
|
60
|
+
|
|
61
|
+
for (const [segmentIndex, segment] of segments.entries()) {
|
|
62
|
+
if (!pointsMatch(currentPoint, segment.start)) return null
|
|
63
|
+
if (currentLayer !== segment.layer) {
|
|
64
|
+
const transitionVia = vias.find(
|
|
65
|
+
(via) =>
|
|
66
|
+
pointsMatch(via.center, segment.start) &&
|
|
67
|
+
via.spanLayers.includes(currentLayer) &&
|
|
68
|
+
via.spanLayers.includes(segment.layer),
|
|
69
|
+
)
|
|
70
|
+
if (!transitionVia) return null
|
|
71
|
+
route.push({
|
|
72
|
+
route_type: "via",
|
|
73
|
+
...segment.start,
|
|
74
|
+
from_layer: currentLayer,
|
|
75
|
+
to_layer: segment.layer,
|
|
76
|
+
via_diameter: transitionVia.diameter,
|
|
77
|
+
via_hole_diameter: transitionVia.holeDiameter,
|
|
78
|
+
})
|
|
79
|
+
route.push({
|
|
80
|
+
route_type: "wire",
|
|
81
|
+
...segment.start,
|
|
82
|
+
width: segment.width,
|
|
83
|
+
layer: segment.layer,
|
|
84
|
+
})
|
|
85
|
+
currentLayer = segment.layer
|
|
86
|
+
}
|
|
87
|
+
route.push({
|
|
88
|
+
route_type: "wire",
|
|
89
|
+
...segment.end,
|
|
90
|
+
width: segment.width,
|
|
91
|
+
layer: segment.layer,
|
|
92
|
+
...(segmentIndex === segments.length - 1 &&
|
|
93
|
+
lastOriginalWire?.route_type === "wire" &&
|
|
94
|
+
lastOriginalWire.end_pcb_port_id
|
|
95
|
+
? { end_pcb_port_id: lastOriginalWire.end_pcb_port_id }
|
|
96
|
+
: {}),
|
|
97
|
+
})
|
|
98
|
+
currentPoint = segment.end
|
|
99
|
+
}
|
|
100
|
+
return route
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createPlanWithSegments(
|
|
104
|
+
plan: FanoutRoutePlan,
|
|
105
|
+
segments: RoutedSegment[],
|
|
106
|
+
): FanoutRoutePlan | null {
|
|
107
|
+
const route = rebuildTraceRoute(plan, segments)
|
|
108
|
+
if (!route) return null
|
|
109
|
+
const length = [...segments, ...(plan.planeEndpointSegments ?? [])].reduce(
|
|
110
|
+
(total, segment) => total + distance(segment.start, segment.end),
|
|
111
|
+
0,
|
|
112
|
+
)
|
|
113
|
+
return {
|
|
114
|
+
...plan,
|
|
115
|
+
trace: { ...plan.trace, route },
|
|
116
|
+
segments,
|
|
117
|
+
length,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function pointIsOutsideDenseBounds(
|
|
122
|
+
point: Point2D,
|
|
123
|
+
bounds: Bounds,
|
|
124
|
+
margin: number,
|
|
125
|
+
): boolean {
|
|
126
|
+
return (
|
|
127
|
+
point.x < bounds.minX - margin ||
|
|
128
|
+
point.x > bounds.maxX + margin ||
|
|
129
|
+
point.y < bounds.minY - margin ||
|
|
130
|
+
point.y > bounds.maxY + margin
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getDenseCopperBounds(bus: PreparedBus): Bounds {
|
|
135
|
+
return bus.componentObstacles.reduce<Bounds>(
|
|
136
|
+
(bounds, obstacle) => ({
|
|
137
|
+
minX: Math.min(bounds.minX, obstacle.center.x - obstacle.width / 2),
|
|
138
|
+
maxX: Math.max(bounds.maxX, obstacle.center.x + obstacle.width / 2),
|
|
139
|
+
minY: Math.min(bounds.minY, obstacle.center.y - obstacle.height / 2),
|
|
140
|
+
maxY: Math.max(bounds.maxY, obstacle.center.y + obstacle.height / 2),
|
|
141
|
+
}),
|
|
142
|
+
{
|
|
143
|
+
minX: Number.POSITIVE_INFINITY,
|
|
144
|
+
maxX: Number.NEGATIVE_INFINITY,
|
|
145
|
+
minY: Number.POSITIVE_INFINITY,
|
|
146
|
+
maxY: Number.NEGATIVE_INFINITY,
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function hasNonAdjacentSelfIntersection(
|
|
152
|
+
segments: readonly RoutedSegment[],
|
|
153
|
+
): boolean {
|
|
154
|
+
for (let firstIndex = 0; firstIndex < segments.length; firstIndex++) {
|
|
155
|
+
const first = segments[firstIndex]!
|
|
156
|
+
for (
|
|
157
|
+
let secondIndex = firstIndex + 2;
|
|
158
|
+
secondIndex < segments.length;
|
|
159
|
+
secondIndex++
|
|
160
|
+
) {
|
|
161
|
+
const second = segments[secondIndex]!
|
|
162
|
+
if (first.layer !== second.layer) continue
|
|
163
|
+
if (
|
|
164
|
+
secondIndex === firstIndex + 2 &&
|
|
165
|
+
pointsMatch(first.end, second.start)
|
|
166
|
+
) {
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
if (
|
|
170
|
+
distanceSegmentToSegment(
|
|
171
|
+
first.start,
|
|
172
|
+
first.end,
|
|
173
|
+
second.start,
|
|
174
|
+
second.end,
|
|
175
|
+
) <= EPSILON
|
|
176
|
+
) {
|
|
177
|
+
return true
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function replacementCopperIsSelfClear(params: {
|
|
185
|
+
plan: FanoutRoutePlan
|
|
186
|
+
segments: readonly RoutedSegment[]
|
|
187
|
+
replacementStartIndex: number
|
|
188
|
+
replacementSegmentCount: number
|
|
189
|
+
clearance: number
|
|
190
|
+
}): boolean {
|
|
191
|
+
const {
|
|
192
|
+
plan,
|
|
193
|
+
segments,
|
|
194
|
+
replacementStartIndex,
|
|
195
|
+
replacementSegmentCount,
|
|
196
|
+
clearance,
|
|
197
|
+
} = params
|
|
198
|
+
const replacementEndIndex =
|
|
199
|
+
replacementStartIndex + replacementSegmentCount - 1
|
|
200
|
+
const vias = getPlanVias(plan)
|
|
201
|
+
const getConnectedPathDistance = (
|
|
202
|
+
firstIndex: number,
|
|
203
|
+
secondIndex: number,
|
|
204
|
+
): number => {
|
|
205
|
+
if (firstIndex === secondIndex) return 0
|
|
206
|
+
const startIndex = Math.min(firstIndex, secondIndex)
|
|
207
|
+
const endIndex = Math.max(firstIndex, secondIndex)
|
|
208
|
+
const startSegment = segments[startIndex]!
|
|
209
|
+
const endSegment = segments[endIndex]!
|
|
210
|
+
if (startSegment.layer !== endSegment.layer) {
|
|
211
|
+
return Number.POSITIVE_INFINITY
|
|
212
|
+
}
|
|
213
|
+
let currentPoint = startSegment.end
|
|
214
|
+
let pathDistance = 0
|
|
215
|
+
for (let index = startIndex + 1; index < endIndex; index++) {
|
|
216
|
+
const segment = segments[index]!
|
|
217
|
+
if (
|
|
218
|
+
segment.layer !== startSegment.layer ||
|
|
219
|
+
!pointsMatch(currentPoint, segment.start)
|
|
220
|
+
) {
|
|
221
|
+
return Number.POSITIVE_INFINITY
|
|
222
|
+
}
|
|
223
|
+
pathDistance += distance(segment.start, segment.end)
|
|
224
|
+
currentPoint = segment.end
|
|
225
|
+
}
|
|
226
|
+
return pointsMatch(currentPoint, segments[endIndex]!.start)
|
|
227
|
+
? pathDistance
|
|
228
|
+
: Number.POSITIVE_INFINITY
|
|
229
|
+
}
|
|
230
|
+
for (
|
|
231
|
+
let replacementIndex = replacementStartIndex;
|
|
232
|
+
replacementIndex <= replacementEndIndex;
|
|
233
|
+
replacementIndex++
|
|
234
|
+
) {
|
|
235
|
+
const replacement = segments[replacementIndex]!
|
|
236
|
+
for (const [otherIndex, other] of segments.entries()) {
|
|
237
|
+
const requiredCenterlineClearance =
|
|
238
|
+
replacement.width / 2 + other.width / 2 + clearance
|
|
239
|
+
if (
|
|
240
|
+
getConnectedPathDistance(replacementIndex, otherIndex) <=
|
|
241
|
+
requiredCenterlineClearance + EPSILON
|
|
242
|
+
) {
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
if (!segmentsAreClear(replacement, other, clearance)) return false
|
|
246
|
+
}
|
|
247
|
+
for (const via of vias) {
|
|
248
|
+
if (!via.spanLayers.includes(replacement.layer)) continue
|
|
249
|
+
if (
|
|
250
|
+
pointsMatch(via.center, replacement.start) ||
|
|
251
|
+
pointsMatch(via.center, replacement.end)
|
|
252
|
+
) {
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
if (
|
|
256
|
+
distancePointToSegment(via.center, replacement.start, replacement.end) <
|
|
257
|
+
via.diameter / 2 + replacement.width / 2 + clearance - EPSILON
|
|
258
|
+
) {
|
|
259
|
+
return false
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return true
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function pointIsInsideBounds(point: Point2D, bounds: Bounds): boolean {
|
|
267
|
+
return (
|
|
268
|
+
point.x >= bounds.minX - EPSILON &&
|
|
269
|
+
point.x <= bounds.maxX + EPSILON &&
|
|
270
|
+
point.y >= bounds.minY - EPSILON &&
|
|
271
|
+
point.y <= bounds.maxY + EPSILON
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function addScaled(point: Point2D, vector: Point2D, scale: number): Point2D {
|
|
276
|
+
return {
|
|
277
|
+
x: point.x + vector.x * scale,
|
|
278
|
+
y: point.y + vector.y * scale,
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function createMeanderPoints(params: {
|
|
283
|
+
segment: RoutedSegment
|
|
284
|
+
toothCount: number
|
|
285
|
+
targetAddedLength: number
|
|
286
|
+
pitch: number
|
|
287
|
+
placementFraction: number
|
|
288
|
+
normalSign: -1 | 1
|
|
289
|
+
}): Point2D[] | null {
|
|
290
|
+
const {
|
|
291
|
+
segment,
|
|
292
|
+
toothCount,
|
|
293
|
+
targetAddedLength,
|
|
294
|
+
pitch,
|
|
295
|
+
placementFraction,
|
|
296
|
+
normalSign,
|
|
297
|
+
} = params
|
|
298
|
+
const dx = segment.end.x - segment.start.x
|
|
299
|
+
const dy = segment.end.y - segment.start.y
|
|
300
|
+
const segmentLength = Math.hypot(dx, dy)
|
|
301
|
+
if (segmentLength <= EPSILON) return null
|
|
302
|
+
const isAxisAligned = Math.abs(dx) <= EPSILON || Math.abs(dy) <= EPSILON
|
|
303
|
+
const isFortyFiveDegree = Math.abs(Math.abs(dx) - Math.abs(dy)) <= EPSILON
|
|
304
|
+
if (!isAxisAligned && !isFortyFiveDegree) return null
|
|
305
|
+
const tangent = { x: dx / segmentLength, y: dy / segmentLength }
|
|
306
|
+
const normal = {
|
|
307
|
+
x: -tangent.y * normalSign,
|
|
308
|
+
y: tangent.x * normalSign,
|
|
309
|
+
}
|
|
310
|
+
const chamfer = Math.min(
|
|
311
|
+
pitch / 2,
|
|
312
|
+
targetAddedLength / (8 * toothCount * (Math.SQRT2 - 1)),
|
|
313
|
+
)
|
|
314
|
+
const plateau = pitch
|
|
315
|
+
const toothSpan = chamfer * 4 + plateau
|
|
316
|
+
const toothGap = pitch
|
|
317
|
+
const occupiedLength =
|
|
318
|
+
toothCount * toothSpan + Math.max(0, toothCount - 1) * toothGap
|
|
319
|
+
const minimumLead = pitch / 4
|
|
320
|
+
if (occupiedLength + minimumLead * 2 > segmentLength + EPSILON) {
|
|
321
|
+
return null
|
|
322
|
+
}
|
|
323
|
+
const minimumAddedLengthPerTooth = 4 * chamfer * (Math.SQRT2 - 1)
|
|
324
|
+
if (targetAddedLength + EPSILON < minimumAddedLengthPerTooth * toothCount) {
|
|
325
|
+
return null
|
|
326
|
+
}
|
|
327
|
+
const verticalRun =
|
|
328
|
+
(targetAddedLength / toothCount - minimumAddedLengthPerTooth) / 2
|
|
329
|
+
const movableLead = segmentLength - occupiedLength - minimumLead * 2
|
|
330
|
+
const leadingLength =
|
|
331
|
+
minimumLead + Math.max(0, movableLead) * placementFraction
|
|
332
|
+
let cursor = addScaled(segment.start, tangent, leadingLength)
|
|
333
|
+
const points: Point2D[] = [{ ...segment.start }, { ...cursor }]
|
|
334
|
+
|
|
335
|
+
for (let toothIndex = 0; toothIndex < toothCount; toothIndex++) {
|
|
336
|
+
cursor = addScaled(addScaled(cursor, tangent, chamfer), normal, chamfer)
|
|
337
|
+
points.push(cursor)
|
|
338
|
+
cursor = addScaled(cursor, normal, verticalRun)
|
|
339
|
+
points.push(cursor)
|
|
340
|
+
cursor = addScaled(addScaled(cursor, tangent, chamfer), normal, chamfer)
|
|
341
|
+
points.push(cursor)
|
|
342
|
+
cursor = addScaled(cursor, tangent, plateau)
|
|
343
|
+
points.push(cursor)
|
|
344
|
+
cursor = addScaled(addScaled(cursor, tangent, chamfer), normal, -chamfer)
|
|
345
|
+
points.push(cursor)
|
|
346
|
+
cursor = addScaled(cursor, normal, -verticalRun)
|
|
347
|
+
points.push(cursor)
|
|
348
|
+
cursor = addScaled(addScaled(cursor, tangent, chamfer), normal, -chamfer)
|
|
349
|
+
points.push(cursor)
|
|
350
|
+
if (toothIndex < toothCount - 1) {
|
|
351
|
+
cursor = addScaled(cursor, tangent, toothGap)
|
|
352
|
+
points.push(cursor)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
points.push({ ...segment.end })
|
|
356
|
+
return points.filter(
|
|
357
|
+
(point, index) => index === 0 || !pointsMatch(point, points[index - 1]!),
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function createTunedPlanCandidates(params: {
|
|
362
|
+
plan: FanoutRoutePlan
|
|
363
|
+
bus: PreparedBus
|
|
364
|
+
targetAddedLength: number
|
|
365
|
+
clearance: number
|
|
366
|
+
sharedBoundary: Bounds
|
|
367
|
+
}): FanoutRoutePlan[] {
|
|
368
|
+
const { plan, bus, targetAddedLength, clearance, sharedBoundary } = params
|
|
369
|
+
const candidates: FanoutRoutePlan[] = []
|
|
370
|
+
const denseCopperBounds = getDenseCopperBounds(bus)
|
|
371
|
+
const denseMargin = plan.segments[0]?.width
|
|
372
|
+
? plan.segments[0].width / 2 + clearance
|
|
373
|
+
: clearance
|
|
374
|
+
const eligibleSegments = plan.segments
|
|
375
|
+
.map((segment, segmentIndex) => ({ segment, segmentIndex }))
|
|
376
|
+
.filter(({ segment }) => segment.layer === plan.targetLayer)
|
|
377
|
+
.toSorted(
|
|
378
|
+
(first, second) =>
|
|
379
|
+
distance(second.segment.start, second.segment.end) -
|
|
380
|
+
distance(first.segment.start, first.segment.end),
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
for (const { segment, segmentIndex } of eligibleSegments) {
|
|
384
|
+
const pitch = segment.width + clearance
|
|
385
|
+
const segmentLength = distance(segment.start, segment.end)
|
|
386
|
+
const maximumToothCount = Math.min(
|
|
387
|
+
12,
|
|
388
|
+
Math.max(0, Math.floor((segmentLength / pitch + 0.5) / 4)),
|
|
389
|
+
)
|
|
390
|
+
for (let toothCount = 1; toothCount <= maximumToothCount; toothCount++) {
|
|
391
|
+
for (const placementFraction of [0.5, 0, 1, 0.25, 0.75]) {
|
|
392
|
+
for (const normalSign of [1, -1] as const) {
|
|
393
|
+
const points = createMeanderPoints({
|
|
394
|
+
segment,
|
|
395
|
+
toothCount,
|
|
396
|
+
targetAddedLength,
|
|
397
|
+
pitch,
|
|
398
|
+
placementFraction,
|
|
399
|
+
normalSign,
|
|
400
|
+
})
|
|
401
|
+
if (!points) continue
|
|
402
|
+
if (
|
|
403
|
+
points.some((point) => !pointIsInsideBounds(point, sharedBoundary))
|
|
404
|
+
) {
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
if (
|
|
408
|
+
points
|
|
409
|
+
.slice(1, -1)
|
|
410
|
+
.some(
|
|
411
|
+
(point) =>
|
|
412
|
+
!pointIsOutsideDenseBounds(
|
|
413
|
+
point,
|
|
414
|
+
denseCopperBounds,
|
|
415
|
+
denseMargin,
|
|
416
|
+
),
|
|
417
|
+
)
|
|
418
|
+
) {
|
|
419
|
+
continue
|
|
420
|
+
}
|
|
421
|
+
const replacementSegments = points.slice(1).map((end, index) => ({
|
|
422
|
+
start: points[index]!,
|
|
423
|
+
end,
|
|
424
|
+
width: segment.width,
|
|
425
|
+
layer: segment.layer,
|
|
426
|
+
}))
|
|
427
|
+
const segments = [
|
|
428
|
+
...plan.segments.slice(0, segmentIndex),
|
|
429
|
+
...replacementSegments,
|
|
430
|
+
...plan.segments.slice(segmentIndex + 1),
|
|
431
|
+
]
|
|
432
|
+
if (hasNonAdjacentSelfIntersection(segments)) continue
|
|
433
|
+
if (
|
|
434
|
+
!replacementCopperIsSelfClear({
|
|
435
|
+
plan,
|
|
436
|
+
segments,
|
|
437
|
+
replacementStartIndex: segmentIndex,
|
|
438
|
+
replacementSegmentCount: replacementSegments.length,
|
|
439
|
+
clearance,
|
|
440
|
+
})
|
|
441
|
+
) {
|
|
442
|
+
continue
|
|
443
|
+
}
|
|
444
|
+
const candidate = createPlanWithSegments(plan, segments)
|
|
445
|
+
if (candidate) candidates.push(candidate)
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return candidates
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function getBusSkew(plans: readonly FanoutRoutePlan[]): number {
|
|
454
|
+
const lengths = plans.map((plan) => plan.length)
|
|
455
|
+
return Math.max(...lengths) - Math.min(...lengths)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Adds straight/45-degree meanders after the dense component escape. Matching
|
|
460
|
+
* is atomic: a constrained bus either satisfies its declared skew with the
|
|
461
|
+
* complete fanout copper still clear, or the complete assignment is rejected.
|
|
462
|
+
*/
|
|
463
|
+
export function matchBusPlanLengths(params: {
|
|
464
|
+
plans: readonly FanoutRoutePlan[]
|
|
465
|
+
preparedBuses: readonly PreparedBus[]
|
|
466
|
+
inputSrj: SimpleRouteJson
|
|
467
|
+
sharedBoundary: Bounds
|
|
468
|
+
clearance: number
|
|
469
|
+
allowSameNetMerges?: boolean
|
|
470
|
+
}):
|
|
471
|
+
| { plans: FanoutRoutePlan[]; failedBus?: never }
|
|
472
|
+
| { plans: null; failedBus: PreparedBus } {
|
|
473
|
+
const {
|
|
474
|
+
preparedBuses,
|
|
475
|
+
inputSrj,
|
|
476
|
+
sharedBoundary,
|
|
477
|
+
clearance,
|
|
478
|
+
allowSameNetMerges = false,
|
|
479
|
+
} = params
|
|
480
|
+
let matchedPlans = [...params.plans]
|
|
481
|
+
const constrainedBuses = preparedBuses.filter(
|
|
482
|
+
(bus) => bus.maxLengthSkew !== undefined && bus.connections.length > 1,
|
|
483
|
+
)
|
|
484
|
+
if (constrainedBuses.length === 0) return { plans: matchedPlans }
|
|
485
|
+
|
|
486
|
+
for (const bus of constrainedBuses) {
|
|
487
|
+
if (bus.termination.type !== "boundary") {
|
|
488
|
+
return { plans: null, failedBus: bus }
|
|
489
|
+
}
|
|
490
|
+
const maximumIterations = bus.connections.length * 2
|
|
491
|
+
for (let iteration = 0; iteration < maximumIterations; iteration++) {
|
|
492
|
+
const busPlans = matchedPlans.filter((plan) => plan.busId === bus.busId)
|
|
493
|
+
if (busPlans.length !== bus.connections.length) {
|
|
494
|
+
return { plans: null, failedBus: bus }
|
|
495
|
+
}
|
|
496
|
+
const maxLengthSkew = bus.maxLengthSkew!
|
|
497
|
+
const skew = getBusSkew(busPlans)
|
|
498
|
+
if (skew <= maxLengthSkew + EPSILON) break
|
|
499
|
+
const shortest = busPlans.toSorted(
|
|
500
|
+
(first, second) =>
|
|
501
|
+
first.length - second.length ||
|
|
502
|
+
first.connectionName.localeCompare(second.connectionName),
|
|
503
|
+
)[0]!
|
|
504
|
+
const longestLength = Math.max(...busPlans.map((plan) => plan.length))
|
|
505
|
+
const deficit = longestLength - shortest.length
|
|
506
|
+
const minimumRequiredAddition = Math.max(
|
|
507
|
+
EPSILON,
|
|
508
|
+
deficit - maxLengthSkew + EPSILON,
|
|
509
|
+
)
|
|
510
|
+
const targetAddedLengths = [
|
|
511
|
+
minimumRequiredAddition,
|
|
512
|
+
minimumRequiredAddition + maxLengthSkew * 0.1,
|
|
513
|
+
deficit - maxLengthSkew * 0.5,
|
|
514
|
+
deficit - maxLengthSkew * 0.75,
|
|
515
|
+
deficit,
|
|
516
|
+
deficit + maxLengthSkew,
|
|
517
|
+
]
|
|
518
|
+
.filter(
|
|
519
|
+
(value, index, values) =>
|
|
520
|
+
value > EPSILON &&
|
|
521
|
+
values.findIndex(
|
|
522
|
+
(candidate) => Math.abs(candidate - value) < EPSILON,
|
|
523
|
+
) === index,
|
|
524
|
+
)
|
|
525
|
+
.toSorted((first, second) => first - second)
|
|
526
|
+
let acceptedPlans: FanoutRoutePlan[] | null = null
|
|
527
|
+
for (const targetAddedLength of targetAddedLengths) {
|
|
528
|
+
const candidates = createTunedPlanCandidates({
|
|
529
|
+
plan: shortest,
|
|
530
|
+
bus,
|
|
531
|
+
targetAddedLength,
|
|
532
|
+
clearance,
|
|
533
|
+
sharedBoundary: bus.sharedBoundary,
|
|
534
|
+
})
|
|
535
|
+
for (const candidate of candidates) {
|
|
536
|
+
const nextPlans = matchedPlans.map((plan) =>
|
|
537
|
+
plan === shortest ? candidate : plan,
|
|
538
|
+
)
|
|
539
|
+
const nextBusPlans = nextPlans.filter(
|
|
540
|
+
(plan) => plan.busId === bus.busId,
|
|
541
|
+
)
|
|
542
|
+
const nextSkew = getBusSkew(nextBusPlans)
|
|
543
|
+
if (nextSkew > skew + EPSILON) continue
|
|
544
|
+
if (
|
|
545
|
+
!fanoutPlansAreClear({
|
|
546
|
+
plans: nextPlans,
|
|
547
|
+
srj: inputSrj,
|
|
548
|
+
sharedBoundary,
|
|
549
|
+
clearance,
|
|
550
|
+
allowSameNetMerges,
|
|
551
|
+
})
|
|
552
|
+
) {
|
|
553
|
+
continue
|
|
554
|
+
}
|
|
555
|
+
acceptedPlans = nextPlans
|
|
556
|
+
break
|
|
557
|
+
}
|
|
558
|
+
if (acceptedPlans) break
|
|
559
|
+
}
|
|
560
|
+
if (!acceptedPlans) return { plans: null, failedBus: bus }
|
|
561
|
+
matchedPlans = acceptedPlans
|
|
562
|
+
}
|
|
563
|
+
const matchedBusPlans = matchedPlans.filter(
|
|
564
|
+
(plan) => plan.busId === bus.busId,
|
|
565
|
+
)
|
|
566
|
+
if (getBusSkew(matchedBusPlans) > bus.maxLengthSkew! + EPSILON) {
|
|
567
|
+
return { plans: null, failedBus: bus }
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return { plans: matchedPlans }
|
|
571
|
+
}
|
package/lib/prepare-buses.ts
CHANGED
|
@@ -580,6 +580,19 @@ function resolveAllowedLayers(
|
|
|
580
580
|
return [...new Set(allowedLayers)]
|
|
581
581
|
}
|
|
582
582
|
|
|
583
|
+
function resolveMaxLengthSkew(
|
|
584
|
+
busId: string,
|
|
585
|
+
value: number | undefined,
|
|
586
|
+
): number | undefined {
|
|
587
|
+
if (value === undefined) return undefined
|
|
588
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
589
|
+
throw new Error(
|
|
590
|
+
`FanoutSolver: bus "${busId}" maxLengthSkew must be a finite non-negative number`,
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
return value
|
|
594
|
+
}
|
|
595
|
+
|
|
583
596
|
export function resolveAvailableBoundaryRegions(
|
|
584
597
|
value: readonly FanoutAvailableCornerAndSideInput[] | undefined,
|
|
585
598
|
): AvailableBoundaryRegion[] | undefined {
|
|
@@ -667,6 +680,15 @@ function resolveBusSpecs(
|
|
|
667
680
|
requestedBus.busId,
|
|
668
681
|
(requestedBus as FanoutBusSpec).allowedLayers,
|
|
669
682
|
)
|
|
683
|
+
const maxLengthSkew = resolveMaxLengthSkew(
|
|
684
|
+
requestedBus.busId,
|
|
685
|
+
requestedBus.maxLengthSkew,
|
|
686
|
+
)
|
|
687
|
+
if (termination.type === "plane" && maxLengthSkew !== undefined) {
|
|
688
|
+
throw new Error(
|
|
689
|
+
`FanoutSolver: plane-terminated bus "${requestedBus.busId}" cannot specify maxLengthSkew`,
|
|
690
|
+
)
|
|
691
|
+
}
|
|
670
692
|
if (
|
|
671
693
|
termination.type === "plane" &&
|
|
672
694
|
resolvedExitFields.preferredExit !== undefined
|
|
@@ -682,6 +704,7 @@ function resolveBusSpecs(
|
|
|
682
704
|
options.sourceComponentId,
|
|
683
705
|
...resolvedExitFields,
|
|
684
706
|
...(allowedLayers === undefined ? {} : { allowedLayers }),
|
|
707
|
+
...(maxLengthSkew === undefined ? {} : { maxLengthSkew }),
|
|
685
708
|
termination,
|
|
686
709
|
})
|
|
687
710
|
}
|
|
@@ -819,7 +842,7 @@ function prepareConnection(params: {
|
|
|
819
842
|
sourceGrid: ComponentGrid
|
|
820
843
|
componentGrids: ComponentGrid[]
|
|
821
844
|
termination: FanoutBusTermination
|
|
822
|
-
exitTargetPoint?: { x: number; y: number }
|
|
845
|
+
exitTargetPoint?: { x: number; y: number; layer?: string }
|
|
823
846
|
}): PreparedConnection {
|
|
824
847
|
const {
|
|
825
848
|
connection,
|
|
@@ -863,7 +886,11 @@ function prepareConnection(params: {
|
|
|
863
886
|
sourceLayer,
|
|
864
887
|
sourceObstacle: sourceMatch.obstacle,
|
|
865
888
|
targetPoint,
|
|
866
|
-
exitTargetPoint: exitTargetPoint ??
|
|
889
|
+
exitTargetPoint: exitTargetPoint ?? {
|
|
890
|
+
x: targetPoint.x,
|
|
891
|
+
y: targetPoint.y,
|
|
892
|
+
},
|
|
893
|
+
hasExplicitLayeredExitTarget: exitTargetPoint?.layer !== undefined,
|
|
867
894
|
}
|
|
868
895
|
}
|
|
869
896
|
throw new Error(
|
|
@@ -1244,6 +1271,9 @@ export function prepareFanoutBuses(
|
|
|
1244
1271
|
}
|
|
1245
1272
|
buses.push({
|
|
1246
1273
|
busId: busSpec.busId,
|
|
1274
|
+
...(busSpec.maxLengthSkew === undefined
|
|
1275
|
+
? {}
|
|
1276
|
+
: { maxLengthSkew: busSpec.maxLengthSkew }),
|
|
1247
1277
|
direction: resolvedExit.direction,
|
|
1248
1278
|
preferredExit: resolvedExit.preferredExit,
|
|
1249
1279
|
...(busSpec.exitEdge ? { exitEdge: busSpec.exitEdge } : {}),
|