@tscircuit/rectdiff 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.
Files changed (47) hide show
  1. package/AGENTS.md +23 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +195 -144
  4. package/lib/RectDiffPipeline.ts +4 -37
  5. package/lib/buildFinalRectDiffVisualization.ts +46 -0
  6. package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +23 -48
  7. package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +21 -12
  8. package/lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts +68 -25
  9. package/lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts +1 -1
  10. package/lib/utils/buildOutlineGraphics.ts +39 -0
  11. package/lib/utils/expandRectFromSeed.ts +19 -11
  12. package/lib/utils/isFullyOccupiedAtPoint.ts +2 -2
  13. package/lib/utils/rectdiff-geometry.ts +13 -20
  14. package/package.json +2 -1
  15. package/scripts/benchmark-slow-problem.ts +94 -0
  16. package/test-assets/keyboard4.json +16165 -0
  17. package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +1 -1
  18. package/tests/solver/bugreport01-be84eb/__snapshots__/bugreport01-be84eb.snap.svg +2 -2
  19. package/tests/solver/bugreport02-bc4361/__snapshots__/bugreport02-bc4361.snap.svg +2 -2
  20. package/tests/solver/bugreport03-fe4a17/__snapshots__/bugreport03-fe4a17.snap.svg +1 -1
  21. package/tests/solver/bugreport07-d3f3be/__snapshots__/bugreport07-d3f3be.snap.svg +1 -1
  22. package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
  23. package/tests/solver/bugreport09-618e09/__snapshots__/bugreport09-618e09.snap.svg +2 -2
  24. package/tests/solver/bugreport10-71239a/__snapshots__/bugreport10-71239a.snap.svg +2 -2
  25. package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +1 -1
  26. package/tests/solver/bugreport11-b2de3c/bugreport11-b2de3c-clearance-equivalence.test.ts +52 -0
  27. package/tests/solver/bugreport12-35ce1c/__snapshots__/bugreport12-35ce1c.snap.svg +1 -1
  28. package/tests/solver/bugreport13-b9a758/__snapshots__/bugreport13-b9a758.snap.svg +2 -2
  29. package/tests/solver/bugreport16-d95f38/__snapshots__/bugreport16-d95f38.snap.svg +2 -2
  30. package/tests/solver/bugreport19/__snapshots__/bugreport19.snap.svg +1 -1
  31. package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
  32. package/tests/solver/bugreport23-LGA15x4/__snapshots__/bugreport23-LGA15x4.snap.svg +1 -1
  33. package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
  34. package/tests/solver/bugreport26-66b0b2/__snapshots__/bugreport26-66b0b2.snap.svg +2 -2
  35. package/tests/solver/bugreport27-dd3734/__snapshots__/bugreport27-dd3734.snap.svg +2 -2
  36. package/tests/solver/bugreport28-18a9ef/__snapshots__/bugreport28-18a9ef.snap.svg +2 -2
  37. package/tests/solver/bugreport29-7deae8/__snapshots__/bugreport29-7deae8.snap.svg +2 -2
  38. package/tests/solver/bugreport30-2174c8/__snapshots__/bugreport30-2174c8.snap.svg +1 -1
  39. package/tests/solver/bugreport33-213d45/__snapshots__/bugreport33-213d45.snap.svg +2 -2
  40. package/tests/solver/bugreport34-e9dea2/__snapshots__/bugreport34-e9dea2.snap.svg +2 -2
  41. package/tests/solver/bugreport35-191db9/__snapshots__/bugreport35-191db9.snap.svg +2 -2
  42. package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +1 -1
  43. package/tests/solver/interaction/__snapshots__/interaction.snap.svg +1 -1
  44. package/tests/solver/multi-point/__snapshots__/multi-point.snap.svg +1 -1
  45. package/tests/solver/pcb_trace_id-should-return-root-connection-name/__snapshots__/pcb_trace_id-should-return-root-connection-name.snap.svg +1 -1
  46. package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c-clearance.snap.svg +0 -44
  47. package/tests/solver/bugreport11-b2de3c/bugreport11-b2de3c-clearance.test.ts +0 -97
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
@@ -242,6 +242,7 @@ declare class RectDiffSeedingSolver extends BaseSolver {
242
242
  private candidates;
243
243
  private placed;
244
244
  private placedIndexByLayer;
245
+ private hardPlacedByLayer;
245
246
  private expansionIndex;
246
247
  private edgeAnalysisDone;
247
248
  private totalSeedsThisGrid;
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 edges = [
692
- [r.x, r.y, r.x + r.width, r.y],
693
- [r.x + r.width, r.y, r.x + r.width, r.y + r.height],
694
- [r.x + r.width, r.y + r.height, r.x, r.y + r.height],
695
- [r.x, r.y + r.height, r.x, r.y]
696
- ];
697
- let best = Infinity;
698
- for (const [x1, y1, x2, y2] of edges) {
699
- const A = p.x - x1, B = p.y - y1, C = x2 - x1, D = y2 - y1;
700
- const dot = A * C + B * D;
701
- const lenSq = C * C + D * D;
702
- let t = lenSq !== 0 ? dot / lenSq : 0;
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]);
@@ -960,6 +954,13 @@ var searchStripUp = ({
960
954
  });
