@tscircuit/fanout-solver 0.0.62 → 0.0.63

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/geometry.ts CHANGED
@@ -190,6 +190,25 @@ export function segmentsAreClear(
190
190
  ): boolean {
191
191
  if (first.layer !== second.layer) return true
192
192
  const requiredDistance = (first.width + second.width) / 2 + clearance
193
+ // Axis separation is a lower bound on the distance between the segments.
194
+ // Leave a conservative tolerance band to the exact check below.
195
+ const broadPhaseDistance = requiredDistance + EPSILON
196
+ if (
197
+ Math.min(first.start.x, first.end.x) -
198
+ Math.max(second.start.x, second.end.x) >
199
+ broadPhaseDistance ||
200
+ Math.min(second.start.x, second.end.x) -
201
+ Math.max(first.start.x, first.end.x) >
202
+ broadPhaseDistance ||
203
+ Math.min(first.start.y, first.end.y) -
204
+ Math.max(second.start.y, second.end.y) >
205
+ broadPhaseDistance ||
206
+ Math.min(second.start.y, second.end.y) -
207
+ Math.max(first.start.y, first.end.y) >
208
+ broadPhaseDistance
209
+ ) {
210
+ return true
211
+ }
193
212
  return (
194
213
  distanceSegmentToSegment(
195
214
  first.start,
package/lib/route-bus.ts CHANGED
@@ -1951,6 +1951,8 @@ function planIsClearOfPlans(params: {
1951
1951
  clearance,
1952
1952
  blockingBusCounts,
1953
1953
  } = params
1954
+ const planSegments = getPlanSegments(plan)
1955
+ const planVias = getPlanVias(plan)
1954
1956
  for (const otherPlan of otherPlans) {
1955
1957
  if (
1956
1958
  allowSameNetMerges &&
@@ -1975,9 +1977,7 @@ function planIsClearOfPlans(params: {
1975
1977
  (blockingBusCounts.get(otherPlan.busId) ?? 0) + 1,
1976
1978
  )
1977
1979
  }
1978
- const planSegments = getPlanSegments(plan)
1979
1980
  const otherSegments = getPlanSegments(otherPlan)
1980
- const planVias = getPlanVias(plan)
1981
1981
  const otherVias = getPlanVias(otherPlan)
1982
1982
  for (const segment of planSegments) {
1983
1983
  for (const otherSegment of otherSegments) {
@@ -824,6 +824,13 @@ export function* routeViaMinimalWindingAlternativesSteps(
824
824
  const sharesNet = (first: string, second: string): boolean =>
825
825
  first === second ||
826
826
  (allowSameNetMerges && connectionsShareElectricalNet(srj, first, second))
827
+ const boundedBlockingSegments = blockingSegments.map((blocker) => ({
828
+ ...blocker,
829
+ minX: Math.min(blocker.segment.start.x, blocker.segment.end.x),
830
+ maxX: Math.max(blocker.segment.start.x, blocker.segment.end.x),
831
+ minY: Math.min(blocker.segment.start.y, blocker.segment.end.y),
832
+ maxY: Math.max(blocker.segment.start.y, blocker.segment.end.y),
833
+ }))
827
834
  const allBlockingVias = [...blockingVias, ...terminalVias]
828
835
  const maximumViaToTraceDistance = allBlockingVias.reduce(
829
836
  (maximum, { via }) =>
@@ -850,6 +857,10 @@ export function* routeViaMinimalWindingAlternativesSteps(
850
857
  }): boolean => {
851
858
  const { segment, terminal, acceptedAttemptSegments } = params
852
859
  const connectionName = terminal.connection.connection.name
860
+ const segmentMinX = Math.min(segment.start.x, segment.end.x)
861
+ const segmentMaxX = Math.max(segment.start.x, segment.end.x)
862
+ const segmentMinY = Math.min(segment.start.y, segment.end.y)
863
+ const segmentMaxY = Math.max(segment.start.y, segment.end.y)
853
864
  const requiredObstacleClearance = segment.width / 2 + clearance
854
865
  for (const obstacle of targetLayerObstacleIndex.querySegment(
855
866
  segment,
@@ -869,8 +880,19 @@ export function* routeViaMinimalWindingAlternativesSteps(
869
880
  return false
870
881
  }
871
882
  }
872
- for (const blocker of blockingSegments) {
883
+ for (const blocker of boundedBlockingSegments) {
873
884
  if (sharesNet(connectionName, blocker.connectionName)) continue
885
+ const margin = (segment.width + blocker.segment.width) / 2 + clearance
886
+ // Keep the full clearance margin in the broad phase; the exact check
887
+ // retains the existing tolerance for nearby copper.
888
+ if (
889
+ segmentMaxX + margin < blocker.minX ||
890
+ segmentMinX - margin > blocker.maxX ||
891
+ segmentMaxY + margin < blocker.minY ||
892
+ segmentMinY - margin > blocker.maxY
893
+ ) {
894
+ continue
895
+ }
874
896
  if (
875
897
  distanceSegmentToSegment(
876
898
  segment.start,
@@ -878,7 +900,7 @@ export function* routeViaMinimalWindingAlternativesSteps(
878
900
  blocker.segment.start,
879
901
  blocker.segment.end,
880
902
  ) <
881
- (segment.width + blocker.segment.width) / 2 + clearance - EPSILON
903
+ margin - EPSILON
882
904
  ) {
883
905
  return false
884
906
  }
@@ -887,13 +909,13 @@ export function* routeViaMinimalWindingAlternativesSteps(
887
909
  if (sharesNet(connectionName, blocker.connectionName)) continue
888
910
  const margin = (segment.width + blocker.segment.width) / 2 + clearance
889
911
  if (
890
- Math.max(segment.start.x, segment.end.x) + margin <
912
+ segmentMaxX + margin <
891
913
  Math.min(blocker.segment.start.x, blocker.segment.end.x) ||
892
- Math.min(segment.start.x, segment.end.x) - margin >
914
+ segmentMinX - margin >
893
915
  Math.max(blocker.segment.start.x, blocker.segment.end.x) ||
894
- Math.max(segment.start.y, segment.end.y) + margin <
916
+ segmentMaxY + margin <
895
917
  Math.min(blocker.segment.start.y, blocker.segment.end.y) ||
896
- Math.min(segment.start.y, segment.end.y) - margin >
918
+ segmentMinY - margin >
897
919
  Math.max(blocker.segment.start.y, blocker.segment.end.y)
898
920
  )
899
921
  continue
@@ -917,10 +939,6 @@ export function* routeViaMinimalWindingAlternativesSteps(
917
939
  )
918
940
  return false
919
941
  }
920
- const segmentMinX = Math.min(segment.start.x, segment.end.x)
921
- const segmentMaxX = Math.max(segment.start.x, segment.end.x)
922
- const segmentMinY = Math.min(segment.start.y, segment.end.y)
923
- const segmentMaxY = Math.max(segment.start.y, segment.end.y)
924
942
  for (
925
943
  let viaIndex = getFirstViaAtOrAfterX(
926
944
  segmentMinX - maximumViaToTraceDistance,
@@ -1151,18 +1169,33 @@ export function* routeViaMinimalWindingAlternativesSteps(
1151
1169
  )
1152
1170
  const previous = new Int32Array(stateCount).fill(-1)
1153
1171
  const heap = new MinHeap()
1154
- const heuristic = (point: Point2D): number => {
1172
+ // A node is revisited with different incoming directions. Its distance
1173
+ // estimate and lane penalty stay fixed throughout this terminal search.
1174
+ const remainingDistances = new Float64Array(nodeCount)
1175
+ const lanePenalties = new Float64Array(nodeCount)
1176
+ const targetTrack = getPerpendicularAxis(
1177
+ terminal.exitPoint,
1178
+ boundaryDirection,
1179
+ )
1180
+ for (let nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) {
1181
+ const point = nodes[nodeIndex]!.point
1155
1182
  const deltaX = Math.abs(point.x - terminal.exitPoint.x)
1156
1183
  const deltaY = Math.abs(point.y - terminal.exitPoint.y)
1157
- return (
1184
+ remainingDistances[nodeIndex] =
1158
1185
  Math.max(deltaX, deltaY) + (Math.SQRT2 - 1) * Math.min(deltaX, deltaY)
1159
- )
1186
+ const nextTrack = getPerpendicularAxis(point, boundaryDirection)
1187
+ lanePenalties[nodeIndex] =
1188
+ laneBias === 0
1189
+ ? 0
1190
+ : laneBias > 0
1191
+ ? Math.max(0, targetTrack - nextTrack) * 0.2
1192
+ : Math.max(0, nextTrack - targetTrack) * 0.2
1160
1193
  }
1161
1194
  for (const start of starts) {
1162
1195
  const state = start.nodeIndex * 9 + 8
1163
1196
  if (start.length >= distances[state]!) continue
1164
1197
  distances[state] = start.length
1165
- const remaining = heuristic(nodes[start.nodeIndex]!.point)
1198
+ const remaining = remainingDistances[start.nodeIndex]!
1166
1199
  heap.push({
1167
1200
  node: start.nodeIndex,
1168
1201
  direction: 8,
@@ -1179,6 +1212,16 @@ export function* routeViaMinimalWindingAlternativesSteps(
1179
1212
  [0, -1],
1180
1213
  [1, -1],
1181
1214
  ] as const
1215
+ // Preserve the original ascending neighbor order, but do not reconsider
1216
+ // the five disallowed turns every time a directed state is expanded.
1217
+ const nextDirectionsByIncoming = Array.from({ length: 9 }, (_, incoming) =>
1218
+ directions.flatMap((_, directionIndex) => {
1219
+ const delta = Math.abs(incoming - directionIndex)
1220
+ return incoming === 8 || Math.min(delta, 8 - delta) <= 1
1221
+ ? [directionIndex]
1222
+ : []
1223
+ }),
1224
+ )
1182
1225
  const startsByNode = new Map<number, ConnectorCandidate[]>()
1183
1226
  for (const start of starts) {
1184
1227
  const values = startsByNode.get(start.nodeIndex) ?? []
@@ -1198,7 +1241,7 @@ export function* routeViaMinimalWindingAlternativesSteps(
1198
1241
  const currentDistance = distances[state]!
1199
1242
  if (
1200
1243
  current.score >
1201
- currentDistance + heuristic(nodes[current.node]!.point) + EPSILON
1244
+ currentDistance + remainingDistances[current.node]! + EPSILON
1202
1245
  )
1203
1246
  continue
1204
1247
  expandedStateCount++
@@ -1257,17 +1300,9 @@ export function* routeViaMinimalWindingAlternativesSteps(
1257
1300
  }
1258
1301
  }
1259
1302
  const node = nodes[current.node]!
1260
- for (
1261
- let directionIndex = 0;
1262
- directionIndex < directions.length;
1263
- directionIndex++
1264
- ) {
1265
- if (current.direction !== 8) {
1266
- const rawDirectionDelta = Math.abs(current.direction - directionIndex)
1267
- if (Math.min(rawDirectionDelta, 8 - rawDirectionDelta) > 1) {
1268
- continue
1269
- }
1270
- }
1303
+ for (const directionIndex of nextDirectionsByIncoming[
1304
+ current.direction
1305
+ ]!) {
1271
1306
  const [deltaColumn, deltaRow] = directions[directionIndex]!
1272
1307
  const column = node.column + deltaColumn
1273
1308
  const row = node.row + deltaRow
@@ -1278,17 +1313,7 @@ export function* routeViaMinimalWindingAlternativesSteps(
1278
1313
  const nextPoint = nodes[nextNode]!.point
1279
1314
  const addsTurn =
1280
1315
  current.direction !== 8 && current.direction !== directionIndex
1281
- const nextTrack = getPerpendicularAxis(nextPoint, boundaryDirection)
1282
- const targetTrack = getPerpendicularAxis(
1283
- terminal.exitPoint,
1284
- boundaryDirection,
1285
- )
1286
- const lanePenalty =
1287
- laneBias === 0
1288
- ? 0
1289
- : laneBias > 0
1290
- ? Math.max(0, targetTrack - nextTrack) * 0.2
1291
- : Math.max(0, nextTrack - targetTrack) * 0.2
1316
+ const lanePenalty = lanePenalties[nextNode]!
1292
1317
  const nextDistance =
1293
1318
  currentDistance +
1294
1319
  (deltaColumn !== 0 && deltaRow !== 0
@@ -1319,7 +1344,7 @@ export function* routeViaMinimalWindingAlternativesSteps(
1319
1344
  if (edgeClearance[edgeIndex] === 2) continue
1320
1345
  distances[nextState] = nextDistance
1321
1346
  previous[nextState] = state
1322
- const remaining = heuristic(nextPoint)
1347
+ const remaining = remainingDistances[nextNode]!
1323
1348
  heap.push({
1324
1349
  node: nextNode,
1325
1350
  direction: directionIndex,
@@ -1488,6 +1513,14 @@ export function* routeViaMinimalWindingAlternativesSteps(
1488
1513
 
1489
1514
  const alternatives: FanoutRoutePlan[][] = []
1490
1515
  const seenAlternativeKeys = new Set<string>()
1516
+ // Different complete route orders can share the same unsuccessful prefix.
1517
+ // Static obstacles/vias, grid and search limits belong to this invocation;
1518
+ // the terminal, lane bias and already accepted copper identify the rest.
1519
+ // A reused failure still emits its normal completion progress below.
1520
+ const failedSearches = new Map<
1521
+ string,
1522
+ { expandedStateCount: number; searchBatch: number }
1523
+ >()
1491
1524
  let routeOrderAttemptCount = 0
1492
1525
  for (const routeOrder of adaptiveRouteOrders()) {
1493
1526
  for (const laneBias of laneBiases) {
@@ -1507,13 +1540,36 @@ export function* routeViaMinimalWindingAlternativesSteps(
1507
1540
  terminalIndex++
1508
1541
  ) {
1509
1542
  const terminal = routeOrder[terminalIndex]!
1543
+ const failedSearchKey = JSON.stringify([
1544
+ terminals.indexOf(terminal),
1545
+ laneBias,
1546
+ acceptedAttemptSegments.map(({ connectionName, segment }) => [
1547
+ connectionName,
1548
+ segment.start.x,
1549
+ segment.start.y,
1550
+ segment.end.x,
1551
+ segment.end.y,
1552
+ segment.width,
1553
+ segment.layer,
1554
+ ]),
1555
+ ])
1556
+ const cachedFailure = failedSearches.get(failedSearchKey)
1510
1557
  const connectionSteps = routeOneSteps({
1511
1558
  terminal,
1512
1559
  acceptedAttemptSegments,
1513
1560
  laneBias,
1514
1561
  })
1515
- let connectionResult = connectionSteps.next()
1516
- let searchBatch = 0
1562
+ let connectionResult: ReturnType<typeof connectionSteps.next> =
1563
+ cachedFailure === undefined
1564
+ ? connectionSteps.next()
1565
+ : {
1566
+ done: true,
1567
+ value: {
1568
+ points: null,
1569
+ expandedStateCount: cachedFailure.expandedStateCount,
1570
+ },
1571
+ }
1572
+ let searchBatch = cachedFailure?.searchBatch ?? 0
1517
1573
  let expandedStateCount = 0
1518
1574
  while (!connectionResult.done) {
1519
1575
  expandedStateCount = connectionResult.value.expandedStateCount
@@ -1560,6 +1616,10 @@ export function* routeViaMinimalWindingAlternativesSteps(
1560
1616
  : {}),
1561
1617
  }
1562
1618
  if (!points) {
1619
+ failedSearches.set(failedSearchKey, {
1620
+ expandedStateCount: finalExpandedStateCount,
1621
+ searchBatch,
1622
+ })
1563
1623
  if (
1564
1624
  adaptiveRouteOrder &&
1565
1625
  maximumRouteOrderAttempts !== undefined &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.62",
3
+ "version": "0.0.63",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",