@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.
Files changed (40) hide show
  1. package/dist/index.js +133 -74
  2. package/lib/RectDiffPipeline.ts +4 -37
  3. package/lib/buildFinalRectDiffVisualization.ts +46 -0
  4. package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +0 -25
  5. package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +12 -2
  6. package/lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts +53 -13
  7. package/lib/utils/buildOutlineGraphics.ts +39 -0
  8. package/lib/utils/expandRectFromSeed.ts +11 -1
  9. package/package.json +1 -1
  10. package/tests/solver/__snapshots__/rectDiffGridSolverPipeline.snap.svg +1 -1
  11. package/tests/solver/bugreport01-be84eb/__snapshots__/bugreport01-be84eb.snap.svg +2 -2
  12. package/tests/solver/bugreport02-bc4361/__snapshots__/bugreport02-bc4361.snap.svg +2 -2
  13. package/tests/solver/bugreport03-fe4a17/__snapshots__/bugreport03-fe4a17.snap.svg +1 -1
  14. package/tests/solver/bugreport07-d3f3be/__snapshots__/bugreport07-d3f3be.snap.svg +1 -1
  15. package/tests/solver/bugreport08-e3ec95/__snapshots__/bugreport08-e3ec95.snap.svg +1 -1
  16. package/tests/solver/bugreport09-618e09/__snapshots__/bugreport09-618e09.snap.svg +2 -2
  17. package/tests/solver/bugreport10-71239a/__snapshots__/bugreport10-71239a.snap.svg +2 -2
  18. package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c.snap.svg +1 -1
  19. package/tests/solver/bugreport11-b2de3c/bugreport11-b2de3c-clearance-equivalence.test.ts +52 -0
  20. package/tests/solver/bugreport12-35ce1c/__snapshots__/bugreport12-35ce1c.snap.svg +1 -1
  21. package/tests/solver/bugreport13-b9a758/__snapshots__/bugreport13-b9a758.snap.svg +2 -2
  22. package/tests/solver/bugreport16-d95f38/__snapshots__/bugreport16-d95f38.snap.svg +2 -2
  23. package/tests/solver/bugreport19/__snapshots__/bugreport19.snap.svg +1 -1
  24. package/tests/solver/bugreport22-2a75ce/__snapshots__/bugreport22-2a75ce.snap.svg +1 -1
  25. package/tests/solver/bugreport23-LGA15x4/__snapshots__/bugreport23-LGA15x4.snap.svg +1 -1
  26. package/tests/solver/bugreport24-05597c/__snapshots__/bugreport24-05597c.snap.svg +1 -1
  27. package/tests/solver/bugreport26-66b0b2/__snapshots__/bugreport26-66b0b2.snap.svg +2 -2
  28. package/tests/solver/bugreport27-dd3734/__snapshots__/bugreport27-dd3734.snap.svg +2 -2
  29. package/tests/solver/bugreport28-18a9ef/__snapshots__/bugreport28-18a9ef.snap.svg +2 -2
  30. package/tests/solver/bugreport29-7deae8/__snapshots__/bugreport29-7deae8.snap.svg +2 -2
  31. package/tests/solver/bugreport30-2174c8/__snapshots__/bugreport30-2174c8.snap.svg +1 -1
  32. package/tests/solver/bugreport33-213d45/__snapshots__/bugreport33-213d45.snap.svg +2 -2
  33. package/tests/solver/bugreport34-e9dea2/__snapshots__/bugreport34-e9dea2.snap.svg +2 -2
  34. package/tests/solver/bugreport35-191db9/__snapshots__/bugreport35-191db9.snap.svg +2 -2
  35. package/tests/solver/bugreport36-bf8303/__snapshots__/bugreport36-bf8303.snap.svg +1 -1
  36. package/tests/solver/interaction/__snapshots__/interaction.snap.svg +1 -1
  37. package/tests/solver/multi-point/__snapshots__/multi-point.snap.svg +1 -1
  38. package/tests/solver/pcb_trace_id-should-return-root-connection-name/__snapshots__/pcb_trace_id-should-return-root-connection-name.snap.svg +1 -1
  39. package/tests/solver/bugreport11-b2de3c/__snapshots__/bugreport11-b2de3c-clearance.snap.svg +0 -44
  40. 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: d,
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((a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance);
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
- 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);
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 > lineStart + EPS4) {
1340
- const start = lineStart;
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 < lineEnd - EPS4) {
1372
+ if (merged[merged.length - 1].end < lineEndQ - EPS4) {
1354
1373
  const start = merged[merged.length - 1].end;
1355
- const end = lineEnd;
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 { 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
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({ x, y, z, distance: d, zSpanLen: span.length, isEdgeSeed: true });
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((a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance);
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 mergeGraphics(mergeGraphics(base, clearance), nodeRects);
2756
+ return mergeGraphics2(mergeGraphics2(base, clearance), nodeRects);
2669
2757
  }
2670
2758
  finalVisualize() {
2671
- const base = createBaseVisualization(
2672
- this.inputProblem.simpleRouteJson,
2673
- "RectDiffPipeline - Final"
2674
- );
2675
- const clearance = buildObstacleClearanceGraphics({
2759
+ return buildFinalRectDiffVisualization({
2676
2760
  srj: this.inputProblem.simpleRouteJson,
2677
- clearance: this.inputProblem.obstacleClearance
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 {
@@ -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
  }
@@ -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: d,
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((a, b) => b.zSpanLen! - a.zSpanLen! || b.distance - a.distance)
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 (coveringIntervals.length === 0) {
20
- const center = (lineStart + lineEnd) / 2
21
- return [{ start: lineStart, end: lineEnd, center }]
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 = [...coveringIntervals].sort((a, b) => a.start - b.start)
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 > lineStart + EPS) {
49
- const start = lineStart
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 < lineEnd - EPS) {
77
+ if (merged[merged.length - 1]!.end < lineEndQ - EPS) {
67
78
  const start = merged[merged.length - 1]!.end
68
- const end = lineEnd
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 { x, y, z } = p
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({ x, y, z, distance: d, zSpanLen: span.length, isEdgeSeed: true })
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((a, b) => b.zSpanLen! - a.zSpanLen! || b.distance - a.distance)
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
+ }