961
955
 
962
956
  // lib/utils/expandRectFromSeed.ts
957
+ var quantize = (value, precision = 1e-6) => Math.round(value / precision) * precision;
958
+ var quantizeRect = (rect) => ({
959
+ x: quantize(rect.x),
960
+ y: quantize(rect.y),
961
+ width: quantize(rect.width),
962
+ height: quantize(rect.height)
963
+ });
963
964
  function maxExpandRight(params) {
964
965
  const { r, bounds, blockers, maxAspect } = params;
965
966
  let maxWidth = bounds.x + bounds.width - r.x;
@@ -1044,17 +1045,10 @@ function maxExpandUp(params) {
1044
1045
  }
1045
1046
  return Math.max(0, e);
1046
1047
  }
1047
- var toRect = (tree) => ({
1048
- x: tree.minX,
1049
- y: tree.minY,
1050
- width: tree.maxX - tree.minX,
1051
- height: tree.maxY - tree.minY
1052
- });
1053
1048
  var addBlocker = (params) => {
1054
1049
  const { rect, seen, blockers } = params;
1055
- const key = `${rect.x}|${rect.y}|${rect.width}|${rect.height}`;
1056
- if (seen.has(key)) return;
1057
- seen.add(key);
1050
+ if (seen.has(rect)) return;
1051
+ seen.add(rect);
1058
1052
  blockers.push(rect);
1059
1053
  };
