@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 +19 -0
- package/lib/route-bus.ts +2 -2
- package/lib/route-via-minimal-winding.ts +100 -40
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
912
|
+
segmentMaxX + margin <
|
|
891
913
|
Math.min(blocker.segment.start.x, blocker.segment.end.x) ||
|
|
892
|
-
|
|
914
|
+
segmentMinX - margin >
|
|
893
915
|
Math.max(blocker.segment.start.x, blocker.segment.end.x) ||
|
|
894
|
-
|
|
916
|
+
segmentMaxY + margin <
|
|
895
917
|
Math.min(blocker.segment.start.y, blocker.segment.end.y) ||
|
|
896
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 +
|
|
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
|
-
|
|
1262
|
-
|
|
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
|
|
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 =
|
|
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
|
|
1516
|
-
|
|
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 &&
|