@tscircuit/rectdiff 0.0.4 → 0.0.5

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 (29) hide show
  1. package/dist/index.d.ts +112 -3
  2. package/dist/index.js +869 -142
  3. package/lib/solvers/RectDiffSolver.ts +125 -24
  4. package/lib/solvers/rectdiff/candidates.ts +150 -104
  5. package/lib/solvers/rectdiff/engine.ts +72 -53
  6. package/lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts +28 -0
  7. package/lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts +83 -0
  8. package/lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts +100 -0
  9. package/lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts +75 -0
  10. package/lib/solvers/rectdiff/gapfill/detection.ts +3 -0
  11. package/lib/solvers/rectdiff/gapfill/engine/addPlacement.ts +27 -0
  12. package/lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts +44 -0
  13. package/lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts +43 -0
  14. package/lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts +42 -0
  15. package/lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts +57 -0
  16. package/lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts +128 -0
  17. package/lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts +78 -0
  18. package/lib/solvers/rectdiff/gapfill/engine.ts +7 -0
  19. package/lib/solvers/rectdiff/gapfill/types.ts +60 -0
  20. package/lib/solvers/rectdiff/geometry.ts +23 -11
  21. package/lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts +253 -0
  22. package/lib/solvers/rectdiff/types.ts +1 -1
  23. package/package.json +1 -1
  24. package/tests/obstacle-extra-layers.test.ts +1 -1
  25. package/tests/obstacle-zlayers.test.ts +1 -1
  26. package/tests/rect-diff-solver.test.ts +1 -4
  27. package/utils/README.md +21 -0
  28. package/utils/rectsEqual.ts +18 -0
  29. package/utils/rectsOverlap.ts +18 -0
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // lib/solvers/RectDiffSolver.ts
2
- import { BaseSolver } from "@tscircuit/solver-utils";
2
+ import { BaseSolver as BaseSolver2 } from "@tscircuit/solver-utils";
3
3
 
4
4
  // lib/solvers/rectdiff/geometry.ts
5
5
  var EPS = 1e-9;
@@ -114,7 +114,17 @@ function maxExpandUp(r, bounds, blockers, maxAspect) {
114
114
  }
115
115
  return Math.max(0, e);
116
116
  }