1060
1054
  var toQueryRect = (params) => {
@@ -1091,23 +1085,22 @@ function expandRectFromSeed(params) {
1091
1085
  const blockersIndex = obsticalIndexByLayer[z];
1092
1086
  if (blockersIndex) {
1093
1087
  for (const entry of blockersIndex.search(query))
1094
- addBlocker({ rect: toRect(entry), seen, blockers });
1088
+ addBlocker({ rect: entry, seen, blockers });
1095
1089
  }
1096
1090
  const placedLayer = placedIndexByLayer[z];
1097
1091
  if (placedLayer) {
1098
1092
  for (const entry of placedLayer.search(query)) {
1099
1093
  const isFullStack = entry.zLayers.length >= totalLayers;
1100
1094
  if (!isFullStack) continue;
1101
- const rect = toRect(entry);
1102
1095
  if (isSelfRect({
1103
- rect,
1096
+ rect: entry,
1104
1097
  startX,
1105
1098
  startY,
1106
1099
  initialW,
1107
1100
  initialH
1108
1101
  }))
1109
1102
  continue;
1110
- addBlocker({ rect, seen, blockers });
1103
+ addBlocker({ rect: entry, seen, blockers });
1111
1104
  }
1112
1105
  }
1113
1106
  }
@@ -1173,7 +1166,7 @@ function expandRectFromSeed(params) {
1173
1166
  if (r.width + EPS4 >= minReq.width && r.height + EPS4 >= minReq.height) {
1174
1167
  const area = r.width * r.height;
1175
1168
  if (area > bestArea) {
1176
- best = r;
1169
+ best = quantizeRect(r);
1177
1170
  bestArea = area;
1178
1171
  }
1179
1172
  }
@@ -1198,9 +1191,9 @@ function isFullyOccupiedAtPoint(params) {
1198
1191
  };
1199
1192
  for (let z = 0; z < params.layerCount; z++) {
1200
1193
  const obstacleIdx = params.obstacleIndexByLayer[z];
1201
- const hasObstacle = !!obstacleIdx && obstacleIdx.search(query).length > 0;
1194
+ const hasObstacle = !!obstacleIdx && obstacleIdx.collides(query);
1202
1195
  const placedIdx = params.placedIndexByLayer[z];
1203
- const hasPlaced = !!placedIdx && placedIdx.search(query).length > 0;
1196
+ const hasPlaced = !!placedIdx && placedIdx.collides(query);
1204
1197
  if (!hasObstacle && !hasPlaced) return false;
1205
1198
  }
1206
1199
  return true;
@@ -1226,7 +1219,7 @@ function longestFreeSpanAroundZ(params) {
1226
1219
  maxY: y
1227
1220
  };
1228
1221
  const obstacleIdx = obstacleIndexByLayer[layer];
1229
- if (obstacleIdx && obstacleIdx.search(query).length > 0) return false;
1222
+ if (obstacleIdx && obstacleIdx.collides(query)) return false;
1230
1223
  const extras = additionalBlockersByLayer?.[layer] ?? [];
1231
1224
  return !extras.some((b) => containsPoint(b, { x, y }));
1232
1225
  };
@@ -1247,6 +1240,7 @@ function longestFreeSpanAroundZ(params) {
1247
1240
  }
1248
1241
 
1249
1242
  // lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts
1243
+ var quantize2 = (value, precision = 1e-6) => Math.round(value / precision) * precision;
1250
1244
  function computeCandidates3D(params) {
1251
1245
  const {
1252
1246
  bounds,
@@ -1257,6 +1251,10 @@ function computeCandidates3D(params) {
1257
1251
  hardPlacedByLayer
1258
1252
  } = params;
1259
1253
  const out = /* @__PURE__ */ new Map();
1254
+ const hardRectsByLayer = Array.from({ length: layerCount }, (_, z) => [
1255
+ ...obstacleIndexByLayer[z]?.all() ?? [],
1256
+ ...hardPlacedByLayer[z] ?? []
1257
+ ]);
1260
1258
  for (let x = bounds.x; x < bounds.x + bounds.width; x += gridSize) {
1261
1259
  for (let y = bounds.y; y < bounds.y + bounds.height; y += gridSize) {
1262
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) {
@@ -1288,20 +1286,18 @@ function computeCandidates3D(params) {
1288
1286
  }
1289
1287
  }
1290
1288
  const anchorZ = bestSpan.length ? bestSpan[Math.floor(bestSpan.length / 2)] : bestZ;
1291
- const hardAtZ = [
1292
- ...obstacleIndexByLayer[anchorZ]?.all() ?? [],
1293
- ...hardPlacedByLayer[anchorZ] ?? []
1294
- ];
1295
- const d = Math.min(
1296
- distancePointToRectEdges({ x, y }, bounds),
1297
- ...hardAtZ.length ? hardAtZ.map((b) => distancePointToRectEdges({ x, y }, b)) : [Infinity]
1298
- );
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
+ }
1294
+ const distance = quantize2(d);
1299
1295
  const k = `${x.toFixed(6)}|${y.toFixed(6)}`;
1300
1296
  const cand = {
1301
1297
  x,
1302
1298
  y,
1303
1299
  z: anchorZ,
1304
- distance: d,
1300
+ distance,
1305
1301
  zSpanLen: bestSpan.length
1306
1302
  };
1307
1303
  const prev = out.get(k);
@@ -1311,18 +1307,34 @@ function computeCandidates3D(params) {
1311
1307
  }
1312
1308
  }
1313
1309
  const arr = Array.from(out.values());
1314
- arr.sort((a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance);
1310
+ arr.sort(
1311
+ (a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance || a.z - b.z || a.x - b.x || a.y - b.y
1312
+ );
1315
1313
  return arr;
1316
1314
  }
1317
1315
 
1318
1316
  // lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts
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;
1319
1324
  function computeUncoveredSegments(params) {
1320
1325
  const { lineStart, lineEnd, coveringIntervals, minSegmentLength } = params;
1321
- if (coveringIntervals.length === 0) {
1322
- const center = (lineStart + lineEnd) / 2;
1323
- return [{ start: lineStart, end: lineEnd, center }];
1324
- }
1325
- const sorted = [...coveringIntervals].sort((a, b) => a.start - b.start);
1326
+ const lineStartQ = quantize3(lineStart);
1327
+ const lineEndQ = quantize3(lineEnd);
1328
+ const normalizedIntervals = coveringIntervals.map((i) => {
1329
+ const s = quantize3(i.start);
1330
+ const e = quantize3(i.end);
1331
+ return { start: Math.min(s, e), end: Math.max(s, e) };
1332
+ }).filter((i) => i.end > i.start + EPS4);
1333
+ if (normalizedIntervals.length === 0) {
1334
+ const center = (lineStartQ + lineEndQ) / 2;
1335
+ return [{ start: lineStartQ, end: lineEndQ, center }];
1336
+ }
1337
+ const sorted = [...normalizedIntervals].sort((a, b) => a.start - b.start);
1326
1338
  const merged = [];
1327
1339
  let current = { ...sorted[0] };
1328
1340
  for (let i = 1; i < sorted.length; i++) {
@@ -1336,8 +1348,8 @@ function computeUncoveredSegments(params) {
1336
1348
  }
1337
1349
  merged.push(current);
1338
1350
  const uncovered = [];
1339
- if (merged[0].start > lineStart + EPS4) {
1340
- const start = lineStart;
1351
+ if (merged[0].start > lineStartQ + EPS4) {
1352
+ const start = lineStartQ;
1341
1353
  const end = merged[0].start;
1342
1354
  if (end - start >= minSegmentLength) {
1343
1355
  uncovered.push({ start, end, center: (start + end) / 2 });
@@ -1350,9 +1362,9 @@ function computeUncoveredSegments(params) {
1350
1362
  uncovered.push({ start, end, center: (start + end) / 2 });
1351
1363
  }
1352
1364
  }
1353
- if (merged[merged.length - 1].end < lineEnd - EPS4) {
1365
+ if (merged[merged.length - 1].end < lineEndQ - EPS4) {
1354
1366
  const start = merged[merged.length - 1].end;
1355
- const end = lineEnd;
1367
+ const end = lineEndQ;
1356
1368
  if (end - start >= minSegmentLength) {
1357
1369
  uncovered.push({ start, end, center: (start + end) / 2 });
1358
1370
  }
@@ -1371,6 +1383,13 @@ function computeEdgeCandidates3D(params) {
1371
1383
  const out = [];
1372
1384
  const \u03B4 = Math.max(minSize * 0.15, EPS4 * 3);
1373
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
+ );
1374
1393
  const key = (p) => `${p.z}|${p.x.toFixed(6)}|${p.y.toFixed(6)}`;
1375
1394
  function fullyOcc(p) {
1376
1395
  return isFullyOccupiedAtPoint({
@@ -1381,18 +1400,20 @@ function computeEdgeCandidates3D(params) {
1381
1400
  });
1382
1401
  }
1383
1402
  function pushIfFree(p) {
1384
- const { x, y, z } = p;
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
- const hard = [
1389
- ...obstacleIndexByLayer[z]?.all() ?? [],
1390
- ...hardPlacedByLayer[z] ?? []
1391
- ];
1392
- const d = Math.min(
1393
- distancePointToRectEdges({ x, y }, bounds),
1394
- ...hard.length ? hard.map((b) => distancePointToRectEdges({ x, y }, b)) : [Infinity]
1395
- );
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
+ }
1416
+ const distance = quantize3(d);
1396
1417
  const k = key({ x, y, z });
1397
1418
  if (dedup.has(k)) return;
1398
1419
  dedup.add(k);
@@ -1406,13 +1427,22 @@ function computeEdgeCandidates3D(params) {
1406
1427
  obstacleIndexByLayer,
1407
1428
  additionalBlockersByLayer: hardPlacedByLayer
1408
1429
  });
1409
- out.push({ x, y, z, distance: d, zSpanLen: span.length, isEdgeSeed: true });
1430
+ out.push({
1431
+ x,
1432
+ y,
1433
+ z,
1434
+ distance,
1435
+ zSpanLen: span.length,
1436
+ isEdgeSeed: true
1437
+ });
1410
1438
  }
1411
1439
  for (let z = 0; z < layerCount; z++) {
1412
- const blockers = [
1413
- ...obstacleIndexByLayer[z]?.all() ?? [],
1414
- ...hardPlacedByLayer[z] ?? []
1415
- ];
1440
+ const blockers = (hardRectsByLayer[z] ?? []).map((b) => ({
1441
+ x: quantize3(b.x),
1442
+ y: quantize3(b.y),
1443
+ width: quantize3(b.width),
1444
+ height: quantize3(b.height)
1445
+ }));
1416
1446
  const corners = [
1417
1447
  { x: bounds.x + \u03B4, y: bounds.y + \u03B4 },
1418
1448
  // top-left
@@ -1585,7 +1615,9 @@ function computeEdgeCandidates3D(params) {
1585
1615
  }
1586
1616
  }
1587
1617
  }
1588
- out.sort((a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance);
1618
+ out.sort(
1619
+ (a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance || a.z - b.z || a.x - b.x || a.y - b.y
1620
+ );
1589
1621
  return out;
1590
1622
  }
1591
1623
 
@@ -1703,6 +1735,7 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
1703
1735
  candidates;
1704
1736
  placed;
1705
1737
  placedIndexByLayer;
1738
+ hardPlacedByLayer;
1706
1739
  expansionIndex;
1707
1740
  edgeAnalysisDone;
1708
1741
  totalSeedsThisGrid;
@@ -1758,6 +1791,7 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
1758
1791
  { length: layerCount },
1759
1792
  () => new RBush3()
1760
1793
  );
1794
+ this.hardPlacedByLayer = Array.from({ length: layerCount }, () => []);
1761
1795
  this.expansionIndex = 0;
1762
1796
  this.edgeAnalysisDone = false;
1763
1797
  this.totalSeedsThisGrid = 0;
@@ -1786,25 +1820,22 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
1786
1820
  maxMultiLayerSpan
1787
1821
  } = this.options;
1788
1822
  const grid = gridSizes[this.gridIndex];
1789
- const hardPlacedByLayer = allLayerNode({
1790
- layerCount: this.layerCount,
1791
- placed: this.placed
1792
- });
1793
1823
  if (this.candidates.length === 0 && this.consumedSeedsThisGrid === 0) {
1794
1824
  this.candidates = computeCandidates3D({
1795
1825
  bounds: this.bounds,
1796
1826
  gridSize: grid,
1797
1827
  layerCount: this.layerCount,
1798
- hardPlacedByLayer,
1828
+ hardPlacedByLayer: this.hardPlacedByLayer,
1799
1829
  obstacleIndexByLayer: this.input.obstacleIndexByLayer,
1800
1830
  placedIndexByLayer: this.placedIndexByLayer
1801
1831
  });
1802
1832
  this.totalSeedsThisGrid = this.candidates.length;
1803
1833
  this.consumedSeedsThisGrid = 0;
1804
1834
  }
1805
- if (this.candidates.length === 0) {
1835
+ if (this.consumedSeedsThisGrid >= this.candidates.length) {
1806
1836
  if (this.gridIndex + 1 < gridSizes.length) {
1807
1837
  this.gridIndex += 1;
1838
+ this.candidates = [];
1808
1839
  this.totalSeedsThisGrid = 0;
1809
1840
  this.consumedSeedsThisGrid = 0;
1810
1841
  return;
@@ -1817,20 +1848,28 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
1817
1848
  layerCount: this.layerCount,
1818
1849
  obstacleIndexByLayer: this.input.obstacleIndexByLayer,
1819
1850
  placedIndexByLayer: this.placedIndexByLayer,
1820
- hardPlacedByLayer
1851
+ hardPlacedByLayer: this.hardPlacedByLayer
1821
1852
  });
1822
1853
  this.edgeAnalysisDone = true;
1823
1854
  this.totalSeedsThisGrid = this.candidates.length;
1824
1855
  this.consumedSeedsThisGrid = 0;
1825
1856
  return;
1826
1857
  }
1858
+ this.candidates = [];
1827
1859
  this.solved = true;
1828
1860
  this.expansionIndex = 0;
1829
1861
  return;
1830
1862
  }
1831
1863
  }
1832
- const cand = this.candidates.shift();
1833
- this.consumedSeedsThisGrid += 1;
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
+ }
1834
1873
  const span = longestFreeSpanAroundZ({
1835
1874
  x: cand.x,
1836
1875
  y: cand.y,
@@ -1839,7 +1878,7 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
1839
1878
  minSpan: minMulti.minLayers,
1840
1879
  maxSpan: maxMultiLayerSpan,
1841
1880
  obstacleIndexByLayer: this.input.obstacleIndexByLayer,
1842
- additionalBlockersByLayer: hardPlacedByLayer
1881
+ additionalBlockersByLayer: this.hardPlacedByLayer
1843
1882
  });
1844
1883
  const attempts = [];
1845
1884
  if (span.length >= minMulti.minLayers) {
@@ -1886,14 +1925,10 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
1886
1925
  },
1887
1926
  newIndex
1888
1927
  );
1889
- this.candidates = this.candidates.filter(
1890
- (c) => !isFullyOccupiedAtPoint({
1891
- layerCount: this.layerCount,
1892
- obstacleIndexByLayer: this.input.obstacleIndexByLayer,
1893
- placedIndexByLayer: this.placedIndexByLayer,
1894
- point: { x: c.x, y: c.y }
1895
- })
1896
- );
1928
+ this.hardPlacedByLayer = allLayerNode({
1929
+ layerCount: this.layerCount,
1930
+ placed: this.placed
1931
+ });
1897
1932
  return;
1898
1933
  }
1899
1934
  }
@@ -1976,29 +2011,6 @@ var RectDiffSeedingSolver = class extends BaseSolver3 {
1976
2011
  });
