@tscircuit/fanout-solver 0.0.35 → 0.0.37

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.
@@ -4,6 +4,8 @@ import type {
4
4
  SimpleRouteConnection,
5
5
  SimpleRouteJson,
6
6
  } from "@tscircuit/capacity-autorouter"
7
+ import { borderTargetIncludesEdge, getCornerBandSide } from "./boundary-exit"
8
+ import { getFanoutExitPositionConfig } from "./fanout-exit-position"
7
9
  import { distance, pointIsInsideObstacle } from "./geometry"
8
10
  import type {
9
11
  Bounds,
@@ -12,6 +14,9 @@ import type {
12
14
  FanoutBusSpec,
13
15
  FanoutBusTermination,
14
16
  FanoutDirection,
17
+ FanoutEdge,
18
+ FanoutExitPosition,
19
+ FanoutExitPositionConfig,
15
20
  FanoutSolverOptions,
16
21
  PreparedBus,
17
22
  PreparedConnection,
@@ -20,6 +25,8 @@ import type {
20
25
  export interface AvailableBoundaryRegion {
21
26
  direction: FanoutDirection
22
27
  preferredExit: FanoutBorderTarget
28
+ /** Physical shared-boundary edge selected by the first region token. */
29
+ exitEdge: FanoutEdge
23
30
  }
24
31
 
25
32
  const FANOUT_BORDER_TARGETS = new Set<FanoutBorderTarget>([
@@ -33,72 +40,90 @@ const FANOUT_BORDER_TARGETS = new Set<FanoutBorderTarget>([
33
40
  "bottom-right",
34
41
  ])
35
42
 
43
+ const FANOUT_EDGES = new Set<FanoutEdge>(["left", "right", "top", "bottom"])
44
+
36
45
  const AVAILABLE_BOUNDARY_REGIONS: Readonly<
37
46
  Record<FanoutAvailableCornerAndSideInput, AvailableBoundaryRegion>
38
47
  > = {
39
48
  top_left: {
40
49
  direction: "up",
41
50
  preferredExit: "top-left",
51
+ exitEdge: "top",
42
52
  },
43
53
  top_middle: {
44
54
  direction: "up",
45
55
  preferredExit: "top",
56
+ exitEdge: "top",
46
57
  },
47
58
  top_right: {
48
59
  direction: "up",
49
60
  preferredExit: "top-right",
61
+ exitEdge: "top",
50
62
  },
51
63
  right_top: {
52
64
  direction: "right",
53
65
  preferredExit: "top-right",
66
+ exitEdge: "right",
54
67
  },
55
68
  right_middle: {
56
69
  direction: "right",
57
70
  preferredExit: "right",
71
+ exitEdge: "right",
58
72
  },
59
73
  right_bottom: {
60
74
  direction: "right",
61
75
  preferredExit: "bottom-right",
76
+ exitEdge: "right",
62
77
  },
63
78
  bottom_right: {
64
79
  direction: "down",
65
80
  preferredExit: "bottom-right",
81
+ exitEdge: "bottom",
66
82
  },
67
83
  bottom_middle: {
68
84
  direction: "down",
69
85
  preferredExit: "bottom",
86
+ exitEdge: "bottom",
70
87
  },
71
88
  bottom_left: {
72
89
  direction: "down",
73
90
  preferredExit: "bottom-left",
91
+ exitEdge: "bottom",
74
92
  },
75
93
  left_bottom: {
76
94
  direction: "left",
77
95
  preferredExit: "bottom-left",
96
+ exitEdge: "left",
78
97
  },
79
98
  left_middle: {
80
99
  direction: "left",
81
100
  preferredExit: "left",
101
+ exitEdge: "left",
82
102
  },
83
103
  left_top: {
84
104
  direction: "left",
85
105
  preferredExit: "top-left",
106
+ exitEdge: "left",
86
107
  },
87
108
  top: {
88
109
  direction: "up",
89
110
  preferredExit: "top",
111
+ exitEdge: "top",
90
112
  },
91
113
  right: {
92
114
  direction: "right",
93
115
  preferredExit: "right",
116
+ exitEdge: "right",
94
117
  },
95
118
  bottom: {
96
119
  direction: "down",
97
120
  preferredExit: "bottom",
121
+ exitEdge: "bottom",
98
122
  },
99
123
  left: {
100
124
  direction: "left",
101
125
  preferredExit: "left",
126
+ exitEdge: "left",
102
127
  },
103
128
  }
104
129
 
@@ -407,6 +432,134 @@ function resolvePreferredExit(
407
432
  return value
408
433
  }
409
434
 
435
+ function resolveExitEdge(
436
+ busId: string,
437
+ value: FanoutEdge | undefined,
438
+ ): FanoutEdge | undefined {
439
+ if (value === undefined) return undefined
440
+ if (!FANOUT_EDGES.has(value)) {
441
+ throw new Error(
442
+ `FanoutSolver: bus "${busId}" has invalid exitEdge "${value}"`,
443
+ )
444
+ }
445
+ return value
446
+ }
447
+
448
+ function resolveExitPosition(
449
+ busId: string,
450
+ value: FanoutExitPosition | undefined,
451
+ ): Readonly<FanoutExitPositionConfig> | undefined {
452
+ if (value === undefined) return undefined
453
+ try {
454
+ return getFanoutExitPositionConfig(value)
455
+ } catch {
456
+ throw new Error(
457
+ `FanoutSolver: bus "${busId}" has invalid exitPosition "${value}"`,
458
+ )
459
+ }
460
+ }
461
+
462
+ function assertExitPositionFieldMatches<T extends string>(params: {
463
+ busId: string
464
+ exitPosition: FanoutExitPosition
465
+ fieldName: "direction" | "preferredExit" | "exitEdge"
466
+ expected: T | undefined
467
+ actual: T | undefined
468
+ sourceName: string
469
+ }): void {
470
+ const { busId, exitPosition, fieldName, expected, actual, sourceName } =
471
+ params
472
+ if (actual === undefined || actual === expected) return
473
+ throw new Error(
474
+ `FanoutSolver: bus "${busId}" exitPosition "${exitPosition}" conflicts with ${sourceName} ${fieldName} "${actual}"`,
475
+ )
476
+ }
477
+
478
+ function resolveBusExitFields(params: {
479
+ busId: string
480
+ requestedBus: FanoutBusSpec
481
+ options: FanoutSolverOptions
482
+ }): Pick<
483
+ FanoutBusSpec,
484
+ "exitPosition" | "direction" | "preferredExit" | "exitEdge"
485
+ > {
486
+ const { busId, requestedBus, options } = params
487
+ const exitPosition = requestedBus.exitPosition
488
+ const exitPositionConfig = resolveExitPosition(busId, exitPosition)
489
+ const busPreferredExit = resolvePreferredExit(
490
+ busId,
491
+ requestedBus.preferredExit,
492
+ )
493
+ const optionPreferredExit = resolvePreferredExit(
494
+ busId,
495
+ options.busExitPreferences?.[busId],
496
+ )
497
+ const busExitEdge = resolveExitEdge(busId, requestedBus.exitEdge)
498
+
499
+ if (exitPositionConfig && exitPosition) {
500
+ for (const [actual, sourceName] of [
501
+ [requestedBus.direction, "bus"],
502
+ [options.busDirections?.[busId], "busDirections"],
503
+ ] as const) {
504
+ assertExitPositionFieldMatches({
505
+ busId,
506
+ exitPosition,
507
+ fieldName: "direction",
508
+ expected: exitPositionConfig.direction,
509
+ actual,
510
+ sourceName,
511
+ })
512
+ }
513
+ for (const [actual, sourceName] of [
514
+ [busPreferredExit, "bus"],
515
+ [optionPreferredExit, "busExitPreferences"],
516
+ ] as const) {
517
+ assertExitPositionFieldMatches({
518
+ busId,
519
+ exitPosition,
520
+ fieldName: "preferredExit",
521
+ expected: exitPositionConfig.preferredExit,
522
+ actual,
523
+ sourceName,
524
+ })
525
+ }
526
+ assertExitPositionFieldMatches({
527
+ busId,
528
+ exitPosition,
529
+ fieldName: "exitEdge",
530
+ expected: exitPositionConfig.exitEdge,
531
+ actual: busExitEdge,
532
+ sourceName: "bus",
533
+ })
534
+ return {
535
+ exitPosition,
536
+ ...(exitPositionConfig.direction
537
+ ? { direction: exitPositionConfig.direction }
538
+ : {}),
539
+ ...(exitPositionConfig.preferredExit
540
+ ? { preferredExit: exitPositionConfig.preferredExit }
541
+ : {}),
542
+ ...(exitPositionConfig.exitEdge
543
+ ? { exitEdge: exitPositionConfig.exitEdge }
544
+ : {}),
545
+ }
546
+ }
547
+
548
+ const direction =
549
+ options.busDirections?.[busId] ??
550
+ requestedBus.direction ??
551
+ options.defaultDirection
552
+ const preferredExit = resolvePreferredExit(
553
+ busId,
554
+ optionPreferredExit ?? busPreferredExit ?? options.defaultPreferredExit,
555
+ )
556
+ return {
557
+ ...(direction ? { direction } : {}),
558
+ ...(preferredExit ? { preferredExit } : {}),
559
+ ...(busExitEdge ? { exitEdge: busExitEdge } : {}),
560
+ }
561
+ }
562
+
410
563
  function resolveAllowedLayers(
411
564
  busId: string,
412
565
  allowedLayers: readonly string[] | undefined,
@@ -446,7 +599,7 @@ export function resolveAvailableBoundaryRegions(
446
599
  `FanoutSolver: invalid availableCornersAndSides value "${input}"`,
447
600
  )
448
601
  }
449
- const key = `${region.direction}:${region.preferredExit}`
602
+ const key = `${region.exitEdge}:${region.direction}:${region.preferredExit}`
450
603
  if (seen.has(key)) continue
451
604
  seen.add(key)
452
605
  regions.push(region)
@@ -505,17 +658,19 @@ function resolveBusSpecs(
505
658
  requestedBus.busId,
506
659
  (requestedBus as FanoutBusSpec).termination,
507
660
  )
508
- const preferredExit = resolvePreferredExit(
509
- requestedBus.busId,
510
- options.busExitPreferences?.[requestedBus.busId] ??
511
- (requestedBus as FanoutBusSpec).preferredExit ??
512
- options.defaultPreferredExit,
513
- )
661
+ const resolvedExitFields = resolveBusExitFields({
662
+ busId: requestedBus.busId,
663
+ requestedBus: requestedBus as FanoutBusSpec,
664
+ options,
665
+ })
514
666
  const allowedLayers = resolveAllowedLayers(
515
667
  requestedBus.busId,
516
668
  (requestedBus as FanoutBusSpec).allowedLayers,
517
669
  )
518
- if (termination.type === "plane" && preferredExit !== undefined) {
670
+ if (
671
+ termination.type === "plane" &&
672
+ resolvedExitFields.preferredExit !== undefined
673
+ ) {
519
674
  throw new Error(
520
675
  `FanoutSolver: plane-terminated bus "${requestedBus.busId}" cannot also specify preferredExit`,
521
676
  )
@@ -525,11 +680,7 @@ function resolveBusSpecs(
525
680
  sourceComponentId:
526
681
  (requestedBus as FanoutBusSpec).sourceComponentId ??
527
682
  options.sourceComponentId,
528
- direction:
529
- options.busDirections?.[requestedBus.busId] ??
530
- (requestedBus as FanoutBusSpec).direction ??
531
- options.defaultDirection,
532
- preferredExit,
683
+ ...resolvedExitFields,
533
684
  ...(allowedLayers === undefined ? {} : { allowedLayers }),
534
685
  termination,
535
686
  })
@@ -540,26 +691,26 @@ function resolveBusSpecs(
540
691
  const inferredBusId = inferBusId(connection)
541
692
  if (inferredBusId) {
542
693
  const existing = specsById.get(inferredBusId)
543
- specsById.set(inferredBusId, {
544
- busId: inferredBusId,
545
- connectionNames: [
546
- ...(existing?.connectionNames ?? []),
547
- connection.name,
548
- ],
549
- direction:
550
- options.busDirections?.[inferredBusId] ??
551
- existing?.direction ??
552
- options.defaultDirection,
553
- sourceComponentId:
554
- existing?.sourceComponentId ?? options.sourceComponentId,
555
- preferredExit: resolvePreferredExit(
556
- inferredBusId,
557
- options.busExitPreferences?.[inferredBusId] ??
558
- existing?.preferredExit ??
559
- options.defaultPreferredExit,
560
- ),
561
- termination: existing?.termination ?? { type: "boundary" },
562
- })
694
+ if (existing) {
695
+ specsById.set(inferredBusId, {
696
+ ...existing,
697
+ connectionNames: [...existing.connectionNames, connection.name],
698
+ })
699
+ } else {
700
+ specsById.set(inferredBusId, {
701
+ busId: inferredBusId,
702
+ connectionNames: [connection.name],
703
+ direction:
704
+ options.busDirections?.[inferredBusId] ?? options.defaultDirection,
705
+ sourceComponentId: options.sourceComponentId,
706
+ preferredExit: resolvePreferredExit(
707
+ inferredBusId,
708
+ options.busExitPreferences?.[inferredBusId] ??
709
+ options.defaultPreferredExit,
710
+ ),
711
+ termination: { type: "boundary" },
712
+ })
713
+ }
563
714
  } else {
564
715
  const singletonBusId = `connection:${connection.name}`
565
716
  specsById.set(singletonBusId, {
@@ -668,7 +819,7 @@ function prepareConnection(params: {
668
819
  sourceGrid: ComponentGrid
669
820
  componentGrids: ComponentGrid[]
670
821
  termination: FanoutBusTermination
671
- exitTargetPoint?: { x: number; y: number }
822
+ exitTargetPoint?: { x: number; y: number; layer?: string }
672
823
  }): PreparedConnection {
673
824
  const {
674
825
  connection,
@@ -712,7 +863,11 @@ function prepareConnection(params: {
712
863
  sourceLayer,
713
864
  sourceObstacle: sourceMatch.obstacle,
714
865
  targetPoint,
715
- exitTargetPoint: exitTargetPoint ?? targetPoint,
866
+ exitTargetPoint: exitTargetPoint ?? {
867
+ x: targetPoint.x,
868
+ y: targetPoint.y,
869
+ },
870
+ hasExplicitLayeredExitTarget: exitTargetPoint?.layer !== undefined,
716
871
  }
717
872
  }
718
873
  throw new Error(
@@ -892,6 +1047,27 @@ function resolveAvailableBusExit(params: {
892
1047
  )[0]!
893
1048
  }
894
1049
 
1050
+ function validateExplicitExitAvailability(params: {
1051
+ busId: string
1052
+ exitEdge: FanoutEdge
1053
+ preferredExit: FanoutBorderTarget
1054
+ availableRegions: readonly AvailableBoundaryRegion[]
1055
+ }): void {
1056
+ const { busId, exitEdge, preferredExit, availableRegions } = params
1057
+ const requestedBandSide = getCornerBandSide(exitEdge, preferredExit)
1058
+ const hasCompatibleRegion = availableRegions.some(
1059
+ (region) =>
1060
+ region.exitEdge === exitEdge &&
1061
+ getCornerBandSide(region.exitEdge, region.preferredExit) ===
1062
+ requestedBandSide,
1063
+ )
1064
+ if (!hasCompatibleRegion) {
1065
+ throw new Error(
1066
+ `FanoutSolver: bus "${busId}" cannot use its requested exit with availableCornersAndSides`,
1067
+ )
1068
+ }
1069
+ }
1070
+
895
1071
  function resolveBusDirection(params: {
896
1072
  busId: string
897
1073
  explicitDirection?: FanoutDirection
@@ -1031,6 +1207,20 @@ export function prepareFanoutBuses(
1031
1207
  sourceGrid,
1032
1208
  preparedConnections,
1033
1209
  } of resolvedBusInputs) {
1210
+ if (busSpec.exitEdge && !busSpec.preferredExit) {
1211
+ throw new Error(
1212
+ `FanoutSolver: bus "${busSpec.busId}" exitEdge requires preferredExit`,
1213
+ )
1214
+ }
1215
+ if (
1216
+ busSpec.exitEdge &&
1217
+ busSpec.preferredExit &&
1218
+ !borderTargetIncludesEdge(busSpec.preferredExit, busSpec.exitEdge)
1219
+ ) {
1220
+ throw new Error(
1221
+ `FanoutSolver: bus "${busSpec.busId}" exitEdge "${busSpec.exitEdge}" is incompatible with preferredExit "${busSpec.preferredExit}"`,
1222
+ )
1223
+ }
1034
1224
  const resolvedExit = resolveBusDirection({
1035
1225
  busId: busSpec.busId,
1036
1226
  explicitDirection:
@@ -1039,12 +1229,29 @@ export function prepareFanoutBuses(
1039
1229
  connections: preparedConnections,
1040
1230
  sharedBoundary,
1041
1231
  availableRegions:
1042
- busSpec.termination?.type === "plane" ? undefined : availableRegions,
1232
+ busSpec.termination?.type === "plane" || busSpec.exitEdge
1233
+ ? undefined
1234
+ : availableRegions,
1043
1235
  })
1236
+ if (
1237
+ busSpec.termination?.type !== "plane" &&
1238
+ busSpec.exitEdge &&
1239
+ resolvedExit.preferredExit &&
1240
+ availableRegions
1241
+ ) {
1242
+ validateExplicitExitAvailability({
1243
+ busId: busSpec.busId,
1244
+ exitEdge: busSpec.exitEdge,
1245
+ preferredExit: resolvedExit.preferredExit,
1246
+ availableRegions,
1247
+ })
1248
+ }
1044
1249
  buses.push({
1045
1250
  busId: busSpec.busId,
1046
1251
  direction: resolvedExit.direction,
1047
1252
  preferredExit: resolvedExit.preferredExit,
1253
+ ...(busSpec.exitEdge ? { exitEdge: busSpec.exitEdge } : {}),
1254
+ cornerBandConnectionCount: 0,
1048
1255
  allowedLayers: busSpec.allowedLayers,
1049
1256
  termination: busSpec.termination ?? { type: "boundary" },
1050
1257
  connections: preparedConnections,
@@ -1059,5 +1266,23 @@ export function prepareFanoutBuses(
1059
1266
  })
1060
1267
  }
1061
1268
 
1269
+ const cornerBandConnectionCounts = new Map<string, number>()
1270
+ for (const bus of buses) {
1271
+ const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
1272
+ if (!bus.exitEdge || !side) continue
1273
+ const key = `${bus.exitEdge}:${side}`
1274
+ cornerBandConnectionCounts.set(
1275
+ key,
1276
+ (cornerBandConnectionCounts.get(key) ?? 0) + bus.connections.length,
1277
+ )
1278
+ }
1279
+ for (const bus of buses) {
1280
+ const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
1281
+ if (!bus.exitEdge || !side) continue
1282
+ bus.cornerBandConnectionCount =
1283
+ cornerBandConnectionCounts.get(`${bus.exitEdge}:${side}`) ??
1284
+ bus.connections.length
1285
+ }
1286
+
1062
1287
  return buses
1063
1288
  }