@tscircuit/rectdiff 0.0.23 → 0.0.24
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/dist/index.js +133 -74
- package/lib/RectDiffPipeline.ts +4 -37
- package/lib/buildFinalRectDiffVisualization.ts +46 -0
- package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +0 -25
- package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +12 -2
- package/lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts +53 -13
- package/lib/utils/buildOutlineGraphics.ts +39 -0
- package/lib/utils/expandRectFromSeed.ts +11 -1
- package/package.json +1 -1
- package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +1 -1
- package/tests/solver/bugreport01-be84eb/__snapshots__/bugreport01-be84eb.snap.svg +2 -2
- package/tests/solver/bugreport02-bc4361/__snapshots__/bugreport02-bc4361.snap.svg +2 -2
- package/tests/solver/bugreport03-fe4a17/__snapshots__/bugreport03-fe4a17.snap.svg +1 -1
- package/tests/solver/bugreport07-d3f3be/__snapshots__/bugreport07-d3f3be.snap.svg +1 -1
- package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
- package/tests/solver/bugreport09-618e09/__snapshots__/bugreport09-618e09.snap.svg +2 -2
- package/tests/solver/bugreport10-71239a/__snapshots__/bugreport10-71239a.snap.svg +2 -2
- package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +1 -1
- package/tests/solver/bugreport11-b2de3c/bugreport11-b2de3c-clearance-equivalence.test.ts +52 -0
- package/tests/solver/bugreport12-35ce1c/__snapshots__/bugreport12-35ce1c.snap.svg +1 -1
- package/tests/solver/bugreport13-b9a758/__snapshots__/bugreport13-b9a758.snap.svg +2 -2
- package/tests/solver/bugreport16-d95f38/__snapshots__/bugreport16-d95f38.snap.svg +2 -2
- package/tests/solver/bugreport19/__snapshots__/bugreport19.snap.svg +1 -1
- package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
- package/tests/solver/bugreport23-LGA15x4/__snapshots__/bugreport23-LGA15x4.snap.svg +1 -1
- package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
- package/tests/solver/bugreport26-66b0b2/__snapshots__/bugreport26-66b0b2.snap.svg +2 -2
- package/tests/solver/bugreport27-dd3734/__snapshots__/bugreport27-dd3734.snap.svg +2 -2
- package/tests/solver/bugreport28-18a9ef/__snapshots__/bugreport28-18a9ef.snap.svg +2 -2
- package/tests/solver/bugreport29-7deae8/__snapshots__/bugreport29-7deae8.snap.svg +2 -2
- package/tests/solver/bugreport30-2174c8/__snapshots__/bugreport30-2174c8.snap.svg +1 -1
- package/tests/solver/bugreport33-213d45/__snapshots__/bugreport33-213d45.snap.svg +2 -2
- package/tests/solver/bugreport34-e9dea2/__snapshots__/bugreport34-e9dea2.snap.svg +2 -2
- package/tests/solver/bugreport35-191db9/__snapshots__/bugreport35-191db9.snap.svg +2 -2
- package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +1 -1
- package/tests/solver/interaction/__snapshots__/interaction.snap.svg +1 -1
- package/tests/solver/multi-point/__snapshots__/multi-point.snap.svg +1 -1
- package/tests/solver/pcb_trace_id-should-return-root-connection-name/__snapshots__/pcb_trace_id-should-return-root-connection-name.snap.svg +1 -1
- package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c-clearance.snap.svg +0 -44
- package/tests/solver/bugreport11-b2de3c/bugreport11-b2de3c-clearance.test.ts +0 -97
package/dist/index.js
CHANGED
|
@@ -960,6 +960,13 @@ var searchStripUp = ({
|
|
|
960
960
|
});
|
|
961
961
|
|
|
962
962
|
// lib/utils/expandRectFromSeed.ts
|
|
963
|
+
var quantize = (value, precision = 1e-6) => Math.round(value / precision) * precision;
|
|
964
|
+
var quantizeRect = (rect) => ({
|
|
965
|
+
x: quantize(rect.x),
|
|
966
|
+
y: quantize(rect.y),
|
|
967
|
+
width: quantize(rect.width),
|
|
968
|
+
height: quantize(rect.height)
|
|
969
|
+
});
|
|
963
970
|
function maxExpandRight(params) {
|
|
964
971
|
const { r, bounds, blockers, maxAspect } = params;
|
|
965
972
|
let maxWidth = bounds.x + bounds.width - r.x;
|
|
@@ -1173,7 +1180,7 @@ function expandRectFromSeed(params) {
|
|
|
1173
1180
|
if (r.width + EPS4 >= minReq.width && r.height + EPS4 >= minReq.height) {
|
|
1174
1181
|
const area = r.width * r.height;
|
|
1175
1182
|
if (area > bestArea) {
|
|
1176
|
-
best = r;
|
|
1183
|
+
best = quantizeRect(r);
|
|
1177
1184
|
bestArea = area;
|
|
1178
1185
|
}
|
|
1179
1186
|
}
|
|
@@ -1247,6 +1254,7 @@ function longestFreeSpanAroundZ(params) {
|
|
|
1247
1254
|
}
|
|
1248
1255
|
|
|
1249
1256
|
// lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts
|
|
1257
|
+
var quantize2 = (value, precision = 1e-6) => Math.round(value / precision) * precision;
|
|
1250
1258
|
function computeCandidates3D(params) {
|
|
1251
1259
|
const {
|
|
1252
1260
|
bounds,
|
|
@@ -1296,12 +1304,13 @@ function computeCandidates3D(params) {
|
|
|
1296
1304
|
distancePointToRectEdges({ x, y }, bounds),
|
|
1297
1305
|
...hardAtZ.length ? hardAtZ.map((b) => distancePointToRectEdges({ x, y }, b)) : [Infinity]
|
|
1298
1306
|
);
|
|
1307
|
+
const distance = quantize2(d);
|
|
1299
1308
|
const k = `${x.toFixed(6)}|${y.toFixed(6)}`;
|
|
1300
1309
|
const cand = {
|
|
1301
1310
|
x,
|
|
1302
1311
|
y,
|
|
1303
1312
|
z: anchorZ,
|
|
1304
|
-
distance
|
|
1313
|
+
distance,
|
|
1305
1314
|
zSpanLen: bestSpan.length
|
|
1306
1315
|
};
|
|
1307
1316
|
const prev = out.get(k);
|
|
@@ -1311,18 +1320,28 @@ function computeCandidates3D(params) {
|
|
|
1311
1320
|
}
|
|
1312
1321
|
}
|
|
1313
1322
|
const arr = Array.from(out.values());
|
|
1314
|
-
arr.sort(
|
|
1323
|
+
arr.sort(
|
|
1324
|
+
(a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance || a.z - b.z || a.x - b.x || a.y - b.y
|
|
1325
|
+
);
|
|
1315
1326
|
return arr;
|
|
1316
1327
|
}
|
|
1317
1328
|
|
|
1318
1329
|
// lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts
|
|
1330
|
+
var quantize3 = (value, precision = 1e-6) => Math.round(value / precision) * precision;
|
|
1319
1331
|
function computeUncoveredSegments(params) {
|
|
1320
1332
|
const { lineStart, lineEnd, coveringIntervals, minSegmentLength } = params;
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1333
|
+
const lineStartQ = quantize3(lineStart);
|
|
1334
|
+
const lineEndQ = quantize3(lineEnd);
|
|
1335
|
+
const normalizedIntervals = coveringIntervals.map((i) => {
|
|
1336
|
+
const s = quantize3(i.start);
|
|
1337
|
+
const e = quantize3(i.end);
|
|
1338
|
+
return { start: Math.min(s, e), end: Math.max(s, e) };
|
|
1339
|
+
}).filter((i) => i.end > i.start + EPS4);
|
|
1340
|
+
if (normalizedIntervals.length === 0) {
|
|
1341
|
+
const center = (lineStartQ + lineEndQ) / 2;
|
|
1342
|
+
return [{ start: lineStartQ, end: lineEndQ, center }];
|
|
1343
|
+
}
|
|
1344
|
+
const sorted = [...normalizedIntervals].sort((a, b) => a.start - b.start);
|
|
1326
1345
|
const merged = [];
|
|
1327
1346
|
let current = { ...sorted[0] };
|
|
1328
1347
|
for (let i = 1; i < sorted.length; i++) {
|
|
@@ -1336,8 +1355,8 @@ function computeUncoveredSegments(params) {
|
|
|
1336
1355
|
}
|
|
1337
1356
|
merged.push(current);
|
|
1338
1357
|
const uncovered = [];
|
|
1339
|
-
if (merged[0].start >
|
|
1340
|
-
const start =
|
|
1358
|
+
if (merged[0].start > lineStartQ + EPS4) {
|
|
1359
|
+
const start = lineStartQ;
|
|
1341
1360
|
const end = merged[0].start;
|
|
1342
1361
|
if (end - start >= minSegmentLength) {
|
|
1343
1362
|
uncovered.push({ start, end, center: (start + end) / 2 });
|
|
@@ -1350,9 +1369,9 @@ function computeUncoveredSegments(params) {
|
|
|
1350
1369
|
uncovered.push({ start, end, center: (start + end) / 2 });
|
|
1351
1370
|
}
|
|
1352
1371
|
}
|
|
1353
|
-
if (merged[merged.length - 1].end <
|
|
1372
|
+
if (merged[merged.length - 1].end < lineEndQ - EPS4) {
|
|
1354
1373
|
const start = merged[merged.length - 1].end;
|
|
1355
|
-
const end =
|
|
1374
|
+
const end = lineEndQ;
|
|
1356
1375
|
if (end - start >= minSegmentLength) {
|
|
1357
1376
|
uncovered.push({ start, end, center: (start + end) / 2 });
|
|
1358
1377
|
}
|
|
@@ -1381,18 +1400,28 @@ function computeEdgeCandidates3D(params) {
|
|
|
1381
1400
|
});
|
|
1382
1401
|
}
|
|
1383
1402
|
function pushIfFree(p) {
|
|
1384
|
-
const
|
|
1403
|
+
const qx = quantize3(p.x);
|
|
1404
|
+
const qy = quantize3(p.y);
|
|
1405
|
+
const { z } = p;
|
|
1406
|
+
const x = qx;
|
|
1407
|
+
const y = qy;
|
|
1385
1408
|
if (x < bounds.x + EPS4 || y < bounds.y + EPS4 || x > bounds.x + bounds.width - EPS4 || y > bounds.y + bounds.height - EPS4)
|
|
1386
1409
|
return;
|
|
1387
1410
|
if (fullyOcc({ x, y })) return;
|
|
1388
1411
|
const hard = [
|
|
1389
1412
|
...obstacleIndexByLayer[z]?.all() ?? [],
|
|
1390
1413
|
...hardPlacedByLayer[z] ?? []
|
|
1391
|
-
]
|
|
1414
|
+
].map((b) => ({
|
|
1415
|
+
x: quantize3(b.x),
|
|
1416
|
+
y: quantize3(b.y),
|
|
1417
|
+
width: quantize3(b.width),
|
|
1418
|
+
height: quantize3(b.height)
|
|
1419
|
+
}));
|
|
1392
1420
|
const d = Math.min(
|
|
1393
1421
|
distancePointToRectEdges({ x, y }, bounds),
|
|
1394
1422
|
...hard.length ? hard.map((b) => distancePointToRectEdges({ x, y }, b)) : [Infinity]
|
|
1395
1423
|
);
|
|
1424
|
+
const distance = quantize3(d);
|
|
1396
1425
|
const k = key({ x, y, z });
|
|
1397
1426
|
if (dedup.has(k)) return;
|
|
1398
1427
|
dedup.add(k);
|
|
@@ -1406,13 +1435,25 @@ function computeEdgeCandidates3D(params) {
|
|
|
1406
1435
|
obstacleIndexByLayer,
|
|
1407
1436
|
additionalBlockersByLayer: hardPlacedByLayer
|
|
1408
1437
|
});
|
|
1409
|
-
out.push({
|
|
1438
|
+
out.push({
|
|
1439
|
+
x,
|
|
1440
|
+
y,
|
|
1441
|
+
z,
|
|
1442
|
+
distance,
|
|
1443
|
+
zSpanLen: span.length,
|
|
1444
|
+
isEdgeSeed: true
|
|
1445
|
+
});
|
|
1410
1446
|
}
|
|
1411
1447
|
for (let z = 0; z < layerCount; z++) {
|
|
1412
1448
|
const blockers = [
|
|
1413
1449
|
...obstacleIndexByLayer[z]?.all() ?? [],
|
|
1414
1450
|
...hardPlacedByLayer[z] ?? []
|
|
1415
|
-
]
|
|
1451
|
+
].map((b) => ({
|
|
1452
|
+
x: quantize3(b.x),
|
|
1453
|
+
y: quantize3(b.y),
|
|
1454
|
+
width: quantize3(b.width),
|
|
1455
|
+
height: quantize3(b.height)
|
|
1456
|
+
}));
|
|
1416
1457
|
const corners = [
|
|
1417
1458
|
{ x: bounds.x + \u03B4, y: bounds.y + \u03B4 },
|
|
1418
1459
|
// top-left
|
|
@@ -1585,7 +1626,9 @@ function computeEdgeCandidates3D(params) {
|
|
|
1585
1626
|
}
|
|
1586
1627
|
}
|
|
1587
1628
|
}
|
|
1588
|
-
out.sort(
|
|
1629
|
+
out.sort(
|
|
1630
|
+
(a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance || a.z - b.z || a.x - b.x || a.y - b.y
|
|
1631
|
+
);
|
|
1589
1632
|
return out;
|
|
1590
1633
|
}
|
|
1591
1634
|
|
|
@@ -1976,29 +2019,6 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
|
|
|
1976
2019
|
});
|
|
1977
2020
|
}
|
|
1978
2021
|
}
|
|
1979
|
-
if (this.input.obstacleClearance && this.input.obstacleClearance > 0) {
|
|
1980
|
-
for (const obstacle of srj.obstacles ?? []) {
|
|
1981
|
-
const pad = this.input.obstacleClearance;
|
|
1982
|
-
const expanded = {
|
|
1983
|
-
x: obstacle.center.x - obstacle.width / 2 - pad,
|
|
1984
|
-
y: obstacle.center.y - obstacle.height / 2 - pad,
|
|
1985
|
-
width: obstacle.width + 2 * pad,
|
|
1986
|
-
height: obstacle.height + 2 * pad
|
|
1987
|
-
};
|
|
1988
|
-
rects.push({
|
|
1989
|
-
center: {
|
|
1990
|
-
x: expanded.x + expanded.width / 2,
|
|
1991
|
-
y: expanded.y + expanded.height / 2
|
|
1992
|
-
},
|
|
1993
|
-
width: expanded.width,
|
|
1994
|
-
height: expanded.height,
|
|
1995
|
-
fill: "rgba(234, 179, 8, 0.15)",
|
|
1996
|
-
stroke: "rgba(202, 138, 4, 0.9)",
|
|
1997
|
-
layer: "obstacle-clearance",
|
|
1998
|
-
label: "clearance"
|
|
1999
|
-
});
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
2022
|
if (this.boardVoidRects) {
|
|
2003
2023
|
let outlineBBox = null;
|
|
2004
2024
|
if (srj.outline && srj.outline.length > 0) {
|
|
@@ -2524,6 +2544,42 @@ z:${layerLabel}`
|
|
|
2524
2544
|
};
|
|
2525
2545
|
}
|
|
2526
2546
|
|
|
2547
|
+
// lib/buildFinalRectDiffVisualization.ts
|
|
2548
|
+
import { mergeGraphics } from "graphics-debug";
|
|
2549
|
+
|
|
2550
|
+
// lib/utils/buildOutlineGraphics.ts
|
|
2551
|
+
var buildOutlineGraphics = ({
|
|
2552
|
+
srj
|
|
2553
|
+
}) => {
|
|
2554
|
+
const hasOutline = srj.outline && srj.outline.length > 1;
|
|
2555
|
+
const lines = hasOutline ? [
|
|
2556
|
+
{
|
|
2557
|
+
points: [...srj.outline, srj.outline[0]],
|
|
2558
|
+
strokeColor: "#111827",
|
|
2559
|
+
strokeWidth: 0.1,
|
|
2560
|
+
label: "outline"
|
|
2561
|
+
}
|
|
2562
|
+
] : [
|
|
2563
|
+
{
|
|
2564
|
+
points: [
|
|
2565
|
+
{ x: srj.bounds.minX, y: srj.bounds.minY },
|
|
2566
|
+
{ x: srj.bounds.maxX, y: srj.bounds.minY },
|
|
2567
|
+
{ x: srj.bounds.maxX, y: srj.bounds.maxY },
|
|
2568
|
+
{ x: srj.bounds.minX, y: srj.bounds.maxY },
|
|
2569
|
+
{ x: srj.bounds.minX, y: srj.bounds.minY }
|
|
2570
|
+
],
|
|
2571
|
+
strokeColor: "#111827",
|
|
2572
|
+
strokeWidth: 0.1,
|
|
2573
|
+
label: "bounds"
|
|
2574
|
+
}
|
|
2575
|
+
];
|
|
2576
|
+
return {
|
|
2577
|
+
title: "SimpleRoute Outline",
|
|
2578
|
+
coordinateSystem: "cartesian",
|
|
2579
|
+
lines
|
|
2580
|
+
};
|
|
2581
|
+
};
|
|
2582
|
+
|
|
2527
2583
|
// lib/utils/renderObstacleClearance.ts
|
|
2528
2584
|
var buildObstacleClearanceGraphics = (params) => {
|
|
2529
2585
|
const { srj, clearance } = params;
|
|
@@ -2565,8 +2621,40 @@ z:${(obstacle.zLayers ?? []).join(",") || "all"}`
|
|
|
2565
2621
|
};
|
|
2566
2622
|
};
|
|
2567
2623
|
|
|
2624
|
+
// lib/buildFinalRectDiffVisualization.ts
|
|
2625
|
+
var buildFinalRectDiffVisualization = ({
|
|
2626
|
+
srj,
|
|
2627
|
+
meshNodes,
|
|
2628
|
+
obstacleClearance
|
|
2629
|
+
}) => {
|
|
2630
|
+
const outline = buildOutlineGraphics({ srj });
|
|
2631
|
+
const clearance = buildObstacleClearanceGraphics({
|
|
2632
|
+
srj,
|
|
2633
|
+
clearance: obstacleClearance
|
|
2634
|
+
});
|
|
2635
|
+
const rects = meshNodes.map((node) => ({
|
|
2636
|
+
center: node.center,
|
|
2637
|
+
width: node.width,
|
|
2638
|
+
height: node.height,
|
|
2639
|
+
stroke: getColorForZLayer(node.availableZ).stroke,
|
|
2640
|
+
fill: node._containsObstacle ? "#fca5a5" : getColorForZLayer(node.availableZ).fill,
|
|
2641
|
+
layer: `z${node.availableZ.join(",")}`,
|
|
2642
|
+
label: `node ${node.capacityMeshNodeId}
|
|
2643
|
+
z:${node.availableZ.join(",")}`
|
|
2644
|
+
}));
|
|
2645
|
+
const nodesGraphic = {
|
|
2646
|
+
title: "RectDiffPipeline - Final",
|
|
2647
|
+
coordinateSystem: "cartesian",
|
|
2648
|
+
rects,
|
|
2649
|
+
lines: [],
|
|
2650
|
+
points: [],
|
|
2651
|
+
texts: []
|
|
2652
|
+
};
|
|
2653
|
+
return mergeGraphics(mergeGraphics(nodesGraphic, outline), clearance);
|
|
2654
|
+
};
|
|
2655
|
+
|
|
2568
2656
|
// lib/RectDiffPipeline.ts
|
|
2569
|
-
import { mergeGraphics } from "graphics-debug";
|
|
2657
|
+
import { mergeGraphics as mergeGraphics2 } from "graphics-debug";
|
|
2570
2658
|
var RectDiffPipeline = class extends BasePipelineSolver3 {
|
|
2571
2659
|
rectDiffGridSolverPipeline;
|
|
2572
2660
|
gapFillSolver;
|
|
@@ -2665,43 +2753,14 @@ var RectDiffPipeline = class extends BasePipelineSolver3 {
|
|
|
2665
2753
|
].join("\n")
|
|
2666
2754
|
}))
|
|
2667
2755
|
};
|
|
2668
|
-
return
|
|
2756
|
+
return mergeGraphics2(mergeGraphics2(base, clearance), nodeRects);
|
|
2669
2757
|
}
|
|
2670
2758
|
finalVisualize() {
|
|
2671
|
-
|
|
2672
|
-
this.inputProblem.simpleRouteJson,
|
|
2673
|
-
"RectDiffPipeline - Final"
|
|
2674
|
-
);
|
|
2675
|
-
const clearance = buildObstacleClearanceGraphics({
|
|
2759
|
+
return buildFinalRectDiffVisualization({
|
|
2676
2760
|
srj: this.inputProblem.simpleRouteJson,
|
|
2677
|
-
|
|
2761
|
+
meshNodes: this.getOutput().meshNodes,
|
|
2762
|
+
obstacleClearance: this.inputProblem.obstacleClearance
|
|
2678
2763
|
});
|
|
2679
|
-
const { meshNodes: outputNodes } = this.getOutput();
|
|
2680
|
-
const initialNodeIds = new Set(
|
|
2681
|
-
(this.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? []).map(
|
|
2682
|
-
(n) => n.capacityMeshNodeId
|
|
2683
|
-
)
|
|
2684
|
-
);
|
|
2685
|
-
const nodeRects = {
|
|
2686
|
-
title: "Final Nodes",
|
|
2687
|
-
coordinateSystem: "cartesian",
|
|
2688
|
-
rects: outputNodes.map((node) => {
|
|
2689
|
-
const isExpanded = !initialNodeIds.has(node.capacityMeshNodeId);
|
|
2690
|
-
return {
|
|
2691
|
-
center: node.center,
|
|
2692
|
-
width: node.width,
|
|
2693
|
-
height: node.height,
|
|
2694
|
-
stroke: isExpanded ? "rgba(0, 128, 0, 0.8)" : "rgba(0, 0, 0, 0.3)",
|
|
2695
|
-
fill: isExpanded ? "rgba(0, 200, 0, 0.3)" : "rgba(100, 100, 100, 0.1)",
|
|
2696
|
-
layer: `z${node.availableZ.join(",")}`,
|
|
2697
|
-
label: [
|
|
2698
|
-
`${isExpanded ? "[expanded] " : ""}node ${node.capacityMeshNodeId}`,
|
|
2699
|
-
`z:${node.availableZ.join(",")}`
|
|
2700
|
-
].join("\n")
|
|
2701
|
-
};
|
|
2702
|
-
})
|
|
2703
|
-
};
|
|
2704
|
-
return mergeGraphics(mergeGraphics(base, clearance), nodeRects);
|
|
2705
2764
|
}
|
|
2706
2765
|
};
|
|
2707
2766
|
export {
|
package/lib/RectDiffPipeline.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { GraphicsObject } from "graphics-debug"
|
|
|
10
10
|
import { GapFillSolverPipeline } from "./solvers/GapFillSolver/GapFillSolverPipeline"
|
|
11
11
|
import { RectDiffGridSolverPipeline } from "./solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline"
|
|
12
12
|
import { createBaseVisualization } from "./rectdiff-visualization"
|
|
13
|
+
import { buildFinalRectDiffVisualization } from "./buildFinalRectDiffVisualization"
|
|
13
14
|
import { computeInverseRects } from "./solvers/RectDiffSeedingSolver/computeInverseRects"
|
|
14
15
|
import { buildZIndexMap } from "./solvers/RectDiffSeedingSolver/layers"
|
|
15
16
|
import { buildObstacleClearanceGraphics } from "./utils/renderObstacleClearance"
|
|
@@ -144,44 +145,10 @@ export class RectDiffPipeline extends BasePipelineSolver<RectDiffPipelineInput>
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
override finalVisualize(): GraphicsObject {
|
|
147
|
-
|
|
148
|
-
this.inputProblem.simpleRouteJson,
|
|
149
|
-
"RectDiffPipeline - Final",
|
|
150
|
-
)
|
|
151
|
-
const clearance = buildObstacleClearanceGraphics({
|
|
148
|
+
return buildFinalRectDiffVisualization({
|
|
152
149
|
srj: this.inputProblem.simpleRouteJson,
|
|
153
|
-
|
|
150
|
+
meshNodes: this.getOutput().meshNodes,
|
|
151
|
+
obstacleClearance: this.inputProblem.obstacleClearance,
|
|
154
152
|
})
|
|
155
|
-
|
|
156
|
-
const { meshNodes: outputNodes } = this.getOutput()
|
|
157
|
-
const initialNodeIds = new Set(
|
|
158
|
-
(this.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? []).map(
|
|
159
|
-
(n) => n.capacityMeshNodeId,
|
|
160
|
-
),
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
const nodeRects: GraphicsObject = {
|
|
164
|
-
title: "Final Nodes",
|
|
165
|
-
coordinateSystem: "cartesian",
|
|
166
|
-
rects: outputNodes.map((node) => {
|
|
167
|
-
const isExpanded = !initialNodeIds.has(node.capacityMeshNodeId)
|
|
168
|
-
return {
|
|
169
|
-
center: node.center,
|
|
170
|
-
width: node.width,
|
|
171
|
-
height: node.height,
|
|
172
|
-
stroke: isExpanded ? "rgba(0, 128, 0, 0.8)" : "rgba(0, 0, 0, 0.3)",
|
|
173
|
-
fill: isExpanded
|
|
174
|
-
? "rgba(0, 200, 0, 0.3)"
|
|
175
|
-
: "rgba(100, 100, 100, 0.1)",
|
|
176
|
-
layer: `z${node.availableZ.join(",")}`,
|
|
177
|
-
label: [
|
|
178
|
-
`${isExpanded ? "[expanded] " : ""}node ${node.capacityMeshNodeId}`,
|
|
179
|
-
`z:${node.availableZ.join(",")}`,
|
|
180
|
-
].join("\n"),
|
|
181
|
-
}
|
|
182
|
-
}),
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return mergeGraphics(mergeGraphics(base, clearance), nodeRects)
|
|
186
153
|
}
|
|
187
154
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { mergeGraphics, type GraphicsObject } from "graphics-debug"
|
|
2
|
+
import type { CapacityMeshNode } from "./types/capacity-mesh-types"
|
|
3
|
+
import type { SimpleRouteJson } from "./types/srj-types"
|
|
4
|
+
import { getColorForZLayer } from "./utils/getColorForZLayer"
|
|
5
|
+
import { buildOutlineGraphics } from "./utils/buildOutlineGraphics"
|
|
6
|
+
import { buildObstacleClearanceGraphics } from "./utils/renderObstacleClearance"
|
|
7
|
+
|
|
8
|
+
type BuildFinalVisualizationParams = {
|
|
9
|
+
srj: SimpleRouteJson
|
|
10
|
+
meshNodes: CapacityMeshNode[]
|
|
11
|
+
obstacleClearance?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const buildFinalRectDiffVisualization = ({
|
|
15
|
+
srj,
|
|
16
|
+
meshNodes,
|
|
17
|
+
obstacleClearance,
|
|
18
|
+
}: BuildFinalVisualizationParams): GraphicsObject => {
|
|
19
|
+
const outline = buildOutlineGraphics({ srj })
|
|
20
|
+
const clearance = buildObstacleClearanceGraphics({
|
|
21
|
+
srj,
|
|
22
|
+
clearance: obstacleClearance,
|
|
23
|
+
})
|
|
24
|
+
const rects = meshNodes.map((node) => ({
|
|
25
|
+
center: node.center,
|
|
26
|
+
width: node.width,
|
|
27
|
+
height: node.height,
|
|
28
|
+
stroke: getColorForZLayer(node.availableZ).stroke,
|
|
29
|
+
fill: node._containsObstacle
|
|
30
|
+
? "#fca5a5"
|
|
31
|
+
: getColorForZLayer(node.availableZ).fill,
|
|
32
|
+
layer: `z${node.availableZ.join(",")}`,
|
|
33
|
+
label: `node ${node.capacityMeshNodeId}\nz:${node.availableZ.join(",")}`,
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
const nodesGraphic: GraphicsObject = {
|
|
37
|
+
title: "RectDiffPipeline - Final",
|
|
38
|
+
coordinateSystem: "cartesian",
|
|
39
|
+
rects,
|
|
40
|
+
lines: [],
|
|
41
|
+
points: [],
|
|
42
|
+
texts: [],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return mergeGraphics(mergeGraphics(nodesGraphic, outline), clearance)
|
|
46
|
+
}
|
|
@@ -392,31 +392,6 @@ export class RectDiffSeedingSolver extends BaseSolver {
|
|
|
392
392
|
}
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
// obstacle clearance visualization (expanded)
|
|
396
|
-
if (this.input.obstacleClearance && this.input.obstacleClearance > 0) {
|
|
397
|
-
for (const obstacle of srj.obstacles ?? []) {
|
|
398
|
-
const pad = this.input.obstacleClearance
|
|
399
|
-
const expanded = {
|
|
400
|
-
x: obstacle.center.x - obstacle.width / 2 - pad,
|
|
401
|
-
y: obstacle.center.y - obstacle.height / 2 - pad,
|
|
402
|
-
width: obstacle.width + 2 * pad,
|
|
403
|
-
height: obstacle.height + 2 * pad,
|
|
404
|
-
}
|
|
405
|
-
rects.push({
|
|
406
|
-
center: {
|
|
407
|
-
x: expanded.x + expanded.width / 2,
|
|
408
|
-
y: expanded.y + expanded.height / 2,
|
|
409
|
-
},
|
|
410
|
-
width: expanded.width,
|
|
411
|
-
height: expanded.height,
|
|
412
|
-
fill: "rgba(234, 179, 8, 0.15)",
|
|
413
|
-
stroke: "rgba(202, 138, 4, 0.9)",
|
|
414
|
-
layer: "obstacle-clearance",
|
|
415
|
-
label: "clearance",
|
|
416
|
-
})
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
395
|
// board void rects (early visualization of mask)
|
|
421
396
|
if (this.boardVoidRects) {
|
|
422
397
|
let outlineBBox: {
|
|
@@ -4,6 +4,8 @@ import { isFullyOccupiedAtPoint } from "../../utils/isFullyOccupiedAtPoint"
|
|
|
4
4
|
import { longestFreeSpanAroundZ } from "./longestFreeSpanAroundZ"
|
|
5
5
|
import type RBush from "rbush"
|
|
6
6
|
import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
7
|
+
const quantize = (value: number, precision = 1e-6) =>
|
|
8
|
+
Math.round(value / precision) * precision
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Compute candidate seed points for a given grid size.
|
|
@@ -83,13 +85,14 @@ export function computeCandidates3D(params: {
|
|
|
83
85
|
? hardAtZ.map((b) => distancePointToRectEdges({ x, y }, b))
|
|
84
86
|
: [Infinity]),
|
|
85
87
|
)
|
|
88
|
+
const distance = quantize(d)
|
|
86
89
|
|
|
87
90
|
const k = `${x.toFixed(6)}|${y.toFixed(6)}`
|
|
88
91
|
const cand: Candidate3D = {
|
|
89
92
|
x,
|
|
90
93
|
y,
|
|
91
94
|
z: anchorZ,
|
|
92
|
-
distance
|
|
95
|
+
distance,
|
|
93
96
|
zSpanLen: bestSpan.length,
|
|
94
97
|
}
|
|
95
98
|
const prev = out.get(k)
|
|
@@ -104,6 +107,13 @@ export function computeCandidates3D(params: {
|
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
const arr = Array.from(out.values())
|
|
107
|
-
arr.sort(
|
|
110
|
+
arr.sort(
|
|
111
|
+
(a, b) =>
|
|
112
|
+
b.zSpanLen! - a.zSpanLen! ||
|
|
113
|
+
b.distance - a.distance ||
|
|
114
|
+
a.z - b.z ||
|
|
115
|
+
a.x - b.x ||
|
|
116
|
+
a.y - b.y,
|
|
117
|
+
)
|
|
108
118
|
return arr
|
|
109
119
|
}
|
|
@@ -4,6 +4,8 @@ import { isFullyOccupiedAtPoint } from "../../utils/isFullyOccupiedAtPoint"
|
|
|
4
4
|
import { longestFreeSpanAroundZ } from "./longestFreeSpanAroundZ"
|
|
5
5
|
import type RBush from "rbush"
|
|
6
6
|
import type { RTreeRect } from "lib/types/capacity-mesh-types"
|
|
7
|
+
const quantize = (value: number, precision = 1e-6) =>
|
|
8
|
+
Math.round(value / precision) * precision
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Compute exact uncovered segments along a 1D line.
|
|
@@ -15,14 +17,23 @@ function computeUncoveredSegments(params: {
|
|
|
15
17
|
minSegmentLength: number
|
|
16
18
|
}): Array<{ start: number; end: number; center: number }> {
|
|
17
19
|
const { lineStart, lineEnd, coveringIntervals, minSegmentLength } = params
|
|
20
|
+
const lineStartQ = quantize(lineStart)
|
|
21
|
+
const lineEndQ = quantize(lineEnd)
|
|
22
|
+
const normalizedIntervals = coveringIntervals
|
|
23
|
+
.map((i) => {
|
|
24
|
+
const s = quantize(i.start)
|
|
25
|
+
const e = quantize(i.end)
|
|
26
|
+
return { start: Math.min(s, e), end: Math.max(s, e) }
|
|
27
|
+
})
|
|
28
|
+
.filter((i) => i.end > i.start + EPS)
|
|
18
29
|
|
|
19
|
-
if (
|
|
20
|
-
const center = (
|
|
21
|
-
return [{ start:
|
|
30
|
+
if (normalizedIntervals.length === 0) {
|
|
31
|
+
const center = (lineStartQ + lineEndQ) / 2
|
|
32
|
+
return [{ start: lineStartQ, end: lineEndQ, center }]
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
// Sort intervals by start position
|
|
25
|
-
const sorted = [...
|
|
36
|
+
const sorted = [...normalizedIntervals].sort((a, b) => a.start - b.start)
|
|
26
37
|
|
|
27
38
|
// Merge overlapping intervals
|
|
28
39
|
const merged: Array<{ start: number; end: number }> = []
|
|
@@ -45,8 +56,8 @@ function computeUncoveredSegments(params: {
|
|
|
45
56
|
const uncovered: Array<{ start: number; end: number; center: number }> = []
|
|
46
57
|
|
|
47
58
|
// Check gap before first interval
|
|
48
|
-
if (merged[0]!.start >
|
|
49
|
-
const start =
|
|
59
|
+
if (merged[0]!.start > lineStartQ + EPS) {
|
|
60
|
+
const start = lineStartQ
|
|
50
61
|
const end = merged[0]!.start
|
|
51
62
|
if (end - start >= minSegmentLength) {
|
|
52
63
|
uncovered.push({ start, end, center: (start + end) / 2 })
|
|
@@ -63,9 +74,9 @@ function computeUncoveredSegments(params: {
|
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
// Check gap after last interval
|
|
66
|
-
if (merged[merged.length - 1]!.end <
|
|
77
|
+
if (merged[merged.length - 1]!.end < lineEndQ - EPS) {
|
|
67
78
|
const start = merged[merged.length - 1]!.end
|
|
68
|
-
const end =
|
|
79
|
+
const end = lineEndQ
|
|
69
80
|
if (end - start >= minSegmentLength) {
|
|
70
81
|
uncovered.push({ start, end, center: (start + end) / 2 })
|
|
71
82
|
}
|
|
@@ -111,7 +122,11 @@ export function computeEdgeCandidates3D(params: {
|
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
function pushIfFree(p: { x: number; y: number; z: number }) {
|
|
114
|
-
const
|
|
125
|
+
const qx = quantize(p.x)
|
|
126
|
+
const qy = quantize(p.y)
|
|
127
|
+
const { z } = p
|
|
128
|
+
const x = qx
|
|
129
|
+
const y = qy
|
|
115
130
|
if (
|
|
116
131
|
x < bounds.x + EPS ||
|
|
117
132
|
y < bounds.y + EPS ||
|
|
@@ -125,13 +140,19 @@ export function computeEdgeCandidates3D(params: {
|
|
|
125
140
|
const hard = [
|
|
126
141
|
...(obstacleIndexByLayer[z]?.all() ?? []),
|
|
127
142
|
...(hardPlacedByLayer[z] ?? []),
|
|
128
|
-
]
|
|
143
|
+
].map((b) => ({
|
|
144
|
+
x: quantize(b.x),
|
|
145
|
+
y: quantize(b.y),
|
|
146
|
+
width: quantize(b.width),
|
|
147
|
+
height: quantize(b.height),
|
|
148
|
+
}))
|
|
129
149
|
const d = Math.min(
|
|
130
150
|
distancePointToRectEdges({ x, y }, bounds),
|
|
131
151
|
...(hard.length
|
|
132
152
|
? hard.map((b) => distancePointToRectEdges({ x, y }, b))
|
|
133
153
|
: [Infinity]),
|
|
134
154
|
)
|
|
155
|
+
const distance = quantize(d)
|
|
135
156
|
|
|
136
157
|
const k = key({ x, y, z })
|
|
137
158
|
if (dedup.has(k)) return
|
|
@@ -148,14 +169,26 @@ export function computeEdgeCandidates3D(params: {
|
|
|
148
169
|
obstacleIndexByLayer,
|
|
149
170
|
additionalBlockersByLayer: hardPlacedByLayer,
|
|
150
171
|
})
|
|
151
|
-
out.push({
|
|
172
|
+
out.push({
|
|
173
|
+
x,
|
|
174
|
+
y,
|
|
175
|
+
z,
|
|
176
|
+
distance,
|
|
177
|
+
zSpanLen: span.length,
|
|
178
|
+
isEdgeSeed: true,
|
|
179
|
+
})
|
|
152
180
|
}
|
|
153
181
|
|
|
154
182
|
for (let z = 0; z < layerCount; z++) {
|
|
155
183
|
const blockers = [
|
|
156
184
|
...(obstacleIndexByLayer[z]?.all() ?? []),
|
|
157
185
|
...(hardPlacedByLayer[z] ?? []),
|
|
158
|
-
]
|
|
186
|
+
].map((b) => ({
|
|
187
|
+
x: quantize(b.x),
|
|
188
|
+
y: quantize(b.y),
|
|
189
|
+
width: quantize(b.width),
|
|
190
|
+
height: quantize(b.height),
|
|
191
|
+
}))
|
|
159
192
|
|
|
160
193
|
// 1) Board edges — find exact uncovered segments along each edge
|
|
161
194
|
|
|
@@ -372,6 +405,13 @@ export function computeEdgeCandidates3D(params: {
|
|
|
372
405
|
}
|
|
373
406
|
|
|
374
407
|
// Strong multi-layer preference then distance.
|
|
375
|
-
out.sort(
|
|
408
|
+
out.sort(
|
|
409
|
+
(a, b) =>
|
|
410
|
+
b.zSpanLen! - a.zSpanLen! ||
|
|
411
|
+
b.distance - a.distance ||
|
|
412
|
+
a.z - b.z ||
|
|
413
|
+
a.x - b.x ||
|
|
414
|
+
a.y - b.y,
|
|
415
|
+
)
|
|
376
416
|
return out
|
|
377
417
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { GraphicsObject, Line } from "graphics-debug"
|
|
2
|
+
import type { SimpleRouteJson } from "../types/srj-types"
|
|
3
|
+
|
|
4
|
+
export type BuildOutlineParams = { srj: SimpleRouteJson }
|
|
5
|
+
|
|
6
|
+
export const buildOutlineGraphics = ({
|
|
7
|
+
srj,
|
|
8
|
+
}: BuildOutlineParams): GraphicsObject => {
|
|
9
|
+
const hasOutline = srj.outline && srj.outline.length > 1
|
|
10
|
+
const lines: NonNullable<Line[]> = hasOutline
|
|
11
|
+
? [
|
|
12
|
+
{
|
|
13
|
+
points: [...srj.outline!, srj.outline![0]!],
|
|
14
|
+
strokeColor: "#111827",
|
|
15
|
+
strokeWidth: 0.1,
|
|
16
|
+
label: "outline",
|
|
17
|
+
},
|
|
18
|
+
]
|
|
19
|
+
: [
|
|
20
|
+
{
|
|
21
|
+
points: [
|
|
22
|
+
{ x: srj.bounds.minX, y: srj.bounds.minY },
|
|
23
|
+
{ x: srj.bounds.maxX, y: srj.bounds.minY },
|
|
24
|
+
{ x: srj.bounds.maxX, y: srj.bounds.maxY },
|
|
25
|
+
{ x: srj.bounds.minX, y: srj.bounds.maxY },
|
|
26
|
+
{ x: srj.bounds.minX, y: srj.bounds.minY },
|
|
27
|
+
],
|
|
28
|
+
strokeColor: "#111827",
|
|
29
|
+
strokeWidth: 0.1,
|
|
30
|
+
label: "bounds",
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
title: "SimpleRoute Outline",
|
|
36
|
+
coordinateSystem: "cartesian",
|
|
37
|
+
lines,
|
|
38
|
+
}
|
|
39
|
+
}
|