1977
2012
  }
1978
2013
  }
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
2014
  if (this.boardVoidRects) {
2003
2015
  let outlineBBox = null;
2004
2016
  if (srj.outline && srj.outline.length > 0) {
@@ -2524,6 +2536,42 @@ z:${layerLabel}`
2524
2536
  };
2525
2537
  }
2526
2538
 
2539
+ // lib/buildFinalRectDiffVisualization.ts
2540
+ import { mergeGraphics } from "graphics-debug";
2541
+
2542
+ // lib/utils/buildOutlineGraphics.ts
2543
+ var buildOutlineGraphics = ({
2544
+ srj
2545
+ }) => {
2546
+ const hasOutline = srj.outline && srj.outline.length > 1;
2547
+ const lines = hasOutline ? [
2548
+ {
2549
+ points: [...srj.outline, srj.outline[0]],
2550
+ strokeColor: "#111827",
2551
+ strokeWidth: 0.1,
2552
+ label: "outline"
2553
+ }
2554
+ ] : [
2555
+ {
2556
+ points: [
2557
+ { x: srj.bounds.minX, y: srj.bounds.minY },
2558
+ { x: srj.bounds.maxX, y: srj.bounds.minY },
2559
+ { x: srj.bounds.maxX, y: srj.bounds.maxY },
2560
+ { x: srj.bounds.minX, y: srj.bounds.maxY },
2561
+ { x: srj.bounds.minX, y: srj.bounds.minY }
2562
+ ],
2563
+ strokeColor: "#111827",
2564
+ strokeWidth: 0.1,
2565
+ label: "bounds"
2566
+ }
2567
+ ];
2568
+ return {
2569
+ title: "SimpleRoute Outline",
2570
+ coordinateSystem: "cartesian",
2571
+ lines
2572
+ };
2573
+ };
2574
+
2527
2575
  // lib/utils/renderObstacleClearance.ts
2528
2576
  var buildObstacleClearanceGraphics = (params) => {
2529
2577
  const { srj, clearance } = params;
@@ -2565,8 +2613,40 @@ z:${(obstacle.zLayers ?? []).join(",") || "all"}`
2565
2613
  };
2566
2614
  };
