@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
|
@@ -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,12 @@ 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
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
interface GridNode {
|
|
@@ -74,6 +85,157 @@ interface BlockingVia {
|
|
|
74
85
|
via: Pick<RoutedVia, "center" | "diameter" | "spanLayers">
|
|
75
86
|
}
|
|
76
87
|
|
|
88
|
+
interface IndexedObstacle {
|
|
89
|
+
obstacle: Obstacle
|
|
90
|
+
minX: number
|
|
91
|
+
maxX: number
|
|
92
|
+
minY: number
|
|
93
|
+
maxY: number
|
|
94
|
+
xRadius: number
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type ShapeAwareObstacle = Obstacle & {
|
|
98
|
+
shape?: "circle"
|
|
99
|
+
ccwRotationDegrees?: number
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getObstacleAxisAlignedBounds(
|
|
103
|
+
obstacle: Obstacle,
|
|
104
|
+
): Omit<IndexedObstacle, "obstacle"> {
|
|
105
|
+
const shapeAwareObstacle = obstacle as ShapeAwareObstacle
|
|
106
|
+
if (shapeAwareObstacle.shape === "circle") {
|
|
107
|
+
const radius = obstacle.width / 2
|
|
108
|
+
return {
|
|
109
|
+
minX: obstacle.center.x - radius,
|
|
110
|
+
maxX: obstacle.center.x + radius,
|
|
111
|
+
minY: obstacle.center.y - radius,
|
|
112
|
+
maxY: obstacle.center.y + radius,
|
|
113
|
+
xRadius: radius,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const rotationRadians =
|
|
118
|
+
((shapeAwareObstacle.ccwRotationDegrees ?? 0) * Math.PI) / 180
|
|
119
|
+
const absoluteCosine = Math.abs(Math.cos(rotationRadians))
|
|
120
|
+
const absoluteSine = Math.abs(Math.sin(rotationRadians))
|
|
121
|
+
const halfWidth = obstacle.width / 2
|
|
122
|
+
const halfHeight = obstacle.height / 2
|
|
123
|
+
const xRadius = absoluteCosine * halfWidth + absoluteSine * halfHeight
|
|
124
|
+
const yRadius = absoluteSine * halfWidth + absoluteCosine * halfHeight
|
|
125
|
+
return {
|
|
126
|
+
minX: obstacle.center.x - xRadius,
|
|
127
|
+
maxX: obstacle.center.x + xRadius,
|
|
128
|
+
minY: obstacle.center.y - yRadius,
|
|
129
|
+
maxY: obstacle.center.y + yRadius,
|
|
130
|
+
xRadius,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* X-sorted broad phase for exact segment-to-obstacle clearance checks.
|
|
136
|
+
* Rotation-aware bounds make the query conservative; callers still use the
|
|
137
|
+
* shape-aware distance function to decide whether copper is actually blocked.
|
|
138
|
+
*/
|
|
139
|
+
export class ObstacleSpatialIndex {
|
|
140
|
+
private readonly obstaclesByCenterX: IndexedObstacle[]
|
|
141
|
+
private readonly maximumXRadius: number
|
|
142
|
+
|
|
143
|
+
constructor(obstacles: readonly Obstacle[]) {
|
|
144
|
+
this.obstaclesByCenterX = obstacles
|
|
145
|
+
.map((obstacle) => ({
|
|
146
|
+
obstacle,
|
|
147
|
+
...getObstacleAxisAlignedBounds(obstacle),
|
|
148
|
+
}))
|
|
149
|
+
.toSorted(
|
|
150
|
+
(first, second) => first.obstacle.center.x - second.obstacle.center.x,
|
|
151
|
+
)
|
|
152
|
+
this.maximumXRadius = this.obstaclesByCenterX.reduce(
|
|
153
|
+
(maximum, obstacle) => Math.max(maximum, obstacle.xRadius),
|
|
154
|
+
0,
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
querySegment(segment: RoutedSegment, margin: number): Obstacle[] {
|
|
159
|
+
const segmentMinX = Math.min(segment.start.x, segment.end.x)
|
|
160
|
+
const segmentMaxX = Math.max(segment.start.x, segment.end.x)
|
|
161
|
+
const segmentMinY = Math.min(segment.start.y, segment.end.y)
|
|
162
|
+
const segmentMaxY = Math.max(segment.start.y, segment.end.y)
|
|
163
|
+
const minimumCenterX = segmentMinX - margin - this.maximumXRadius
|
|
164
|
+
const maximumCenterX = segmentMaxX + margin + this.maximumXRadius
|
|
165
|
+
let low = 0
|
|
166
|
+
let high = this.obstaclesByCenterX.length
|
|
167
|
+
while (low < high) {
|
|
168
|
+
const middle = Math.floor((low + high) / 2)
|
|
169
|
+
if (this.obstaclesByCenterX[middle]!.obstacle.center.x < minimumCenterX) {
|
|
170
|
+
low = middle + 1
|
|
171
|
+
} else {
|
|
172
|
+
high = middle
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const candidates: Obstacle[] = []
|
|
177
|
+
for (
|
|
178
|
+
let obstacleIndex = low;
|
|
179
|
+
obstacleIndex < this.obstaclesByCenterX.length;
|
|
180
|
+
obstacleIndex++
|
|
181
|
+
) {
|
|
182
|
+
const indexedObstacle = this.obstaclesByCenterX[obstacleIndex]!
|
|
183
|
+
if (indexedObstacle.obstacle.center.x > maximumCenterX) break
|
|
184
|
+
if (
|
|
185
|
+
indexedObstacle.maxX < segmentMinX - margin ||
|
|
186
|
+
indexedObstacle.minX > segmentMaxX + margin ||
|
|
187
|
+
indexedObstacle.maxY < segmentMinY - margin ||
|
|
188
|
+
indexedObstacle.minY > segmentMaxY + margin
|
|
189
|
+
) {
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
candidates.push(indexedObstacle.obstacle)
|
|
193
|
+
}
|
|
194
|
+
return candidates
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function* iterateUniqueRouteOrders<T>(params: {
|
|
199
|
+
initialOrderFactories: ReadonlyArray<() => readonly T[]>
|
|
200
|
+
rotationBase: readonly T[]
|
|
201
|
+
getItemKey: (item: T) => string
|
|
202
|
+
maximumOrderCount?: number
|
|
203
|
+
}): Generator<readonly T[]> {
|
|
204
|
+
const {
|
|
205
|
+
initialOrderFactories,
|
|
206
|
+
rotationBase,
|
|
207
|
+
getItemKey,
|
|
208
|
+
maximumOrderCount = Number.POSITIVE_INFINITY,
|
|
209
|
+
} = params
|
|
210
|
+
const seenOrderKeys = new Set<string>()
|
|
211
|
+
let yieldedOrderCount = 0
|
|
212
|
+
const getOrderKey = (order: readonly T[]): string =>
|
|
213
|
+
order.map(getItemKey).join("\u0000")
|
|
214
|
+
|
|
215
|
+
for (const createOrder of initialOrderFactories) {
|
|
216
|
+
if (yieldedOrderCount >= maximumOrderCount) return
|
|
217
|
+
const order = createOrder()
|
|
218
|
+
const key = getOrderKey(order)
|
|
219
|
+
if (seenOrderKeys.has(key)) continue
|
|
220
|
+
seenOrderKeys.add(key)
|
|
221
|
+
yieldedOrderCount++
|
|
222
|
+
yield order
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for (let offset = 1; offset < rotationBase.length; offset++) {
|
|
226
|
+
if (yieldedOrderCount >= maximumOrderCount) return
|
|
227
|
+
const order = [
|
|
228
|
+
...rotationBase.slice(offset),
|
|
229
|
+
...rotationBase.slice(0, offset),
|
|
230
|
+
]
|
|
231
|
+
const key = getOrderKey(order)
|
|
232
|
+
if (seenOrderKeys.has(key)) continue
|
|
233
|
+
seenOrderKeys.add(key)
|
|
234
|
+
yieldedOrderCount++
|
|
235
|
+
yield order
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
77
239
|
interface HeapEntry {
|
|
78
240
|
node: number
|
|
79
241
|
direction: number
|
|
@@ -234,9 +396,13 @@ function getPlanVias(plan: FanoutRoutePlan): RoutedVia[] {
|
|
|
234
396
|
function getBlockingCopper(params: {
|
|
235
397
|
srj: SimpleRouteJson
|
|
236
398
|
acceptedPlans: readonly FanoutRoutePlan[]
|
|
399
|
+
allowBlindAndBuriedVias: boolean
|
|
237
400
|
}): { segments: BlockingSegment[]; vias: BlockingVia[] } {
|
|
238
|
-
const { srj, acceptedPlans } = params
|
|
239
|
-
const routedTraceCopper = getAllRoutedTraceCopper(
|
|
401
|
+
const { srj, acceptedPlans, allowBlindAndBuriedVias } = params
|
|
402
|
+
const routedTraceCopper = getAllRoutedTraceCopper(
|
|
403
|
+
srj,
|
|
404
|
+
allowBlindAndBuriedVias,
|
|
405
|
+
)
|
|
240
406
|
return {
|
|
241
407
|
segments: [
|
|
242
408
|
...routedTraceCopper.flatMap((copper) =>
|
|
@@ -280,6 +446,7 @@ function buildPlan(params: {
|
|
|
280
446
|
traceWidth: number
|
|
281
447
|
viaDiameter: number
|
|
282
448
|
viaHoleDiameter: number
|
|
449
|
+
allowBlindAndBuriedVias: boolean
|
|
283
450
|
}): FanoutRoutePlan {
|
|
284
451
|
const {
|
|
285
452
|
bus,
|
|
@@ -290,6 +457,7 @@ function buildPlan(params: {
|
|
|
290
457
|
traceWidth,
|
|
291
458
|
viaDiameter,
|
|
292
459
|
viaHoleDiameter,
|
|
460
|
+
allowBlindAndBuriedVias,
|
|
293
461
|
} = params
|
|
294
462
|
const connection = terminal.connection
|
|
295
463
|
const sourcePoint = {
|
|
@@ -302,12 +470,14 @@ function buildPlan(params: {
|
|
|
302
470
|
width: traceWidth,
|
|
303
471
|
layer: connection.sourceLayer,
|
|
304
472
|
}
|
|
473
|
+
const hasSourceDogbone = distance(sourcePoint, terminal.viaPoint) > EPSILON
|
|
305
474
|
const targetSegments = getSegments(targetLayerPoints, traceWidth, targetLayer)
|
|
306
|
-
const spanLayers =
|
|
307
|
-
connection.sourceLayer,
|
|
308
|
-
targetLayer,
|
|
475
|
+
const spanLayers = getViaSpanLayers({
|
|
476
|
+
fromLayer: connection.sourceLayer,
|
|
477
|
+
toLayer: targetLayer,
|
|
309
478
|
layerNames,
|
|
310
|
-
|
|
479
|
+
allowBlindAndBuriedVias,
|
|
480
|
+
})
|
|
311
481
|
const via: RoutedVia = {
|
|
312
482
|
center: terminal.viaPoint,
|
|
313
483
|
diameter: viaDiameter,
|
|
@@ -326,12 +496,16 @@ function buildPlan(params: {
|
|
|
326
496
|
? { start_pcb_port_id: connection.sourcePoint.pcb_port_id }
|
|
327
497
|
: {}),
|
|
328
498
|
},
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
499
|
+
...(hasSourceDogbone
|
|
500
|
+
? [
|
|
501
|
+
{
|
|
502
|
+
route_type: "wire" as const,
|
|
503
|
+
...terminal.viaPoint,
|
|
504
|
+
width: traceWidth,
|
|
505
|
+
layer: connection.sourceLayer,
|
|
506
|
+
},
|
|
507
|
+
]
|
|
508
|
+
: []),
|
|
335
509
|
{
|
|
336
510
|
route_type: "via",
|
|
337
511
|
...terminal.viaPoint,
|
|
@@ -357,7 +531,10 @@ function buildPlan(params: {
|
|
|
357
531
|
connectionName: connection.connection.name,
|
|
358
532
|
sourcePointIndex: connection.sourcePointIndex,
|
|
359
533
|
})
|
|
360
|
-
const segments = [
|
|
534
|
+
const segments = [
|
|
535
|
+
...(hasSourceDogbone ? [sourceSegment] : []),
|
|
536
|
+
...targetSegments,
|
|
537
|
+
]
|
|
361
538
|
const cornerBandSide = getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
362
539
|
return {
|
|
363
540
|
busId: bus.busId,
|
|
@@ -398,9 +575,15 @@ function buildPlan(params: {
|
|
|
398
575
|
}
|
|
399
576
|
}
|
|
400
577
|
|
|
401
|
-
export function
|
|
578
|
+
export function routeViaMinimalWindingAlternatives(
|
|
402
579
|
params: RouteViaMinimalWindingParams,
|
|
403
|
-
|
|
580
|
+
maximumAlternatives = 1,
|
|
581
|
+
): FanoutRoutePlan[][] {
|
|
582
|
+
if (!Number.isInteger(maximumAlternatives) || maximumAlternatives < 1) {
|
|
583
|
+
throw new Error(
|
|
584
|
+
`FanoutSolver: maximum winding alternatives must be a positive integer, received ${maximumAlternatives}`,
|
|
585
|
+
)
|
|
586
|
+
}
|
|
404
587
|
const {
|
|
405
588
|
srj,
|
|
406
589
|
bus,
|
|
@@ -412,8 +595,27 @@ export function routeViaMinimalWinding(
|
|
|
412
595
|
viaDiameter,
|
|
413
596
|
viaHoleDiameter,
|
|
414
597
|
clearance,
|
|
598
|
+
allowBlindAndBuriedVias = true,
|
|
415
599
|
allowSameNetMerges = false,
|
|
600
|
+
maximumRouteOrderAttempts,
|
|
601
|
+
reservedVias = [],
|
|
602
|
+
gridStepDivisor = 1,
|
|
416
603
|
} = params
|
|
604
|
+
if (
|
|
605
|
+
maximumRouteOrderAttempts !== undefined &&
|
|
606
|
+
(!Number.isInteger(maximumRouteOrderAttempts) ||
|
|
607
|
+
maximumRouteOrderAttempts < 1)
|
|
608
|
+
) {
|
|
609
|
+
throw new Error(
|
|
610
|
+
`FanoutSolver: maximumRouteOrderAttempts must be a positive integer, received ${maximumRouteOrderAttempts}`,
|
|
611
|
+
)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (gridStepDivisor !== 1 && gridStepDivisor !== 2) {
|
|
615
|
+
throw new Error(
|
|
616
|
+
`FanoutSolver: gridStepDivisor must be 1 or 2, received ${gridStepDivisor}`,
|
|
617
|
+
)
|
|
618
|
+
}
|
|
417
619
|
if (
|
|
418
620
|
terminals.length === 0 ||
|
|
419
621
|
!bus.exitEdge ||
|
|
@@ -421,17 +623,17 @@ export function routeViaMinimalWinding(
|
|
|
421
623
|
(terminal) => terminal.connection.sourceLayer === targetLayer,
|
|
422
624
|
)
|
|
423
625
|
) {
|
|
424
|
-
return
|
|
626
|
+
return []
|
|
425
627
|
}
|
|
426
628
|
|
|
427
|
-
const gridStep = traceWidth + clearance
|
|
428
|
-
if (!Number.isFinite(gridStep) || gridStep <= 0) return
|
|
629
|
+
const gridStep = (traceWidth + clearance) / gridStepDivisor
|
|
630
|
+
if (!Number.isFinite(gridStep) || gridStep <= 0) return []
|
|
429
631
|
const { minX, maxX, minY, maxY } = bus.sharedBoundary
|
|
430
632
|
const columnCount = Math.floor((maxX - minX) / gridStep) + 1
|
|
431
633
|
const rowCount = Math.floor((maxY - minY) / gridStep) + 1
|
|
432
634
|
const nodeCount = columnCount * rowCount
|
|
433
635
|
if (columnCount < 2 || rowCount < 2 || nodeCount > MAX_GRID_NODE_COUNT) {
|
|
434
|
-
return
|
|
636
|
+
return []
|
|
435
637
|
}
|
|
436
638
|
const nodes: GridNode[] = Array.from({ length: nodeCount }, (_, index) => {
|
|
437
639
|
const column = index % columnCount
|
|
@@ -445,7 +647,14 @@ export function routeViaMinimalWinding(
|
|
|
445
647
|
const targetLayerObstacles = srj.obstacles.filter((obstacle) =>
|
|
446
648
|
obstacle.layers.includes(targetLayer),
|
|
447
649
|
)
|
|
448
|
-
const
|
|
650
|
+
const targetLayerObstacleIndex = new ObstacleSpatialIndex(
|
|
651
|
+
targetLayerObstacles,
|
|
652
|
+
)
|
|
653
|
+
const blockingCopper = getBlockingCopper({
|
|
654
|
+
srj,
|
|
655
|
+
acceptedPlans,
|
|
656
|
+
allowBlindAndBuriedVias,
|
|
657
|
+
})
|
|
449
658
|
const blockingSegments = blockingCopper.segments.filter(({ segment }) => {
|
|
450
659
|
if (segment.layer !== targetLayer) return false
|
|
451
660
|
const margin = (segment.width + traceWidth) / 2 + clearance
|
|
@@ -466,22 +675,54 @@ export function routeViaMinimalWinding(
|
|
|
466
675
|
via.center.y > maxY + margin
|
|
467
676
|
)
|
|
468
677
|
})
|
|
678
|
+
blockingVias.push(
|
|
679
|
+
...reservedVias.filter(({ via }) => {
|
|
680
|
+
if (!via.spanLayers.includes(targetLayer)) return false
|
|
681
|
+
const margin = via.diameter / 2 + traceWidth / 2 + clearance
|
|
682
|
+
return !(
|
|
683
|
+
via.center.x < minX - margin ||
|
|
684
|
+
via.center.x > maxX + margin ||
|
|
685
|
+
via.center.y < minY - margin ||
|
|
686
|
+
via.center.y > maxY + margin
|
|
687
|
+
)
|
|
688
|
+
}),
|
|
689
|
+
)
|
|
469
690
|
const terminalVias: BlockingVia[] = terminals.map((terminal) => ({
|
|
470
691
|
connectionName: terminal.connection.connection.name,
|
|
471
692
|
via: {
|
|
472
693
|
center: terminal.viaPoint,
|
|
473
694
|
diameter: viaDiameter,
|
|
474
|
-
spanLayers:
|
|
475
|
-
terminal.connection.sourceLayer,
|
|
476
|
-
targetLayer,
|
|
695
|
+
spanLayers: getViaSpanLayers({
|
|
696
|
+
fromLayer: terminal.connection.sourceLayer,
|
|
697
|
+
toLayer: targetLayer,
|
|
477
698
|
layerNames,
|
|
478
|
-
|
|
699
|
+
allowBlindAndBuriedVias,
|
|
700
|
+
}),
|
|
479
701
|
},
|
|
480
702
|
}))
|
|
481
703
|
const boundaryDirection = getDirectionForExitEdge(bus.exitEdge)
|
|
482
704
|
const sharesNet = (first: string, second: string): boolean =>
|
|
483
705
|
first === second ||
|
|
484
706
|
(allowSameNetMerges && connectionsShareElectricalNet(srj, first, second))
|
|
707
|
+
const allBlockingVias = [...blockingVias, ...terminalVias]
|
|
708
|
+
const maximumViaToTraceDistance = allBlockingVias.reduce(
|
|
709
|
+
(maximum, { via }) =>
|
|
710
|
+
Math.max(maximum, via.diameter / 2 + traceWidth / 2 + clearance),
|
|
711
|
+
traceWidth / 2 + clearance,
|
|
712
|
+
)
|
|
713
|
+
const viasByX = allBlockingVias.toSorted(
|
|
714
|
+
(first, second) => first.via.center.x - second.via.center.x,
|
|
715
|
+
)
|
|
716
|
+
const getFirstViaAtOrAfterX = (minimumX: number): number => {
|
|
717
|
+
let low = 0
|
|
718
|
+
let high = viasByX.length
|
|
719
|
+
while (low < high) {
|
|
720
|
+
const middle = Math.floor((low + high) / 2)
|
|
721
|
+
if (viasByX[middle]!.via.center.x < minimumX) low = middle + 1
|
|
722
|
+
else high = middle
|
|
723
|
+
}
|
|
724
|
+
return low
|
|
725
|
+
}
|
|
485
726
|
|
|
486
727
|
const segmentIsClear = (params: {
|
|
487
728
|
segment: RoutedSegment
|
|
@@ -490,7 +731,11 @@ export function routeViaMinimalWinding(
|
|
|
490
731
|
}): boolean => {
|
|
491
732
|
const { segment, terminal, acceptedAttemptSegments } = params
|
|
492
733
|
const connectionName = terminal.connection.connection.name
|
|
493
|
-
|
|
734
|
+
const requiredObstacleClearance = segment.width / 2 + clearance
|
|
735
|
+
for (const obstacle of targetLayerObstacleIndex.querySegment(
|
|
736
|
+
segment,
|
|
737
|
+
requiredObstacleClearance,
|
|
738
|
+
)) {
|
|
494
739
|
if (
|
|
495
740
|
obstacle.connectedTo.includes(connectionName) ||
|
|
496
741
|
(allowSameNetMerges &&
|
|
@@ -500,7 +745,7 @@ export function routeViaMinimalWinding(
|
|
|
500
745
|
}
|
|
501
746
|
if (
|
|
502
747
|
distanceSegmentToObstacle(segment, obstacle) <
|
|
503
|
-
|
|
748
|
+
requiredObstacleClearance - EPSILON
|
|
504
749
|
) {
|
|
505
750
|
return false
|
|
506
751
|
}
|
|
@@ -533,20 +778,35 @@ export function routeViaMinimalWinding(
|
|
|
533
778
|
return false
|
|
534
779
|
}
|
|
535
780
|
}
|
|
536
|
-
|
|
781
|
+
const segmentMinX = Math.min(segment.start.x, segment.end.x)
|
|
782
|
+
const segmentMaxX = Math.max(segment.start.x, segment.end.x)
|
|
783
|
+
const segmentMinY = Math.min(segment.start.y, segment.end.y)
|
|
784
|
+
const segmentMaxY = Math.max(segment.start.y, segment.end.y)
|
|
785
|
+
for (
|
|
786
|
+
let viaIndex = getFirstViaAtOrAfterX(
|
|
787
|
+
segmentMinX - maximumViaToTraceDistance,
|
|
788
|
+
);
|
|
789
|
+
viaIndex < viasByX.length;
|
|
790
|
+
viaIndex++
|
|
791
|
+
) {
|
|
792
|
+
const blocker = viasByX[viaIndex]!
|
|
793
|
+
if (blocker.via.center.x > segmentMaxX + maximumViaToTraceDistance) {
|
|
794
|
+
break
|
|
795
|
+
}
|
|
537
796
|
if (sharesNet(connectionName, blocker.connectionName)) continue
|
|
797
|
+
const requiredDistance =
|
|
798
|
+
blocker.via.diameter / 2 + segment.width / 2 + clearance
|
|
538
799
|
if (
|
|
539
|
-
|
|
540
|
-
blocker.via.
|
|
800
|
+
blocker.via.center.x < segmentMinX - requiredDistance ||
|
|
801
|
+
blocker.via.center.x > segmentMaxX + requiredDistance ||
|
|
802
|
+
blocker.via.center.y < segmentMinY - requiredDistance ||
|
|
803
|
+
blocker.via.center.y > segmentMaxY + requiredDistance
|
|
541
804
|
) {
|
|
542
|
-
|
|
805
|
+
continue
|
|
543
806
|
}
|
|
544
|
-
}
|
|
545
|
-
for (const blocker of terminalVias) {
|
|
546
|
-
if (sharesNet(connectionName, blocker.connectionName)) continue
|
|
547
807
|
if (
|
|
548
808
|
distancePointToSegment(blocker.via.center, segment.start, segment.end) <
|
|
549
|
-
|
|
809
|
+
requiredDistance - EPSILON
|
|
550
810
|
) {
|
|
551
811
|
return false
|
|
552
812
|
}
|
|
@@ -807,48 +1067,83 @@ export function routeViaMinimalWinding(
|
|
|
807
1067
|
)
|
|
808
1068
|
)
|
|
809
1069
|
})
|
|
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
|
-
for (let offset = 1; offset < targetOrderedTerminals.length; offset++) {
|
|
835
|
-
routeOrderCandidates.push([
|
|
836
|
-
...targetOrderedTerminals.slice(offset),
|
|
837
|
-
...targetOrderedTerminals.slice(0, offset),
|
|
1070
|
+
const viaTracks = terminals.map((terminal) =>
|
|
1071
|
+
getPerpendicularAxis(terminal.viaPoint, boundaryDirection),
|
|
1072
|
+
)
|
|
1073
|
+
const targetTracks = targetOrderedTerminals.map((terminal) =>
|
|
1074
|
+
getPerpendicularAxis(terminal.exitPoint, boundaryDirection),
|
|
1075
|
+
)
|
|
1076
|
+
const viasAreBeforeTargets =
|
|
1077
|
+
Math.max(...viaTracks) < Math.min(...targetTracks) - EPSILON
|
|
1078
|
+
const viasAreAfterTargets =
|
|
1079
|
+
Math.min(...viaTracks) > Math.max(...targetTracks) + EPSILON
|
|
1080
|
+
const laneBiases = viasAreBeforeTargets
|
|
1081
|
+
? ([1, 0, -1] as const)
|
|
1082
|
+
: viasAreAfterTargets
|
|
1083
|
+
? ([-1, 0, 1] as const)
|
|
1084
|
+
: ([0, 1, -1] as const)
|
|
1085
|
+
const initialRouteOrderFactories: Array<
|
|
1086
|
+
() => readonly ViaMinimalWindingTerminal[]
|
|
1087
|
+
> = []
|
|
1088
|
+
if (viasAreBeforeTargets) {
|
|
1089
|
+
initialRouteOrderFactories.push(() => [
|
|
1090
|
+
...targetOrderedTerminals.slice(1),
|
|
1091
|
+
targetOrderedTerminals[0]!,
|
|
838
1092
|
])
|
|
1093
|
+
} else if (viasAreAfterTargets) {
|
|
1094
|
+
initialRouteOrderFactories.push(() => [...targetOrderedTerminals].reverse())
|
|
839
1095
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
.
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1096
|
+
initialRouteOrderFactories.push(
|
|
1097
|
+
() => targetOrderedTerminals,
|
|
1098
|
+
() => [...targetOrderedTerminals].reverse(),
|
|
1099
|
+
() =>
|
|
1100
|
+
terminals.toSorted(
|
|
1101
|
+
(first, second) =>
|
|
1102
|
+
first.viaPoint.x - second.viaPoint.x ||
|
|
1103
|
+
first.viaPoint.y - second.viaPoint.y,
|
|
1104
|
+
),
|
|
1105
|
+
() =>
|
|
1106
|
+
terminals.toSorted(
|
|
1107
|
+
(first, second) =>
|
|
1108
|
+
second.viaPoint.x - first.viaPoint.x ||
|
|
1109
|
+
second.viaPoint.y - first.viaPoint.y,
|
|
1110
|
+
),
|
|
1111
|
+
() =>
|
|
1112
|
+
terminals.toSorted(
|
|
1113
|
+
(first, second) =>
|
|
1114
|
+
first.viaPoint.y - second.viaPoint.y ||
|
|
1115
|
+
first.viaPoint.x - second.viaPoint.x,
|
|
1116
|
+
),
|
|
1117
|
+
() =>
|
|
1118
|
+
terminals.toSorted(
|
|
1119
|
+
(first, second) =>
|
|
1120
|
+
second.viaPoint.y - first.viaPoint.y ||
|
|
1121
|
+
second.viaPoint.x - first.viaPoint.x,
|
|
1122
|
+
),
|
|
1123
|
+
)
|
|
1124
|
+
const maximumRouteOrderCount =
|
|
1125
|
+
maximumRouteOrderAttempts === undefined
|
|
1126
|
+
? undefined
|
|
1127
|
+
: Math.ceil(maximumRouteOrderAttempts / laneBiases.length)
|
|
1128
|
+
const routeOrders = iterateUniqueRouteOrders({
|
|
1129
|
+
initialOrderFactories: initialRouteOrderFactories,
|
|
1130
|
+
rotationBase: targetOrderedTerminals,
|
|
1131
|
+
getItemKey: (terminal) => terminal.connection.connection.name,
|
|
1132
|
+
maximumOrderCount: maximumRouteOrderCount,
|
|
848
1133
|
})
|
|
849
1134
|
|
|
1135
|
+
const alternatives: FanoutRoutePlan[][] = []
|
|
1136
|
+
const seenAlternativeKeys = new Set<string>()
|
|
1137
|
+
let routeOrderAttemptCount = 0
|
|
850
1138
|
for (const routeOrder of routeOrders) {
|
|
851
|
-
for (const laneBias of
|
|
1139
|
+
for (const laneBias of laneBiases) {
|
|
1140
|
+
if (
|
|
1141
|
+
maximumRouteOrderAttempts !== undefined &&
|
|
1142
|
+
routeOrderAttemptCount >= maximumRouteOrderAttempts
|
|
1143
|
+
) {
|
|
1144
|
+
return alternatives
|
|
1145
|
+
}
|
|
1146
|
+
routeOrderAttemptCount++
|
|
852
1147
|
const acceptedAttemptSegments: BlockingSegment[] = []
|
|
853
1148
|
const routedPointsByConnectionName = new Map<string, Point2D[]>()
|
|
854
1149
|
let failed = false
|
|
@@ -872,7 +1167,7 @@ export function routeViaMinimalWinding(
|
|
|
872
1167
|
)
|
|
873
1168
|
}
|
|
874
1169
|
if (failed) continue
|
|
875
|
-
|
|
1170
|
+
const plans = terminals.map((terminal) => {
|
|
876
1171
|
const targetLayerPoints = routedPointsByConnectionName.get(
|
|
877
1172
|
terminal.connection.connection.name,
|
|
878
1173
|
)
|
|
@@ -890,9 +1185,30 @@ export function routeViaMinimalWinding(
|
|
|
890
1185
|
traceWidth,
|
|
891
1186
|
viaDiameter,
|
|
892
1187
|
viaHoleDiameter,
|
|
1188
|
+
allowBlindAndBuriedVias,
|
|
893
1189
|
})
|
|
894
1190
|
})
|
|
1191
|
+
const alternativeKey = plans
|
|
1192
|
+
.map((plan) =>
|
|
1193
|
+
plan.segments
|
|
1194
|
+
.map(
|
|
1195
|
+
(segment) =>
|
|
1196
|
+
`${segment.start.x},${segment.start.y},${segment.end.x},${segment.end.y},${segment.layer}`,
|
|
1197
|
+
)
|
|
1198
|
+
.join(";"),
|
|
1199
|
+
)
|
|
1200
|
+
.join("|")
|
|
1201
|
+
if (seenAlternativeKeys.has(alternativeKey)) continue
|
|
1202
|
+
seenAlternativeKeys.add(alternativeKey)
|
|
1203
|
+
alternatives.push(plans)
|
|
1204
|
+
if (alternatives.length >= maximumAlternatives) return alternatives
|
|
895
1205
|
}
|
|
896
1206
|
}
|
|
897
|
-
return
|
|
1207
|
+
return alternatives
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
export function routeViaMinimalWinding(
|
|
1211
|
+
params: RouteViaMinimalWindingParams,
|
|
1212
|
+
): FanoutRoutePlan[] | null {
|
|
1213
|
+
return routeViaMinimalWindingAlternatives(params, 1)[0] ?? null
|
|
898
1214
|
}
|