@tscircuit/rectdiff 0.0.24 → 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/AGENTS.md +23 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +69 -77
- package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +23 -23
- package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +9 -10
- package/lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts +22 -19
- package/lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts +1 -1
- package/lib/utils/expandRectFromSeed.ts +8 -10
- package/lib/utils/isFullyOccupiedAtPoint.ts +2 -2
- package/lib/utils/rectdiff-geometry.ts +13 -20
- package/package.json +2 -1
- package/scripts/benchmark-slow-problem.ts +94 -0
- package/test-assets/keyboard4.json +16165 -0
- package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +1 -1
- package/tests/solver/bugreport03-fe4a17/__snapshots__/bugreport03-fe4a17.snap.svg +1 -1
- package/tests/solver/bugreport09-618e09/__snapshots__/bugreport09-618e09.snap.svg +1 -1
- package/tests/solver/bugreport10-71239a/__snapshots__/bugreport10-71239a.snap.svg +1 -1
- package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +1 -1
- package/tests/solver/bugreport35-191db9/__snapshots__/bugreport35-191db9.snap.svg +1 -1
- package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +1 -1
package/AGENTS.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
This is an algorithm solver repo designed to be integrated into the tscircuit
|
|
2
|
+
autorouter.
|
|
3
|
+
|
|
4
|
+
It uses the SimpleRouteJson type as input (see lib/types/srj-types.ts)
|
|
5
|
+
|
|
6
|
+
We use svg snapshots to create visual snapshots as our primary method of
|
|
7
|
+
ensuring the algorithm works correctly. We use `export BUN_UPDATE_SNAPSHOTS=1`
|
|
8
|
+
and `bun test path/to/test.test.ts` to update the snapshots.
|
|
9
|
+
|
|
10
|
+
We use the `graphics-debug` package to turn `GraphicsObject` into SVGs using
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { getSvgFromGraphicsObject } from "graphics-debug"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
We use `cosmos` `*.page.tsx` files inside the `pages` directory to create
|
|
17
|
+
examples that can be debugged for a human.
|
|
18
|
+
|
|
19
|
+
We discovered the algorithm via `experiments/rect3d_visualizer.html` and we're
|
|
20
|
+
now formalizing it into a solver pattern and binding to our normal types.
|
|
21
|
+
|
|
22
|
+
The `test-assets` directory contains assets for test, typically simple route
|
|
23
|
+
json files with test cases that can be importing into pages or tests.
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -688,24 +688,18 @@ function containsPoint(r, p) {
|
|
|
688
688
|
return p.x >= r.x - EPS4 && p.x <= r.x + r.width + EPS4 && p.y >= r.y - EPS4 && p.y <= r.y + r.height + EPS4;
|
|
689
689
|
}
|
|
690
690
|
function distancePointToRectEdges(p, r) {
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
t = clamp(t, 0, 1);
|
|
704
|
-
const xx = x1 + t * C;
|
|
705
|
-
const yy = y1 + t * D;
|
|
706
|
-
best = Math.min(best, Math.hypot(p.x - xx, p.y - yy));
|
|
707
|
-
}
|
|
708
|
-
return best;
|
|
691
|
+
const minX = r.x;
|
|
692
|
+
const maxX = r.x + r.width;
|
|
693
|
+
const minY = r.y;
|
|
694
|
+
const maxY = r.y + r.height;
|
|
695
|
+
if (p.x >= minX && p.x <= maxX && p.y >= minY && p.y <= maxY) {
|
|
696
|
+
return Math.min(p.x - minX, maxX - p.x, p.y - minY, maxY - p.y);
|
|
697
|
+
}
|
|
698
|
+
const dx = p.x < minX ? minX - p.x : p.x > maxX ? p.x - maxX : 0;
|
|
699
|
+
const dy = p.y < minY ? minY - p.y : p.y > maxY ? p.y - maxY : 0;
|
|
700
|
+
if (dx === 0) return dy;
|
|
701
|
+
if (dy === 0) return dx;
|
|
702
|
+
return Math.hypot(dx, dy);
|
|
709
703
|
}
|
|
710
704
|
function intersect1D(r1, r2) {
|
|
711
705
|
const lo = Math.max(r1[0], r2[0]);
|
|
@@ -1051,17 +1045,10 @@ function maxExpandUp(params) {
|
|
|
1051
1045
|
}
|
|
1052
1046
|
return Math.max(0, e);
|
|
1053
1047
|
}
|
|
1054
|
-
var toRect = (tree) => ({
|
|
1055
|
-
x: tree.minX,
|
|
1056
|
-
y: tree.minY,
|
|
1057
|
-
width: tree.maxX - tree.minX,
|
|
1058
|
-
height: tree.maxY - tree.minY
|
|
1059
|
-
});
|
|
1060
1048
|
var addBlocker = (params) => {
|
|
1061
1049
|
const { rect, seen, blockers } = params;
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
seen.add(key);
|
|
1050
|
+
if (seen.has(rect)) return;
|
|
1051
|
+
seen.add(rect);
|
|
1065
1052
|
blockers.push(rect);
|
|
1066
1053
|
};
|
|
1067
1054
|
var toQueryRect = (params) => {
|
|
@@ -1098,23 +1085,22 @@ function expandRectFromSeed(params) {
|
|
|
1098
1085
|
const blockersIndex = obsticalIndexByLayer[z];
|
|
1099
1086
|
if (blockersIndex) {
|
|
1100
1087
|
for (const entry of blockersIndex.search(query))
|
|
1101
|
-
addBlocker({ rect:
|
|
1088
|
+
addBlocker({ rect: entry, seen, blockers });
|
|
1102
1089
|
}
|
|
1103
1090
|
const placedLayer = placedIndexByLayer[z];
|
|
1104
1091
|
if (placedLayer) {
|
|
1105
1092
|
for (const entry of placedLayer.search(query)) {
|
|
1106
1093
|
const isFullStack = entry.zLayers.length >= totalLayers;
|
|
1107
1094
|
if (!isFullStack) continue;
|
|
1108
|
-
const rect = toRect(entry);
|
|
1109
1095
|
if (isSelfRect({
|
|
1110
|
-
rect,
|
|
1096
|
+
rect: entry,
|
|
1111
1097
|
startX,
|
|
1112
1098
|
startY,
|
|
1113
1099
|
initialW,
|
|
1114
1100
|
initialH
|
|
1115
1101
|
}))
|
|
1116
1102
|
continue;
|
|
1117
|
-
addBlocker({ rect, seen, blockers });
|
|
1103
|
+
addBlocker({ rect: entry, seen, blockers });
|
|
1118
1104
|
}
|
|
1119
1105
|
}
|
|
1120
1106
|
}
|
|
@@ -1205,9 +1191,9 @@ function isFullyOccupiedAtPoint(params) {
|
|
|
1205
1191
|
};
|
|
1206
1192
|
for (let z = 0; z < params.layerCount; z++) {
|
|
1207
1193
|
const obstacleIdx = params.obstacleIndexByLayer[z];
|
|
1208
|
-
const hasObstacle = !!obstacleIdx && obstacleIdx.
|
|
1194
|
+
const hasObstacle = !!obstacleIdx && obstacleIdx.collides(query);
|
|
1209
1195
|
const placedIdx = params.placedIndexByLayer[z];
|
|
1210
|
-
const hasPlaced = !!placedIdx && placedIdx.
|
|
1196
|
+
const hasPlaced = !!placedIdx && placedIdx.collides(query);
|
|
1211
1197
|
if (!hasObstacle && !hasPlaced) return false;
|
|
1212
1198
|
}
|
|
1213
1199
|
return true;
|
|
@@ -1233,7 +1219,7 @@ function longestFreeSpanAroundZ(params) {
|
|
|
1233
1219
|
maxY: y
|
|
1234
1220
|
};
|
|
1235
1221
|
const obstacleIdx = obstacleIndexByLayer[layer];
|
|
1236
|
-
if (obstacleIdx && obstacleIdx.
|
|
1222
|
+
if (obstacleIdx && obstacleIdx.collides(query)) return false;
|
|
1237
1223
|
const extras = additionalBlockersByLayer?.[layer] ?? [];
|
|
1238
1224
|
return !extras.some((b) => containsPoint(b, { x, y }));
|
|
1239
1225
|
};
|
|
@@ -1265,6 +1251,10 @@ function computeCandidates3D(params) {
|
|
|
1265
1251
|
hardPlacedByLayer
|
|
1266
1252
|
} = params;
|
|
1267
1253
|
const out = /* @__PURE__ */ new Map();
|
|
1254
|
+
const hardRectsByLayer = Array.from({ length: layerCount }, (_, z) => [
|
|
1255
|
+
...obstacleIndexByLayer[z]?.all() ?? [],
|
|
1256
|
+
...hardPlacedByLayer[z] ?? []
|
|
1257
|
+
]);
|
|
1268
1258
|
for (let x = bounds.x; x < bounds.x + bounds.width; x += gridSize) {
|
|
1269
1259
|
for (let y = bounds.y; y < bounds.y + bounds.height; y += gridSize) {
|
|
1270
1260
|
if (Math.abs(x - bounds.x) < EPS4 || Math.abs(y - bounds.y) < EPS4 || x > bounds.x + bounds.width - gridSize - EPS4 || y > bounds.y + bounds.height - gridSize - EPS4) {
|
|
@@ -1296,14 +1286,11 @@ function computeCandidates3D(params) {
|
|
|
1296
1286
|
}
|
|
1297
1287
|
}
|
|
1298
1288
|
const anchorZ = bestSpan.length ? bestSpan[Math.floor(bestSpan.length / 2)] : bestZ;
|
|
1299
|
-
const hardAtZ = [
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
distancePointToRectEdges({ x, y }, bounds),
|
|
1305
|
-
...hardAtZ.length ? hardAtZ.map((b) => distancePointToRectEdges({ x, y }, b)) : [Infinity]
|
|
1306
|
-
);
|
|
1289
|
+
const hardAtZ = hardRectsByLayer[anchorZ] ?? [];
|
|
1290
|
+
let d = distancePointToRectEdges({ x, y }, bounds);
|
|
1291
|
+
for (const blocker of hardAtZ) {
|
|
1292
|
+
d = Math.min(d, distancePointToRectEdges({ x, y }, blocker));
|
|
1293
|
+
}
|
|
1307
1294
|
const distance = quantize2(d);
|
|
1308
1295
|
const k = `${x.toFixed(6)}|${y.toFixed(6)}`;
|
|
1309
1296
|
const cand = {
|
|
@@ -1328,6 +1315,12 @@ function computeCandidates3D(params) {
|
|
|
1328
1315
|
|
|
1329
1316
|
// lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts
|
|
1330
1317
|
var quantize3 = (value, precision = 1e-6) => Math.round(value / precision) * precision;
|
|
1318
|
+
var toRect = (rect) => "minX" in rect ? {
|
|
1319
|
+
x: rect.minX,
|
|
1320
|
+
y: rect.minY,
|
|
1321
|
+
width: rect.maxX - rect.minX,
|
|
1322
|
+
height: rect.maxY - rect.minY
|
|
1323
|
+
} : rect;
|
|
1331
1324
|
function computeUncoveredSegments(params) {
|
|
1332
1325
|
const { lineStart, lineEnd, coveringIntervals, minSegmentLength } = params;
|
|
1333
1326
|
const lineStartQ = quantize3(lineStart);
|
|
@@ -1390,6 +1383,13 @@ function computeEdgeCandidates3D(params) {
|
|
|
1390
1383
|
const out = [];
|
|
1391
1384
|
const \u03B4 = Math.max(minSize * 0.15, EPS4 * 3);
|
|
1392
1385
|
const dedup = /* @__PURE__ */ new Set();
|
|
1386
|
+
const hardRectsByLayer = Array.from(
|
|
1387
|
+
{ length: layerCount },
|
|
1388
|
+
(_, z) => [
|
|
1389
|
+
...obstacleIndexByLayer[z]?.all() ?? [],
|
|
1390
|
+
...hardPlacedByLayer[z] ?? []
|
|
1391
|
+
].map(toRect)
|
|
1392
|
+
);
|
|
1393
1393
|
const key = (p) => `${p.z}|${p.x.toFixed(6)}|${p.y.toFixed(6)}`;
|
|
1394
1394
|
function fullyOcc(p) {
|
|
1395
1395
|
return isFullyOccupiedAtPoint({
|
|
@@ -1408,19 +1408,11 @@ function computeEdgeCandidates3D(params) {
|
|
|
1408
1408
|
if (x < bounds.x + EPS4 || y < bounds.y + EPS4 || x > bounds.x + bounds.width - EPS4 || y > bounds.y + bounds.height - EPS4)
|
|
1409
1409
|
return;
|
|
1410
1410
|
if (fullyOcc({ x, y })) return;
|
|
1411
|
-
const hard = [
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
y: quantize3(b.y),
|
|
1417
|
-
width: quantize3(b.width),
|
|
1418
|
-
height: quantize3(b.height)
|
|
1419
|
-
}));
|
|
1420
|
-
const d = Math.min(
|
|
1421
|
-
distancePointToRectEdges({ x, y }, bounds),
|
|
1422
|
-
...hard.length ? hard.map((b) => distancePointToRectEdges({ x, y }, b)) : [Infinity]
|
|
1423
|
-
);
|
|
1411
|
+
const hard = hardRectsByLayer[z] ?? [];
|
|
1412
|
+
let d = distancePointToRectEdges({ x, y }, bounds);
|
|
1413
|
+
for (const blocker of hard) {
|
|
1414
|
+
d = Math.min(d, distancePointToRectEdges({ x, y }, blocker));
|
|
1415
|
+
}
|
|
1424
1416
|
const distance = quantize3(d);
|
|
1425
1417
|
const k = key({ x, y, z });
|
|
1426
1418
|
if (dedup.has(k)) return;
|
|
@@ -1445,10 +1437,7 @@ function computeEdgeCandidates3D(params) {
|
|
|
1445
1437
|
});
|
|
1446
1438
|
}
|
|
1447
1439
|
for (let z = 0; z < layerCount; z++) {
|
|
1448
|
-
const blockers = [
|
|
1449
|
-
...obstacleIndexByLayer[z]?.all() ?? [],
|
|
1450
|
-
...hardPlacedByLayer[z] ?? []
|
|
1451
|
-
].map((b) => ({
|
|
1440
|
+
const blockers = (hardRectsByLayer[z] ?? []).map((b) => ({
|
|
1452
1441
|
x: quantize3(b.x),
|
|
1453
1442
|
y: quantize3(b.y),
|
|
1454
1443
|
width: quantize3(b.width),
|
|
@@ -1746,6 +1735,7 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
|
|
|
1746
1735
|
candidates;
|
|
1747
1736
|
placed;
|
|
1748
1737
|
placedIndexByLayer;
|
|
1738
|
+
hardPlacedByLayer;
|
|
1749
1739
|
expansionIndex;
|
|
1750
1740
|
edgeAnalysisDone;
|
|
1751
1741
|
totalSeedsThisGrid;
|
|
@@ -1801,6 +1791,7 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
|
|
|
1801
1791
|
{ length: layerCount },
|
|
1802
1792
|
() => new RBush3()
|
|
1803
1793
|
);
|
|
1794
|
+
this.hardPlacedByLayer = Array.from({ length: layerCount }, () => []);
|
|
1804
1795
|
this.expansionIndex = 0;
|
|
1805
1796
|
this.edgeAnalysisDone = false;
|
|
1806
1797
|
this.totalSeedsThisGrid = 0;
|
|
@@ -1829,25 +1820,22 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
|
|
|
1829
1820
|
maxMultiLayerSpan
|
|
1830
1821
|
} = this.options;
|
|
1831
1822
|
const grid = gridSizes[this.gridIndex];
|
|
1832
|
-
const hardPlacedByLayer = allLayerNode({
|
|
1833
|
-
layerCount: this.layerCount,
|
|
1834
|
-
placed: this.placed
|
|
1835
|
-
});
|
|
1836
1823
|
if (this.candidates.length === 0 && this.consumedSeedsThisGrid === 0) {
|
|
1837
1824
|
this.candidates = computeCandidates3D({
|
|
1838
1825
|
bounds: this.bounds,
|
|
1839
1826
|
gridSize: grid,
|
|
1840
1827
|
layerCount: this.layerCount,
|
|
1841
|
-
hardPlacedByLayer,
|
|
1828
|
+
hardPlacedByLayer: this.hardPlacedByLayer,
|
|
1842
1829
|
obstacleIndexByLayer: this.input.obstacleIndexByLayer,
|
|
1843
1830
|
placedIndexByLayer: this.placedIndexByLayer
|
|
1844
1831
|
});
|
|
1845
1832
|
this.totalSeedsThisGrid = this.candidates.length;
|
|
1846
1833
|
this.consumedSeedsThisGrid = 0;
|
|
1847
1834
|
}
|
|
1848
|
-
if (this.candidates.length
|
|
1835
|
+
if (this.consumedSeedsThisGrid >= this.candidates.length) {
|
|
1849
1836
|
if (this.gridIndex + 1 < gridSizes.length) {
|
|
1850
1837
|
this.gridIndex += 1;
|
|
1838
|
+
this.candidates = [];
|
|
1851
1839
|
this.totalSeedsThisGrid = 0;
|
|
1852
1840
|
this.consumedSeedsThisGrid = 0;
|
|
1853
1841
|
return;
|
|
@@ -1860,20 +1848,28 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
|
|
|
1860
1848
|
layerCount: this.layerCount,
|
|
1861
1849
|
obstacleIndexByLayer: this.input.obstacleIndexByLayer,
|
|
1862
1850
|
placedIndexByLayer: this.placedIndexByLayer,
|
|
1863
|
-
hardPlacedByLayer
|
|
1851
|
+
hardPlacedByLayer: this.hardPlacedByLayer
|
|
1864
1852
|
});
|
|
1865
1853
|
this.edgeAnalysisDone = true;
|
|
1866
1854
|
this.totalSeedsThisGrid = this.candidates.length;
|
|
1867
1855
|
this.consumedSeedsThisGrid = 0;
|
|
1868
1856
|
return;
|
|
1869
1857
|
}
|
|
1858
|
+
this.candidates = [];
|
|
1870
1859
|
this.solved = true;
|
|
1871
1860
|
this.expansionIndex = 0;
|
|
1872
1861
|
return;
|
|
1873
1862
|
}
|
|
1874
1863
|
}
|
|
1875
|
-
const cand = this.candidates.
|
|
1876
|
-
|
|
1864
|
+
const cand = this.candidates[this.consumedSeedsThisGrid++];
|
|
1865
|
+
if (isFullyOccupiedAtPoint({
|
|
1866
|
+
layerCount: this.layerCount,
|
|
1867
|
+
obstacleIndexByLayer: this.input.obstacleIndexByLayer,
|
|
1868
|
+
placedIndexByLayer: this.placedIndexByLayer,
|
|
1869
|
+
point: { x: cand.x, y: cand.y }
|
|
1870
|
+
})) {
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1877
1873
|
const span = longestFreeSpanAroundZ({
|
|
1878
1874
|
x: cand.x,
|
|
1879
1875
|
y: cand.y,
|
|
@@ -1882,7 +1878,7 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
|
|
|
1882
1878
|
minSpan: minMulti.minLayers,
|
|
1883
1879
|
maxSpan: maxMultiLayerSpan,
|
|
1884
1880
|
obstacleIndexByLayer: this.input.obstacleIndexByLayer,
|
|
1885
|
-
additionalBlockersByLayer: hardPlacedByLayer
|
|
1881
|
+
additionalBlockersByLayer: this.hardPlacedByLayer
|
|
1886
1882
|
});
|
|
1887
1883
|
const attempts = [];
|
|
1888
1884
|
if (span.length >= minMulti.minLayers) {
|
|
@@ -1929,14 +1925,10 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
|
|
|
1929
1925
|
},
|
|
1930
1926
|
newIndex
|
|
1931
1927
|
);
|
|
1932
|
-
this.
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
placedIndexByLayer: this.placedIndexByLayer,
|
|
1937
|
-
point: { x: c.x, y: c.y }
|
|
1938
|
-
})
|
|
1939
|
-
);
|
|
1928
|
+
this.hardPlacedByLayer = allLayerNode({
|
|
1929
|
+
layerCount: this.layerCount,
|
|
1930
|
+
placed: this.placed
|
|
1931
|
+
});
|
|
1940
1932
|
return;
|
|
1941
1933
|
}
|
|
1942
1934
|
}
|
|
@@ -57,6 +57,7 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
57
57
|
private candidates!: Candidate3D[]
|
|
58
58
|
private placed!: Placed3D[]
|
|
59
59
|
private placedIndexByLayer!: Array<RBush<RTreeRect>>
|
|
60
|
+
private hardPlacedByLayer!: XYRect[][]
|
|
60
61
|
private expansionIndex!: number
|
|
61
62
|
private edgeAnalysisDone!: boolean
|
|
62
63
|
private totalSeedsThisGrid!: number
|
|
@@ -131,6 +132,7 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
131
132
|
{ length: layerCount },
|
|
132
133
|
() => new RBush<RTreeRect>(),
|
|
133
134
|
)
|
|
135
|
+
this.hardPlacedByLayer = Array.from({ length: layerCount }, () => [])
|
|
134
136
|
this.expansionIndex = 0
|
|
135
137
|
this.edgeAnalysisDone = false
|
|
136
138
|
this.totalSeedsThisGrid = 0
|
|
@@ -164,19 +166,13 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
164
166
|
} = this.options
|
|
165
167
|
const grid = gridSizes[this.gridIndex]!
|
|
166
168
|
|
|
167
|
-
// Build hard-placed map once per micro-step (cheap)
|
|
168
|
-
const hardPlacedByLayer = allLayerNode({
|
|
169
|
-
layerCount: this.layerCount,
|
|
170
|
-
placed: this.placed,
|
|
171
|
-
})
|
|
172
|
-
|
|
173
169
|
// Ensure candidates exist for this grid
|
|
174
170
|
if (this.candidates.length === 0 && this.consumedSeedsThisGrid === 0) {
|
|
175
171
|
this.candidates = computeCandidates3D({
|
|
176
172
|
bounds: this.bounds,
|
|
177
173
|
gridSize: grid,
|
|
178
174
|
layerCount: this.layerCount,
|
|
179
|
-
hardPlacedByLayer,
|
|
175
|
+
hardPlacedByLayer: this.hardPlacedByLayer,
|
|
180
176
|
obstacleIndexByLayer: this.input.obstacleIndexByLayer,
|
|
181
177
|
placedIndexByLayer: this.placedIndexByLayer,
|
|
182
178
|
})
|
|
@@ -185,9 +181,10 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
185
181
|
}
|
|
186
182
|
|
|
187
183
|
// If no candidates remain, advance grid or run edge pass or switch phase
|
|
188
|
-
if (this.candidates.length
|
|
184
|
+
if (this.consumedSeedsThisGrid >= this.candidates.length) {
|
|
189
185
|
if (this.gridIndex + 1 < gridSizes.length) {
|
|
190
186
|
this.gridIndex += 1
|
|
187
|
+
this.candidates = []
|
|
191
188
|
this.totalSeedsThisGrid = 0
|
|
192
189
|
this.consumedSeedsThisGrid = 0
|
|
193
190
|
return
|
|
@@ -200,13 +197,14 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
200
197
|
layerCount: this.layerCount,
|
|
201
198
|
obstacleIndexByLayer: this.input.obstacleIndexByLayer,
|
|
202
199
|
placedIndexByLayer: this.placedIndexByLayer,
|
|
203
|
-
hardPlacedByLayer,
|
|
200
|
+
hardPlacedByLayer: this.hardPlacedByLayer,
|
|
204
201
|
})
|
|
205
202
|
this.edgeAnalysisDone = true
|
|
206
203
|
this.totalSeedsThisGrid = this.candidates.length
|
|
207
204
|
this.consumedSeedsThisGrid = 0
|
|
208
205
|
return
|
|
209
206
|
}
|
|
207
|
+
this.candidates = []
|
|
210
208
|
this.solved = true
|
|
211
209
|
this.expansionIndex = 0
|
|
212
210
|
return
|
|
@@ -214,8 +212,17 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
214
212
|
}
|
|
215
213
|
|
|
216
214
|
// Consume exactly one candidate
|
|
217
|
-
const cand = this.candidates.
|
|
218
|
-
|
|
215
|
+
const cand = this.candidates[this.consumedSeedsThisGrid++]!
|
|
216
|
+
if (
|
|
217
|
+
isFullyOccupiedAtPoint({
|
|
218
|
+
layerCount: this.layerCount,
|
|
219
|
+
obstacleIndexByLayer: this.input.obstacleIndexByLayer,
|
|
220
|
+
placedIndexByLayer: this.placedIndexByLayer,
|
|
221
|
+
point: { x: cand.x, y: cand.y },
|
|
222
|
+
})
|
|
223
|
+
) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
219
226
|
|
|
220
227
|
// Evaluate attempts — multi-layer span first (computed ignoring soft nodes)
|
|
221
228
|
const span = longestFreeSpanAroundZ({
|
|
@@ -226,7 +233,7 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
226
233
|
minSpan: minMulti.minLayers,
|
|
227
234
|
maxSpan: maxMultiLayerSpan,
|
|
228
235
|
obstacleIndexByLayer: this.input.obstacleIndexByLayer,
|
|
229
|
-
additionalBlockersByLayer: hardPlacedByLayer,
|
|
236
|
+
additionalBlockersByLayer: this.hardPlacedByLayer,
|
|
230
237
|
})
|
|
231
238
|
|
|
232
239
|
const attempts: Array<{
|
|
@@ -285,17 +292,10 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
285
292
|
},
|
|
286
293
|
newIndex,
|
|
287
294
|
)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
!isFullyOccupiedAtPoint({
|
|
293
|
-
layerCount: this.layerCount,
|
|
294
|
-
obstacleIndexByLayer: this.input.obstacleIndexByLayer,
|
|
295
|
-
placedIndexByLayer: this.placedIndexByLayer,
|
|
296
|
-
point: { x: c.x, y: c.y },
|
|
297
|
-
}),
|
|
298
|
-
)
|
|
295
|
+
this.hardPlacedByLayer = allLayerNode({
|
|
296
|
+
layerCount: this.layerCount,
|
|
297
|
+
placed: this.placed,
|
|
298
|
+
})
|
|
299
299
|
|
|
300
300
|
return // processed one candidate
|
|
301
301
|
}
|
|
@@ -27,6 +27,10 @@ export function computeCandidates3D(params: {
|
|
|
27
27
|
hardPlacedByLayer,
|
|
28
28
|
} = params
|
|
29
29
|
const out = new Map<string, Candidate3D>() // key by (x,y)
|
|
30
|
+
const hardRectsByLayer = Array.from({ length: layerCount }, (_, z) => [
|
|
31
|
+
...(obstacleIndexByLayer[z]?.all() ?? []),
|
|
32
|
+
...(hardPlacedByLayer[z] ?? []),
|
|
33
|
+
])
|
|
30
34
|
|
|
31
35
|
for (let x = bounds.x; x < bounds.x + bounds.width; x += gridSize) {
|
|
32
36
|
for (let y = bounds.y; y < bounds.y + bounds.height; y += gridSize) {
|
|
@@ -75,16 +79,11 @@ export function computeCandidates3D(params: {
|
|
|
75
79
|
: bestZ
|
|
76
80
|
|
|
77
81
|
// Distance heuristic against hard blockers only (obstacles + full-stack)
|
|
78
|
-
const hardAtZ = [
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
distancePointToRectEdges({ x, y }, bounds),
|
|
84
|
-
...(hardAtZ.length
|
|
85
|
-
? hardAtZ.map((b) => distancePointToRectEdges({ x, y }, b))
|
|
86
|
-
: [Infinity]),
|
|
87
|
-
)
|
|
82
|
+
const hardAtZ = hardRectsByLayer[anchorZ] ?? []
|
|
83
|
+
let d = distancePointToRectEdges({ x, y }, bounds)
|
|
84
|
+
for (const blocker of hardAtZ) {
|
|
85
|
+
d = Math.min(d, distancePointToRectEdges({ x, y }, blocker))
|
|
86
|
+
}
|
|
88
87
|
const distance = quantize(d)
|
|
89
88
|
|
|
90
89
|
const k = `${x.toFixed(6)}|${y.toFixed(6)}`
|
|
@@ -7,6 +7,16 @@ import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
|
7
7
|
const quantize = (value: number, precision = 1e-6) =>
|
|
8
8
|
Math.round(value / precision) * precision
|
|
9
9
|
|
|
10
|
+
const toRect = (rect: XYRect | RTreeRect): XYRect =>
|
|
11
|
+
"minX" in rect
|
|
12
|
+
? {
|
|
13
|
+
x: rect.minX,
|
|
14
|
+
y: rect.minY,
|
|
15
|
+
width: rect.maxX - rect.minX,
|
|
16
|
+
height: rect.maxY - rect.minY,
|
|
17
|
+
}
|
|
18
|
+
: rect
|
|
19
|
+
|
|
10
20
|
/**
|
|
11
21
|
* Compute exact uncovered segments along a 1D line.
|
|
12
22
|
*/
|
|
@@ -109,6 +119,12 @@ export function computeEdgeCandidates3D(params: {
|
|
|
109
119
|
// Use small inset from edges for placement
|
|
110
120
|
const δ = Math.max(minSize * 0.15, EPS * 3)
|
|
111
121
|
const dedup = new Set<string>()
|
|
122
|
+
const hardRectsByLayer = Array.from({ length: layerCount }, (_, z) =>
|
|
123
|
+
[
|
|
124
|
+
...(obstacleIndexByLayer[z]?.all() ?? []),
|
|
125
|
+
...(hardPlacedByLayer[z] ?? []),
|
|
126
|
+
].map(toRect),
|
|
127
|
+
)
|
|
112
128
|
const key = (p: { x: number; y: number; z: number }) =>
|
|
113
129
|
`${p.z}|${p.x.toFixed(6)}|${p.y.toFixed(6)}`
|
|
114
130
|
|
|
@@ -137,21 +153,11 @@ export function computeEdgeCandidates3D(params: {
|
|
|
137
153
|
if (fullyOcc({ x, y })) return // new rule: only drop if truly impossible
|
|
138
154
|
|
|
139
155
|
// Distance uses obstacles + hard nodes (soft nodes ignored for ranking)
|
|
140
|
-
const hard = [
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
y: quantize(b.y),
|
|
146
|
-
width: quantize(b.width),
|
|
147
|
-
height: quantize(b.height),
|
|
148
|
-
}))
|
|
149
|
-
const d = Math.min(
|
|
150
|
-
distancePointToRectEdges({ x, y }, bounds),
|
|
151
|
-
...(hard.length
|
|
152
|
-
? hard.map((b) => distancePointToRectEdges({ x, y }, b))
|
|
153
|
-
: [Infinity]),
|
|
154
|
-
)
|
|
156
|
+
const hard = hardRectsByLayer[z] ?? []
|
|
157
|
+
let d = distancePointToRectEdges({ x, y }, bounds)
|
|
158
|
+
for (const blocker of hard) {
|
|
159
|
+
d = Math.min(d, distancePointToRectEdges({ x, y }, blocker))
|
|
160
|
+
}
|
|
155
161
|
const distance = quantize(d)
|
|
156
162
|
|
|
157
163
|
const k = key({ x, y, z })
|
|
@@ -180,10 +186,7 @@ export function computeEdgeCandidates3D(params: {
|
|
|
180
186
|
}
|
|
181
187
|
|
|
182
188
|
for (let z = 0; z < layerCount; z++) {
|
|
183
|
-
const blockers = [
|
|
184
|
-
...(obstacleIndexByLayer[z]?.all() ?? []),
|
|
185
|
-
...(hardPlacedByLayer[z] ?? []),
|
|
186
|
-
].map((b) => ({
|
|
189
|
+
const blockers = (hardRectsByLayer[z] ?? []).map((b) => ({
|
|
187
190
|
x: quantize(b.x),
|
|
188
191
|
y: quantize(b.y),
|
|
189
192
|
width: quantize(b.width),
|
|
@@ -35,7 +35,7 @@ export function longestFreeSpanAroundZ(params: {
|
|
|
35
35
|
maxY: y,
|
|
36
36
|
}
|
|
37
37
|
const obstacleIdx = obstacleIndexByLayer[layer]
|
|
38
|
-
if (obstacleIdx && obstacleIdx.
|
|
38
|
+
if (obstacleIdx && obstacleIdx.collides(query)) return false
|
|
39
39
|
|
|
40
40
|
const extras = additionalBlockersByLayer?.[layer] ?? []
|
|
41
41
|
return !extras.some((b) => containsPoint(b, { x, y }))
|
|
@@ -175,14 +175,13 @@ const toRect = (tree: RTreeRect): XYRect => ({
|
|
|
175
175
|
})
|
|
176
176
|
|
|
177
177
|
const addBlocker = (params: {
|
|
178
|
-
rect:
|
|
179
|
-
seen: Set<
|
|
178
|
+
rect: RTreeRect
|
|
179
|
+
seen: Set<RTreeRect>
|
|
180
180
|
blockers: XYRect[]
|
|
181
181
|
}) => {
|
|
182
182
|
const { rect, seen, blockers } = params
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
seen.add(key)
|
|
183
|
+
if (seen.has(rect)) return
|
|
184
|
+
seen.add(rect)
|
|
186
185
|
blockers.push(rect)
|
|
187
186
|
}
|
|
188
187
|
|
|
@@ -225,7 +224,7 @@ export function expandRectFromSeed(params: {
|
|
|
225
224
|
const initialW = Math.max(minSide, minReq.width)
|
|
226
225
|
const initialH = Math.max(minSide, minReq.height)
|
|
227
226
|
const blockers: XYRect[] = []
|
|
228
|
-
const seen = new Set<
|
|
227
|
+
const seen = new Set<RTreeRect>()
|
|
229
228
|
const totalLayers = placedIndexByLayer.length
|
|
230
229
|
|
|
231
230
|
// Ignore the existing placement we are expanding so it doesn't self-block.
|
|
@@ -237,7 +236,7 @@ export function expandRectFromSeed(params: {
|
|
|
237
236
|
const blockersIndex = obsticalIndexByLayer[z]
|
|
238
237
|
if (blockersIndex) {
|
|
239
238
|
for (const entry of blockersIndex.search(query))
|
|
240
|
-
addBlocker({ rect:
|
|
239
|
+
addBlocker({ rect: entry, seen, blockers })
|
|
241
240
|
}
|
|
242
241
|
|
|
243
242
|
const placedLayer = placedIndexByLayer[z]
|
|
@@ -245,10 +244,9 @@ export function expandRectFromSeed(params: {
|
|
|
245
244
|
for (const entry of placedLayer.search(query)) {
|
|
246
245
|
const isFullStack = entry.zLayers.length >= totalLayers
|
|
247
246
|
if (!isFullStack) continue
|
|
248
|
-
const rect = toRect(entry)
|
|
249
247
|
if (
|
|
250
248
|
isSelfRect({
|
|
251
|
-
rect,
|
|
249
|
+
rect: entry,
|
|
252
250
|
startX,
|
|
253
251
|
startY,
|
|
254
252
|
initialW,
|
|
@@ -256,7 +254,7 @@ export function expandRectFromSeed(params: {
|
|
|
256
254
|
})
|
|
257
255
|
)
|
|
258
256
|
continue
|
|
259
|
-
addBlocker({ rect, seen, blockers })
|
|
257
|
+
addBlocker({ rect: entry, seen, blockers })
|
|
260
258
|
}
|
|
261
259
|
}
|
|
262
260
|
}
|
|
@@ -17,10 +17,10 @@ export function isFullyOccupiedAtPoint(params: OccupancyParams): boolean {
|
|
|
17
17
|
}
|
|
18
18
|
for (let z = 0; z < params.layerCount; z++) {
|
|
19
19
|
const obstacleIdx = params.obstacleIndexByLayer[z]
|
|
20
|
-
const hasObstacle = !!obstacleIdx && obstacleIdx.
|
|
20
|
+
const hasObstacle = !!obstacleIdx && obstacleIdx.collides(query)
|
|
21
21
|
|
|
22
22
|
const placedIdx = params.placedIndexByLayer[z]
|
|
23
|
-
const hasPlaced = !!placedIdx && placedIdx.
|
|
23
|
+
const hasPlaced = !!placedIdx && placedIdx.collides(query)
|
|
24
24
|
|
|
25
25
|
if (!hasObstacle && !hasPlaced) return false
|
|
26
26
|
}
|
|
@@ -30,27 +30,20 @@ export function distancePointToRectEdges(
|
|
|
30
30
|
p: { x: number; y: number },
|
|
31
31
|
r: XYRect,
|
|
32
32
|
) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
for (const [x1, y1, x2, y2] of edges) {
|
|
41
|
-
const A = p.x - x1,
|
|
42
|
-
B = p.y - y1,
|
|
43
|
-
C = x2 - x1,
|
|
44
|
-
D = y2 - y1
|
|
45
|
-
const dot = A * C + B * D
|
|
46
|
-
const lenSq = C * C + D * D
|
|
47
|
-
let t = lenSq !== 0 ? dot / lenSq : 0
|
|
48
|
-
t = clamp(t, 0, 1)
|
|
49
|
-
const xx = x1 + t * C
|
|
50
|
-
const yy = y1 + t * D
|
|
51
|
-
best = Math.min(best, Math.hypot(p.x - xx, p.y - yy))
|
|
33
|
+
const minX = r.x
|
|
34
|
+
const maxX = r.x + r.width
|
|
35
|
+
const minY = r.y
|
|
36
|
+
const maxY = r.y + r.height
|
|
37
|
+
|
|
38
|
+
if (p.x >= minX && p.x <= maxX && p.y >= minY && p.y <= maxY) {
|
|
39
|
+
return Math.min(p.x - minX, maxX - p.x, p.y - minY, maxY - p.y)
|
|
52
40
|
}
|
|
53
|
-
|
|
41
|
+
|
|
42
|
+
const dx = p.x < minX ? minX - p.x : p.x > maxX ? p.x - maxX : 0
|
|
43
|
+
const dy = p.y < minY ? minY - p.y : p.y > maxY ? p.y - maxY : 0
|
|
44
|
+
if (dx === 0) return dy
|
|
45
|
+
if (dy === 0) return dx
|
|
46
|
+
return Math.hypot(dx, dy)
|
|
54
47
|
}
|
|
55
48
|
|
|
56
49
|
/** Find the intersection of two 1D intervals, or null if they don't overlap. */
|