2567
2615
 
2616
+ // lib/buildFinalRectDiffVisualization.ts
2617
+ var buildFinalRectDiffVisualization = ({
2618
+ srj,
2619
+ meshNodes,
2620
+ obstacleClearance
2621
+ }) => {
2622
+ const outline = buildOutlineGraphics({ srj });
2623
+ const clearance = buildObstacleClearanceGraphics({
2624
+ srj,
2625
+ clearance: obstacleClearance
2626
+ });
2627
+ const rects = meshNodes.map((node) => ({
2628
+ center: node.center,
2629
+ width: node.width,
2630
+ height: node.height,
2631
+ stroke: getColorForZLayer(node.availableZ).stroke,
2632
+ fill: node._containsObstacle ? "#fca5a5" : getColorForZLayer(node.availableZ).fill,
2633
+ layer: `z${node.availableZ.join(",")}`,
2634
+ label: `node ${node.capacityMeshNodeId}
2635
+ z:${node.availableZ.join(",")}`
2636
+ }));
2637
+ const nodesGraphic = {
2638
+ title: "RectDiffPipeline - Final",
2639
+ coordinateSystem: "cartesian",
2640
+ rects,
2641
+ lines: [],
2642
+ points: [],
2643
+ texts: []
2644
+ };
2645
+ return mergeGraphics(mergeGraphics(nodesGraphic, outline), clearance);
2646
+ };
2647
+
2568
2648
  // lib/RectDiffPipeline.ts
