@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/route-bus.ts CHANGED
@@ -24,7 +24,8 @@ import {
24
24
  obstacleSharesElectricalNet,
25
25
  } from "./net-identity"
26
26
  import {
27
- routeViaMinimalWindingAlternatives,
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 routeBusAlternatives(
2378
+ export function* routeBusAlternativesSteps(
2371
2379
  params: RouteBusParams,
2372
2380
  maxAlternatives = 1,
2373
- ): FanoutRoutePlan[][] {
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 viaMinimalAlternatives = routeViaMinimalWindingAlternatives(
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
- maximumFlow(source: number, sink: number, limit: number): number {
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 createFlowGrid(params: {
443
+ function* createFlowGridSteps(params: {
331
444
  boundary: PreparedBus["sharedBoundary"]
332
445
  obstacles: Obstacle[]
333
446
  traceWidth: number
334
447
  clearance: number
335
- }): FlowGrid {
336
- const { boundary, obstacles, traceWidth, clearance } = params
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 routeDirectionGroup(params: {
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
- }): DirectionGroupResult | null {
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 = flow.maximumFlow(source, sink, terminals.length)
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 routeWithAdaptiveExits(params: {
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
- }): FanoutRoutePlan[] | null {
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: ReturnType<typeof routeDirectionGroup> = null
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 = routeDirectionGroup({
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 routeSingleLayerWithAdaptiveExits(
1445
+ export function* routeSingleLayerWithAdaptiveExitsSteps(
1179
1446
  params: FlowRoutingParams,
1180
- ): FanoutRoutePlan[] | null {
1181
- const { srj, buses, traceWidth, clearance, availableBoundaryRegions } = params
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 grid = createFlowGrid({
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 = routeWithAdaptiveExits({
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
+ }