@tscircuit/fanout-solver 0.0.38 → 0.0.40
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 +905 -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 +237 -34
- package/lib/match-component-dogbone-via-sites.ts +655 -0
- package/lib/route-bus.ts +1026 -130
- package/lib/route-via-minimal-winding.ts +423 -76
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
Obstacle,
|
|
2
3
|
SimpleRouteJson,
|
|
3
4
|
SimplifiedPcbTrace,
|
|
4
5
|
} from "@tscircuit/capacity-autorouter"
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
distanceSegmentToSegment,
|
|
12
13
|
} from "./geometry"
|
|
13
14
|
import { getAllRoutedTraceCopper } from "./get-routed-trace-copper"
|
|
14
|
-
import {
|
|
15
|
+
import { getViaSpanLayers } from "./layer-names"
|
|
15
16
|
import {
|
|
16
17
|
connectionsShareElectricalNet,
|
|
17
18
|
obstacleSharesElectricalNet,
|
|
@@ -37,6 +38,11 @@ export interface ViaMinimalWindingTerminal {
|
|
|
37
38
|
exitPoint: Point2D
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
export interface ViaMinimalWindingReservedVia {
|
|
42
|
+
connectionName: string
|
|
43
|
+
via: Pick<RoutedVia, "center" | "diameter" | "spanLayers">
|
|
44
|
+
}
|
|
45
|
+
|
|
40
46
|
export interface RouteViaMinimalWindingParams {
|
|
41
47
|
srj: SimpleRouteJson
|
|
42
48
|
bus: PreparedBus
|
|
@@ -48,7 +54,14 @@ export interface RouteViaMinimalWindingParams {
|
|
|
48
54
|
viaDiameter: number
|
|
49
55
|
viaHoleDiameter: number
|
|
50
56
|
clearance: number
|
|
57
|
+
allowBlindAndBuriedVias?: boolean
|
|
51
58
|
allowSameNetMerges?: boolean
|
|
59
|
+
maximumRouteOrderAttempts?: number
|
|
60
|
+
reservedVias?: readonly ViaMinimalWindingReservedVia[]
|
|
61
|
+
/** Use a finer uniform grid for narrow channels between reserved vias. */
|
|
62
|
+
gridStepDivisor?: 1 | 2
|
|
63
|
+
/** Bias bounded fixed-site searches toward the remote target band. */
|
|
64
|
+
preferTargetDirectedLaneBias?: boolean
|
|
52
65
|
}
|
|
53
66
|
|
|
54
67
|
interface GridNode {
|
|
@@ -74,6 +87,157 @@ interface BlockingVia {
|
|
|
74
87
|
via: Pick<RoutedVia, "center" | "diameter" | "spanLayers">
|
|
75
88
|
}
|
|
76
89
|
|
|
90
|
+
interface IndexedObstacle {
|
|
91
|
+
obstacle: Obstacle
|
|
92
|
+
minX: number
|
|
93
|
+
maxX: number
|
|
94
|
+
minY: number
|
|
95
|
+
maxY: number
|
|
96
|
+
xRadius: number
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type ShapeAwareObstacle = Obstacle & {
|
|
100
|
+
shape?: "circle"
|
|
101
|
+
ccwRotationDegrees?: number
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getObstacleAxisAlignedBounds(
|
|
105
|
+
obstacle: Obstacle,
|
|
106
|
+
): Omit<IndexedObstacle, "obstacle"> {
|
|
107
|
+
const shapeAwareObstacle = obstacle as ShapeAwareObstacle
|
|
108
|
+
if (shapeAwareObstacle.shape === "circle") {
|
|
109
|
+
const radius = obstacle.width / 2
|
|
110
|
+
return {
|
|
111
|
+
minX: obstacle.center.x - radius,
|
|
112
|
+
maxX: obstacle.center.x + radius,
|
|
113
|
+
minY: obstacle.center.y - radius,
|
|
114
|
+
maxY: obstacle.center.y + radius,
|
|
115
|
+
xRadius: radius,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const rotationRadians =
|
|
120
|
+
((shapeAwareObstacle.ccwRotationDegrees ?? 0) * Math.PI) / 180
|
|
121
|
+
const absoluteCosine = Math.abs(Math.cos(rotationRadians))
|
|
122
|
+
const absoluteSine = Math.abs(Math.sin(rotationRadians))
|
|
123
|
+
const halfWidth = obstacle.width / 2
|
|
124
|
+
const halfHeight = obstacle.height / 2
|
|
125
|
+
const xRadius = absoluteCosine * halfWidth + absoluteSine * halfHeight
|
|
126
|
+
const yRadius = absoluteSine * halfWidth + absoluteCosine * halfHeight
|
|
127
|
+
return {
|
|
128
|
+
minX: obstacle.center.x - xRadius,
|
|
129
|
+
maxX: obstacle.center.x + xRadius,
|
|
130
|
+
minY: obstacle.center.y - yRadius,
|
|
131
|
+
maxY: obstacle.center.y + yRadius,
|
|
132
|
+
xRadius,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* X-sorted broad phase for exact segment-to-obstacle clearance checks.
|
|
138
|
+
* Rotation-aware bounds make the query conservative; callers still use the
|
|
139
|
+
* shape-aware distance function to decide whether copper is actually blocked.
|
|
140
|
+
*/
|
|
141
|
+
export class ObstacleSpatialIndex {
|
|
142
|
+
private readonly obstaclesByCenterX: IndexedObstacle[]
|
|
143
|
+
private readonly maximumXRadius: number
|
|
144
|
+
|
|
145
|
+
constructor(obstacles: readonly Obstacle[]) {
|
|
146
|
+
this.obstaclesByCenterX = obstacles
|
|
147
|
+
.map((obstacle) => ({
|
|
148
|
+
obstacle,
|
|
149
|
+
...getObstacleAxisAlignedBounds(obstacle),
|
|
150
|
+
}))
|
|
151
|
+
.toSorted(
|
|
152
|
+
(first, second) => first.obstacle.center.x - second.obstacle.center.x,
|
|
153
|
+
)
|
|
154
|
+
this.maximumXRadius = this.obstaclesByCenterX.reduce(
|
|
155
|
+
(maximum, obstacle) => Math.max(maximum, obstacle.xRadius),
|
|
156
|
+
0,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
querySegment(segment: RoutedSegment, margin: number): Obstacle[] {
|
|
161
|
+
const segmentMinX = Math.min(segment.start.x, segment.end.x)
|
|
162
|
+
const segmentMaxX = Math.max(segment.start.x, segment.end.x)
|
|
163
|
+
const segmentMinY = Math.min(segment.start.y, segment.end.y)
|
|
164
|
+
const segmentMaxY = Math.max(segment.start.y, segment.end.y)
|
|
165
|
+
const minimumCenterX = segmentMinX - margin - this.maximumXRadius
|
|
166
|
+
const maximumCenterX = segmentMaxX + margin + this.maximumXRadius
|
|
167
|
+
let low = 0
|
|
168
|
+
let high = this.obstaclesByCenterX.length
|
|
169
|
+
while (low < high) {
|
|
170
|
+
const middle = Math.floor((low + high) / 2)
|
|
171
|
+
if (this.obstaclesByCenterX[middle]!.obstacle.center.x < minimumCenterX) {
|
|
172
|
+
low = middle + 1
|
|
173
|
+
} else {
|
|
174
|
+
high = middle
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const candidates: Obstacle[] = []
|
|
179
|
+
for (
|
|
180
|
+
let obstacleIndex = low;
|
|
181
|
+
obstacleIndex < this.obstaclesByCenterX.length;
|
|
182
|
+
obstacleIndex++
|
|
183
|
+
) {
|
|
184
|
+
const indexedObstacle = this.obstaclesByCenterX[obstacleIndex]!
|
|
185
|
+
if (indexedObstacle.obstacle.center.x > maximumCenterX) break
|
|
186
|
+
if (
|
|
187
|
+
indexedObstacle.maxX < segmentMinX - margin ||
|
|
188
|
+
indexedObstacle.minX > segmentMaxX + margin ||
|
|
189
|
+
indexedObstacle.maxY < segmentMinY - margin ||
|
|
190
|
+
indexedObstacle.minY > segmentMaxY + margin
|
|
191
|
+
) {
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
candidates.push(indexedObstacle.obstacle)
|
|
195
|
+
}
|
|
196
|
+
return candidates
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function* iterateUniqueRouteOrders<T>(params: {
|
|
201
|
+
initialOrderFactories: ReadonlyArray<() => readonly T[]>
|
|
202
|
+
rotationBase: readonly T[]
|
|
203
|
+
getItemKey: (item: T) => string
|
|
204
|
+
maximumOrderCount?: number
|
|
205
|
+
}): Generator<readonly T[]> {
|
|
206
|
+
const {
|
|
207
|
+
initialOrderFactories,
|
|
208
|
+
rotationBase,
|
|
209
|
+
getItemKey,
|
|
210
|
+
maximumOrderCount = Number.POSITIVE_INFINITY,
|
|
211
|
+
} = params
|
|
212
|
+
const seenOrderKeys = new Set<string>()
|
|
213
|
+
let yieldedOrderCount = 0
|
|
214
|
+
const getOrderKey = (order: readonly T[]): string =>
|
|
215
|
+
order.map(getItemKey).join("\u0000")
|
|
216
|
+
|
|
217
|
+
for (const createOrder of initialOrderFactories) {
|
|
218
|
+
if (yieldedOrderCount >= maximumOrderCount) return
|
|
219
|
+
const order = createOrder()
|
|
220
|
+
const key = getOrderKey(order)
|
|
221
|
+
if (seenOrderKeys.has(key)) continue
|
|
222
|
+
seenOrderKeys.add(key)
|
|
223
|
+
yieldedOrderCount++
|
|
224
|
+
yield order
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (let offset = 1; offset < rotationBase.length; offset++) {
|
|
228
|
+
if (yieldedOrderCount >= maximumOrderCount) return
|
|
229
|
+
const order = [
|
|
230
|
+
...rotationBase.slice(offset),
|
|
231
|
+
...rotationBase.slice(0, offset),
|
|
232
|
+
]
|
|
233
|
+
const key = getOrderKey(order)
|
|
234
|
+
if (seenOrderKeys.has(key)) continue
|
|
235
|
+
seenOrderKeys.add(key)
|
|
236
|
+
yieldedOrderCount++
|
|
237
|
+
yield order
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
77
241
|
interface HeapEntry {
|
|
78
242
|
node: number
|
|
79
243
|
direction: number
|
|
@@ -234,9 +398,13 @@ function getPlanVias(plan: FanoutRoutePlan): RoutedVia[] {
|
|
|
234
398
|
function getBlockingCopper(params: {
|
|
235
399
|
srj: SimpleRouteJson
|
|
236
400
|
acceptedPlans: readonly FanoutRoutePlan[]
|
|
401
|
+
allowBlindAndBuriedVias: boolean
|
|
237
402
|
}): { segments: BlockingSegment[]; vias: BlockingVia[] } {
|
|
238
|
-
const { srj, acceptedPlans } = params
|
|
239
|
-
const routedTraceCopper = getAllRoutedTraceCopper(
|
|
403
|
+
const { srj, acceptedPlans, allowBlindAndBuriedVias } = params
|
|
404
|
+
const routedTraceCopper = getAllRoutedTraceCopper(
|
|
405
|
+
srj,
|
|
406
|
+
allowBlindAndBuriedVias,
|
|
407
|
+
)
|
|
240
408
|
return {
|
|
241
409
|
segments: [
|
|
242
410
|
...routedTraceCopper.flatMap((copper) =>
|
|
@@ -280,6 +448,7 @@ function buildPlan(params: {
|
|
|
280
448
|
traceWidth: number
|
|
281
449
|
viaDiameter: number
|
|
282
450
|
viaHoleDiameter: number
|
|
451
|
+
allowBlindAndBuriedVias: boolean
|
|
283
452
|
}): FanoutRoutePlan {
|
|
284
453
|
const {
|
|
285
454
|
bus,
|
|
@@ -290,6 +459,7 @@ function buildPlan(params: {
|
|
|
290
459
|
traceWidth,
|
|
291
460
|
viaDiameter,
|
|
292
461
|
viaHoleDiameter,
|
|
462
|
+
allowBlindAndBuriedVias,
|
|
293
463
|
} = params
|
|
294
464
|
const connection = terminal.connection
|
|
295
465
|
const sourcePoint = {
|
|
@@ -302,12 +472,14 @@ function buildPlan(params: {
|
|
|
302
472
|
width: traceWidth,
|
|
303
473
|
layer: connection.sourceLayer,
|
|
304
474
|
}
|
|
475
|
+
const hasSourceDogbone = distance(sourcePoint, terminal.viaPoint) > EPSILON
|
|
305
476
|
const targetSegments = getSegments(targetLayerPoints, traceWidth, targetLayer)
|
|
306
|
-
const spanLayers =
|
|
307
|
-
connection.sourceLayer,
|
|
308
|
-
targetLayer,
|
|
477
|
+
const spanLayers = getViaSpanLayers({
|
|
478
|
+
fromLayer: connection.sourceLayer,
|
|
479
|
+
toLayer: targetLayer,
|
|
309
480
|
layerNames,
|
|
310
|
-
|
|
481
|
+
allowBlindAndBuriedVias,
|
|
482
|
+
})
|
|
311
483
|
const via: RoutedVia = {
|
|
312
484
|
center: terminal.viaPoint,
|
|
313
485
|
diameter: viaDiameter,
|
|
@@ -326,12 +498,16 @@ function buildPlan(params: {
|
|
|
326
498
|
? { start_pcb_port_id: connection.sourcePoint.pcb_port_id }
|
|
327
499
|
: {}),
|
|
328
500
|
},
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
501
|
+
...(hasSourceDogbone
|
|
502
|
+
? [
|
|
503
|
+
{
|
|
504
|
+
route_type: "wire" as const,
|
|
505
|
+
...terminal.viaPoint,
|
|
506
|
+
width: traceWidth,
|
|
507
|
+
layer: connection.sourceLayer,
|
|
508
|
+
},
|
|
509
|
+
]
|
|
510
|
+
: []),
|
|
335
511
|
{
|
|
336
512
|
route_type: "via",
|
|
337
513
|
...terminal.viaPoint,
|
|
@@ -357,7 +533,10 @@ function buildPlan(params: {
|
|
|
357
533
|
connectionName: connection.connection.name,
|
|
358
534
|
sourcePointIndex: connection.sourcePointIndex,
|
|
359
535
|
})
|
|
360
|
-
const segments = [
|
|
536
|
+
const segments = [
|
|
537
|
+
...(hasSourceDogbone ? [sourceSegment] : []),
|
|
538
|
+
...targetSegments,
|
|
539
|
+
]
|
|
361
540
|
const cornerBandSide = getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
362
541
|
return {
|
|
363
542
|
busId: bus.busId,
|
|
@@ -398,9 +577,15 @@ function buildPlan(params: {
|
|
|
398
577
|
}
|
|
399
578
|
}
|
|
400
579
|
|
|
401
|
-
export function
|
|
580
|
+
export function routeViaMinimalWindingAlternatives(
|
|
402
581
|
params: RouteViaMinimalWindingParams,
|
|
403
|
-
|
|
582
|
+
maximumAlternatives = 1,
|
|
583
|
+
): FanoutRoutePlan[][] {
|
|
584
|
+
if (!Number.isInteger(maximumAlternatives) || maximumAlternatives < 1) {
|
|
585
|
+
throw new Error(
|
|
586
|
+
`FanoutSolver: maximum winding alternatives must be a positive integer, received ${maximumAlternatives}`,
|
|
587
|
+
)
|
|
588
|
+
}
|
|
404
589
|
const {
|
|
405
590
|
srj,
|
|
406
591
|
bus,
|
|
@@ -412,8 +597,28 @@ export function routeViaMinimalWinding(
|
|
|
412
597
|
viaDiameter,
|
|
413
598
|
viaHoleDiameter,
|
|
414
599
|
clearance,
|
|
600
|
+
allowBlindAndBuriedVias = true,
|
|
415
601
|
allowSameNetMerges = false,
|
|
602
|
+
maximumRouteOrderAttempts,
|
|
603
|
+
reservedVias = [],
|
|
604
|
+
gridStepDivisor = 1,
|
|
605
|
+
preferTargetDirectedLaneBias = false,
|
|
416
606
|
} = params
|
|
607
|
+
if (
|
|
608
|
+
maximumRouteOrderAttempts !== undefined &&
|
|
609
|
+
(!Number.isInteger(maximumRouteOrderAttempts) ||
|
|
610
|
+
maximumRouteOrderAttempts < 1)
|
|
611
|
+
) {
|
|
612
|
+
throw new Error(
|
|
613
|
+
`FanoutSolver: maximumRouteOrderAttempts must be a positive integer, received ${maximumRouteOrderAttempts}`,
|
|
614
|
+
)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (gridStepDivisor !== 1 && gridStepDivisor !== 2) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
`FanoutSolver: gridStepDivisor must be 1 or 2, received ${gridStepDivisor}`,
|
|
620
|
+
)
|
|
621
|
+
}
|
|
417
622
|
if (
|
|
418
623
|
terminals.length === 0 ||
|
|
419
624
|
!bus.exitEdge ||
|
|
@@ -421,17 +626,17 @@ export function routeViaMinimalWinding(
|
|
|
421
626
|
(terminal) => terminal.connection.sourceLayer === targetLayer,
|
|
422
627
|
)
|
|
423
628
|
) {
|
|
424
|
-
return
|
|
629
|
+
return []
|
|
425
630
|
}
|
|
426
631
|
|
|
427
|
-
const gridStep = traceWidth + clearance
|
|
428
|
-
if (!Number.isFinite(gridStep) || gridStep <= 0) return
|
|
632
|
+
const gridStep = (traceWidth + clearance) / gridStepDivisor
|
|
633
|
+
if (!Number.isFinite(gridStep) || gridStep <= 0) return []
|
|
429
634
|
const { minX, maxX, minY, maxY } = bus.sharedBoundary
|
|
430
635
|
const columnCount = Math.floor((maxX - minX) / gridStep) + 1
|
|
431
636
|
const rowCount = Math.floor((maxY - minY) / gridStep) + 1
|
|
432
637
|
const nodeCount = columnCount * rowCount
|
|
433
638
|
if (columnCount < 2 || rowCount < 2 || nodeCount > MAX_GRID_NODE_COUNT) {
|
|
434
|
-
return
|
|
639
|
+
return []
|
|
435
640
|
}
|
|
436
641
|
const nodes: GridNode[] = Array.from({ length: nodeCount }, (_, index) => {
|
|
437
642
|
const column = index % columnCount
|
|
@@ -445,7 +650,14 @@ export function routeViaMinimalWinding(
|
|
|
445
650
|
const targetLayerObstacles = srj.obstacles.filter((obstacle) =>
|
|
446
651
|
obstacle.layers.includes(targetLayer),
|
|
447
652
|
)
|
|
448
|
-
const
|
|
653
|
+
const targetLayerObstacleIndex = new ObstacleSpatialIndex(
|
|
654
|
+
targetLayerObstacles,
|
|
655
|
+
)
|
|
656
|
+
const blockingCopper = getBlockingCopper({
|
|
657
|
+
srj,
|
|
658
|
+
acceptedPlans,
|
|
659
|
+
allowBlindAndBuriedVias,
|
|
660
|
+
})
|
|
449
661
|
const blockingSegments = blockingCopper.segments.filter(({ segment }) => {
|
|
450
662
|
if (segment.layer !== targetLayer) return false
|
|
451
663
|
const margin = (segment.width + traceWidth) / 2 + clearance
|
|
@@ -466,23 +678,54 @@ export function routeViaMinimalWinding(
|
|
|
466
678
|
via.center.y > maxY + margin
|
|
467
679
|
)
|
|
468
680
|
})
|
|
681
|
+
blockingVias.push(
|
|
682
|
+
...reservedVias.filter(({ via }) => {
|
|
683
|
+
if (!via.spanLayers.includes(targetLayer)) return false
|
|
684
|
+
const margin = via.diameter / 2 + traceWidth / 2 + clearance
|
|
685
|
+
return !(
|
|
686
|
+
via.center.x < minX - margin ||
|
|
687
|
+
via.center.x > maxX + margin ||
|
|
688
|
+
via.center.y < minY - margin ||
|
|
689
|
+
via.center.y > maxY + margin
|
|
690
|
+
)
|
|
691
|
+
}),
|
|
692
|
+
)
|
|
469
693
|
const terminalVias: BlockingVia[] = terminals.map((terminal) => ({
|
|
470
694
|
connectionName: terminal.connection.connection.name,
|
|
471
695
|
via: {
|
|
472
696
|
center: terminal.viaPoint,
|
|
473
697
|
diameter: viaDiameter,
|
|
474
|
-
spanLayers:
|
|
475
|
-
terminal.connection.sourceLayer,
|
|
476
|
-
targetLayer,
|
|
698
|
+
spanLayers: getViaSpanLayers({
|
|
699
|
+
fromLayer: terminal.connection.sourceLayer,
|
|
700
|
+
toLayer: targetLayer,
|
|
477
701
|
layerNames,
|
|
478
|
-
|
|
702
|
+
allowBlindAndBuriedVias,
|
|
703
|
+
}),
|
|
479
704
|
},
|
|
480
705
|
}))
|
|
481
706
|
const boundaryDirection = getDirectionForExitEdge(bus.exitEdge)
|
|
482
707
|
const sharesNet = (first: string, second: string): boolean =>
|
|
483
708
|
first === second ||
|
|
484
709
|
(allowSameNetMerges && connectionsShareElectricalNet(srj, first, second))
|
|
485
|
-
|
|
710
|
+
const allBlockingVias = [...blockingVias, ...terminalVias]
|
|
711
|
+
const maximumViaToTraceDistance = allBlockingVias.reduce(
|
|
712
|
+
(maximum, { via }) =>
|
|
713
|
+
Math.max(maximum, via.diameter / 2 + traceWidth / 2 + clearance),
|
|
714
|
+
traceWidth / 2 + clearance,
|
|
715
|
+
)
|
|
716
|
+
const viasByX = allBlockingVias.toSorted(
|
|
717
|
+
(first, second) => first.via.center.x - second.via.center.x,
|
|
718
|
+
)
|
|
719
|
+
const getFirstViaAtOrAfterX = (minimumX: number): number => {
|
|
720
|
+
let low = 0
|
|
721
|
+
let high = viasByX.length
|
|
722
|
+
while (low < high) {
|
|
723
|
+
const middle = Math.floor((low + high) / 2)
|
|
724
|
+
if (viasByX[middle]!.via.center.x < minimumX) low = middle + 1
|
|
725
|
+
else high = middle
|
|
726
|
+
}
|
|
727
|
+
return low
|
|
728
|
+
}
|
|
486
729
|
const segmentIsClear = (params: {
|
|
487
730
|
segment: RoutedSegment
|
|
488
731
|
terminal: ViaMinimalWindingTerminal
|
|
@@ -490,7 +733,11 @@ export function routeViaMinimalWinding(
|
|
|
490
733
|
}): boolean => {
|
|
491
734
|
const { segment, terminal, acceptedAttemptSegments } = params
|
|
492
735
|
const connectionName = terminal.connection.connection.name
|
|
493
|
-
|
|
736
|
+
const requiredObstacleClearance = segment.width / 2 + clearance
|
|
737
|
+
for (const obstacle of targetLayerObstacleIndex.querySegment(
|
|
738
|
+
segment,
|
|
739
|
+
requiredObstacleClearance,
|
|
740
|
+
)) {
|
|
494
741
|
if (
|
|
495
742
|
obstacle.connectedTo.includes(connectionName) ||
|
|
496
743
|
(allowSameNetMerges &&
|
|
@@ -500,7 +747,7 @@ export function routeViaMinimalWinding(
|
|
|
500
747
|
}
|
|
501
748
|
if (
|
|
502
749
|
distanceSegmentToObstacle(segment, obstacle) <
|
|
503
|
-
|
|
750
|
+
requiredObstacleClearance - EPSILON
|
|
504
751
|
) {
|
|
505
752
|
return false
|
|
506
753
|
}
|
|
@@ -533,20 +780,35 @@ export function routeViaMinimalWinding(
|
|
|
533
780
|
return false
|
|
534
781
|
}
|
|
535
782
|
}
|
|
536
|
-
|
|
783
|
+
const segmentMinX = Math.min(segment.start.x, segment.end.x)
|
|
784
|
+
const segmentMaxX = Math.max(segment.start.x, segment.end.x)
|
|
785
|
+
const segmentMinY = Math.min(segment.start.y, segment.end.y)
|
|
786
|
+
const segmentMaxY = Math.max(segment.start.y, segment.end.y)
|
|
787
|
+
for (
|
|
788
|
+
let viaIndex = getFirstViaAtOrAfterX(
|
|
789
|
+
segmentMinX - maximumViaToTraceDistance,
|
|
790
|
+
);
|
|
791
|
+
viaIndex < viasByX.length;
|
|
792
|
+
viaIndex++
|
|
793
|
+
) {
|
|
794
|
+
const blocker = viasByX[viaIndex]!
|
|
795
|
+
if (blocker.via.center.x > segmentMaxX + maximumViaToTraceDistance) {
|
|
796
|
+
break
|
|
797
|
+
}
|
|
537
798
|
if (sharesNet(connectionName, blocker.connectionName)) continue
|
|
799
|
+
const requiredDistance =
|
|
800
|
+
blocker.via.diameter / 2 + segment.width / 2 + clearance
|
|
538
801
|
if (
|
|
539
|
-
|
|
540
|
-
blocker.via.
|
|
802
|
+
blocker.via.center.x < segmentMinX - requiredDistance ||
|
|
803
|
+
blocker.via.center.x > segmentMaxX + requiredDistance ||
|
|
804
|
+
blocker.via.center.y < segmentMinY - requiredDistance ||
|
|
805
|
+
blocker.via.center.y > segmentMaxY + requiredDistance
|
|
541
806
|
) {
|
|
542
|
-
|
|
807
|
+
continue
|
|
543
808
|
}
|
|
544
|
-
}
|
|
545
|
-
for (const blocker of terminalVias) {
|
|
546
|
-
if (sharesNet(connectionName, blocker.connectionName)) continue
|
|
547
809
|
if (
|
|
548
810
|
distancePointToSegment(blocker.via.center, segment.start, segment.end) <
|
|
549
|
-
|
|
811
|
+
requiredDistance - EPSILON
|
|
550
812
|
) {
|
|
551
813
|
return false
|
|
552
814
|
}
|
|
@@ -582,7 +844,7 @@ export function routeViaMinimalWinding(
|
|
|
582
844
|
nodeIndex,
|
|
583
845
|
points,
|
|
584
846
|
radialDistance: connectorDistance,
|
|
585
|
-
length:
|
|
847
|
+
length: segments.reduce(
|
|
586
848
|
(total, segment) => total + distance(segment.start, segment.end),
|
|
587
849
|
0,
|
|
588
850
|
),
|
|
@@ -807,48 +1069,110 @@ export function routeViaMinimalWinding(
|
|
|
807
1069
|
)
|
|
808
1070
|
)
|
|
809
1071
|
})
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
),
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1072
|
+
const viaTracks = terminals.map((terminal) =>
|
|
1073
|
+
getPerpendicularAxis(terminal.viaPoint, boundaryDirection),
|
|
1074
|
+
)
|
|
1075
|
+
const targetTracks = targetOrderedTerminals.map((terminal) =>
|
|
1076
|
+
getPerpendicularAxis(terminal.exitPoint, boundaryDirection),
|
|
1077
|
+
)
|
|
1078
|
+
const meanViaTrack =
|
|
1079
|
+
viaTracks.reduce((sum, track) => sum + track, 0) / viaTracks.length
|
|
1080
|
+
const meanTargetTrack =
|
|
1081
|
+
targetTracks.reduce((sum, track) => sum + track, 0) / targetTracks.length
|
|
1082
|
+
const viasAreBeforeTargets =
|
|
1083
|
+
Math.max(...viaTracks) < Math.min(...targetTracks) - EPSILON
|
|
1084
|
+
const viasAreAfterTargets =
|
|
1085
|
+
Math.min(...viaTracks) > Math.max(...targetTracks) + EPSILON
|
|
1086
|
+
const laneBiases = preferTargetDirectedLaneBias
|
|
1087
|
+
? viasAreBeforeTargets
|
|
1088
|
+
? ([0, 1, -1] as const)
|
|
1089
|
+
: viasAreAfterTargets
|
|
1090
|
+
? ([0, -1, 1] as const)
|
|
1091
|
+
: bus.direction === boundaryDirection &&
|
|
1092
|
+
meanTargetTrack > meanViaTrack + EPSILON
|
|
1093
|
+
? ([1, 0, -1] as const)
|
|
1094
|
+
: bus.direction === boundaryDirection &&
|
|
1095
|
+
meanTargetTrack < meanViaTrack - EPSILON
|
|
1096
|
+
? ([-1, 0, 1] as const)
|
|
1097
|
+
: ([0, 1, -1] as const)
|
|
1098
|
+
: viasAreBeforeTargets
|
|
1099
|
+
? ([1, 0, -1] as const)
|
|
1100
|
+
: viasAreAfterTargets
|
|
1101
|
+
? ([-1, 0, 1] as const)
|
|
1102
|
+
: ([0, 1, -1] as const)
|
|
1103
|
+
const initialRouteOrderFactories: Array<
|
|
1104
|
+
() => readonly ViaMinimalWindingTerminal[]
|
|
1105
|
+
> = []
|
|
1106
|
+
if (viasAreBeforeTargets) {
|
|
1107
|
+
initialRouteOrderFactories.push(() => [
|
|
1108
|
+
...targetOrderedTerminals.slice(1),
|
|
1109
|
+
targetOrderedTerminals[0]!,
|
|
838
1110
|
])
|
|
1111
|
+
} else if (viasAreAfterTargets) {
|
|
1112
|
+
initialRouteOrderFactories.push(() => [...targetOrderedTerminals].reverse())
|
|
839
1113
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1114
|
+
initialRouteOrderFactories.push(
|
|
1115
|
+
...(preferTargetDirectedLaneBias &&
|
|
1116
|
+
bus.direction === boundaryDirection &&
|
|
1117
|
+
meanTargetTrack < meanViaTrack - EPSILON &&
|
|
1118
|
+
!viasAreBeforeTargets &&
|
|
1119
|
+
!viasAreAfterTargets
|
|
1120
|
+
? [
|
|
1121
|
+
() => [...targetOrderedTerminals].reverse(),
|
|
1122
|
+
() => targetOrderedTerminals,
|
|
1123
|
+
]
|
|
1124
|
+
: [
|
|
1125
|
+
() => targetOrderedTerminals,
|
|
1126
|
+
() => [...targetOrderedTerminals].reverse(),
|
|
1127
|
+
]),
|
|
1128
|
+
() =>
|
|
1129
|
+
terminals.toSorted(
|
|
1130
|
+
(first, second) =>
|
|
1131
|
+
first.viaPoint.x - second.viaPoint.x ||
|
|
1132
|
+
first.viaPoint.y - second.viaPoint.y,
|
|
1133
|
+
),
|
|
1134
|
+
() =>
|
|
1135
|
+
terminals.toSorted(
|
|
1136
|
+
(first, second) =>
|
|
1137
|
+
second.viaPoint.x - first.viaPoint.x ||
|
|
1138
|
+
second.viaPoint.y - first.viaPoint.y,
|
|
1139
|
+
),
|
|
1140
|
+
() =>
|
|
1141
|
+
terminals.toSorted(
|
|
1142
|
+
(first, second) =>
|
|
1143
|
+
first.viaPoint.y - second.viaPoint.y ||
|
|
1144
|
+
first.viaPoint.x - second.viaPoint.x,
|
|
1145
|
+
),
|
|
1146
|
+
() =>
|
|
1147
|
+
terminals.toSorted(
|
|
1148
|
+
(first, second) =>
|
|
1149
|
+
second.viaPoint.y - first.viaPoint.y ||
|
|
1150
|
+
second.viaPoint.x - first.viaPoint.x,
|
|
1151
|
+
),
|
|
1152
|
+
)
|
|
1153
|
+
const maximumRouteOrderCount =
|
|
1154
|
+
maximumRouteOrderAttempts === undefined
|
|
1155
|
+
? undefined
|
|
1156
|
+
: Math.ceil(maximumRouteOrderAttempts / laneBiases.length)
|
|
1157
|
+
const routeOrders = iterateUniqueRouteOrders({
|
|
1158
|
+
initialOrderFactories: initialRouteOrderFactories,
|
|
1159
|
+
rotationBase: targetOrderedTerminals,
|
|
1160
|
+
getItemKey: (terminal) => terminal.connection.connection.name,
|
|
1161
|
+
maximumOrderCount: maximumRouteOrderCount,
|
|
848
1162
|
})
|
|
849
1163
|
|
|
1164
|
+
const alternatives: FanoutRoutePlan[][] = []
|
|
1165
|
+
const seenAlternativeKeys = new Set<string>()
|
|
1166
|
+
let routeOrderAttemptCount = 0
|
|
850
1167
|
for (const routeOrder of routeOrders) {
|
|
851
|
-
for (const laneBias of
|
|
1168
|
+
for (const laneBias of laneBiases) {
|
|
1169
|
+
if (
|
|
1170
|
+
maximumRouteOrderAttempts !== undefined &&
|
|
1171
|
+
routeOrderAttemptCount >= maximumRouteOrderAttempts
|
|
1172
|
+
) {
|
|
1173
|
+
return alternatives
|
|
1174
|
+
}
|
|
1175
|
+
routeOrderAttemptCount++
|
|
852
1176
|
const acceptedAttemptSegments: BlockingSegment[] = []
|
|
853
1177
|
const routedPointsByConnectionName = new Map<string, Point2D[]>()
|
|
854
1178
|
let failed = false
|
|
@@ -872,7 +1196,7 @@ export function routeViaMinimalWinding(
|
|
|
872
1196
|
)
|
|
873
1197
|
}
|
|
874
1198
|
if (failed) continue
|
|
875
|
-
|
|
1199
|
+
const plans = terminals.map((terminal) => {
|
|
876
1200
|
const targetLayerPoints = routedPointsByConnectionName.get(
|
|
877
1201
|
terminal.connection.connection.name,
|
|
878
1202
|
)
|
|
@@ -890,9 +1214,32 @@ export function routeViaMinimalWinding(
|
|
|
890
1214
|
traceWidth,
|
|
891
1215
|
viaDiameter,
|
|
892
1216
|
viaHoleDiameter,
|
|
1217
|
+
allowBlindAndBuriedVias,
|
|
893
1218
|
})
|
|
894
1219
|
})
|
|
1220
|
+
const alternativeKey = plans
|
|
1221
|
+
.map((plan) =>
|
|
1222
|
+
plan.segments
|
|
1223
|
+
.map(
|
|
1224
|
+
(segment) =>
|
|
1225
|
+
`${segment.start.x},${segment.start.y},${segment.end.x},${segment.end.y},${segment.layer}`,
|
|
1226
|
+
)
|
|
1227
|
+
.join(";"),
|
|
1228
|
+
)
|
|
1229
|
+
.join("|")
|
|
1230
|
+
if (seenAlternativeKeys.has(alternativeKey)) continue
|
|
1231
|
+
seenAlternativeKeys.add(alternativeKey)
|
|
1232
|
+
alternatives.push(plans)
|
|
1233
|
+
if (alternatives.length >= maximumAlternatives) {
|
|
1234
|
+
return alternatives
|
|
1235
|
+
}
|
|
895
1236
|
}
|
|
896
1237
|
}
|
|
897
|
-
return
|
|
1238
|
+
return alternatives
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
export function routeViaMinimalWinding(
|
|
1242
|
+
params: RouteViaMinimalWindingParams,
|
|
1243
|
+
): FanoutRoutePlan[] | null {
|
|
1244
|
+
return routeViaMinimalWindingAlternatives(params, 1)[0] ?? null
|
|
898
1245
|
}
|