2569
- import { mergeGraphics } from "graphics-debug";
2649
+ import { mergeGraphics as mergeGraphics2 } from "graphics-debug";
2570
2650
  var RectDiffPipeline = class extends BasePipelineSolver3 {
2571
2651
  rectDiffGridSolverPipeline;
2572
2652
  gapFillSolver;
@@ -2665,43 +2745,14 @@ var RectDiffPipeline = class extends BasePipelineSolver3 {
2665
2745
  ].join("\n")
2666
2746
  }))
2667
2747
  };
2668
- return mergeGraphics(mergeGraphics(base, clearance), nodeRects);
2748
+ return mergeGraphics2(mergeGraphics2(base, clearance), nodeRects);
2669
2749
  }
2670
2750
  finalVisualize() {
2671
- const base = createBaseVisualization(
2672
- this.inputProblem.simpleRouteJson,
2673
- "RectDiffPipeline - Final"
2674
- );
2675
- const clearance = buildObstacleClearanceGraphics({
2751
+ return buildFinalRectDiffVisualization({
2676
2752
  srj: this.inputProblem.simpleRouteJson,
2677
- clearance: this.inputProblem.obstacleClearance
2753
+ meshNodes: this.getOutput().meshNodes,
2754
+ obstacleClearance: this.inputProblem.obstacleClearance
2678
2755
  });
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
2756
  }
2706
2757
  };
2707
2758
  export {
@@ -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
- const base = createBaseVisualization(
148
- this.inputProblem.simpleRouteJson,
149
- "RectDiffPipeline - Final",
150
- )
151
- const clearance = buildObstacleClearanceGraphics({
148
+ return buildFinalRectDiffVisualization({
152
149
  srj: this.inputProblem.simpleRouteJson,
153
- clearance: this.inputProblem.obstacleClearance,
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
  }