@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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // lib/solvers/RectDiffSolver.ts
2
- import { BaseSolver as BaseSolver2 } from "@tscircuit/solver-utils";
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
- for (const ob of srj.obstacles ?? []) {
693
- const r = obstacleToXYRect(ob);
694
- if (!r) continue;
695
- const zs = obstacleZs(ob, zIndexByName);
696
- const invalidZs = zs.filter((z) => z < 0 || z >= layerCount);
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 ((!ob.zLayers || ob.zLayers.length === 0) && zs.length) ob.zLayers = zs;
705
- for (const z of zs) obstaclesByLayer[z].push(r);
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 BaseSolver2 {
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
- 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
- }
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
- 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;
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
- rects.push({
1785
- center: {
1786
- x: (boardBounds.minX + boardBounds.maxX) / 2,
1787
- y: (boardBounds.minY + boardBounds.maxY) / 2
1788
- },
1789
- width: boardBounds.maxX - boardBounds.minX,
1790
- height: boardBounds.maxY - boardBounds.minY,
1791
- fill: "none",
1792
- stroke: "#111827",
1793
- label: "board"
1794
- });
1795
- for (const ob of this.srj.obstacles ?? []) {
1796
- if (ob.type === "rect" || ob.type === "oval") {
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: ob.center.x, y: ob.center.y },
1799
- width: ob.width,
1800
- height: ob.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
  };