@tscircuit/fanout-solver 0.0.48 → 0.0.49
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/lib/fanout-solver.ts +847 -177
- package/lib/route-bus.ts +35 -4
- package/lib/route-single-layer-adaptive-exits.ts +323 -16
- package/lib/route-via-minimal-winding.ts +253 -8
- package/package.json +1 -1
package/lib/route-bus.ts
CHANGED
|
@@ -24,7 +24,8 @@ import {
|
|
|
24
24
|
obstacleSharesElectricalNet,
|
|
25
25
|
} from "./net-identity"
|
|
26
26
|
import {
|
|
27
|
-
|
|
27
|
+
routeViaMinimalWindingAlternativesSteps,
|
|
28
|
+
type RouteViaMinimalWindingProgress,
|
|
28
29
|
type ViaMinimalWindingReservedVia,
|
|
29
30
|
} from "./route-via-minimal-winding"
|
|
30
31
|
import type {
|
|
@@ -64,6 +65,13 @@ export interface RouteBusParams {
|
|
|
64
65
|
cornerBandTargetTrackOffset?: number
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
export interface RouteBusAlternativesProgress {
|
|
69
|
+
phase: "via-minimal-winding"
|
|
70
|
+
busId: string
|
|
71
|
+
targetLayer: string
|
|
72
|
+
winding: RouteViaMinimalWindingProgress
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
interface TrackCandidate {
|
|
68
76
|
value: number
|
|
69
77
|
kind: "corridor" | "gap" | "margin" | "preferred"
|
|
@@ -2367,10 +2375,11 @@ function routePlaneTerminatedBus(
|
|
|
2367
2375
|
return null
|
|
2368
2376
|
}
|
|
2369
2377
|
|
|
2370
|
-
export function
|
|
2378
|
+
export function* routeBusAlternativesSteps(
|
|
2371
2379
|
params: RouteBusParams,
|
|
2372
2380
|
maxAlternatives = 1,
|
|
2373
|
-
|
|
2381
|
+
includeVisualization = false,
|
|
2382
|
+
): Generator<RouteBusAlternativesProgress, FanoutRoutePlan[][], void> {
|
|
2374
2383
|
const {
|
|
2375
2384
|
srj,
|
|
2376
2385
|
bus,
|
|
@@ -2765,7 +2774,7 @@ export function routeBusAlternatives(
|
|
|
2765
2774
|
.join("|")}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}`
|
|
2766
2775
|
if (seenTerminalSignatures.has(terminalSignature)) continue
|
|
2767
2776
|
seenTerminalSignatures.add(terminalSignature)
|
|
2768
|
-
const
|
|
2777
|
+
const windingSteps = routeViaMinimalWindingAlternativesSteps(
|
|
2769
2778
|
{
|
|
2770
2779
|
srj,
|
|
2771
2780
|
bus,
|
|
@@ -2796,7 +2805,19 @@ export function routeBusAlternatives(
|
|
|
2796
2805
|
: terminalPattern.maximumRouteOrderAttempts === undefined
|
|
2797
2806
|
? Math.min(2, maxAlternatives - alternatives.length)
|
|
2798
2807
|
: 2,
|
|
2808
|
+
includeVisualization,
|
|
2799
2809
|
)
|
|
2810
|
+
let windingResult = windingSteps.next()
|
|
2811
|
+
while (!windingResult.done) {
|
|
2812
|
+
yield {
|
|
2813
|
+
phase: "via-minimal-winding",
|
|
2814
|
+
busId: bus.busId,
|
|
2815
|
+
targetLayer,
|
|
2816
|
+
winding: windingResult.value,
|
|
2817
|
+
}
|
|
2818
|
+
windingResult = windingSteps.next()
|
|
2819
|
+
}
|
|
2820
|
+
const viaMinimalAlternatives = windingResult.value
|
|
2800
2821
|
for (const viaMinimalPlans of viaMinimalAlternatives) {
|
|
2801
2822
|
const combinedPlansAreClear = fanoutPlansAreClear({
|
|
2802
2823
|
plans: [...acceptedPlans, ...viaMinimalPlans],
|
|
@@ -2949,6 +2970,16 @@ export function routeBusAlternatives(
|
|
|
2949
2970
|
return alternatives
|
|
2950
2971
|
}
|
|
2951
2972
|
|
|
2973
|
+
export function routeBusAlternatives(
|
|
2974
|
+
params: RouteBusParams,
|
|
2975
|
+
maxAlternatives = 1,
|
|
2976
|
+
): FanoutRoutePlan[][] {
|
|
2977
|
+
const steps = routeBusAlternativesSteps(params, maxAlternatives)
|
|
2978
|
+
let result = steps.next()
|
|
2979
|
+
while (!result.done) result = steps.next()
|
|
2980
|
+
return result.value
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2952
2983
|
export function routeBus(params: RouteBusParams): FanoutRoutePlan[] | null {
|
|
2953
2984
|
return routeBusAlternatives(params, 1)[0] ?? null
|
|
2954
2985
|
}
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
SimpleRouteJson,
|
|
4
4
|
SimplifiedPcbTrace,
|
|
5
5
|
} from "@tscircuit/capacity-autorouter"
|
|
6
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
6
7
|
import { createFanoutOutputIds } from "./fanout-output-ids"
|
|
7
8
|
import {
|
|
8
9
|
distance,
|
|
@@ -26,6 +27,10 @@ interface FlowRoutingParams {
|
|
|
26
27
|
traceWidth: number
|
|
27
28
|
clearance: number
|
|
28
29
|
availableBoundaryRegions?: AvailableBoundaryRegion[]
|
|
30
|
+
onProgress?: (
|
|
31
|
+
visualization: GraphicsObject,
|
|
32
|
+
stats: Record<string, unknown>,
|
|
33
|
+
) => void
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
interface FlowItem {
|
|
@@ -73,6 +78,22 @@ interface FlowEdge {
|
|
|
73
78
|
isSink?: boolean
|
|
74
79
|
}
|
|
75
80
|
|
|
81
|
+
interface FlowVisualizationUpdate {
|
|
82
|
+
phase: string
|
|
83
|
+
processed?: number
|
|
84
|
+
total?: number
|
|
85
|
+
direction?: FanoutDirection | "any"
|
|
86
|
+
grid?: {
|
|
87
|
+
points: readonly Point2D[]
|
|
88
|
+
obstacleFreeNodes: Uint8Array
|
|
89
|
+
processedNodeCount?: number
|
|
90
|
+
}
|
|
91
|
+
candidatePoints?: readonly Point2D[]
|
|
92
|
+
segments?: readonly RoutedSegment[]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type ReportFlowProgress = (update: FlowVisualizationUpdate) => void
|
|
96
|
+
|
|
76
97
|
const EPSILON = 1e-9
|
|
77
98
|
const OBSTACLE_BIN_SIZE = 1
|
|
78
99
|
const FANOUT_FLOW_DEBUG_ENABLED =
|
|
@@ -86,6 +107,88 @@ const FANOUT_FLOW_DEBUG_ENABLED =
|
|
|
86
107
|
}
|
|
87
108
|
).process?.env?.FANOUT_FLOW_DEBUG === "1"
|
|
88
109
|
|
|
110
|
+
function visualizeFlowProgress(params: {
|
|
111
|
+
boundary: PreparedBus["sharedBoundary"]
|
|
112
|
+
sourcePoints: readonly Point2D[]
|
|
113
|
+
traceWidth: number
|
|
114
|
+
update: FlowVisualizationUpdate
|
|
115
|
+
}): GraphicsObject {
|
|
116
|
+
const { boundary, sourcePoints, traceWidth, update } = params
|
|
117
|
+
const width = boundary.maxX - boundary.minX
|
|
118
|
+
const height = boundary.maxY - boundary.minY
|
|
119
|
+
const annotationSize = Math.max(Math.min(width, height) * 0.02, 0.2)
|
|
120
|
+
const gridSampleStride = update.grid
|
|
121
|
+
? Math.max(1, Math.ceil(update.grid.points.length / 1_200))
|
|
122
|
+
: 1
|
|
123
|
+
const sampledGridPoints = update.grid
|
|
124
|
+
? update.grid.points.flatMap((point, index) =>
|
|
125
|
+
index % gridSampleStride === 0 ? [{ point, index }] : [],
|
|
126
|
+
)
|
|
127
|
+
: []
|
|
128
|
+
const processedNodeCount =
|
|
129
|
+
update.grid?.processedNodeCount ?? update.grid?.points.length ?? 0
|
|
130
|
+
return {
|
|
131
|
+
title: `Adaptive exits: ${update.phase}`,
|
|
132
|
+
rects: [
|
|
133
|
+
{
|
|
134
|
+
center: {
|
|
135
|
+
x: (boundary.minX + boundary.maxX) / 2,
|
|
136
|
+
y: (boundary.minY + boundary.maxY) / 2,
|
|
137
|
+
},
|
|
138
|
+
width,
|
|
139
|
+
height,
|
|
140
|
+
fill: "rgba(0, 0, 0, 0)",
|
|
141
|
+
stroke: "rgba(14, 165, 233, 0.9)",
|
|
142
|
+
label: "adaptive flow grid boundary",
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
points: [
|
|
146
|
+
...sampledGridPoints.map(({ point, index: node }) => {
|
|
147
|
+
const processed = node < processedNodeCount
|
|
148
|
+
return {
|
|
149
|
+
...point,
|
|
150
|
+
color: !processed
|
|
151
|
+
? "rgba(148, 163, 184, 0.18)"
|
|
152
|
+
: update.grid!.obstacleFreeNodes[node]
|
|
153
|
+
? "rgba(34, 197, 94, 0.38)"
|
|
154
|
+
: "rgba(100, 116, 139, 0.28)",
|
|
155
|
+
label: processed
|
|
156
|
+
? update.grid!.obstacleFreeNodes[node]
|
|
157
|
+
? "available flow node"
|
|
158
|
+
: "blocked flow node"
|
|
159
|
+
: "unprocessed flow node",
|
|
160
|
+
}
|
|
161
|
+
}),
|
|
162
|
+
...sourcePoints.map((point) => ({
|
|
163
|
+
...point,
|
|
164
|
+
color: "#f97316",
|
|
165
|
+
label: "adaptive route source",
|
|
166
|
+
})),
|
|
167
|
+
...(update.candidatePoints ?? []).map((point) => ({
|
|
168
|
+
...point,
|
|
169
|
+
color: "#06b6d4",
|
|
170
|
+
label: "selected terminal node",
|
|
171
|
+
})),
|
|
172
|
+
],
|
|
173
|
+
lines: (update.segments ?? []).map((segment) => ({
|
|
174
|
+
points: [segment.start, segment.end],
|
|
175
|
+
strokeColor: "rgba(250, 204, 21, 0.9)",
|
|
176
|
+
strokeWidth: Math.max(traceWidth, annotationSize * 0.4),
|
|
177
|
+
label: "current adaptive route",
|
|
178
|
+
})),
|
|
179
|
+
texts: [
|
|
180
|
+
{
|
|
181
|
+
x: boundary.minX,
|
|
182
|
+
y: boundary.maxY + annotationSize * 2,
|
|
183
|
+
text: `${update.phase}${update.direction ? ` · ${update.direction}` : ""}${update.processed !== undefined && update.total !== undefined ? ` · ${update.processed}/${update.total}` : ""}`,
|
|
184
|
+
color: "#0f172a",
|
|
185
|
+
fontSize: annotationSize * 1.5,
|
|
186
|
+
anchorSide: "bottom_left",
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
89
192
|
function getNetKey(connection: PreparedConnection): string {
|
|
90
193
|
const simpleRouteConnection =
|
|
91
194
|
connection.connection as typeof connection.connection & {
|
|
@@ -313,27 +416,38 @@ class Dinic {
|
|
|
313
416
|
return 0
|
|
314
417
|
}
|
|
315
418
|
|
|
316
|
-
|
|
419
|
+
*maximumFlowSteps(
|
|
420
|
+
source: number,
|
|
421
|
+
sink: number,
|
|
422
|
+
limit: number,
|
|
423
|
+
): Generator<number, number, unknown> {
|
|
317
424
|
let flow = 0
|
|
318
425
|
while (flow < limit && this.buildLevels(source, sink)) {
|
|
319
426
|
this.nextEdges.fill(0)
|
|
427
|
+
let flowSinceYield = 0
|
|
320
428
|
while (flow < limit) {
|
|
321
429
|
const sent = this.sendFlow(source, sink)
|
|
322
430
|
if (sent === 0) break
|
|
323
431
|
flow += sent
|
|
432
|
+
flowSinceYield += sent
|
|
433
|
+
if (flowSinceYield >= 4 && flow < limit) {
|
|
434
|
+
flowSinceYield = 0
|
|
435
|
+
yield flow
|
|
436
|
+
}
|
|
324
437
|
}
|
|
325
438
|
}
|
|
326
439
|
return flow
|
|
327
440
|
}
|
|
328
441
|
}
|
|
329
442
|
|
|
330
|
-
function
|
|
443
|
+
function* createFlowGridSteps(params: {
|
|
331
444
|
boundary: PreparedBus["sharedBoundary"]
|
|
332
445
|
obstacles: Obstacle[]
|
|
333
446
|
traceWidth: number
|
|
334
447
|
clearance: number
|
|
335
|
-
|
|
336
|
-
|
|
448
|
+
reportProgress: ReportFlowProgress
|
|
449
|
+
}): Generator<void, FlowGrid, unknown> {
|
|
450
|
+
const { boundary, obstacles, traceWidth, clearance, reportProgress } = params
|
|
337
451
|
const step = traceWidth + clearance
|
|
338
452
|
const columnCount = Math.round((boundary.maxX - boundary.minX) / step) + 1
|
|
339
453
|
const rowCount = Math.round((boundary.maxY - boundary.minY) / step) + 1
|
|
@@ -374,6 +488,14 @@ function createFlowGrid(params: {
|
|
|
374
488
|
obstacleIndexesByBin.set(key, indexes)
|
|
375
489
|
}
|
|
376
490
|
}
|
|
491
|
+
if ((obstacleIndex + 1) % 128 === 0) {
|
|
492
|
+
reportProgress({
|
|
493
|
+
phase: "index-obstacles",
|
|
494
|
+
processed: obstacleIndex + 1,
|
|
495
|
+
total: obstacles.length,
|
|
496
|
+
})
|
|
497
|
+
yield
|
|
498
|
+
}
|
|
377
499
|
}
|
|
378
500
|
const getNearbyObstacles = (first: Point2D, second = first): Obstacle[] => {
|
|
379
501
|
const minBinX = Math.floor(Math.min(first.x, second.x) / OBSTACLE_BIN_SIZE)
|
|
@@ -404,6 +526,19 @@ function createFlowGrid(params: {
|
|
|
404
526
|
) {
|
|
405
527
|
obstacleFreeNodes[node] = 1
|
|
406
528
|
}
|
|
529
|
+
if ((node + 1) % 2048 === 0) {
|
|
530
|
+
reportProgress({
|
|
531
|
+
phase: "classify-flow-grid",
|
|
532
|
+
processed: node + 1,
|
|
533
|
+
total: nodeCount,
|
|
534
|
+
grid: {
|
|
535
|
+
points,
|
|
536
|
+
obstacleFreeNodes,
|
|
537
|
+
processedNodeCount: node + 1,
|
|
538
|
+
},
|
|
539
|
+
})
|
|
540
|
+
yield
|
|
541
|
+
}
|
|
407
542
|
}
|
|
408
543
|
const neighbors: number[][] = Array.from({ length: nodeCount }, () => [])
|
|
409
544
|
for (let node = 0; node < nodeCount; node++) {
|
|
@@ -437,6 +572,15 @@ function createFlowGrid(params: {
|
|
|
437
572
|
neighbors[node]!.push(nextNode)
|
|
438
573
|
neighbors[nextNode]!.push(node)
|
|
439
574
|
}
|
|
575
|
+
if ((node + 1) % 2048 === 0) {
|
|
576
|
+
reportProgress({
|
|
577
|
+
phase: "connect-flow-grid",
|
|
578
|
+
processed: node + 1,
|
|
579
|
+
total: nodeCount,
|
|
580
|
+
grid: { points, obstacleFreeNodes, processedNodeCount: nodeCount },
|
|
581
|
+
})
|
|
582
|
+
yield
|
|
583
|
+
}
|
|
440
584
|
}
|
|
441
585
|
return {
|
|
442
586
|
boundary,
|
|
@@ -450,7 +594,7 @@ function createFlowGrid(params: {
|
|
|
450
594
|
}
|
|
451
595
|
}
|
|
452
596
|
|
|
453
|
-
function
|
|
597
|
+
function* routeDirectionGroupSteps(params: {
|
|
454
598
|
direction: FanoutDirection | "any"
|
|
455
599
|
availableDirections?: ReadonlySet<FanoutDirection>
|
|
456
600
|
items: FlowItem[]
|
|
@@ -461,7 +605,8 @@ function routeDirectionGroup(params: {
|
|
|
461
605
|
occupiedNodes: Uint8Array
|
|
462
606
|
acceptedSegments: RoutedSegment[]
|
|
463
607
|
connectorSelectionOffset?: number
|
|
464
|
-
|
|
608
|
+
reportProgress: ReportFlowProgress
|
|
609
|
+
}): Generator<void, DirectionGroupResult | null, unknown> {
|
|
465
610
|
const {
|
|
466
611
|
direction,
|
|
467
612
|
availableDirections,
|
|
@@ -473,6 +618,7 @@ function routeDirectionGroup(params: {
|
|
|
473
618
|
occupiedNodes,
|
|
474
619
|
acceptedSegments,
|
|
475
620
|
connectorSelectionOffset = 0,
|
|
621
|
+
reportProgress,
|
|
476
622
|
} = params
|
|
477
623
|
if (items.length === 0) {
|
|
478
624
|
return { routes: [], usedNodes: [], unmatchedItems: [] }
|
|
@@ -532,6 +678,7 @@ function routeDirectionGroup(params: {
|
|
|
532
678
|
return { x, y, distanceSquared: x * x + y * y }
|
|
533
679
|
}).sort((first, second) => first.distanceSquared - second.distanceSquared)
|
|
534
680
|
const terminals: FlowTerminal[] = []
|
|
681
|
+
let processedTerminalGroupCount = 0
|
|
535
682
|
for (const equivalentItems of equivalentItemsByKey.values()) {
|
|
536
683
|
const item = equivalentItems[0]!
|
|
537
684
|
const maxConnectorLength =
|
|
@@ -567,12 +714,32 @@ function routeDirectionGroup(params: {
|
|
|
567
714
|
}
|
|
568
715
|
if (candidates.length === 0) return null
|
|
569
716
|
terminals.push({ item, equivalentItems, candidates })
|
|
717
|
+
processedTerminalGroupCount++
|
|
718
|
+
if (processedTerminalGroupCount % 8 === 0) {
|
|
719
|
+
reportProgress({
|
|
720
|
+
phase: "discover-terminal-connectors",
|
|
721
|
+
direction,
|
|
722
|
+
processed: processedTerminalGroupCount,
|
|
723
|
+
total: equivalentItemsByKey.size,
|
|
724
|
+
grid: {
|
|
725
|
+
points: grid.points,
|
|
726
|
+
obstacleFreeNodes: freeNodes,
|
|
727
|
+
processedNodeCount: gridNodeCount,
|
|
728
|
+
},
|
|
729
|
+
candidatePoints: terminals.flatMap((value) =>
|
|
730
|
+
value.candidates.map((candidate) => pointForNode(candidate.node)),
|
|
731
|
+
),
|
|
732
|
+
segments: acceptedSegments,
|
|
733
|
+
})
|
|
734
|
+
yield
|
|
735
|
+
}
|
|
570
736
|
}
|
|
571
737
|
|
|
572
738
|
const selectedConnectorSegments: Array<{
|
|
573
739
|
netKey: string
|
|
574
740
|
segments: RoutedSegment[]
|
|
575
741
|
}> = []
|
|
742
|
+
let selectedConnectorCount = 0
|
|
576
743
|
for (const terminal of [...terminals].sort(
|
|
577
744
|
(first, second) => first.candidates.length - second.candidates.length,
|
|
578
745
|
)) {
|
|
@@ -609,6 +776,28 @@ function routeDirectionGroup(params: {
|
|
|
609
776
|
netKey: terminal.item.netKey,
|
|
610
777
|
segments: getSegments(candidate.connectorPoints, traceWidth),
|
|
611
778
|
})
|
|
779
|
+
selectedConnectorCount++
|
|
780
|
+
if (selectedConnectorCount % 8 === 0) {
|
|
781
|
+
reportProgress({
|
|
782
|
+
phase: "select-terminal-connectors",
|
|
783
|
+
direction,
|
|
784
|
+
processed: selectedConnectorCount,
|
|
785
|
+
total: terminals.length,
|
|
786
|
+
grid: {
|
|
787
|
+
points: grid.points,
|
|
788
|
+
obstacleFreeNodes: freeNodes,
|
|
789
|
+
processedNodeCount: gridNodeCount,
|
|
790
|
+
},
|
|
791
|
+
candidatePoints: terminals.flatMap((value) =>
|
|
792
|
+
value.candidates.map((candidate) => pointForNode(candidate.node)),
|
|
793
|
+
),
|
|
794
|
+
segments: [
|
|
795
|
+
...acceptedSegments,
|
|
796
|
+
...selectedConnectorSegments.flatMap((selected) => selected.segments),
|
|
797
|
+
],
|
|
798
|
+
})
|
|
799
|
+
yield
|
|
800
|
+
}
|
|
612
801
|
}
|
|
613
802
|
|
|
614
803
|
const terminalNodes = new Set(
|
|
@@ -715,8 +904,53 @@ function routeDirectionGroup(params: {
|
|
|
715
904
|
if (isTarget) {
|
|
716
905
|
flow.addEdge(gridOutStart + node, sink, 1, { isSink: true })
|
|
717
906
|
}
|
|
907
|
+
if ((node + 1) % 2048 === 0) {
|
|
908
|
+
reportProgress({
|
|
909
|
+
phase: "build-flow-network",
|
|
910
|
+
direction,
|
|
911
|
+
processed: node + 1,
|
|
912
|
+
total: gridNodeCount,
|
|
913
|
+
grid: {
|
|
914
|
+
points: grid.points,
|
|
915
|
+
obstacleFreeNodes: freeNodes,
|
|
916
|
+
processedNodeCount: gridNodeCount,
|
|
917
|
+
},
|
|
918
|
+
candidatePoints: terminals.map((terminal) =>
|
|
919
|
+
pointForNode(terminal.candidates[0]!.node),
|
|
920
|
+
),
|
|
921
|
+
segments: [
|
|
922
|
+
...acceptedSegments,
|
|
923
|
+
...selectedConnectorSegments.flatMap((selected) => selected.segments),
|
|
924
|
+
],
|
|
925
|
+
})
|
|
926
|
+
yield
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
const flowSteps = flow.maximumFlowSteps(source, sink, terminals.length)
|
|
930
|
+
let flowResult = flowSteps.next()
|
|
931
|
+
while (!flowResult.done) {
|
|
932
|
+
reportProgress({
|
|
933
|
+
phase: "augment-terminal-flow",
|
|
934
|
+
direction,
|
|
935
|
+
processed: flowResult.value,
|
|
936
|
+
total: terminals.length,
|
|
937
|
+
grid: {
|
|
938
|
+
points: grid.points,
|
|
939
|
+
obstacleFreeNodes: freeNodes,
|
|
940
|
+
processedNodeCount: gridNodeCount,
|
|
941
|
+
},
|
|
942
|
+
candidatePoints: terminals.map((terminal) =>
|
|
943
|
+
pointForNode(terminal.candidates[0]!.node),
|
|
944
|
+
),
|
|
945
|
+
segments: [
|
|
946
|
+
...acceptedSegments,
|
|
947
|
+
...selectedConnectorSegments.flatMap((selected) => selected.segments),
|
|
948
|
+
],
|
|
949
|
+
})
|
|
950
|
+
yield
|
|
951
|
+
flowResult = flowSteps.next()
|
|
718
952
|
}
|
|
719
|
-
const achievedFlow =
|
|
953
|
+
const achievedFlow = flowResult.value
|
|
720
954
|
const terminalWasMatched = (terminalIndex: number) => {
|
|
721
955
|
const terminalNode = terminalStart + terminalIndex
|
|
722
956
|
return flow.edges[source]!.some(
|
|
@@ -797,6 +1031,24 @@ function routeDirectionGroup(params: {
|
|
|
797
1031
|
for (const item of terminal.equivalentItems) {
|
|
798
1032
|
routes.push({ item, points, segments })
|
|
799
1033
|
}
|
|
1034
|
+
if ((terminalIndex + 1) % 8 === 0) {
|
|
1035
|
+
reportProgress({
|
|
1036
|
+
phase: "extract-flow-routes",
|
|
1037
|
+
direction,
|
|
1038
|
+
processed: terminalIndex + 1,
|
|
1039
|
+
total: terminals.length,
|
|
1040
|
+
grid: {
|
|
1041
|
+
points: grid.points,
|
|
1042
|
+
obstacleFreeNodes: freeNodes,
|
|
1043
|
+
processedNodeCount: gridNodeCount,
|
|
1044
|
+
},
|
|
1045
|
+
segments: [
|
|
1046
|
+
...acceptedSegments,
|
|
1047
|
+
...routes.flatMap((route) => route.segments),
|
|
1048
|
+
],
|
|
1049
|
+
})
|
|
1050
|
+
yield
|
|
1051
|
+
}
|
|
800
1052
|
}
|
|
801
1053
|
return { routes, usedNodes, unmatchedItems }
|
|
802
1054
|
}
|
|
@@ -972,14 +1224,15 @@ function getDirectionForBoundaryPoint(
|
|
|
972
1224
|
return null
|
|
973
1225
|
}
|
|
974
1226
|
|
|
975
|
-
function
|
|
1227
|
+
function* routeWithAdaptiveExitsSteps(params: {
|
|
976
1228
|
items: FlowItem[]
|
|
977
1229
|
grid: FlowGrid
|
|
978
1230
|
obstacles: Obstacle[]
|
|
979
1231
|
traceWidth: number
|
|
980
1232
|
clearance: number
|
|
981
1233
|
availableBoundaryRegions?: AvailableBoundaryRegion[]
|
|
982
|
-
|
|
1234
|
+
reportProgress: ReportFlowProgress
|
|
1235
|
+
}): Generator<void, FanoutRoutePlan[] | null, unknown> {
|
|
983
1236
|
const {
|
|
984
1237
|
items,
|
|
985
1238
|
grid,
|
|
@@ -987,6 +1240,7 @@ function routeWithAdaptiveExits(params: {
|
|
|
987
1240
|
traceWidth,
|
|
988
1241
|
clearance,
|
|
989
1242
|
availableBoundaryRegions,
|
|
1243
|
+
reportProgress,
|
|
990
1244
|
} = params
|
|
991
1245
|
const availableDirections = availableBoundaryRegions
|
|
992
1246
|
? new Set(availableBoundaryRegions.map((region) => region.direction))
|
|
@@ -998,7 +1252,7 @@ function routeWithAdaptiveExits(params: {
|
|
|
998
1252
|
item.connection.sourceObstacle.height > 2,
|
|
999
1253
|
),
|
|
1000
1254
|
)
|
|
1001
|
-
let unrestricted:
|
|
1255
|
+
let unrestricted: DirectionGroupResult | null = null
|
|
1002
1256
|
for (let mergeRound = 0; mergeRound < 4; mergeRound++) {
|
|
1003
1257
|
const directlyRoutedItems = items.filter((item) => !mergeItems.has(item))
|
|
1004
1258
|
let bestResult: DirectionGroupResult | null = null
|
|
@@ -1007,7 +1261,7 @@ function routeWithAdaptiveExits(params: {
|
|
|
1007
1261
|
connectorSelectionOffset < 4;
|
|
1008
1262
|
connectorSelectionOffset++
|
|
1009
1263
|
) {
|
|
1010
|
-
const result =
|
|
1264
|
+
const result = yield* routeDirectionGroupSteps({
|
|
1011
1265
|
direction: "any",
|
|
1012
1266
|
availableDirections,
|
|
1013
1267
|
items: directlyRoutedItems,
|
|
@@ -1018,6 +1272,7 @@ function routeWithAdaptiveExits(params: {
|
|
|
1018
1272
|
occupiedNodes: new Uint8Array(grid.nodeCount),
|
|
1019
1273
|
acceptedSegments: [],
|
|
1020
1274
|
connectorSelectionOffset,
|
|
1275
|
+
reportProgress,
|
|
1021
1276
|
})
|
|
1022
1277
|
if (!result) continue
|
|
1023
1278
|
if (!bestResult || result.routes.length > bestResult.routes.length) {
|
|
@@ -1119,6 +1374,18 @@ function routeWithAdaptiveExits(params: {
|
|
|
1119
1374
|
}
|
|
1120
1375
|
if (!mergedRoute) return null
|
|
1121
1376
|
routes.push(mergedRoute)
|
|
1377
|
+
reportProgress({
|
|
1378
|
+
phase: "merge-same-net-routes",
|
|
1379
|
+
processed: routes.length,
|
|
1380
|
+
total: items.length,
|
|
1381
|
+
grid: {
|
|
1382
|
+
points: grid.points,
|
|
1383
|
+
obstacleFreeNodes: grid.obstacleFreeNodes,
|
|
1384
|
+
processedNodeCount: grid.nodeCount,
|
|
1385
|
+
},
|
|
1386
|
+
segments: routes.flatMap((route) => route.segments),
|
|
1387
|
+
})
|
|
1388
|
+
yield
|
|
1122
1389
|
}
|
|
1123
1390
|
const plans = routes.map((route) => buildPlan(route, traceWidth))
|
|
1124
1391
|
if (
|
|
@@ -1175,10 +1442,17 @@ function routeWithAdaptiveExits(params: {
|
|
|
1175
1442
|
)
|
|
1176
1443
|
}
|
|
1177
1444
|
|
|
1178
|
-
export function
|
|
1445
|
+
export function* routeSingleLayerWithAdaptiveExitsSteps(
|
|
1179
1446
|
params: FlowRoutingParams,
|
|
1180
|
-
): FanoutRoutePlan[] | null {
|
|
1181
|
-
const {
|
|
1447
|
+
): Generator<void, FanoutRoutePlan[] | null, unknown> {
|
|
1448
|
+
const {
|
|
1449
|
+
srj,
|
|
1450
|
+
buses,
|
|
1451
|
+
traceWidth,
|
|
1452
|
+
clearance,
|
|
1453
|
+
availableBoundaryRegions,
|
|
1454
|
+
onProgress,
|
|
1455
|
+
} = params
|
|
1182
1456
|
if (buses.some((bus) => bus.connections.length !== 1)) return null
|
|
1183
1457
|
const items = buses.flatMap((bus) =>
|
|
1184
1458
|
bus.connections.map(
|
|
@@ -1198,25 +1472,58 @@ export function routeSingleLayerWithAdaptiveExits(
|
|
|
1198
1472
|
)
|
|
1199
1473
|
const boundary = buses[0]?.sharedBoundary
|
|
1200
1474
|
if (!boundary) return []
|
|
1201
|
-
const
|
|
1475
|
+
const sourcePoints = items.map((item) => item.source)
|
|
1476
|
+
const reportProgress: ReportFlowProgress = (update) => {
|
|
1477
|
+
onProgress?.(
|
|
1478
|
+
visualizeFlowProgress({ boundary, sourcePoints, traceWidth, update }),
|
|
1479
|
+
{
|
|
1480
|
+
adaptivePhase: update.phase,
|
|
1481
|
+
...(update.direction ? { adaptiveDirection: update.direction } : {}),
|
|
1482
|
+
...(update.processed !== undefined
|
|
1483
|
+
? { adaptiveWorkUnit: update.processed }
|
|
1484
|
+
: {}),
|
|
1485
|
+
...(update.total !== undefined
|
|
1486
|
+
? { adaptiveWorkUnitCount: update.total }
|
|
1487
|
+
: {}),
|
|
1488
|
+
},
|
|
1489
|
+
)
|
|
1490
|
+
}
|
|
1491
|
+
reportProgress({
|
|
1492
|
+
phase: "prepare-flow-grid",
|
|
1493
|
+
processed: 0,
|
|
1494
|
+
total: topObstacles.length,
|
|
1495
|
+
})
|
|
1496
|
+
const grid = yield* createFlowGridSteps({
|
|
1202
1497
|
boundary,
|
|
1203
1498
|
obstacles: topObstacles,
|
|
1204
1499
|
traceWidth,
|
|
1205
1500
|
clearance,
|
|
1501
|
+
reportProgress,
|
|
1206
1502
|
})
|
|
1207
1503
|
if (FANOUT_FLOW_DEBUG_ENABLED) {
|
|
1208
1504
|
console.error("single-layer adaptive-exit grid ready", {
|
|
1209
1505
|
nodeCount: grid.nodeCount,
|
|
1210
1506
|
})
|
|
1211
1507
|
}
|
|
1212
|
-
const adaptivePlans =
|
|
1508
|
+
const adaptivePlans = yield* routeWithAdaptiveExitsSteps({
|
|
1213
1509
|
items,
|
|
1214
1510
|
grid,
|
|
1215
1511
|
obstacles: topObstacles,
|
|
1216
1512
|
traceWidth,
|
|
1217
1513
|
clearance,
|
|
1218
1514
|
availableBoundaryRegions,
|
|
1515
|
+
reportProgress,
|
|
1219
1516
|
})
|
|
1220
1517
|
if (adaptivePlans) return adaptivePlans
|
|
1221
1518
|
return null
|
|
1222
1519
|
}
|
|
1520
|
+
|
|
1521
|
+
export function routeSingleLayerWithAdaptiveExits(
|
|
1522
|
+
params: FlowRoutingParams,
|
|
1523
|
+
): FanoutRoutePlan[] | null {
|
|
1524
|
+
const steps = routeSingleLayerWithAdaptiveExitsSteps(params)
|
|
1525
|
+
while (true) {
|
|
1526
|
+
const result = steps.next()
|
|
1527
|
+
if (result.done) return result.value
|
|
1528
|
+
}
|
|
1529
|
+
}
|