@tscircuit/rectdiff 0.0.6 → 0.0.7
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 +0 -97
- package/dist/index.js +168 -655
- package/lib/solvers/RectDiffSolver.ts +48 -67
- package/lib/solvers/rectdiff/engine.ts +26 -7
- package/lib/solvers/rectdiff/geometry/computeInverseRects.ts +108 -0
- package/lib/solvers/rectdiff/geometry/isPointInPolygon.ts +20 -0
- package/lib/solvers/rectdiff/types.ts +1 -0
- package/package.json +1 -1
- package/tests/__snapshots__/board-outline.snap.svg +59 -0
- package/tests/board-outline.test.ts +18 -0
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// lib/solvers/RectDiffSolver.ts
|
|
2
|
-
import { BaseSolver
|
|
2
|
+
import { BaseSolver } from "@tscircuit/solver-utils";
|
|
3
3
|
|
|
4
4
|
// lib/solvers/rectdiff/geometry.ts
|
|
5
5
|
var EPS = 1e-9;
|
|
@@ -600,6 +600,92 @@ function computeEdgeCandidates3D(params) {
|
|
|
600
600
|
return out;
|
|
601
601
|
}
|
|
602
602
|
|
|
603
|
+
// lib/solvers/rectdiff/geometry/isPointInPolygon.ts
|
|
604
|
+
function isPointInPolygon(p, polygon) {
|
|
605
|
+
let inside = false;
|
|
606
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
607
|
+
const xi = polygon[i].x, yi = polygon[i].y;
|
|
608
|
+
const xj = polygon[j].x, yj = polygon[j].y;
|
|
609
|
+
const intersect = yi > p.y !== yj > p.y && p.x < (xj - xi) * (p.y - yi) / (yj - yi) + xi;
|
|
610
|
+
if (intersect) inside = !inside;
|
|
611
|
+
}
|
|
612
|
+
return inside;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// lib/solvers/rectdiff/geometry/computeInverseRects.ts
|
|
616
|
+
function computeInverseRects(bounds, polygon) {
|
|
617
|
+
if (!polygon || polygon.length < 3) return [];
|
|
618
|
+
const xs = /* @__PURE__ */ new Set([bounds.x, bounds.x + bounds.width]);
|
|
619
|
+
const ys = /* @__PURE__ */ new Set([bounds.y, bounds.y + bounds.height]);
|
|
620
|
+
for (const p of polygon) {
|
|
621
|
+
xs.add(p.x);
|
|
622
|
+
ys.add(p.y);
|
|
623
|
+
}
|
|
624
|
+
const xSorted = Array.from(xs).sort((a, b) => a - b);
|
|
625
|
+
const ySorted = Array.from(ys).sort((a, b) => a - b);
|
|
626
|
+
const rawRects = [];
|
|
627
|
+
for (let i = 0; i < xSorted.length - 1; i++) {
|
|
628
|
+
for (let j = 0; j < ySorted.length - 1; j++) {
|
|
629
|
+
const x0 = xSorted[i];
|
|
630
|
+
const x1 = xSorted[i + 1];
|
|
631
|
+
const y0 = ySorted[j];
|
|
632
|
+
const y1 = ySorted[j + 1];
|
|
633
|
+
const cx = (x0 + x1) / 2;
|
|
634
|
+
const cy = (y0 + y1) / 2;
|
|
635
|
+
if (cx >= bounds.x && cx <= bounds.x + bounds.width && cy >= bounds.y && cy <= bounds.y + bounds.height) {
|
|
636
|
+
if (!isPointInPolygon({ x: cx, y: cy }, polygon)) {
|
|
637
|
+
rawRects.push({ x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
const finalRects = [];
|
|
643
|
+
rawRects.sort((a, b) => {
|
|
644
|
+
if (Math.abs(a.y - b.y) > EPS) return a.y - b.y;
|
|
645
|
+
return a.x - b.x;
|
|
646
|
+
});
|
|
647
|
+
let current = null;
|
|
648
|
+
for (const r of rawRects) {
|
|
649
|
+
if (!current) {
|
|
650
|
+
current = r;
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
const sameY = Math.abs(current.y - r.y) < EPS;
|
|
654
|
+
const sameHeight = Math.abs(current.height - r.height) < EPS;
|
|
655
|
+
const touchesX = Math.abs(current.x + current.width - r.x) < EPS;
|
|
656
|
+
if (sameY && sameHeight && touchesX) {
|
|
657
|
+
current.width += r.width;
|
|
658
|
+
} else {
|
|
659
|
+
finalRects.push(current);
|
|
660
|
+
current = r;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (current) finalRects.push(current);
|
|
664
|
+
finalRects.sort((a, b) => {
|
|
665
|
+
if (Math.abs(a.x - b.x) > EPS) return a.x - b.x;
|
|
666
|
+
return a.y - b.y;
|
|
667
|
+
});
|
|
668
|
+
const mergedVertical = [];
|
|
669
|
+
current = null;
|
|
670
|
+
for (const r of finalRects) {
|
|
671
|
+
if (!current) {
|
|
672
|
+
current = r;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
const sameX = Math.abs(current.x - r.x) < EPS;
|
|
676
|
+
const sameWidth = Math.abs(current.width - r.width) < EPS;
|
|
677
|
+
const touchesY = Math.abs(current.y + current.height - r.y) < EPS;
|
|
678
|
+
if (sameX && sameWidth && touchesY) {
|
|
679
|
+
current.height += r.height;
|
|
680
|
+
} else {
|
|
681
|
+
mergedVertical.push(current);
|
|
682
|
+
current = r;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (current) mergedVertical.push(current);
|
|
686
|
+
return mergedVertical;
|
|
687
|
+
}
|
|
688
|
+
|
|
603
689
|
// lib/solvers/rectdiff/layers.ts
|
|
604
690
|
function layerSortKey(n) {
|
|
605
691
|
const L = n.toLowerCase();
|
|
@@ -689,11 +775,20 @@ function initState(srj, opts) {
|
|
|
689
775
|
{ length: layerCount },
|
|
690
776
|
() => []
|
|
691
777
|
);
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const
|
|
696
|
-
|
|
778
|
+
let boardVoidRects = [];
|
|
779
|
+
if (srj.outline && srj.outline.length > 2) {
|
|
780
|
+
boardVoidRects = computeInverseRects(bounds, srj.outline);
|
|
781
|
+
for (const voidR of boardVoidRects) {
|
|
782
|
+
for (let z = 0; z < layerCount; z++) {
|
|
783
|
+
obstaclesByLayer[z].push(voidR);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
for (const obstacle of srj.obstacles ?? []) {
|
|
788
|
+
const rect = obstacleToXYRect(obstacle);
|
|
789
|
+
if (!rect) continue;
|
|
790
|
+
const zLayers = obstacleZs(obstacle, zIndexByName);
|
|
791
|
+
const invalidZs = zLayers.filter((z) => z < 0 || z >= layerCount);
|
|
697
792
|
if (invalidZs.length) {
|
|
698
793
|
throw new Error(
|
|
699
794
|
`RectDiffSolver: obstacle uses z-layer indices ${invalidZs.join(
|
|
@@ -701,8 +796,9 @@ function initState(srj, opts) {
|
|
|
701
796
|
)} outside 0-${layerCount - 1}`
|
|
702
797
|
);
|
|
703
798
|
}
|
|
704
|
-
if ((!
|
|
705
|
-
|
|
799
|
+
if ((!obstacle.zLayers || obstacle.zLayers.length === 0) && zLayers.length)
|
|
800
|
+
obstacle.zLayers = zLayers;
|
|
801
|
+
for (const z of zLayers) obstaclesByLayer[z].push(rect);
|
|
706
802
|
}
|
|
707
803
|
const trace = Math.max(0.01, srj.minTraceWidth || 0.15);
|
|
708
804
|
const defaults = {
|
|
@@ -731,6 +827,7 @@ function initState(srj, opts) {
|
|
|
731
827
|
bounds,
|
|
732
828
|
options,
|
|
733
829
|
obstaclesByLayer,
|
|
830
|
+
boardVoidRects,
|
|
734
831
|
phase: "GRID",
|
|
735
832
|
gridIndex: 0,
|
|
736
833
|
candidates: [],
|
|
@@ -963,7 +1060,9 @@ function finalizeRects(state) {
|
|
|
963
1060
|
layersByObstacleRect.set(rect, layerIndices);
|
|
964
1061
|
}
|
|
965
1062
|
});
|
|
1063
|
+
const voidSet = new Set(state.boardVoidRects || []);
|
|
966
1064
|
for (const [rect, layerIndices] of layersByObstacleRect.entries()) {
|
|
1065
|
+
if (voidSet.has(rect)) continue;
|
|
967
1066
|
out.push({
|
|
968
1067
|
minX: rect.x,
|
|
969
1068
|
minY: rect.y,
|
|
@@ -1059,603 +1158,16 @@ function findUncoveredPoints({ sampleResolution = 0.05 }, ctx) {
|
|
|
1059
1158
|
return uncovered;
|
|
1060
1159
|
}
|
|
1061
1160
|
|
|
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
|
-
|
|
1646
1161
|
// lib/solvers/RectDiffSolver.ts
|
|
1647
|
-
var RectDiffSolver = class extends
|
|
1162
|
+
var RectDiffSolver = class extends BaseSolver {
|
|
1648
1163
|
srj;
|
|
1649
1164
|
gridOptions;
|
|
1650
|
-
gapFillOptions;
|
|
1651
1165
|
state;
|
|
1652
1166
|
_meshNodes = [];
|
|
1653
1167
|
constructor(opts) {
|
|
1654
1168
|
super();
|
|
1655
1169
|
this.srj = opts.simpleRouteJson;
|
|
1656
1170
|
this.gridOptions = opts.gridOptions ?? {};
|
|
1657
|
-
this.gapFillOptions = opts.gapFillOptions ?? {};
|
|
1658
|
-
this.activeSubSolver = null;
|
|
1659
1171
|
}
|
|
1660
1172
|
_setup() {
|
|
1661
1173
|
this.state = initState(this.srj, this.gridOptions);
|
|
@@ -1671,37 +1183,7 @@ var RectDiffSolver = class extends BaseSolver2 {
|
|
|
1671
1183
|
} else if (this.state.phase === "EXPANSION") {
|
|
1672
1184
|
stepExpansion(this.state);
|
|
1673
1185
|
} else if (this.state.phase === "GAP_FILL") {
|
|
1674
|
-
|
|
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
|
-
}
|
|
1186
|
+
this.state.phase = "DONE";
|
|
1705
1187
|
} else if (this.state.phase === "DONE") {
|
|
1706
1188
|
if (!this.solved) {
|
|
1707
1189
|
const rects = finalizeRects(this.state);
|
|
@@ -1713,20 +1195,13 @@ var RectDiffSolver = class extends BaseSolver2 {
|
|
|
1713
1195
|
this.stats.phase = this.state.phase;
|
|
1714
1196
|
this.stats.gridIndex = this.state.gridIndex;
|
|
1715
1197
|
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
|
-
}
|
|
1720
1198
|
}
|
|
1721
1199
|
/** Compute solver progress (0 to 1). */
|
|
1722
1200
|
computeProgress() {
|
|
1723
1201
|
if (this.solved || this.state.phase === "DONE") {
|
|
1724
1202
|
return 1;
|
|
1725
1203
|
}
|
|
1726
|
-
|
|
1727
|
-
return 0.85 + 0.1 * this.activeSubSolver.computeProgress();
|
|
1728
|
-
}
|
|
1729
|
-
return computeProgress(this.state) * 0.85;
|
|
1204
|
+
return computeProgress(this.state);
|
|
1730
1205
|
}
|
|
1731
1206
|
getOutput() {
|
|
1732
1207
|
return { meshNodes: this._meshNodes };
|
|
@@ -1770,34 +1245,42 @@ var RectDiffSolver = class extends BaseSolver2 {
|
|
|
1770
1245
|
}
|
|
1771
1246
|
/** Streaming visualization: board + obstacles + current placements. */
|
|
1772
1247
|
visualize() {
|
|
1773
|
-
if (this.activeSubSolver) {
|
|
1774
|
-
return this.activeSubSolver.visualize();
|
|
1775
|
-
}
|
|
1776
1248
|
const rects = [];
|
|
1777
1249
|
const points = [];
|
|
1250
|
+
const lines = [];
|
|
1778
1251
|
const boardBounds = {
|
|
1779
1252
|
minX: this.srj.bounds.minX,
|
|
1780
1253
|
maxX: this.srj.bounds.maxX,
|
|
1781
1254
|
minY: this.srj.bounds.minY,
|
|
1782
1255
|
maxY: this.srj.bounds.maxY
|
|
1783
1256
|
};
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1257
|
+
if (this.srj.outline && this.srj.outline.length > 1) {
|
|
1258
|
+
lines.push({
|
|
1259
|
+
points: [...this.srj.outline, this.srj.outline[0]],
|
|
1260
|
+
// Close the loop by adding the first point again
|
|
1261
|
+
strokeColor: "#111827",
|
|
1262
|
+
strokeWidth: 0.01,
|
|
1263
|
+
label: "outline"
|
|
1264
|
+
});
|
|
1265
|
+
} else {
|
|
1266
|
+
rects.push({
|
|
1267
|
+
center: {
|
|
1268
|
+
x: (boardBounds.minX + boardBounds.maxX) / 2,
|
|
1269
|
+
y: (boardBounds.minY + boardBounds.maxY) / 2
|
|
1270
|
+
},
|
|
1271
|
+
width: boardBounds.maxX - boardBounds.minX,
|
|
1272
|
+
height: boardBounds.maxY - boardBounds.minY,
|
|
1273
|
+
fill: "none",
|
|
1274
|
+
stroke: "#111827",
|
|
1275
|
+
label: "board"
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
for (const obstacle of this.srj.obstacles ?? []) {
|
|
1279
|
+
if (obstacle.type === "rect" || obstacle.type === "oval") {
|
|
1797
1280
|
rects.push({
|
|
1798
|
-
center: { x:
|
|
1799
|
-
width:
|
|
1800
|
-
height:
|
|
1281
|
+
center: { x: obstacle.center.x, y: obstacle.center.y },
|
|
1282
|
+
width: obstacle.width,
|
|
1283
|
+
height: obstacle.height,
|
|
1801
1284
|
fill: "#fee2e2",
|
|
1802
1285
|
stroke: "#ef4444",
|
|
1803
1286
|
layer: "obstacle",
|
|
@@ -1805,6 +1288,34 @@ var RectDiffSolver = class extends BaseSolver2 {
|
|
|
1805
1288
|
});
|
|
1806
1289
|
}
|
|
1807
1290
|
}
|
|
1291
|
+
if (this.state?.boardVoidRects) {
|
|
1292
|
+
let outlineBBox = null;
|
|
1293
|
+
if (this.srj.outline && this.srj.outline.length > 0) {
|
|
1294
|
+
const xs = this.srj.outline.map((p) => p.x);
|
|
1295
|
+
const ys = this.srj.outline.map((p) => p.y);
|
|
1296
|
+
const minX = Math.min(...xs);
|
|
1297
|
+
const minY = Math.min(...ys);
|
|
1298
|
+
outlineBBox = {
|
|
1299
|
+
x: minX,
|
|
1300
|
+
y: minY,
|
|
1301
|
+
width: Math.max(...xs) - minX,
|
|
1302
|
+
height: Math.max(...ys) - minY
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
for (const r of this.state.boardVoidRects) {
|
|
1306
|
+
if (outlineBBox && !overlaps(r, outlineBBox)) {
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
rects.push({
|
|
1310
|
+
center: { x: r.x + r.width / 2, y: r.y + r.height / 2 },
|
|
1311
|
+
width: r.width,
|
|
1312
|
+
height: r.height,
|
|
1313
|
+
fill: "rgba(0, 0, 0, 0.5)",
|
|
1314
|
+
stroke: "none",
|
|
1315
|
+
label: "void"
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1808
1319
|
if (this.state?.candidates?.length) {
|
|
1809
1320
|
for (const cand of this.state.candidates) {
|
|
1810
1321
|
points.push({
|
|
@@ -1837,7 +1348,9 @@ z:${p.zLayers.join(",")}`
|
|
|
1837
1348
|
title: `RectDiff (${this.state?.phase ?? "init"})`,
|
|
1838
1349
|
coordinateSystem: "cartesian",
|
|
1839
1350
|
rects,
|
|
1840
|
-
points
|
|
1351
|
+
points,
|
|
1352
|
+
lines
|
|
1353
|
+
// Include lines in the returned GraphicsObject
|
|
1841
1354
|
};
|
|
1842
1355
|
}
|
|
1843
1356
|
};
|