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