@tscircuit/rectdiff 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +112 -3
- package/dist/index.js +869 -142
- package/lib/solvers/RectDiffSolver.ts +144 -32
- package/lib/solvers/rectdiff/candidates.ts +150 -104
- package/lib/solvers/rectdiff/engine.ts +72 -53
- package/lib/solvers/rectdiff/gapfill/detection/deduplicateGaps.ts +28 -0
- package/lib/solvers/rectdiff/gapfill/detection/findAllGaps.ts +83 -0
- package/lib/solvers/rectdiff/gapfill/detection/findGapsOnLayer.ts +100 -0
- package/lib/solvers/rectdiff/gapfill/detection/mergeUncoveredCells.ts +75 -0
- package/lib/solvers/rectdiff/gapfill/detection.ts +3 -0
- package/lib/solvers/rectdiff/gapfill/engine/addPlacement.ts +27 -0
- package/lib/solvers/rectdiff/gapfill/engine/calculateCoverage.ts +44 -0
- package/lib/solvers/rectdiff/gapfill/engine/findUncoveredPoints.ts +43 -0
- package/lib/solvers/rectdiff/gapfill/engine/getGapFillProgress.ts +42 -0
- package/lib/solvers/rectdiff/gapfill/engine/initGapFillState.ts +57 -0
- package/lib/solvers/rectdiff/gapfill/engine/stepGapFill.ts +128 -0
- package/lib/solvers/rectdiff/gapfill/engine/tryExpandGap.ts +78 -0
- package/lib/solvers/rectdiff/gapfill/engine.ts +7 -0
- package/lib/solvers/rectdiff/gapfill/types.ts +60 -0
- package/lib/solvers/rectdiff/geometry.ts +23 -11
- package/lib/solvers/rectdiff/subsolvers/GapFillSubSolver.ts +253 -0
- package/lib/solvers/rectdiff/types.ts +1 -1
- package/package.json +1 -1
- package/pages/board-with-cutout.page.tsx +11 -0
- package/test-assets/board-with-cutout.json +148 -0
- package/tests/obstacle-extra-layers.test.ts +1 -1
- package/tests/obstacle-zlayers.test.ts +1 -1
- package/tests/rect-diff-solver.test.ts +1 -4
- package/utils/README.md +21 -0
- package/utils/rectsEqual.ts +18 -0
- 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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 = "
|
|
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
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
1721
|
+
/** Compute solver progress (0 to 1). */
|
|
1028
1722
|
computeProgress() {
|
|
1029
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: (
|
|
1060
|
-
y: (
|
|
1786
|
+
x: (boardBounds.minX + boardBounds.maxX) / 2,
|
|
1787
|
+
y: (boardBounds.minY + boardBounds.maxY) / 2
|
|
1061
1788
|
},
|
|
1062
|
-
width:
|
|
1063
|
-
height:
|
|
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:
|
|
1837
|
+
title: `RectDiff (${this.state?.phase ?? "init"})`,
|
|
1111
1838
|
coordinateSystem: "cartesian",
|
|
1112
1839
|
rects,
|
|
1113
1840
|
points
|