@tscircuit/fanout-solver 0.0.24 → 0.0.26

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.
@@ -1,4 +1,5 @@
1
1
  import type { Obstacle, SimpleRouteJson } from "@tscircuit/capacity-autorouter"
2
+ import { createFanoutOutputIds } from "./fanout-output-ids"
2
3
  import type { FanoutRoutePlan, SimpleRouteJsonWithFanoutPlanes } from "./types"
3
4
 
4
5
  function createViaObstacle(
@@ -17,8 +18,11 @@ function createViaObstacle(
17
18
  }
18
19
  return layerIndex
19
20
  })
21
+ const outputIds = createFanoutOutputIds(plan)
20
22
  return {
21
- obstacleId: `${endpoint ? "fanout-plane-endpoint-via" : "fanout-via"}:${plan.connectionName}`,
23
+ obstacleId: endpoint
24
+ ? outputIds.planeEndpointViaObstacleId
25
+ : outputIds.viaObstacleId,
22
26
  type: "rect",
23
27
  center: via.center,
24
28
  width: via.diameter,
@@ -50,14 +54,15 @@ export function buildOutputSimpleRouteJson(params: {
50
54
  `FanoutSolver: output connection index ${plan.connectionIndex} is missing`,
51
55
  )
52
56
  }
57
+ const outputIds = createFanoutOutputIds(plan)
53
58
  connection.pointsToConnect[plan.sourcePointIndex] = {
54
59
  x: plan.exitPoint.x,
55
60
  y: plan.exitPoint.y,
56
61
  layer: plan.targetLayer,
57
62
  pointId:
58
63
  plan.termination.type === "plane"
59
- ? `fanout-plane:${plan.connectionName}`
60
- : `fanout-exit:${plan.connectionName}`,
64
+ ? outputIds.planeExitPointId
65
+ : outputIds.boundaryExitPointId,
61
66
  }
62
67
  if (plan.termination.type === "plane") {
63
68
  planeTerminatedConnectionNames.add(plan.connectionName)
@@ -5,6 +5,7 @@ import {
5
5
  type SimpleRouteJson,
6
6
  type SimplifiedPcbTrace,
7
7
  } from "@tscircuit/capacity-autorouter"
8
+ import { createFanoutCompletionTraceId } from "./fanout-output-ids"
8
9
  import {
9
10
  distance,
10
11
  distancePointToSegment,
@@ -226,7 +227,11 @@ function createBranchTrace(params: {
226
227
 
227
228
  return {
228
229
  type: "pcb_trace",
229
- pcb_trace_id: `fanout-completion:${plan.connectionName}:${candidateIndex}`,
230
+ pcb_trace_id: createFanoutCompletionTraceId({
231
+ connectionName: plan.connectionName,
232
+ sourcePointIndex: plan.sourcePointIndex,
233
+ candidateIndex,
234
+ }),
230
235
  connection_name: plan.connectionName,
231
236
  route,
232
237
  }
@@ -0,0 +1,33 @@
1
+ export interface FanoutOutputIds {
2
+ boundaryExitPointId: string
3
+ planeExitPointId: string
4
+ traceId: string
5
+ viaObstacleId: string
6
+ planeEndpointPointId: string
7
+ planeEndpointTraceId: string
8
+ planeEndpointViaObstacleId: string
9
+ }
10
+
11
+ export function createFanoutOutputIds(input: {
12
+ connectionName: string
13
+ sourcePointIndex: number
14
+ }): FanoutOutputIds {
15
+ const endpointKey = `${input.connectionName}:source-${input.sourcePointIndex}`
16
+ return {
17
+ boundaryExitPointId: `fanout-exit:${endpointKey}`,
18
+ planeExitPointId: `fanout-plane:${endpointKey}`,
19
+ traceId: `fanout:${endpointKey}`,
20
+ viaObstacleId: `fanout-via:${endpointKey}`,
21
+ planeEndpointPointId: `fanout-plane-endpoint-point:${endpointKey}`,
22
+ planeEndpointTraceId: `fanout-plane-endpoint:${endpointKey}`,
23
+ planeEndpointViaObstacleId: `fanout-plane-endpoint-via:${endpointKey}`,
24
+ }
25
+ }
26
+
27
+ export function createFanoutCompletionTraceId(input: {
28
+ connectionName: string
29
+ sourcePointIndex: number
30
+ candidateIndex: number
31
+ }): string {
32
+ return `fanout-completion:${input.connectionName}:source-${input.sourcePointIndex}:${input.candidateIndex}`
33
+ }
@@ -208,15 +208,14 @@ function getBusDistanceToBoundary(bus: PreparedBus): number {
208
208
  }
209
209
  }
210
210
 
211
- function busUsesOriginalEndpointTracks(bus: PreparedBus): boolean {
211
+ function busUsesDestinationGuidedTracks(bus: PreparedBus): boolean {
212
212
  const isHorizontal = bus.direction === "left" || bus.direction === "right"
213
213
  return bus.connections.some((connection) => {
214
+ const exitTargetPoint = connection.exitTargetPoint ?? connection.targetPoint
214
215
  const sourceTrack = isHorizontal
215
216
  ? connection.sourcePoint.y
216
217
  : connection.sourcePoint.x
217
- const targetTrack = isHorizontal
218
- ? connection.targetPoint.y
219
- : connection.targetPoint.x
218
+ const targetTrack = isHorizontal ? exitTargetPoint.y : exitTargetPoint.x
220
219
  return Math.abs(sourceTrack - targetTrack) > 1e-6
221
220
  })
222
221
  }
@@ -293,7 +292,7 @@ function createPreferredLayerAssignment(params: {
293
292
  )
294
293
  if (
295
294
  routableEscapeLayers.includes(sourceLayer) &&
296
- (busUsesOriginalEndpointTracks(bus) || busIsOnOutwardComponentEdge(bus))
295
+ (busUsesDestinationGuidedTracks(bus) || busIsOnOutwardComponentEdge(bus))
297
296
  ) {
298
297
  assignment[bus.busId] = sourceLayer
299
298
  } else if (viaLayers.length > 0) {
@@ -795,7 +794,7 @@ export class FanoutSolver extends BaseSolver {
795
794
  (count, bus) => {
796
795
  if (
797
796
  bus.termination.type !== "boundary" ||
798
- !busUsesOriginalEndpointTracks(bus)
797
+ !busUsesDestinationGuidedTracks(bus)
799
798
  ) {
800
799
  return count
801
800
  }
@@ -840,7 +839,7 @@ export class FanoutSolver extends BaseSolver {
840
839
  )
841
840
  }
842
841
  const sourceLayer = bus.connections[0]?.sourceLayer
843
- const preferSourceLayer = busUsesOriginalEndpointTracks(bus)
842
+ const preferSourceLayer = busUsesDestinationGuidedTracks(bus)
844
843
  const orderedLayers = candidateLayers.toSorted(
845
844
  (first, second) =>
846
845
  (layerLoads.get(first) ?? 0) - (layerLoads.get(second) ?? 0) ||
package/lib/index.ts CHANGED
@@ -35,6 +35,7 @@ export type {
35
35
  FanoutSolverOutput,
36
36
  FanoutValidationIssue,
37
37
  FanoutValidationReport,
38
+ Point2D,
38
39
  PreparedBus,
39
40
  SimpleRouteJsonWithFanoutPlanes,
40
41
  } from "./types"
@@ -643,6 +643,7 @@ function prepareConnection(params: {
643
643
  sourceGrid: ComponentGrid
644
644
  componentGrids: ComponentGrid[]
645
645
  termination: FanoutBusTermination
646
+ exitTargetPoint?: { x: number; y: number }
646
647
  }): PreparedConnection {
647
648
  const {
648
649
  connection,
@@ -650,6 +651,7 @@ function prepareConnection(params: {
650
651
  sourceGrid,
651
652
  componentGrids,
652
653
  termination,
654
+ exitTargetPoint,
653
655
  } = params
654
656
  for (
655
657
  let sourcePointIndex = 0;
@@ -671,6 +673,12 @@ function prepareConnection(params: {
671
673
  `FanoutSolver: connection "${connection.name}" has no source layer shared with its BGA pad`,
672
674
  )
673
675
  }
676
+ const targetPoint = chooseTargetPoint(
677
+ sourcePoint,
678
+ connection,
679
+ sourcePointIndex,
680
+ termination,
681
+ )
674
682
  return {
675
683
  connection,
676
684
  connectionIndex,
@@ -678,12 +686,8 @@ function prepareConnection(params: {
678
686
  sourcePointIndex,
679
687
  sourceLayer,
680
688
  sourceObstacle: sourceMatch.obstacle,
681
- targetPoint: chooseTargetPoint(
682
- sourcePoint,
683
- connection,
684
- sourcePointIndex,
685
- termination,
686
- ),
689
+ targetPoint,
690
+ exitTargetPoint: exitTargetPoint ?? targetPoint,
687
691
  }
688
692
  }
689
693
  throw new Error(
@@ -698,8 +702,10 @@ function inferDirection(
698
702
  let dx = 0
699
703
  let dy = 0
700
704
  for (const preparedConnection of connections) {
701
- dx += preparedConnection.targetPoint.x - preparedConnection.sourcePoint.x
702
- dy += preparedConnection.targetPoint.y - preparedConnection.sourcePoint.y
705
+ const exitTargetPoint =
706
+ preparedConnection.exitTargetPoint ?? preparedConnection.targetPoint
707
+ dx += exitTargetPoint.x - preparedConnection.sourcePoint.x
708
+ dy += exitTargetPoint.y - preparedConnection.sourcePoint.y
703
709
  }
704
710
  if (Math.abs(dx) < 1e-9 && Math.abs(dy) < 1e-9) {
705
711
  throw new Error(
@@ -941,6 +947,20 @@ export function prepareFanoutBuses(
941
947
  srj.connections.map((connection, index) => [connection.name, index]),
942
948
  )
943
949
  const resolvedBusInputs = resolveBusSpecs(srj, options).map((busSpec) => {
950
+ for (const [connectionName, point] of Object.entries(
951
+ busSpec.connectionExitTargets ?? {},
952
+ )) {
953
+ if (!busSpec.connectionNames.includes(connectionName)) {
954
+ throw new Error(
955
+ `FanoutSolver: connectionExitTargets contains connection "${connectionName}" outside bus "${busSpec.busId}"`,
956
+ )
957
+ }
958
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) {
959
+ throw new Error(
960
+ `FanoutSolver: connectionExitTargets for connection "${connectionName}" must contain finite x and y coordinates`,
961
+ )
962
+ }
963
+ }
944
964
  const connections = busSpec.connectionNames.map((connectionName) => {
945
965
  const connectionIndex = connectionIndexByName.get(connectionName)
946
966
  if (connectionIndex === undefined) {
@@ -962,6 +982,7 @@ export function prepareFanoutBuses(
962
982
  sourceGrid,
963
983
  componentGrids,
964
984
  termination: busSpec.termination ?? { type: "boundary" },
985
+ exitTargetPoint: busSpec.connectionExitTargets?.[connection.name],
965
986
  }),
966
987
  )
967
988
  return { busSpec, sourceGrid, preparedConnections }
package/lib/route-bus.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  distanceSegmentToObstacle,
11
11
  segmentsAreClear,
12
12
  } from "./geometry"
13
+ import { createFanoutOutputIds } from "./fanout-output-ids"
13
14
  import { getLayerSpan } from "./layer-names"
14
15
  import {
15
16
  connectionsShareElectricalNet,
@@ -294,7 +295,7 @@ function getPreferredTrack(params: {
294
295
  connection: PreparedConnection
295
296
  }): number {
296
297
  return getPerpendicularAxis(
297
- params.connection.targetPoint,
298
+ params.connection.exitTargetPoint ?? params.connection.targetPoint,
298
299
  params.bus.direction,
299
300
  )
300
301
  }
@@ -636,6 +637,10 @@ function buildPlan(params: {
636
637
  })
637
638
  }
638
639
 
640
+ const outputIds = createFanoutOutputIds({
641
+ connectionName: preparedConnection.connection.name,
642
+ sourcePointIndex: preparedConnection.sourcePointIndex,
643
+ })
639
644
  return {
640
645
  busId: bus.busId,
641
646
  connectionName: preparedConnection.connection.name,
@@ -651,16 +656,16 @@ function buildPlan(params: {
651
656
  exitPoint,
652
657
  trace: {
653
658
  type: "pcb_trace",
654
- pcb_trace_id: `fanout:${preparedConnection.connection.name}`,
659
+ pcb_trace_id: outputIds.traceId,
655
660
  connection_name: preparedConnection.connection.name,
656
661
  connectsTo: [
657
- preparedConnection.connection.name,
658
662
  ...(preparedConnection.sourcePoint.pointId
659
663
  ? [preparedConnection.sourcePoint.pointId]
660
664
  : []),
661
665
  ...(preparedConnection.sourcePoint.pcb_port_id
662
666
  ? [preparedConnection.sourcePoint.pcb_port_id]
663
667
  : []),
668
+ outputIds.boundaryExitPointId,
664
669
  ],
665
670
  route,
666
671
  },
@@ -804,18 +809,22 @@ function addPlaneEndpointTerminal(params: {
804
809
  toLayer: targetEndpointLayer,
805
810
  spanLayers,
806
811
  }
812
+ const outputIds = createFanoutOutputIds({
813
+ connectionName: preparedConnection.connection.name,
814
+ sourcePointIndex: preparedConnection.sourcePointIndex,
815
+ })
807
816
  const planeEndpointTrace: SimplifiedPcbTrace = {
808
817
  type: "pcb_trace",
809
- pcb_trace_id: `fanout-plane-endpoint:${preparedConnection.connection.name}`,
818
+ pcb_trace_id: outputIds.planeEndpointTraceId,
810
819
  connection_name: preparedConnection.connection.name,
811
820
  connectsTo: [
812
- preparedConnection.connection.name,
813
821
  ...(preparedConnection.targetPoint.pointId
814
822
  ? [preparedConnection.targetPoint.pointId]
815
823
  : []),
816
824
  ...(preparedConnection.targetPoint.pcb_port_id
817
825
  ? [preparedConnection.targetPoint.pcb_port_id]
818
826
  : []),
827
+ outputIds.planeEndpointPointId,
819
828
  ],
820
829
  route: [
821
830
  {
@@ -9,6 +9,7 @@ import {
9
9
  distanceSegmentToObstacle,
10
10
  distanceSegmentToSegment,
11
11
  } from "./geometry"
12
+ import { createFanoutOutputIds } from "./fanout-output-ids"
12
13
  import { type AvailableBoundaryRegion, getRegionAnchor } from "./prepare-buses"
13
14
  import type {
14
15
  FanoutDirection,
@@ -815,6 +816,10 @@ function buildPlan(route: FlowRoute, traceWidth: number): FanoutRoutePlan {
815
816
  : {}),
816
817
  }),
817
818
  )
819
+ const outputIds = createFanoutOutputIds({
820
+ connectionName: item.connection.connection.name,
821
+ sourcePointIndex: item.connection.sourcePointIndex,
822
+ })
818
823
  return {
819
824
  busId: item.bus.busId,
820
825
  connectionName: item.connection.connection.name,
@@ -830,17 +835,16 @@ function buildPlan(route: FlowRoute, traceWidth: number): FanoutRoutePlan {
830
835
  exitPoint: points.at(-1)!,
831
836
  trace: {
832
837
  type: "pcb_trace",
833
- pcb_trace_id: `fanout:${item.connection.connection.name}`,
838
+ pcb_trace_id: outputIds.traceId,
834
839
  connection_name: item.connection.connection.name,
835
840
  connectsTo: [
836
- item.connection.connection.name,
837
- item.netKey,
838
841
  ...(item.connection.sourcePoint.pointId
839
842
  ? [item.connection.sourcePoint.pointId]
840
843
  : []),
841
844
  ...(item.connection.sourcePoint.pcb_port_id
842
845
  ? [item.connection.sourcePoint.pcb_port_id]
843
846
  : []),
847
+ outputIds.boundaryExitPointId,
844
848
  ],
845
849
  route: traceRoute,
846
850
  },
@@ -8,6 +8,7 @@ import {
8
8
  distanceSegmentToObstacle,
9
9
  distanceSegmentToSegment,
10
10
  } from "./geometry"
11
+ import { createFanoutOutputIds } from "./fanout-output-ids"
11
12
  import type {
12
13
  FanoutBorderDistribution,
13
14
  FanoutCorner,
@@ -197,7 +198,10 @@ function getCornerChannelPrefixes(params: {
197
198
  maximum: bus.componentBounds.maxX,
198
199
  }
199
200
  const directionalDistance = sign * (componentExitAxis - sourceAxis)
200
- const targetTrack = getPerpendicularAxis(connection.targetPoint, direction)
201
+ const targetTrack = getPerpendicularAxis(
202
+ connection.exitTargetPoint ?? connection.targetPoint,
203
+ direction,
204
+ )
201
205
  const lanePitch = traceWidth + clearance
202
206
  const exitAxis = getExitAxis(bus)
203
207
  const candidates = ([-1, 1] as const)
@@ -280,7 +284,7 @@ function completeCornerChannelRoute(params: {
280
284
  const minimumTrack = boundaryMinimum + traceWidth / 2
281
285
  const maximumTrack = boundaryMaximum - traceWidth / 2
282
286
  const targetTrack = getPerpendicularAxis(
283
- item.connection.targetPoint,
287
+ item.connection.exitTargetPoint ?? item.connection.targetPoint,
284
288
  direction,
285
289
  )
286
290
  const trackStep = lanePitch / 2
@@ -748,6 +752,10 @@ function buildPlan(path: RoutedPath): FanoutRoutePlan {
748
752
  ? { start_pcb_port_id: item.connection.sourcePoint.pcb_port_id }
749
753
  : {}),
750
754
  }))
755
+ const outputIds = createFanoutOutputIds({
756
+ connectionName: item.connection.connection.name,
757
+ sourcePointIndex: item.connection.sourcePointIndex,
758
+ })
751
759
  return {
752
760
  busId: item.bus.busId,
753
761
  connectionName: item.connection.connection.name,
@@ -763,16 +771,16 @@ function buildPlan(path: RoutedPath): FanoutRoutePlan {
763
771
  exitPoint: points.at(-1)!,
764
772
  trace: {
765
773
  type: "pcb_trace",
766
- pcb_trace_id: `fanout:${item.connection.connection.name}`,
774
+ pcb_trace_id: outputIds.traceId,
767
775
  connection_name: item.connection.connection.name,
768
776
  connectsTo: [
769
- item.connection.connection.name,
770
777
  ...(item.connection.sourcePoint.pointId
771
778
  ? [item.connection.sourcePoint.pointId]
772
779
  : []),
773
780
  ...(item.connection.sourcePoint.pcb_port_id
774
781
  ? [item.connection.sourcePoint.pcb_port_id]
775
782
  : []),
783
+ outputIds.boundaryExitPointId,
776
784
  ],
777
785
  route,
778
786
  },
package/lib/types.ts CHANGED
@@ -63,6 +63,15 @@ export interface FanoutBusSpec extends SimpleRouteBus {
63
63
  sourceComponentId?: string
64
64
  direction?: FanoutDirection
65
65
  preferredExit?: FanoutBorderTarget
66
+ /**
67
+ * Preferred downstream routing point for each connection after it leaves the
68
+ * fanout boundary. This is routing guidance only and does not replace the
69
+ * connection's electrical endpoint in SimpleRouteJson.
70
+ *
71
+ * A caller coordinating two fanouts can pass the paired fanout's selected
72
+ * exit here so both sides aim toward the same track.
73
+ */
74
+ connectionExitTargets?: Readonly<Record<string, Point2D>>
66
75
  /**
67
76
  * Defaults to `{ type: "boundary" }`.
68
77
  *
@@ -209,6 +218,8 @@ export interface PreparedConnection {
209
218
  sourceLayer: string
210
219
  sourceObstacle: Obstacle
211
220
  targetPoint: ConnectionPoint
221
+ /** Preferred downstream point used to choose the boundary exit track. */
222
+ exitTargetPoint?: Point2D
212
223
  }
213
224
 
214
225
  export interface PreparedBus {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",
@@ -42,16 +42,22 @@
42
42
  "@biomejs/biome": "2.5.5",
43
43
  "@tsci/tscircuit.dataset-srj19-bga-passive-overlays": "github:tscircuit/dataset-srj19#c1206f3",
44
44
  "@tsci/tscircuit.dataset-srj29-bga-decoupling": "github:tscircuit/dataset-srj29-bga-decoupling#bfdf7bc",
45
+ "@tscircuit/alphabet": "0.0.25",
46
+ "@tscircuit/circuit-json-util": "0.0.97",
45
47
  "@tscircuit/footprinter": "0.0.394",
46
48
  "@types/bun": "1.3.14",
47
49
  "@types/react": "19.2.14",
48
50
  "@types/react-dom": "19.2.3",
49
51
  "bun-match-svg": "^0.0.16",
50
52
  "circuit-json": "0.0.455",
53
+ "circuit-json-to-connectivity-map": "0.0.23",
54
+ "circuit-to-svg": "0.0.364",
55
+ "format-si-unit": "0.0.7",
51
56
  "react": "19.2.8",
52
57
  "react-cosmos": "7.3.0",
53
58
  "react-cosmos-plugin-vite": "7.3.0",
54
59
  "react-dom": "19.2.8",
60
+ "schematic-symbols": "0.0.231",
55
61
  "vite": "8.1.5"
56
62
  },
57
63
  "peerDependencies": {