117
- function expandRectFromSeed(startX, startY, gridSize, bounds, blockers, initialCellRatio, maxAspectRatio, minReq) {
117
+ function expandRectFromSeed(params) {
118
+ const {
119
+ startX,
120
+ startY,
121
+ gridSize,
122
+ bounds,
123
+ blockers,
124
+ initialCellRatio,
125
+ maxAspectRatio,
126
+ minReq
127
+ } = params;
118
128
  const minSide = Math.max(1e-9, gridSize * initialCellRatio);
119
129
  const initialW = Math.max(minSide, minReq.width);
120
130
  const initialH = Math.max(minSide, minReq.height);
@@ -202,7 +212,8 @@ function subtractRect2D(A, B) {
202
212
  }
203
213
 
204
214
  // lib/solvers/rectdiff/candidates.ts
205
- function isFullyOccupiedAllLayers(x, y, layerCount, obstaclesByLayer, placedByLayer) {
215
+ function isFullyOccupiedAllLayers(params) {
216
+ const { x, y, layerCount, obstaclesByLayer, placedByLayer } = params;
206
217
  for (let z = 0; z < layerCount; z++) {
207
218
  const obs = obstaclesByLayer[z] ?? [];
208
219
  const placed = placedByLayer[z] ?? [];
@@ -211,36 +222,42 @@ function isFullyOccupiedAllLayers(x, y, layerCount, obstaclesByLayer, placedByLa
211
222
  }
212
223
  return true;
213
224
  }
214
- function computeCandidates3D(bounds, gridSize, layerCount, obstaclesByLayer, placedByLayer, hardPlacedByLayer) {
225
+ function computeCandidates3D(params) {
226
+ const {
227
+ bounds,
228
+ gridSize,
229
+ layerCount,
230
+ obstaclesByLayer,
231
+ placedByLayer,
232
+ hardPlacedByLayer
233
+ } = params;
215
234
  const out = /* @__PURE__ */ new Map();
216
235
  for (let x = bounds.x; x < bounds.x + bounds.width; x += gridSize) {
217
236
  for (let y = bounds.y; y < bounds.y + bounds.height; y += gridSize) {
218
237
  if (Math.abs(x - bounds.x) < EPS || Math.abs(y - bounds.y) < EPS || x > bounds.x + bounds.width - gridSize - EPS || y > bounds.y + bounds.height - gridSize - EPS) {
219
238
  continue;
220
239
  }
221
- if (isFullyOccupiedAllLayers(
240
+ if (isFullyOccupiedAllLayers({
222
241
  x,
223
242
  y,
224
243
  layerCount,
225
244
  obstaclesByLayer,
226
245
  placedByLayer
227
- ))
246
+ }))
228
247
  continue;
229
248
  let bestSpan = [];
230
249
  let bestZ = 0;
231
250
  for (let z = 0; z < layerCount; z++) {
232
- const s = longestFreeSpanAroundZ(
251
+ const s = longestFreeSpanAroundZ({
233
252
  x,
234
253
  y,
235
254
  z,
236
255
  layerCount,
237
- 1,
238
- void 0,
239
- // no cap here
256
+ minSpan: 1,
257
+ maxSpan: void 0,
240
258
  obstaclesByLayer,
241
- hardPlacedByLayer
242
- // IMPORTANT: ignore soft nodes
243
- );
259
+ placedByLayer: hardPlacedByLayer
260
+ });
244
261
  if (s.length > bestSpan.length) {
245
262
  bestSpan = s;
246
263
  bestZ = z;
@@ -273,7 +290,17 @@ function computeCandidates3D(bounds, gridSize, layerCount, obstaclesByLayer, pla
273
290
  arr.sort((a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance);
274
291
  return arr;
275
292
  }
276
- function longestFreeSpanAroundZ(x, y, z, layerCount, minSpan, maxSpan, obstaclesByLayer, placedByLayer) {
293
+ function longestFreeSpanAroundZ(params) {
294
+ const {
295
+ x,
296
+ y,
297
+ z,
298
+ layerCount,
299
+ minSpan,
300
+ maxSpan,
301
+ obstaclesByLayer,
302
+ placedByLayer
303
+ } = params;
277
304
  const isFreeAt = (layer) => {
278
305
  const blockers = [
279
306
  ...obstaclesByLayer[layer] ?? [],
@@ -300,7 +327,8 @@ function computeDefaultGridSizes(bounds) {
300
327
  const ref = Math.max(bounds.width, bounds.height);
301
328
  return [ref / 8, ref / 16, ref / 32];
302
329
  }
303
- function computeUncoveredSegments(lineStart, lineEnd, coveringIntervals, minSegmentLength) {
330
+ function computeUncoveredSegments(params) {
331
+ const { lineStart, lineEnd, coveringIntervals, minSegmentLength } = params;
304
332
  if (coveringIntervals.length === 0) {
305
333
  const center = (lineStart + lineEnd) / 2;
306
334
  return [{ start: lineStart, end: lineEnd, center }];
@@ -342,19 +370,27 @@ function computeUncoveredSegments(lineStart, lineEnd, coveringIntervals, minSegm
342
370
  }
343
371
  return uncovered;
344
372
  }
345
- function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer, placedByLayer, hardPlacedByLayer) {
373
+ function computeEdgeCandidates3D(params) {
374
+ const {
375
+ bounds,
376
+ minSize,
377
+ layerCount,
378
+ obstaclesByLayer,
379
+ placedByLayer,
380
+ hardPlacedByLayer
381
+ } = params;
346
382
  const out = [];
347
383
  const \u03B4 = Math.max(minSize * 0.15, EPS * 3);
348
384
  const dedup = /* @__PURE__ */ new Set();
349
385
  const key = (x, y, z) => `${z}|${x.toFixed(6)}|${y.toFixed(6)}`;
350
386
  function fullyOcc(x, y) {
351
- return isFullyOccupiedAllLayers(
387
+ return isFullyOccupiedAllLayers({
352
388
  x,
353
389
  y,
354
390
  layerCount,
355
391
  obstaclesByLayer,
356
392
  placedByLayer
357
- );
393
+ });
358
394
  }
359
395
  function pushIfFree(x, y, z) {
360
396
  if (x < bounds.x + EPS || y < bounds.y + EPS || x > bounds.x + bounds.width - EPS || y > bounds.y + bounds.height - EPS)
@@ -371,16 +407,16 @@ function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer,
371
407
  const k = key(x, y, z);
372
408
  if (dedup.has(k)) return;
373
409
  dedup.add(k);
374
- const span = longestFreeSpanAroundZ(
410
+ const span = longestFreeSpanAroundZ({
375
411
  x,
376
412
  y,
377
413
  z,
378
414
  layerCount,
379
- 1,
380
- void 0,
415
+ minSpan: 1,
416
+ maxSpan: void 0,
381
417
  obstaclesByLayer,
382
- hardPlacedByLayer
383
- );
418
+ placedByLayer: hardPlacedByLayer
419
+ });
384
420
  out.push({ x, y, z, distance: d, zSpanLen: span.length, isEdgeSeed: true });
385
421
  }
386
422
  for (let z = 0; z < layerCount; z++) {
@@ -406,12 +442,12 @@ function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer,
406
442
  start: Math.max(bounds.x, b.x),
407
443
  end: Math.min(bounds.x + bounds.width, b.x + b.width)
408
444
  }));
409
- const topUncovered = computeUncoveredSegments(
410
- bounds.x + \u03B4,
411
- bounds.x + bounds.width - \u03B4,
412
- topCovering,
413
- minSize * 0.5
414
- );
445
+ const topUncovered = computeUncoveredSegments({
446
+ lineStart: bounds.x + \u03B4,
447
+ lineEnd: bounds.x + bounds.width - \u03B4,
448
+ coveringIntervals: topCovering,
449
+ minSegmentLength: minSize * 0.5
450
+ });
415
451
  for (const seg of topUncovered) {
416
452
  const segLen = seg.end - seg.start;
417
453
  if (segLen >= minSize) {
@@ -427,12 +463,12 @@ function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer,
427
463
  start: Math.max(bounds.x, b.x),
428
464
  end: Math.min(bounds.x + bounds.width, b.x + b.width)
429
465
  }));
430
- const bottomUncovered = computeUncoveredSegments(
431
- bounds.x + \u03B4,
432
- bounds.x + bounds.width - \u03B4,
433
- bottomCovering,
434
- minSize * 0.5
435
- );
466
+ const bottomUncovered = computeUncoveredSegments({
467
+ lineStart: bounds.x + \u03B4,
468
+ lineEnd: bounds.x + bounds.width - \u03B4,
469
+ coveringIntervals: bottomCovering,
470
+ minSegmentLength: minSize * 0.5
471
+ });
436
472
  for (const seg of bottomUncovered) {
437
473
  const segLen = seg.end - seg.start;
438
474
  if (segLen >= minSize) {
@@ -448,12 +484,12 @@ function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer,
448
484
  start: Math.max(bounds.y, b.y),
449
485
  end: Math.min(bounds.y + bounds.height, b.y + b.height)
450
486
  }));
451
- const leftUncovered = computeUncoveredSegments(
452
- bounds.y + \u03B4,
453
- bounds.y + bounds.height - \u03B4,
454
- leftCovering,
455
- minSize * 0.5
456
- );
487
+ const leftUncovered = computeUncoveredSegments({
488
+ lineStart: bounds.y + \u03B4,
489
+ lineEnd: bounds.y + bounds.height - \u03B4,
490
+ coveringIntervals: leftCovering,
491
+ minSegmentLength: minSize * 0.5
492
+ });
457
493
  for (const seg of leftUncovered) {
458
494
  const segLen = seg.end - seg.start;
459
495
  if (segLen >= minSize) {
@@ -469,12 +505,12 @@ function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer,
469
505
  start: Math.max(bounds.y, b.y),
470
506
  end: Math.min(bounds.y + bounds.height, b.y + b.height)
471
507
  }));
472
- const rightUncovered = computeUncoveredSegments(
473
- bounds.y + \u03B4,
474
- bounds.y + bounds.height - \u03B4,
475
- rightCovering,
476
- minSize * 0.5
477
- );
508
+ const rightUncovered = computeUncoveredSegments({
509
+ lineStart: bounds.y + \u03B4,
510
+ lineEnd: bounds.y + bounds.height - \u03B4,
511
+ coveringIntervals: rightCovering,
512
+ minSegmentLength: minSize * 0.5
513
+ });
478
514
  for (const seg of rightUncovered) {
479
515
  const segLen = seg.end - seg.start;
480
516
  if (segLen >= minSize) {
@@ -494,12 +530,12 @@ function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer,
494
530
  start: Math.max(b.y, bl.y),
495
531
  end: Math.min(b.y + b.height, bl.y + bl.height)
496
532
  }));
497
- const obLeftUncovered = computeUncoveredSegments(
498
- b.y,
499
- b.y + b.height,
500
- obLeftCovering,
501
- minSize * 0.5
502
- );
533
+ const obLeftUncovered = computeUncoveredSegments({
534
+ lineStart: b.y,
535
+ lineEnd: b.y + b.height,
536
+ coveringIntervals: obLeftCovering,
537
+ minSegmentLength: minSize * 0.5
538
+ });
503
539
  for (const seg of obLeftUncovered) {
504
540
  pushIfFree(obLeftX, seg.center, z);
505
541
  }
@@ -512,12 +548,12 @@ function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer,
512
548
  start: Math.max(b.y, bl.y),
513
549
  end: Math.min(b.y + b.height, bl.y + bl.height)
514
550
  }));
515
- const obRightUncovered = computeUncoveredSegments(
516
- b.y,
517
- b.y + b.height,
518
- obRightCovering,
519
- minSize * 0.5
520
- );
551
+ const obRightUncovered = computeUncoveredSegments({
552
+ lineStart: b.y,
553
+ lineEnd: b.y + b.height,
554
+ coveringIntervals: obRightCovering,
555
+ minSegmentLength: minSize * 0.5
556
+ });
521
557
  for (const seg of obRightUncovered) {
522
558
  pushIfFree(obRightX, seg.center, z);
523
559
  }
@@ -530,12 +566,12 @@ function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer,
530
566
  start: Math.max(b.x, bl.x),
531
567
  end: Math.min(b.x + b.width, bl.x + bl.width)
532
568
  }));
533
- const obTopUncovered = computeUncoveredSegments(
534
- b.x,
535
- b.x + b.width,
536
- obTopCovering,
537
- minSize * 0.5
538
- );
569
+ const obTopUncovered = computeUncoveredSegments({
570
+ lineStart: b.x,
571
+ lineEnd: b.x + b.width,
572
+ coveringIntervals: obTopCovering,
573
+ minSegmentLength: minSize * 0.5
574
+ });
539
575
  for (const seg of obTopUncovered) {
540
576
  pushIfFree(seg.center, obTopY, z);
541
577
  }
@@ -548,12 +584,12 @@ function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer,
548
584
  start: Math.max(b.x, bl.x),
549
585
  end: Math.min(b.x + b.width, bl.x + bl.width)
550
586
  }));
551
- const obBottomUncovered = computeUncoveredSegments(
552
- b.x,
553
- b.x + b.width,
554
- obBottomCovering,
555
- minSize * 0.5
556
- );
587
+ const obBottomUncovered = computeUncoveredSegments({
588
+ lineStart: b.x,
589
+ lineEnd: b.x + b.width,
590
+ coveringIntervals: obBottomCovering,
591
+ minSegmentLength: minSize * 0.5
592
+ });
557
593
  for (const seg of obBottomUncovered) {
558
594
  pushIfFree(seg.center, obBottomY, z);
559
595
  }
@@ -715,11 +751,11 @@ function buildHardPlacedByLayer(state) {
715
751
  }
716
752
  return out;
717
753
  }
718
- function isFullyOccupiedAtPoint(state, x, y) {
754
+ function isFullyOccupiedAtPoint(state, point) {
719
755
  for (let z = 0; z < state.layerCount; z++) {
720
756
  const obs = state.obstaclesByLayer[z] ?? [];
721
757
  const placed = state.placedByLayer[z] ?? [];
722
- const occ = obs.some((b) => containsPoint(b, x, y)) || placed.some((b) => containsPoint(b, x, y));
758
+ const occ = obs.some((b) => containsPoint(b, point.x, point.y)) || placed.some((b) => containsPoint(b, point.x, point.y));
723
759
  if (!occ) return false;
724
760
  }
725
761
  return true;
@@ -783,16 +819,14 @@ function stepGrid(state) {
783
819
  const grid = gridSizes[state.gridIndex];
784
820
  const hardPlacedByLayer = buildHardPlacedByLayer(state);
785
821
  if (state.candidates.length === 0 && state.consumedSeedsThisGrid === 0) {
786
- state.candidates = computeCandidates3D(
787
- state.bounds,
788
- grid,
789
- state.layerCount,
790
- state.obstaclesByLayer,
791
- state.placedByLayer,
792
- // all nodes (soft + hard) for fully-occupied test
822
+ state.candidates = computeCandidates3D({
823
+ bounds: state.bounds,
824
+ gridSize: grid,
825
+ layerCount: state.layerCount,
826
+ obstaclesByLayer: state.obstaclesByLayer,
827
+ placedByLayer: state.placedByLayer,
793
828
  hardPlacedByLayer
794
- // hard blockers for ranking/span
795
- );
829
+ });
796
830
  state.totalSeedsThisGrid = state.candidates.length;
797
831
  state.consumedSeedsThisGrid = 0;
798
832
  }
@@ -805,15 +839,14 @@ function stepGrid(state) {
805
839
  } else {
806
840
  if (!state.edgeAnalysisDone) {
807
841
  const minSize = Math.min(minSingle.width, minSingle.height);
808
- state.candidates = computeEdgeCandidates3D(
809
- state.bounds,
842
+ state.candidates = computeEdgeCandidates3D({
843
+ bounds: state.bounds,
810
844
  minSize,
811
- state.layerCount,
812
- state.obstaclesByLayer,
813
- state.placedByLayer,
814
- // for fully-occupied test
845
+ layerCount: state.layerCount,
846
+ obstaclesByLayer: state.obstaclesByLayer,
847
+ placedByLayer: state.placedByLayer,
815
848
  hardPlacedByLayer
816
- );
849
+ });
817
850
  state.edgeAnalysisDone = true;
818
851
  state.totalSeedsThisGrid = state.candidates.length;
819
852
  state.consumedSeedsThisGrid = 0;
@@ -826,17 +859,16 @@ function stepGrid(state) {
826
859
  }
827
860
  const cand = state.candidates.shift();
828
861
  state.consumedSeedsThisGrid += 1;
829
- const span = longestFreeSpanAroundZ(
830
- cand.x,
831
- cand.y,
832
- cand.z,
833
- state.layerCount,
834
- minMulti.minLayers,
835
- maxMultiLayerSpan,
836
- state.obstaclesByLayer,
837
- hardPlacedByLayer
838
- // ignore soft nodes for span
839
- );
862
+ const span = longestFreeSpanAroundZ({
863
+ x: cand.x,
864
+ y: cand.y,
865
+ z: cand.z,
866
+ layerCount: state.layerCount,
867
+ minSpan: minMulti.minLayers,
868
+ maxSpan: maxMultiLayerSpan,
869
+ obstaclesByLayer: state.obstaclesByLayer,
870
+ placedByLayer: hardPlacedByLayer
871
+ });
840
872
  const attempts = [];
841
873
  if (span.length >= minMulti.minLayers) {
842
874
  attempts.push({
@@ -858,31 +890,30 @@ function stepGrid(state) {
858
890
  hardBlockers.push(...state.obstaclesByLayer[z]);
859
891
  if (hardPlacedByLayer[z]) hardBlockers.push(...hardPlacedByLayer[z]);
860
892
  }
861
- const rect = expandRectFromSeed(
862
- cand.x,
863
- cand.y,
864
- grid,
865
- state.bounds,
866
- hardBlockers,
867
- // soft nodes DO NOT block expansion
893
+ const rect = expandRectFromSeed({
894
+ startX: cand.x,
895
+ startY: cand.y,
896
+ gridSize: grid,
897
+ bounds: state.bounds,
898
+ blockers: hardBlockers,
868
899
  initialCellRatio,
869
900
  maxAspectRatio,
870
- attempt.minReq
871
- );
901
+ minReq: attempt.minReq
902
+ });
872
903
  if (!rect) continue;
873
904
  const placed = { rect, zLayers: [...attempt.layers] };
874
905
  const newIndex = state.placed.push(placed) - 1;
875
906
  for (const z of attempt.layers) state.placedByLayer[z].push(rect);
876
907
  resizeSoftOverlaps(state, newIndex);
877
908
  state.candidates = state.candidates.filter(
878
- (c) => !isFullyOccupiedAtPoint(state, c.x, c.y)
909
+ (c) => !isFullyOccupiedAtPoint(state, { x: c.x, y: c.y })
879
910
  );
880
911
  return;
881
912
  }
882
913
  }
883
914
  function stepExpansion(state) {
884
915
  if (state.expansionIndex >= state.placed.length) {
885
- state.phase = "DONE";
916
+ state.phase = "GAP_FILL";
886
917
  return;
887
918
  }
888
919
  const idx = state.expansionIndex;
@@ -895,18 +926,16 @@ function stepExpansion(state) {
895
926
  hardBlockers.push(...hardPlacedByLayer[z] ?? []);
896
927
  }
897
928
  const oldRect = p.rect;
898
- const expanded = expandRectFromSeed(
899
- p.rect.x + p.rect.width / 2,
900
- p.rect.y + p.rect.height / 2,
901
- lastGrid,
902
- state.bounds,
903
- hardBlockers,
904
- 0,
905
- // seed bias off
906
- null,
907
- // no aspect cap in expansion pass
908
- { width: p.rect.width, height: p.rect.height }
909
- );
929
+ const expanded = expandRectFromSeed({
930
+ startX: p.rect.x + p.rect.width / 2,
931
+ startY: p.rect.y + p.rect.height / 2,
932
+ gridSize: lastGrid,
933
+ bounds: state.bounds,
934
+ blockers: hardBlockers,
935
+ initialCellRatio: 0,
936
+ maxAspectRatio: null,
937
+ minReq: { width: p.rect.width, height: p.rect.height }
938
+ });
910
939
  if (expanded) {
911
940
  state.placed[idx] = { rect: expanded, zLayers: p.zLayers };
912
941
  for (const z of p.zLayers) {
@@ -986,18 +1015,647 @@ function rectsToMeshNodes(rects) {
986
1015
  return out;
987
1016
  }
988
1017
 
1018
+ // lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts
1019
+ function calculateCoverage({ sampleResolution = 0.1 }, ctx) {
1020
+ const { bounds, layerCount, obstaclesByLayer, placedByLayer } = ctx;
1021
+ let totalPoints = 0;
1022
+ let coveredPoints = 0;
1023
+ for (let z = 0; z < layerCount; z++) {
1024
+ const obstacles = obstaclesByLayer[z] ?? [];
1025
+ const placed = placedByLayer[z] ?? [];
1026
+ const allRects = [...obstacles, ...placed];
1027
+ for (let x = bounds.x; x <= bounds.x + bounds.width; x += sampleResolution) {
1028
+ for (let y = bounds.y; y <= bounds.y + bounds.height; y += sampleResolution) {
1029
+ totalPoints++;
1030
+ const isCovered = allRects.some(
1031
+ (r) => x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height
1032
+ );
1033
+ if (isCovered) coveredPoints++;
1034
+ }
1035
+ }
1036
+ }
1037
+ return totalPoints > 0 ? coveredPoints / totalPoints : 1;
1038
+ }
1039
+
1040
+ // lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts
1041
+ function findUncoveredPoints({ sampleResolution = 0.05 }, ctx) {
1042
+ const { bounds, layerCount, obstaclesByLayer, placedByLayer } = ctx;
1043
+ const uncovered = [];
1044
+ for (let z = 0; z < layerCount; z++) {
1045
+ const obstacles = obstaclesByLayer[z] ?? [];
1046
+ const placed = placedByLayer[z] ?? [];
1047
+ const allRects = [...obstacles, ...placed];
1048
+ for (let x = bounds.x; x <= bounds.x + bounds.width; x += sampleResolution) {
1049
+ for (let y = bounds.y; y <= bounds.y + bounds.height; y += sampleResolution) {
1050
+ const isCovered = allRects.some(
1051
+ (r) => x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height
1052
+ );
1053
+ if (!isCovered) {
1054
+ uncovered.push({ x, y, z });
1055
+ }
1056
+ }
1057
+ }
1058
+ }
1059
+ return uncovered;
1060
+ }
1061
+
1062
+ // lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts
1063
+ function getGapFillProgress(state) {
1064
+ if (state.done) return 1;
1065
+ const iterationProgress = state.iteration / state.options.maxIterations;
1066
+ const gapProgress = state.gapsFound.length > 0 ? state.gapIndex / state.gapsFound.length : 0;
1067
+ let stageProgress = 0;
1068
+ switch (state.stage) {
1069
+ case "scan":
1070
+ stageProgress = 0;
1071
+ break;
1072
+ case "select":
1073
+ stageProgress = 0.25;
1074
+ break;
1075
+ case "expand":
1076
+ stageProgress = 0.5;
1077
+ break;
1078
+ case "place":
1079
+ stageProgress = 0.75;
1080
+ break;
1081
+ }
1082
+ const gapStageProgress = state.gapsFound.length > 0 ? stageProgress / (state.gapsFound.length * 4) : 0;
1083
+ return Math.min(
1084
+ 0.999,
1085
+ iterationProgress + (gapProgress + gapStageProgress) / state.options.maxIterations
1086
+ );
1087
+ }
1088
+
1089
+ // lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts
1090
+ var DEFAULT_OPTIONS = {
1091
+ minWidth: 0.1,
1092
+ minHeight: 0.1,
1093
+ maxIterations: 10,
1094
+ targetCoverage: 0.999,
1095
+ scanResolution: 0.5
1096
+ };
1097
+ function initGapFillState({
1098
+ placed,
1099
+ options
1100
+ }, ctx) {
1101
+ const opts = { ...DEFAULT_OPTIONS, ...options };
1102
+ const placedCopy = placed.map((p) => ({
1103
+ rect: { ...p.rect },
1104
+ zLayers: [...p.zLayers]
1105
+ }));
1106
+ const placedByLayerCopy = ctx.placedByLayer.map(
1107
+ (layer) => layer.map((r) => ({ ...r }))
1108
+ );
1109
+ return {
1110
+ bounds: { ...ctx.bounds },
1111
+ layerCount: ctx.layerCount,
1112
+ obstaclesByLayer: ctx.obstaclesByLayer,
1113
+ placed: placedCopy,
1114
+ placedByLayer: placedByLayerCopy,
1115
+ options: opts,
1116
+ iteration: 0,
1117
+ gapsFound: [],
1118
+ gapIndex: 0,
1119
+ done: false,
1120
+ initialGapCount: 0,
1121
+ filledCount: 0,
1122
+ // Four-stage visualization state
1123
+ stage: "scan",
1124
+ currentGap: null,
1125
+ currentSeed: null,
1126
+ expandedRect: null
1127
+ };
1128
+ }
1129
+
1130
+ // lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts
1131
+ function mergeUncoveredCells(cells) {
1132
+ if (cells.length === 0) return [];
1133
+ const byXW = /* @__PURE__ */ new Map();
1134
+ for (const c of cells) {
1135
+ const key = `${c.x.toFixed(9)}|${c.w.toFixed(9)}`;
1136
+ const arr = byXW.get(key) ?? [];
1137
+ arr.push(c);
1138
+ byXW.set(key, arr);
1139
+ }
1140
+ const verticalStrips = [];
1141
+ for (const stripCells of byXW.values()) {
1142
+ stripCells.sort((a, b) => a.y - b.y);
1143
+ let current = null;
1144
+ for (const c of stripCells) {
1145
+ if (!current) {
1146
+ current = { x: c.x, y: c.y, width: c.w, height: c.h };
1147
+ } else if (Math.abs(current.y + current.height - c.y) < EPS) {
1148
+ current.height += c.h;
1149
+ } else {
1150
+ verticalStrips.push(current);
1151
+ current = { x: c.x, y: c.y, width: c.w, height: c.h };
1152
+ }
1153
+ }
1154
+ if (current) verticalStrips.push(current);
1155
+ }
1156
+ const byYH = /* @__PURE__ */ new Map();
1157
+ for (const r of verticalStrips) {
1158
+ const key = `${r.y.toFixed(9)}|${r.height.toFixed(9)}`;
1159
+ const arr = byYH.get(key) ?? [];
1160
+ arr.push(r);
1161
+ byYH.set(key, arr);
1162
+ }
1163
+ const merged = [];
1164
+ for (const rowRects of byYH.values()) {
1165
+ rowRects.sort((a, b) => a.x - b.x);
1166
+ let current = null;
1167
+ for (const r of rowRects) {
1168
+ if (!current) {
1169
+ current = { ...r };
1170
+ } else if (Math.abs(current.x + current.width - r.x) < EPS) {
1171
+ current.width += r.width;
1172
+ } else {
1173
+ merged.push(current);
1174
+ current = { ...r };
1175
+ }
1176
+ }
1177
+ if (current) merged.push(current);
1178
+ }
1179
+ return merged;
1180
+ }
1181
+
1182
+ // lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts
1183
+ function findGapsOnLayer({
1184
+ bounds,
1185
+ obstacles,
1186
+ placed,
1187
+ scanResolution
1188
+ }) {
1189
+ const blockers = [...obstacles, ...placed];
1190
+ const xCoords = /* @__PURE__ */ new Set();
1191
+ xCoords.add(bounds.x);
1192
+ xCoords.add(bounds.x + bounds.width);
1193
+ for (const b of blockers) {
1194
+ if (b.x > bounds.x && b.x < bounds.x + bounds.width) {
1195
+ xCoords.add(b.x);
1196
+ }
1197
+ if (b.x + b.width > bounds.x && b.x + b.width < bounds.x + bounds.width) {
1198
+ xCoords.add(b.x + b.width);
1199
+ }
1200
+ }
1201
+ for (let x = bounds.x; x <= bounds.x + bounds.width; x += scanResolution) {
1202
+ xCoords.add(x);
1203
+ }
1204
+ const sortedX = Array.from(xCoords).sort((a, b) => a - b);
1205
+ const yCoords = /* @__PURE__ */ new Set();
1206
+ yCoords.add(bounds.y);
1207
+ yCoords.add(bounds.y + bounds.height);
1208
+ for (const b of blockers) {
1209
+ if (b.y > bounds.y && b.y < bounds.y + bounds.height) {
1210
+ yCoords.add(b.y);
1211
+ }
1212
+ if (b.y + b.height > bounds.y && b.y + b.height < bounds.y + bounds.height) {
1213
+ yCoords.add(b.y + b.height);
1214
+ }
1215
+ }
1216
+ for (let y = bounds.y; y <= bounds.y + bounds.height; y += scanResolution) {
1217
+ yCoords.add(y);
1218
+ }
1219
+ const sortedY = Array.from(yCoords).sort((a, b) => a - b);
1220
+ const uncoveredCells = [];
1221
+ for (let i = 0; i < sortedX.length - 1; i++) {
1222
+ for (let j = 0; j < sortedY.length - 1; j++) {
1223
+ const cellX = sortedX[i];
1224
+ const cellY = sortedY[j];
1225
+ const cellW = sortedX[i + 1] - cellX;
1226
+ const cellH = sortedY[j + 1] - cellY;
1227
+ if (cellW <= EPS || cellH <= EPS) continue;
1228
+ const cellCenterX = cellX + cellW / 2;
1229
+ const cellCenterY = cellY + cellH / 2;
1230
+ const isCovered = blockers.some(
1231
+ (b) => cellCenterX >= b.x - EPS && cellCenterX <= b.x + b.width + EPS && cellCenterY >= b.y - EPS && cellCenterY <= b.y + b.height + EPS
1232
+ );
1233
+ if (!isCovered) {
1234
+ uncoveredCells.push({ x: cellX, y: cellY, w: cellW, h: cellH });
1235
+ }
1236
+ }
1237
+ }
1238
+ return mergeUncoveredCells(uncoveredCells);
1239
+ }
1240
+
1241
+ // utils/rectsOverlap.ts
1242
+ function rectsOverlap(a, b) {
1243
+ return !(a.x + a.width <= b.x + EPS || b.x + b.width <= a.x + EPS || a.y + a.height <= b.y + EPS || b.y + b.height <= a.y + EPS);
1244
+ }
1245
+
1246
+ // utils/rectsEqual.ts
1247
+ function rectsEqual(a, b) {
1248
+ return Math.abs(a.x - b.x) < EPS && Math.abs(a.y - b.y) < EPS && Math.abs(a.width - b.width) < EPS && Math.abs(a.height - b.height) < EPS;
1249
+ }
1250
+
1251
+ // lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts
1252
+ function deduplicateGaps(gaps) {
1253
+ const result = [];
1254
+ for (const gap of gaps) {
1255
+ const existing = result.find(
1256
+ (g) => rectsEqual(g.rect, gap.rect) || rectsOverlap(g.rect, gap.rect) && gap.zLayers.some((z) => g.zLayers.includes(z))
1257
+ );
1258
+ if (!existing) {
1259
+ result.push(gap);
1260
+ } else if (gap.zLayers.length > existing.zLayers.length) {
1261
+ const idx = result.indexOf(existing);
1262
+ result[idx] = gap;
1263
+ }
1264
+ }
1265
+ return result;
1266
+ }
1267
+
1268
+ // lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts
1269
+ function findAllGaps({
1270
+ scanResolution,
1271
+ minWidth,
1272
+ minHeight
1273
+ }, ctx) {
1274
+ const { bounds, layerCount, obstaclesByLayer, placedByLayer } = ctx;
1275
+ const gapsByLayer = [];
1276
+ for (let z = 0; z < layerCount; z++) {
1277
+ const obstacles = obstaclesByLayer[z] ?? [];
1278
+ const placed = placedByLayer[z] ?? [];
1279
+ const gaps = findGapsOnLayer({ bounds, obstacles, placed, scanResolution });
1280
+ gapsByLayer.push(gaps);
1281
+ }
1282
+ const allGaps = [];
1283
+ for (let z = 0; z < layerCount; z++) {
1284
+ for (const gap of gapsByLayer[z]) {
1285
+ if (gap.width < minWidth - EPS || gap.height < minHeight - EPS) continue;
1286
+ const zLayers = [z];
1287
+ for (let zu = z + 1; zu < layerCount; zu++) {
1288
+ const hasOverlap = gapsByLayer[zu].some((g) => rectsOverlap(g, gap));
1289
+ if (hasOverlap) zLayers.push(zu);
1290
+ else break;
1291
+ }
1292
+ for (let zd = z - 1; zd >= 0; zd--) {
1293
+ const hasOverlap = gapsByLayer[zd].some((g) => rectsOverlap(g, gap));
1294
+ if (hasOverlap && !zLayers.includes(zd)) zLayers.unshift(zd);
1295
+ else break;
1296
+ }
1297
+ allGaps.push({
1298
+ rect: gap,
1299
+ zLayers: zLayers.sort((a, b) => a - b),
1300
+ centerX: gap.x + gap.width / 2,
1301
+ centerY: gap.y + gap.height / 2,
1302
+ area: gap.width * gap.height
1303
+ });
1304
+ }
1305
+ }
1306
+ const deduped = deduplicateGaps(allGaps);
1307
+ deduped.sort((a, b) => {
1308
+ const layerDiff = b.zLayers.length - a.zLayers.length;
1309
+ if (layerDiff !== 0) return layerDiff;
1310
+ return b.area - a.area;
1311
+ });
1312
+ return deduped;
1313
+ }
1314
+
1315
+ // lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts
1316
+ function tryExpandGap(state, {
1317
+ gap,
1318
+ seed
1319
+ }) {
1320
+ const blockers = [];
1321
+ for (const z of gap.zLayers) {
1322
+ blockers.push(...state.obstaclesByLayer[z] ?? []);
1323
+ blockers.push(...state.placedByLayer[z] ?? []);
1324
+ }
1325
+ const rect = expandRectFromSeed({
1326
+ startX: seed.x,
1327
+ startY: seed.y,
1328
+ gridSize: Math.min(gap.rect.width, gap.rect.height),
1329
+ bounds: state.bounds,
1330
+ blockers,
1331
+ initialCellRatio: 0,
1332
+ maxAspectRatio: null,
1333
+ minReq: { width: state.options.minWidth, height: state.options.minHeight }
1334
+ });
1335
+ if (!rect) {
1336
+ const seeds = [
1337
+ { x: gap.rect.x + state.options.minWidth / 2, y: gap.centerY },
1338
+ {
1339
+ x: gap.rect.x + gap.rect.width - state.options.minWidth / 2,
1340
+ y: gap.centerY
1341
+ },
1342
+ { x: gap.centerX, y: gap.rect.y + state.options.minHeight / 2 },
1343
+ {
1344
+ x: gap.centerX,
1345
+ y: gap.rect.y + gap.rect.height - state.options.minHeight / 2
1346
+ }
1347
+ ];
1348
+ for (const altSeed of seeds) {
1349
+ const altRect = expandRectFromSeed({
1350
+ startX: altSeed.x,
1351
+ startY: altSeed.y,
1352
+ gridSize: Math.min(gap.rect.width, gap.rect.height),
1353
+ bounds: state.bounds,
1354
+ blockers,
1355
+ initialCellRatio: 0,
1356
+ maxAspectRatio: null,
1357
+ minReq: {
1358
+ width: state.options.minWidth,
1359
+ height: state.options.minHeight
1360
+ }
1361
+ });
1362
+ if (altRect) {
1363
+ return altRect;
1364
+ }
1365
+ }
1366
+ return null;
1367
+ }
1368
+ return rect;
1369
+ }
1370
+
1371
+ // lib/solvers/rectdiff/gapfill/engine/addPlacement.ts
1372
+ function addPlacement(state, {
1373
+ rect,
1374
+ zLayers
1375
+ }) {
1376
+ const placed = { rect, zLayers: [...zLayers] };
1377
+ state.placed.push(placed);
1378
+ for (const z of zLayers) {
1379
+ if (!state.placedByLayer[z]) {
1380
+ state.placedByLayer[z] = [];
1381
+ }
1382
+ state.placedByLayer[z].push(rect);
1383
+ }
1384
+ }
1385
+
1386
+ // lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts
1387
+ function stepGapFill(state) {
1388
+ if (state.done) return false;
1389
+ switch (state.stage) {
1390
+ case "scan": {
1391
+ if (state.gapsFound.length === 0 || state.gapIndex >= state.gapsFound.length) {
1392
+ if (state.iteration >= state.options.maxIterations) {
1393
+ state.done = true;
1394
+ return false;
1395
+ }
1396
+ state.gapsFound = findAllGaps(
1397
+ {
1398
+ scanResolution: state.options.scanResolution,
1399
+ minWidth: state.options.minWidth,
1400
+ minHeight: state.options.minHeight
1401
+ },
1402
+ {
1403
+ bounds: state.bounds,
1404
+ layerCount: state.layerCount,
1405
+ obstaclesByLayer: state.obstaclesByLayer,
1406
+ placedByLayer: state.placedByLayer
1407
+ }
1408
+ );
1409
+ if (state.iteration === 0) {
1410
+ state.initialGapCount = state.gapsFound.length;
1411
+ }
1412
+ state.gapIndex = 0;
1413
+ state.iteration++;
1414
+ if (state.gapsFound.length === 0) {
1415
+ state.done = true;
1416
+ return false;
1417
+ }
1418
+ }
1419
+ state.stage = "select";
1420
+ return true;
1421
+ }
1422
+ case "select": {
1423
+ if (state.gapIndex >= state.gapsFound.length) {
1424
+ state.stage = "scan";
1425
+ return true;
1426
+ }
1427
+ state.currentGap = state.gapsFound[state.gapIndex];
1428
+ state.currentSeed = {
1429
+ x: state.currentGap.centerX,
1430
+ y: state.currentGap.centerY
1431
+ };
1432
+ state.expandedRect = null;
1433
+ state.stage = "expand";
1434
+ return true;
1435
+ }
1436
+ case "expand": {
1437
+ if (!state.currentGap) {
1438
+ state.stage = "select";
1439
+ return true;
1440
+ }
1441
+ const expandedRect = tryExpandGap(state, {
1442
+ gap: state.currentGap,
1443
+ seed: state.currentSeed
1444
+ });
1445
+ state.expandedRect = expandedRect;
1446
+ state.stage = "place";
1447
+ return true;
1448
+ }
1449
+ case "place": {
1450
+ if (state.expandedRect && state.currentGap) {
1451
+ addPlacement(state, {
1452
+ rect: state.expandedRect,
1453
+ zLayers: state.currentGap.zLayers
1454
+ });
1455
+ state.filledCount++;
1456
+ }
1457
+ state.gapIndex++;
1458
+ state.currentGap = null;
1459
+ state.currentSeed = null;
1460
+ state.expandedRect = null;
1461
+ state.stage = "select";
1462
+ return true;
1463
+ }
1464
+ default:
1465
+ state.stage = "scan";
1466
+ return true;
1467
+ }
1468
+ }
1469
+
1470
+ // lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts
1471
+ import { BaseSolver } from "@tscircuit/solver-utils";
1472
+ var GapFillSubSolver = class extends BaseSolver {
1473
+ state;
1474
+ layerCtx;
1475
+ constructor(params) {
1476
+ super();
1477
+ this.layerCtx = params.layerCtx;
1478
+ this.state = initGapFillState(
1479
+ {
1480
+ placed: params.placed,
1481
+ options: params.options
1482
+ },
1483
+ params.layerCtx
1484
+ );
1485
+ }
1486
+ /**
1487
+ * Execute one step of the gap fill algorithm.
1488
+ * Each gap goes through four stages: scan for gaps, select a target gap,
1489
+ * expand a rectangle from seed point, then place the final result.
1490
+ */
1491
+ _step() {
1492
+ const stillWorking = stepGapFill(this.state);
1493
+ if (!stillWorking) {
1494
+ this.solved = true;
1495
+ }
1496
+ }
1497
+ /**
1498
+ * Calculate progress as a value between 0 and 1.
1499
+ * Accounts for iterations, gaps processed, and current stage within each gap.
1500
+ */
1501
+ computeProgress() {
1502
+ return getGapFillProgress(this.state);
1503
+ }
1504
+ /**
1505
+ * Get all placed rectangles including original ones plus newly created gap-fill rectangles.
1506
+ */
1507
+ getPlaced() {
1508
+ return this.state.placed;
1509
+ }
1510
+ /**
1511
+ * Get placed rectangles organized by Z-layer for efficient layer-based operations.
1512
+ */
1513
+ getPlacedByLayer() {
1514
+ return this.state.placedByLayer;
1515
+ }
1516
+ getOutput() {
1517
+ return {
1518
+ placed: this.state.placed,
1519
+ placedByLayer: this.state.placedByLayer,
1520
+ filledCount: this.state.filledCount
1521
+ };
1522
+ }
1523
+ /** Zen visualization: show four-stage gap filling process. */
1524
+ visualize() {
1525
+ const rects = [];
1526
+ const points = [];
1527
+ rects.push({
1528
+ center: {
1529
+ x: this.layerCtx.bounds.x + this.layerCtx.bounds.width / 2,
1530
+ y: this.layerCtx.bounds.y + this.layerCtx.bounds.height / 2
1531
+ },
1532
+ width: this.layerCtx.bounds.width,
1533
+ height: this.layerCtx.bounds.height,
1534
+ fill: "none",
1535
+ stroke: "#e5e7eb",
1536
+ label: ""
1537
+ });
1538
+ switch (this.state.stage) {
1539
+ case "scan": {
1540
+ rects.push({
1541
+ center: {
1542
+ x: this.layerCtx.bounds.x + this.layerCtx.bounds.width / 2,
1543
+ y: this.layerCtx.bounds.y + this.layerCtx.bounds.height / 2
1544
+ },
1545
+ width: this.layerCtx.bounds.width,
1546
+ height: this.layerCtx.bounds.height,
1547
+ fill: "#dbeafe",
1548
+ stroke: "#3b82f6",
1549
+ label: "scanning"
1550
+ });
1551
+ break;
1552
+ }
1553
+ case "select": {
1554
+ if (this.state.currentGap) {
1555
+ rects.push({
1556
+ center: {
1557
+ x: this.state.currentGap.rect.x + this.state.currentGap.rect.width / 2,
1558
+ y: this.state.currentGap.rect.y + this.state.currentGap.rect.height / 2
1559
+ },
1560
+ width: this.state.currentGap.rect.width,
1561
+ height: this.state.currentGap.rect.height,
1562
+ fill: "#fecaca",
1563
+ stroke: "#ef4444",
1564
+ label: "target gap"
1565
+ });
1566
+ if (this.state.currentSeed) {
1567
+ points.push({
1568
+ x: this.state.currentSeed.x,
1569
+ y: this.state.currentSeed.y,
1570
+ color: "#dc2626",
1571
+ label: "seed"
1572
+ });
1573
+ }
1574
+ }
1575
+ break;
1576
+ }
1577
+ case "expand": {
1578
+ if (this.state.currentGap) {
1579
+ rects.push({
1580
+ center: {
1581
+ x: this.state.currentGap.rect.x + this.state.currentGap.rect.width / 2,
1582
+ y: this.state.currentGap.rect.y + this.state.currentGap.rect.height / 2
1583
+ },
1584
+ width: this.state.currentGap.rect.width,
1585
+ height: this.state.currentGap.rect.height,
1586
+ fill: "none",
1587
+ stroke: "#f87171",
1588
+ label: ""
1589
+ });
1590
+ }
1591
+ if (this.state.currentSeed) {
1592
+ points.push({
1593
+ x: this.state.currentSeed.x,
1594
+ y: this.state.currentSeed.y,
1595
+ color: "#f59e0b",
1596
+ label: "expanding"
1597
+ });
1598
+ }
1599
+ if (this.state.expandedRect) {
1600
+ rects.push({
1601
+ center: {
1602
+ x: this.state.expandedRect.x + this.state.expandedRect.width / 2,
1603
+ y: this.state.expandedRect.y + this.state.expandedRect.height / 2
1604
+ },
1605
+ width: this.state.expandedRect.width,
1606
+ height: this.state.expandedRect.height,
1607
+ fill: "#fef3c7",
1608
+ stroke: "#f59e0b",
1609
+ label: "expanding"
1610
+ });
1611
+ }
1612
+ break;
1613
+ }
1614
+ case "place": {
1615
+ if (this.state.expandedRect) {
1616
+ rects.push({
1617
+ center: {
1618
+ x: this.state.expandedRect.x + this.state.expandedRect.width / 2,
1619
+ y: this.state.expandedRect.y + this.state.expandedRect.height / 2
1620
+ },
1621
+ width: this.state.expandedRect.width,
1622
+ height: this.state.expandedRect.height,
1623
+ fill: "#bbf7d0",
1624
+ stroke: "#22c55e",
1625
+ label: "placed"
1626
+ });
1627
+ }
1628
+ break;
1629
+ }
1630
+ }
1631
+ const stageNames = {
1632
+ scan: "scanning",
1633
+ select: "selecting",
1634
+ expand: "expanding",
1635
+ place: "placing"
1636
+ };
1637
+ return {
1638
+ title: `GapFill (${stageNames[this.state.stage]}): ${this.state.filledCount} filled`,
1639
+ coordinateSystem: "cartesian",
1640
+ rects,
1641
+ points
1642
+ };
1643
+ }
1644
+ };
1645
+
989
1646
  // lib/solvers/RectDiffSolver.ts
990
- var RectDiffSolver = class extends BaseSolver {
1647
+ var RectDiffSolver = class extends BaseSolver2 {
991
1648
  srj;
992
- mode;
993
1649
  gridOptions;
1650
+ gapFillOptions;
994
1651
  state;
995
1652
  _meshNodes = [];
996
1653
  constructor(opts) {
997
1654
  super();
998
1655
  this.srj = opts.simpleRouteJson;
999
- this.mode = opts.mode ?? "grid";
1000
1656
  this.gridOptions = opts.gridOptions ?? {};
1657
+ this.gapFillOptions = opts.gapFillOptions ?? {};
1658
+ this.activeSubSolver = null;
1001
1659
  }
1002
1660
  _setup() {
1003
1661
  this.state = initState(this.srj, this.gridOptions);
@@ -1006,12 +1664,44 @@ var RectDiffSolver = class extends BaseSolver {
1006
1664
  gridIndex: this.state.gridIndex
1007
1665
  };
1008
1666
  }
1009
- /** IMPORTANT: exactly ONE small step per call */
1667
+ /** Exactly ONE small step per call. */
1010
1668
  _step() {
1011
1669
  if (this.state.phase === "GRID") {
1012
1670
  stepGrid(this.state);
1013
1671
  } else if (this.state.phase === "EXPANSION") {
1014
1672
  stepExpansion(this.state);
1673
+ } else if (this.state.phase === "GAP_FILL") {
1674
+ if (!this.activeSubSolver || !(this.activeSubSolver instanceof GapFillSubSolver)) {
1675
+ const minTrace = this.srj.minTraceWidth || 0.15;
1676
+ const minGapSize = Math.max(0.01, minTrace / 10);
1677
+ const boundsSize = Math.min(
1678
+ this.state.bounds.width,
1679
+ this.state.bounds.height
1680
+ );
1681
+ this.activeSubSolver = new GapFillSubSolver({
1682
+ placed: this.state.placed,
1683
+ options: {
1684
+ minWidth: minGapSize,
1685
+ minHeight: minGapSize,
1686
+ scanResolution: Math.max(0.05, boundsSize / 100),
1687
+ ...this.gapFillOptions
1688
+ },
1689
+ layerCtx: {
1690
+ bounds: this.state.bounds,
1691
+ layerCount: this.state.layerCount,
1692
+ obstaclesByLayer: this.state.obstaclesByLayer,
1693
+ placedByLayer: this.state.placedByLayer
1694
+ }
1695
+ });
1696
+ }
1697
+ this.activeSubSolver.step();
1698
+ if (this.activeSubSolver.solved) {
1699
+ const output = this.activeSubSolver.getOutput();
1700
+ this.state.placed = output.placed;
1701
+ this.state.placedByLayer = output.placedByLayer;
1702
+ this.activeSubSolver = null;
1703
+ this.state.phase = "DONE";
1704
+ }
1015
1705
  } else if (this.state.phase === "DONE") {
1016
1706
  if (!this.solved) {
1017
1707
  const rects = finalizeRects(this.state);
@@ -1023,44 +1713,81 @@ var RectDiffSolver = class extends BaseSolver {
1023
1713
  this.stats.phase = this.state.phase;
1024
1714
  this.stats.gridIndex = this.state.gridIndex;
1025
1715
  this.stats.placed = this.state.placed.length;
1716
+ if (this.activeSubSolver instanceof GapFillSubSolver) {
1717
+ const output = this.activeSubSolver.getOutput();
1718
+ this.stats.gapsFilled = output.filledCount;
1719
+ }
1026
1720
  }
1027
- // Let BaseSolver update this.progress automatically if present.
1721
+ /** Compute solver progress (0 to 1). */
1028
1722
  computeProgress() {
1029
- return computeProgress(this.state);
1723
+ if (this.solved || this.state.phase === "DONE") {
1724
+ return 1;
1725
+ }
1726
+ if (this.state.phase === "GAP_FILL" && this.activeSubSolver instanceof GapFillSubSolver) {
1727
+ return 0.85 + 0.1 * this.activeSubSolver.computeProgress();
1728
+ }
1729
+ return computeProgress(this.state) * 0.85;
1030
1730
  }
1031
1731
  getOutput() {
1032
1732
  return { meshNodes: this._meshNodes };
1033
1733
  }
1034
- // Helper to get color based on z layer
1734
+ /** Get coverage percentage (0-1). */
1735
+ getCoverage(sampleResolution = 0.05) {
1736
+ return calculateCoverage(
1737
+ { sampleResolution },
1738
+ {
1739
+ bounds: this.state.bounds,
1740
+ layerCount: this.state.layerCount,
1741
+ obstaclesByLayer: this.state.obstaclesByLayer,
1742
+ placedByLayer: this.state.placedByLayer
1743
+ }
1744
+ );
1745
+ }
1746
+ /** Find uncovered points for debugging gaps. */
1747
+ getUncoveredPoints(sampleResolution = 0.05) {
1748
+ return findUncoveredPoints(
1749
+ { sampleResolution },
1750
+ {
1751
+ bounds: this.state.bounds,
1752
+ layerCount: this.state.layerCount,
1753
+ obstaclesByLayer: this.state.obstaclesByLayer,
1754
+ placedByLayer: this.state.placedByLayer
1755
+ }
1756
+ );
1757
+ }
1758
+ /** Get color based on z layer for visualization. */
1035
1759
  getColorForZLayer(zLayers) {
1036
1760
  const minZ = Math.min(...zLayers);
1037
1761
  const colors = [
1038
1762
  { fill: "#dbeafe", stroke: "#3b82f6" },
1039
- // blue (z=0)
1040
1763
  { fill: "#fef3c7", stroke: "#f59e0b" },
1041
- // amber (z=1)
1042
1764
  { fill: "#d1fae5", stroke: "#10b981" },
1043
- // green (z=2)
1044
1765
  { fill: "#e9d5ff", stroke: "#a855f7" },
1045
- // purple (z=3)
1046
1766
  { fill: "#fed7aa", stroke: "#f97316" },
1047
- // orange (z=4)
1048
1767
  { fill: "#fecaca", stroke: "#ef4444" }
1049
- // red (z=5)
1050
1768
  ];
1051
1769
  return colors[minZ % colors.length];
1052
1770
  }
1053
- // Streaming visualization: board + obstacles + current placements.
1771
+ /** Streaming visualization: board + obstacles + current placements. */
1054
1772
  visualize() {
1773
+ if (this.activeSubSolver) {
1774
+ return this.activeSubSolver.visualize();
1775
+ }
1055
1776
  const rects = [];
1056
1777
  const points = [];
1778
+ const boardBounds = {
1779
+ minX: this.srj.bounds.minX,
1780
+ maxX: this.srj.bounds.maxX,
1781
+ minY: this.srj.bounds.minY,
1782
+ maxY: this.srj.bounds.maxY
1783
+ };
1057
1784
  rects.push({
1058
1785
  center: {
1059
- x: (this.srj.bounds.minX + this.srj.bounds.maxX) / 2,
1060
- y: (this.srj.bounds.minY + this.srj.bounds.maxY) / 2
1786
+ x: (boardBounds.minX + boardBounds.maxX) / 2,
1787
+ y: (boardBounds.minY + boardBounds.maxY) / 2
1061
1788
  },
1062
- width: this.srj.bounds.maxX - this.srj.bounds.minX,
1063
- height: this.srj.bounds.maxY - this.srj.bounds.minY,
1789
+ width: boardBounds.maxX - boardBounds.minX,
1790
+ height: boardBounds.maxY - boardBounds.minY,
1064
1791
  fill: "none",
1065
1792
  stroke: "#111827",
1066
1793
  label: "board"
@@ -1107,7 +1834,7 @@ z:${p.zLayers.join(",")}`
1107
1834
  }
1108
1835
  }
1109
1836
  return {
1110
- title: "RectDiff (incremental)",
1837
+ title: `RectDiff (${this.state?.phase ?? "init"})`,
1111
1838
  coordinateSystem: "cartesian",
1112
1839
  rects,
1113
1840
  points