@tscircuit/fanout-solver 0.0.23 → 0.0.25

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 CHANGED
@@ -61,12 +61,12 @@ and treats each bus-layer decision atomically.
61
61
  - Uses a straight pad-pair escape when the via fits. Otherwise it uses a 45°
62
62
  four-pad interstitial escape and nested side bands that spread deeper
63
63
  two-layer buses around already-routed outer buses.
64
- - `compactBusTracks` bends each bus into a trace/clearance-pitch routing
65
- envelope, so a wide pad row does not consume a disproportionately wide
66
- breakout corridor.
67
- - `preferOriginalEndpointTracks` projects boundary tracks onto the original
68
- downstream pad coordinates and prefers their layer when legal. This lets
69
- ordered edge-pad buses make direct, visibly continuous pad connections.
64
+ - `compactBusTracks` provides a trace/clearance-pitch fallback envelope when
65
+ the original endpoint track cannot route, so a wide pad row does not consume
66
+ a disproportionately wide breakout corridor.
67
+ - Boundary tracks project onto the original downstream pad coordinates and
68
+ prefer their layer when legal. This lets ordered edge-pad buses make direct,
69
+ visibly continuous pad connections.
70
70
  - Chamfers orthogonal routing corners into 45° segments before validating and
71
71
  emitting the fanout.
72
72
  - Verifies oriented-pad, via, trace, and already-routed fanout clearance on
@@ -125,7 +125,6 @@ const fanoutSolver = new FanoutSolver(simpleRouteJson, {
125
125
  availableCornersAndSides: ["top_left", "top", "top_right"],
126
126
  borderDistribution: "even",
127
127
  compactBusTracks: true,
128
- preferOriginalEndpointTracks: true,
129
128
  buses: [
130
129
  {
131
130
  busId: "ground",
@@ -40,7 +40,6 @@ interface ResolvedFanoutConfig {
40
40
  viaHoleDiameter: number
41
41
  clearance: number
42
42
  compactBusTracks: boolean
43
- preferOriginalEndpointTracks: boolean
44
43
  allowSameNetMerges: boolean
45
44
  singleLayerPushAndShove: boolean
46
45
  singleLayerAdaptiveExits: boolean
@@ -132,7 +131,6 @@ function resolveConfig(
132
131
  viaHoleDiameter,
133
132
  clearance,
134
133
  compactBusTracks: options.compactBusTracks ?? false,
135
- preferOriginalEndpointTracks: options.preferOriginalEndpointTracks ?? false,
136
134
  allowSameNetMerges: options.allowSameNetMerges ?? false,
137
135
  singleLayerPushAndShove: options.singleLayerPushAndShove ?? false,
138
136
  singleLayerAdaptiveExits: options.singleLayerAdaptiveExits ?? false,
@@ -210,6 +208,18 @@ function getBusDistanceToBoundary(bus: PreparedBus): number {
210
208
  }
211
209
  }
212
210
 
211
+ function busUsesDestinationGuidedTracks(bus: PreparedBus): boolean {
212
+ const isHorizontal = bus.direction === "left" || bus.direction === "right"
213
+ return bus.connections.some((connection) => {
214
+ const exitTargetPoint = connection.exitTargetPoint ?? connection.targetPoint
215
+ const sourceTrack = isHorizontal
216
+ ? connection.sourcePoint.y
217
+ : connection.sourcePoint.x
218
+ const targetTrack = isHorizontal ? exitTargetPoint.y : exitTargetPoint.x
219
+ return Math.abs(sourceTrack - targetTrack) > 1e-6
220
+ })
221
+ }
222
+
213
223
  function busIsOnOutwardComponentEdge(bus: PreparedBus): boolean {
214
224
  const isHorizontal = bus.direction === "left" || bus.direction === "right"
215
225
  const directionalCoordinates = isHorizontal
@@ -256,14 +266,8 @@ function createPreferredLayerAssignment(params: {
256
266
  buses: PreparedBus[]
257
267
  escapeLayers: string[]
258
268
  escapeLayersByBusId: Readonly<Record<string, readonly string[]>>
259
- preferOriginalEndpointTracks: boolean
260
269
  }): Readonly<Record<string, string>> {
261
- const {
262
- buses,
263
- escapeLayers,
264
- escapeLayersByBusId,
265
- preferOriginalEndpointTracks,
266
- } = params
270
+ const { buses, escapeLayers, escapeLayersByBusId } = params
267
271
  const assignment: Record<string, string> = {}
268
272
  const directionsByComponent = new Map<string, Set<PreparedBus["direction"]>>()
269
273
  let nextViaLayerIndex = 0
@@ -288,7 +292,7 @@ function createPreferredLayerAssignment(params: {
288
292
  )
289
293
  if (
290
294
  routableEscapeLayers.includes(sourceLayer) &&
291
- (preferOriginalEndpointTracks || busIsOnOutwardComponentEdge(bus))
295
+ (busUsesDestinationGuidedTracks(bus) || busIsOnOutwardComponentEdge(bus))
292
296
  ) {
293
297
  assignment[bus.busId] = sourceLayer
294
298
  } else if (viaLayers.length > 0) {
@@ -347,7 +351,6 @@ function getCandidateEscapeLayersForBus(params: {
347
351
  viaHoleDiameter: config.viaHoleDiameter,
348
352
  clearance: config.clearance,
349
353
  compactBusTracks: config.compactBusTracks,
350
- preferOriginalEndpointTracks: config.preferOriginalEndpointTracks,
351
354
  allowSameNetMerges: config.allowSameNetMerges,
352
355
  staticClearanceCache,
353
356
  }) !== null,
@@ -458,7 +461,6 @@ export class FanoutSolver extends BaseSolver {
458
461
  buses: this.preparedBuses,
459
462
  escapeLayers: this.config.escapeLayers,
460
463
  escapeLayersByBusId,
461
- preferOriginalEndpointTracks: this.config.preferOriginalEndpointTracks,
462
464
  }),
463
465
  generatedAssignments,
464
466
  maxAssignments: this.config.maxLayerCombinations,
@@ -605,7 +607,6 @@ export class FanoutSolver extends BaseSolver {
605
607
  viaHoleDiameter: this.config.viaHoleDiameter,
606
608
  clearance: this.config.clearance,
607
609
  compactBusTracks: this.config.compactBusTracks,
608
- preferOriginalEndpointTracks: this.config.preferOriginalEndpointTracks,
609
610
  allowSameNetMerges: this.config.allowSameNetMerges,
610
611
  staticClearanceCache: this.routeStaticClearanceCache,
611
612
  blockingBusCounts: currentBusBlockingCounts,
@@ -791,7 +792,12 @@ export class FanoutSolver extends BaseSolver {
791
792
  const viaCount = getPlanViaCount(state.plans)
792
793
  const offEndpointLayerConnectionCount = this.preparedBuses.reduce(
793
794
  (count, bus) => {
794
- if (bus.termination.type !== "boundary") return count
795
+ if (
796
+ bus.termination.type !== "boundary" ||
797
+ !busUsesDestinationGuidedTracks(bus)
798
+ ) {
799
+ return count
800
+ }
795
801
  const sourceLayer = bus.connections[0]?.sourceLayer
796
802
  return state.assignment[bus.busId] === sourceLayer
797
803
  ? count
@@ -802,9 +808,7 @@ export class FanoutSolver extends BaseSolver {
802
808
  return (
803
809
  routeLength +
804
810
  viaCount * 0.1 +
805
- (this.config.preferOriginalEndpointTracks
806
- ? offEndpointLayerConnectionCount * 10_000
807
- : 0) +
811
+ offEndpointLayerConnectionCount * 10_000 +
808
812
  assignmentLoadPenalty(
809
813
  state.assignment,
810
814
  this.preparedBuses,
@@ -835,10 +839,11 @@ export class FanoutSolver extends BaseSolver {
835
839
  )
836
840
  }
837
841
  const sourceLayer = bus.connections[0]?.sourceLayer
842
+ const preferSourceLayer = busUsesDestinationGuidedTracks(bus)
838
843
  const orderedLayers = candidateLayers.toSorted(
839
844
  (first, second) =>
840
845
  (layerLoads.get(first) ?? 0) - (layerLoads.get(second) ?? 0) ||
841
- (this.config.preferOriginalEndpointTracks
846
+ (preferSourceLayer
842
847
  ? Number(second === sourceLayer) - Number(first === sourceLayer)
843
848
  : Number(first === sourceLayer) -
844
849
  Number(second === sourceLayer)) ||
@@ -858,8 +863,6 @@ export class FanoutSolver extends BaseSolver {
858
863
  viaHoleDiameter: this.config.viaHoleDiameter,
859
864
  clearance: this.config.clearance,
860
865
  compactBusTracks: this.config.compactBusTracks,
861
- preferOriginalEndpointTracks:
862
- this.config.preferOriginalEndpointTracks,
863
866
  allowSameNetMerges: this.config.allowSameNetMerges,
864
867
  staticClearanceCache: this.routeStaticClearanceCache,
865
868
  },
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
@@ -39,7 +39,6 @@ export interface RouteBusParams {
39
39
  viaHoleDiameter: number
40
40
  clearance: number
41
41
  compactBusTracks: boolean
42
- preferOriginalEndpointTracks?: boolean
43
42
  allowSameNetMerges?: boolean
44
43
  staticClearanceCache?: RouteBusStaticClearanceCache
45
44
  blockingBusCounts?: Map<string, number>
@@ -293,10 +292,19 @@ function getTrackCandidates(params: {
293
292
  function getPreferredTrack(params: {
294
293
  bus: PreparedBus
295
294
  connection: PreparedConnection
295
+ }): number {
296
+ return getPerpendicularAxis(
297
+ params.connection.exitTargetPoint ?? params.connection.targetPoint,
298
+ params.bus.direction,
299
+ )
300
+ }
301
+
302
+ function getLegacyPreferredTrack(params: {
303
+ bus: PreparedBus
304
+ connection: PreparedConnection
296
305
  targetUsesVia: boolean
297
306
  interstitialEscape: boolean
298
307
  compactBusTracks: boolean
299
- preferOriginalEndpointTracks: boolean
300
308
  traceWidth: number
301
309
  viaDiameter: number
302
310
  clearance: number
@@ -307,7 +315,6 @@ function getPreferredTrack(params: {
307
315
  targetUsesVia,
308
316
  interstitialEscape,
309
317
  compactBusTracks,
310
- preferOriginalEndpointTracks,
311
318
  traceWidth,
312
319
  viaDiameter,
313
320
  clearance,
@@ -319,9 +326,6 @@ function getPreferredTrack(params: {
319
326
  connection.sourcePoint,
320
327
  bus.direction,
321
328
  )
322
- if (preferOriginalEndpointTracks) {
323
- return getPerpendicularAxis(connection.targetPoint, bus.direction)
324
- }
325
329
  if (compactBusTracks) {
326
330
  const connectionRank = getConnectionRank(bus, connection)
327
331
  const componentCenter =
@@ -332,11 +336,9 @@ function getPreferredTrack(params: {
332
336
  (traceWidth + clearance)
333
337
  )
334
338
  }
335
- if (!targetUsesVia) return sourceTrack
336
- if (!interstitialEscape) return sourceTrack
339
+ if (!targetUsesVia || !interstitialEscape) return sourceTrack
337
340
 
338
341
  const depthInRows = getDepthInRows(bus)
339
-
340
342
  const trackPitch = traceWidth + clearance
341
343
  const halfConnectionCount = Math.ceil(bus.connections.length / 2)
342
344
  const sideBandWidth = (halfConnectionCount - 1) * trackPitch
@@ -1296,7 +1298,6 @@ export function routeBusAlternatives(
1296
1298
  viaHoleDiameter,
1297
1299
  clearance,
1298
1300
  compactBusTracks,
1299
- preferOriginalEndpointTracks = false,
1300
1301
  staticClearanceCache,
1301
1302
  blockingBusCounts,
1302
1303
  allowSameNetMerges = false,
@@ -1359,23 +1360,42 @@ export function routeBusAlternatives(
1359
1360
 
1360
1361
  const preparedConnection = connectionOrder[connectionIndex]!
1361
1362
  const connectionRank = getConnectionRank(bus, preparedConnection)
1362
- const trackCandidates = getTrackCandidates({
1363
- bus,
1364
- connection: preparedConnection,
1365
- preferredTrack: getPreferredTrack({
1363
+ const preferredTracks = [
1364
+ getPreferredTrack({
1365
+ bus,
1366
+ connection: preparedConnection,
1367
+ }),
1368
+ getLegacyPreferredTrack({
1366
1369
  bus,
1367
1370
  connection: preparedConnection,
1368
1371
  targetUsesVia,
1369
1372
  interstitialEscape,
1370
1373
  compactBusTracks,
1371
- preferOriginalEndpointTracks,
1372
1374
  traceWidth,
1373
1375
  viaDiameter,
1374
1376
  clearance,
1375
1377
  }),
1376
- traceWidth,
1377
- clearance,
1378
- })
1378
+ ].filter(
1379
+ (track, index, tracks) =>
1380
+ tracks.findIndex((candidate) => Math.abs(candidate - track) < 1e-9) ===
1381
+ index,
1382
+ )
1383
+ const trackCandidates = preferredTracks
1384
+ .flatMap((preferredTrack) =>
1385
+ getTrackCandidates({
1386
+ bus,
1387
+ connection: preparedConnection,
1388
+ preferredTrack,
1389
+ traceWidth,
1390
+ clearance,
1391
+ }),
1392
+ )
1393
+ .filter(
1394
+ (track, index, tracks) =>
1395
+ tracks.findIndex(
1396
+ (candidate) => Math.abs(candidate.value - track.value) < 1e-9,
1397
+ ) === index,
1398
+ )
1379
1399
  for (
1380
1400
  let trackIndex = 0;
1381
1401
  trackIndex < trackCandidates.length;
@@ -197,7 +197,10 @@ function getCornerChannelPrefixes(params: {
197
197
  maximum: bus.componentBounds.maxX,
198
198
  }
199
199
  const directionalDistance = sign * (componentExitAxis - sourceAxis)
200
- const targetTrack = getPerpendicularAxis(connection.targetPoint, direction)
200
+ const targetTrack = getPerpendicularAxis(
201
+ connection.exitTargetPoint ?? connection.targetPoint,
202
+ direction,
203
+ )
201
204
  const lanePitch = traceWidth + clearance
202
205
  const exitAxis = getExitAxis(bus)
203
206
  const candidates = ([-1, 1] as const)
@@ -280,7 +283,7 @@ function completeCornerChannelRoute(params: {
280
283
  const minimumTrack = boundaryMinimum + traceWidth / 2
281
284
  const maximumTrack = boundaryMaximum - traceWidth / 2
282
285
  const targetTrack = getPerpendicularAxis(
283
- item.connection.targetPoint,
286
+ item.connection.exitTargetPoint ?? item.connection.targetPoint,
284
287
  direction,
285
288
  )
286
289
  const trackStep = lanePitch / 2
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
  *
@@ -102,13 +111,6 @@ export interface FanoutSolverOptions {
102
111
  viaHoleDiameter?: number
103
112
  clearance?: number
104
113
  compactBusTracks?: boolean
105
- /**
106
- * Prefer the perpendicular coordinate of each original downstream endpoint
107
- * when assigning boundary tracks. Ordered edge-pad buses can then enter
108
- * same-layer pads directly instead of requiring a second global router to
109
- * undo compacted fanout tracks.
110
- */
111
- preferOriginalEndpointTracks?: boolean
112
114
  /** Allow branches belonging to the same electrical net to share copper. */
113
115
  allowSameNetMerges?: boolean
114
116
  singleLayerPushAndShove?: boolean
@@ -216,6 +218,8 @@ export interface PreparedConnection {
216
218
  sourceLayer: string
217
219
  sourceObstacle: Obstacle
218
220
  targetPoint: ConnectionPoint
221
+ /** Preferred downstream point used to choose the boundary exit track. */
222
+ exitTargetPoint?: Point2D
219
223
  }
220
224
 
221
225
  export interface PreparedBus {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",