@tscircuit/rectdiff 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/index.d.ts +163 -27
  2. package/dist/index.js +1885 -1676
  3. package/lib/RectDiffPipeline.ts +18 -17
  4. package/lib/{solvers/rectdiff/types.ts → rectdiff-types.ts} +0 -34
  5. package/lib/{solvers/rectdiff/visualization.ts → rectdiff-visualization.ts} +2 -1
  6. package/lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts +9 -12
  7. package/lib/solvers/GapFillSolver/GapFillSolverPipeline.ts +1 -1
  8. package/lib/solvers/GapFillSolver/visuallyOffsetLine.ts +5 -2
  9. package/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +205 -0
  10. package/lib/solvers/{rectdiff → RectDiffExpansionSolver}/rectsToMeshNodes.ts +1 -2
  11. package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +87 -0
  12. package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +516 -0
  13. package/lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts +109 -0
  14. package/lib/solvers/RectDiffSeedingSolver/computeDefaultGridSizes.ts +9 -0
  15. package/lib/solvers/{rectdiff/candidates.ts → RectDiffSeedingSolver/computeEdgeCandidates3D.ts} +39 -220
  16. package/lib/solvers/{rectdiff/geometry → RectDiffSeedingSolver}/computeInverseRects.ts +6 -2
  17. package/lib/solvers/{rectdiff → RectDiffSeedingSolver}/layers.ts +4 -3
  18. package/lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts +52 -0
  19. package/lib/utils/buildHardPlacedByLayer.ts +14 -0
  20. package/lib/{solvers/rectdiff/geometry.ts → utils/expandRectFromSeed.ts} +21 -120
  21. package/lib/utils/finalizeRects.ts +49 -0
  22. package/lib/utils/isFullyOccupiedAtPoint.ts +21 -0
  23. package/lib/utils/rectdiff-geometry.ts +94 -0
  24. package/lib/utils/resizeSoftOverlaps.ts +74 -0
  25. package/package.json +1 -1
  26. package/tests/board-outline.test.ts +2 -1
  27. package/tests/obstacle-extra-layers.test.ts +1 -1
  28. package/tests/obstacle-zlayers.test.ts +1 -1
  29. package/utils/rectsEqual.ts +2 -2
  30. package/utils/rectsOverlap.ts +2 -2
  31. package/lib/solvers/RectDiffSolver.ts +0 -231
  32. package/lib/solvers/rectdiff/engine.ts +0 -481
  33. /package/lib/solvers/{rectdiff/geometry → RectDiffSeedingSolver}/isPointInPolygon.ts +0 -0
package/dist/index.js CHANGED
@@ -1,612 +1,721 @@
1
1
  // lib/RectDiffPipeline.ts
2
2
  import {
3
3
  BasePipelineSolver as BasePipelineSolver3,
4
- definePipelineStep as definePipelineStep2
4
+ definePipelineStep as definePipelineStep3
5
+ } from "@tscircuit/solver-utils";
6
+
7
+ // lib/solvers/GapFillSolver/GapFillSolverPipeline.ts
8
+ import {
9
+ BasePipelineSolver,
10
+ definePipelineStep
5
11
  } from "@tscircuit/solver-utils";
6
12
 
7
- // lib/solvers/RectDiffSolver.ts
13
+ // lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts
8
14
  import { BaseSolver } from "@tscircuit/solver-utils";
15
+ import Flatbush from "flatbush";
9
16
 
10
- // lib/solvers/rectdiff/geometry.ts
11
- var EPS = 1e-9;
12
- var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
13
- var gt = (a, b) => a > b + EPS;
14
- var gte = (a, b) => a > b - EPS;
15
- var lt = (a, b) => a < b - EPS;
16
- var lte = (a, b) => a < b + EPS;
17
- function overlaps(a, b) {
18
- 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);
19
- }
20
- function containsPoint(r, x, y) {
21
- return x >= r.x - EPS && x <= r.x + r.width + EPS && y >= r.y - EPS && y <= r.y + r.height + EPS;
22
- }
23
- function distancePointToRectEdges(px, py, r) {
24
- const edges = [
25
- [r.x, r.y, r.x + r.width, r.y],
26
- [r.x + r.width, r.y, r.x + r.width, r.y + r.height],
27
- [r.x + r.width, r.y + r.height, r.x, r.y + r.height],
28
- [r.x, r.y + r.height, r.x, r.y]
29
- ];
30
- let best = Infinity;
31
- for (const [x1, y1, x2, y2] of edges) {
32
- const A = px - x1, B = py - y1, C = x2 - x1, D = y2 - y1;
33
- const dot = A * C + B * D;
34
- const lenSq = C * C + D * D;
35
- let t = lenSq !== 0 ? dot / lenSq : 0;
36
- t = clamp(t, 0, 1);
37
- const xx = x1 + t * C;
38
- const yy = y1 + t * D;
39
- best = Math.min(best, Math.hypot(px - xx, py - yy));
17
+ // lib/solvers/GapFillSolver/projectToUncoveredSegments.ts
18
+ var EPS = 1e-4;
19
+ function projectToUncoveredSegments(primaryEdge, overlappingEdges) {
20
+ const isHorizontal = Math.abs(primaryEdge.start.y - primaryEdge.end.y) < EPS;
21
+ const isVertical = Math.abs(primaryEdge.start.x - primaryEdge.end.x) < EPS;
22
+ if (!isHorizontal && !isVertical) return [];
23
+ const axis = isHorizontal ? "x" : "y";
24
+ const perp = isHorizontal ? "y" : "x";
25
+ const lineCoord = primaryEdge.start[perp];
26
+ const p0 = primaryEdge.start[axis];
27
+ const p1 = primaryEdge.end[axis];
28
+ const pMin = Math.min(p0, p1);
29
+ const pMax = Math.max(p0, p1);
30
+ const clamp2 = (v) => Math.max(pMin, Math.min(pMax, v));
31
+ const intervals = [];
32
+ for (const e of overlappingEdges) {
33
+ if (e === primaryEdge) continue;
34
+ const eIsHorizontal = Math.abs(e.start.y - e.end.y) < EPS;
35
+ const eIsVertical = Math.abs(e.start.x - e.end.x) < EPS;
36
+ if (axis === "x" && !eIsHorizontal) continue;
37
+ if (axis === "y" && !eIsVertical) continue;
38
+ if (Math.abs(e.start[perp] - lineCoord) > EPS) continue;
39
+ const eMin = Math.min(e.start[axis], e.end[axis]);
40
+ const eMax = Math.max(e.start[axis], e.end[axis]);
41
+ const s = clamp2(eMin);
42
+ const t = clamp2(eMax);
43
+ if (t - s > EPS) intervals.push({ s, e: t });
40
44
  }
41
- return best;
42
- }
43
- function maxExpandRight(r, bounds, blockers, maxAspect) {
44
- let maxWidth = bounds.x + bounds.width - r.x;
45
- for (const b of blockers) {
46
- const verticallyOverlaps = r.y + r.height > b.y + EPS && b.y + b.height > r.y + EPS;
47
- if (verticallyOverlaps) {
48
- if (gte(b.x, r.x + r.width)) {
49
- maxWidth = Math.min(maxWidth, b.x - r.x);
50
- } else if (b.x + b.width > r.x + r.width - EPS && b.x < r.x + r.width + EPS) {
51
- return 0;
45
+ if (intervals.length === 0) {
46
+ return [
47
+ {
48
+ ...primaryEdge,
49
+ start: { ...primaryEdge.start },
50
+ end: { ...primaryEdge.end }
52
51
  }
53
- }
54
- }
55
- let e = Math.max(0, maxWidth - r.width);
56
- if (e <= 0) return 0;
57
- if (maxAspect != null) {
58
- const w = r.width, h = r.height;
59
- if (w >= h) e = Math.min(e, maxAspect * h - w);
52
+ ];
60
53
  }
61
- return Math.max(0, e);
62
- }
63
- function maxExpandDown(r, bounds, blockers, maxAspect) {
64
- let maxHeight = bounds.y + bounds.height - r.y;
65
- for (const b of blockers) {
66
- const horizOverlaps = r.x + r.width > b.x + EPS && b.x + b.width > r.x + EPS;
67
- if (horizOverlaps) {
68
- if (gte(b.y, r.y + r.height)) {
69
- maxHeight = Math.min(maxHeight, b.y - r.y);
70
- } else if (b.y + b.height > r.y + r.height - EPS && b.y < r.y + r.height + EPS) {
71
- return 0;
72
- }
73
- }
54
+ intervals.sort((a, b) => a.s - b.s);
55
+ const merged = [];
56
+ for (const it of intervals) {
57
+ const last = merged[merged.length - 1];
58
+ if (!last || it.s > last.e + EPS) merged.push({ ...it });
59
+ else last.e = Math.max(last.e, it.e);
74
60
  }
75
- let e = Math.max(0, maxHeight - r.height);
76
- if (e <= 0) return 0;
77
- if (maxAspect != null) {
78
- const w = r.width, h = r.height;
79
- if (h >= w) e = Math.min(e, maxAspect * w - h);
61
+ const uncovered = [];
62
+ let cursor = pMin;
63
+ for (const m of merged) {
64
+ if (m.s > cursor + EPS) uncovered.push({ s: cursor, e: m.s });
65
+ cursor = Math.max(cursor, m.e);
66
+ if (cursor >= pMax - EPS) break;
80
67
  }
81
- return Math.max(0, e);
68
+ if (pMax > cursor + EPS) uncovered.push({ s: cursor, e: pMax });
69
+ if (uncovered.length === 0) return [];
70
+ return uncovered.filter((u) => u.e - u.s > EPS).map((u) => {
71
+ const start = axis === "x" ? { x: u.s, y: lineCoord } : { x: lineCoord, y: u.s };
72
+ const end = axis === "x" ? { x: u.e, y: lineCoord } : { x: lineCoord, y: u.e };
73
+ return {
74
+ parent: primaryEdge.parent,
75
+ facingDirection: primaryEdge.facingDirection,
76
+ start,
77
+ end,
78
+ z: primaryEdge.z
79
+ };
80
+ });
82
81
  }
83
- function maxExpandLeft(r, bounds, blockers, maxAspect) {
84
- let minX = bounds.x;
85
- for (const b of blockers) {
86
- const verticallyOverlaps = r.y + r.height > b.y + EPS && b.y + b.height > r.y + EPS;
87
- if (verticallyOverlaps) {
88
- if (lte(b.x + b.width, r.x)) {
89
- minX = Math.max(minX, b.x + b.width);
90
- } else if (b.x < r.x + EPS && b.x + b.width > r.x - EPS) {
91
- return 0;
92
- }
93
- }
82
+
83
+ // lib/solvers/GapFillSolver/edge-constants.ts
84
+ var EDGES = [
85
+ {
86
+ facingDirection: "x-",
87
+ dx: -1,
88
+ dy: 0,
89
+ startX: -0.5,
90
+ startY: 0.5,
91
+ endX: -0.5,
92
+ endY: -0.5
93
+ },
94
+ {
95
+ facingDirection: "x+",
96
+ dx: 1,
97
+ dy: 0,
98
+ startX: 0.5,
99
+ startY: 0.5,
100
+ endX: 0.5,
101
+ endY: -0.5
102
+ },
103
+ {
104
+ facingDirection: "y-",
105
+ dx: 0,
106
+ dy: -1,
107
+ startX: -0.5,
108
+ startY: -0.5,
109
+ endX: 0.5,
110
+ endY: -0.5
111
+ },
112
+ {
113
+ facingDirection: "y+",
114
+ dx: 0,
115
+ dy: 1,
116
+ startX: 0.5,
117
+ startY: 0.5,
118
+ endX: -0.5,
119
+ endY: 0.5
94
120
  }
95
- let e = Math.max(0, r.x - minX);
96
- if (e <= 0) return 0;
97
- if (maxAspect != null) {
98
- const w = r.width, h = r.height;
99
- if (w >= h) e = Math.min(e, maxAspect * h - w);
121
+ ];
122
+ var EDGE_MAP = {
123
+ "x-": EDGES.find((e) => e.facingDirection === "x-"),
124
+ "x+": EDGES.find((e) => e.facingDirection === "x+"),
125
+ "y-": EDGES.find((e) => e.facingDirection === "y-"),
126
+ "y+": EDGES.find((e) => e.facingDirection === "y+")
127
+ };
128
+
129
+ // lib/solvers/GapFillSolver/visuallyOffsetLine.ts
130
+ var OFFSET_DIR_MAP = {
131
+ "x-": {
132
+ x: -1,
133
+ y: 0
134
+ },
135
+ "x+": {
136
+ x: 1,
137
+ y: 0
138
+ },
139
+ "y-": {
140
+ x: 0,
141
+ y: -1
142
+ },
143
+ "y+": {
144
+ x: 0,
145
+ y: 1
100
146
  }
101
- return Math.max(0, e);
102
- }
103
- function maxExpandUp(r, bounds, blockers, maxAspect) {
104
- let minY = bounds.y;
105
- for (const b of blockers) {
106
- const horizOverlaps = r.x + r.width > b.x + EPS && b.x + b.width > r.x + EPS;
107
- if (horizOverlaps) {
108
- if (lte(b.y + b.height, r.y)) {
109
- minY = Math.max(minY, b.y + b.height);
110
- } else if (b.y < r.y + EPS && b.y + b.height > r.y - EPS) {
111
- return 0;
147
+ };
148
+ var visuallyOffsetLine = (line, options) => {
149
+ const { dir, amt } = options;
150
+ const offset = OFFSET_DIR_MAP[dir];
151
+ return line.map((p) => ({
152
+ x: p.x + offset.x * amt,
153
+ y: p.y + offset.y * amt
154
+ }));
155
+ };
156
+
157
+ // lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts
158
+ import "@tscircuit/math-utils";
159
+ var EPS2 = 1e-4;
160
+ var FindSegmentsWithAdjacentEmptySpaceSolver = class extends BaseSolver {
161
+ constructor(input) {
162
+ super();
163
+ this.input = input;
164
+ for (const node of this.input.meshNodes) {
165
+ for (const edge of EDGES) {
166
+ let start = {
167
+ x: node.center.x + node.width * edge.startX,
168
+ y: node.center.y + node.height * edge.startY
169
+ };
170
+ let end = {
171
+ x: node.center.x + node.width * edge.endX,
172
+ y: node.center.y + node.height * edge.endY
173
+ };
174
+ if (start.x > end.x) {
175
+ ;
176
+ [start, end] = [end, start];
177
+ }
178
+ if (Math.abs(start.x - end.x) < EPS2 && start.y > end.y) {
179
+ ;
180
+ [start, end] = [end, start];
181
+ }
182
+ for (const z of node.availableZ) {
183
+ this.unprocessedEdges.push({
184
+ parent: node,
185
+ start,
186
+ end,
187
+ facingDirection: edge.facingDirection,
188
+ z
189
+ });
190
+ }
112
191
  }
113
192
  }
193
+ this.allEdges = [...this.unprocessedEdges];
194
+ this.edgeSpatialIndex = new Flatbush(this.allEdges.length);
195
+ for (const edge of this.allEdges) {
196
+ this.edgeSpatialIndex.add(
197
+ edge.start.x,
198
+ edge.start.y,
199
+ edge.end.x,
200
+ edge.end.y
201
+ );
202
+ }
203
+ this.edgeSpatialIndex.finish();
114
204
  }
115
- let e = Math.max(0, r.y - minY);
116
- if (e <= 0) return 0;
117
- if (maxAspect != null) {
118
- const w = r.width, h = r.height;
119
- if (h >= w) e = Math.min(e, maxAspect * w - h);
120
- }
121
- return Math.max(0, e);
122
- }
123
- function expandRectFromSeed(params) {
124
- const {
125
- startX,
126
- startY,
127
- gridSize,
128
- bounds,
129
- blockers,
130
- initialCellRatio,
131
- maxAspectRatio,
132
- minReq
133
- } = params;
134
- const minSide = Math.max(1e-9, gridSize * initialCellRatio);
135
- const initialW = Math.max(minSide, minReq.width);
136
- const initialH = Math.max(minSide, minReq.height);
137
- const strategies = [
138
- { ox: 0, oy: 0 },
139
- { ox: -initialW, oy: 0 },
140
- { ox: 0, oy: -initialH },
141
- { ox: -initialW, oy: -initialH },
142
- { ox: -initialW / 2, oy: -initialH / 2 }
143
- ];
144
- let best = null;
145
- let bestArea = 0;
146
- STRATS: for (const s of strategies) {
147
- let r = {
148
- x: startX + s.ox,
149
- y: startY + s.oy,
150
- width: initialW,
151
- height: initialH
205
+ allEdges;
206
+ unprocessedEdges = [];
207
+ segmentsWithAdjacentEmptySpace = [];
208
+ edgeSpatialIndex;
209
+ lastCandidateEdge = null;
210
+ lastOverlappingEdges = null;
211
+ lastUncoveredSegments = null;
212
+ _step() {
213
+ if (this.unprocessedEdges.length === 0) {
214
+ this.solved = true;
215
+ this.lastCandidateEdge = null;
216
+ this.lastOverlappingEdges = null;
217
+ this.lastUncoveredSegments = null;
218
+ return;
219
+ }
220
+ const candidateEdge = this.unprocessedEdges.shift();
221
+ this.lastCandidateEdge = candidateEdge;
222
+ const nearbyEdges = this.edgeSpatialIndex.search(
223
+ candidateEdge.start.x - EPS2,
224
+ candidateEdge.start.y - EPS2,
225
+ candidateEdge.end.x + EPS2,
226
+ candidateEdge.end.y + EPS2
227
+ );
228
+ const overlappingEdges = nearbyEdges.map((i) => this.allEdges[i]).filter((e) => e.z === candidateEdge.z);
229
+ this.lastOverlappingEdges = overlappingEdges;
230
+ const uncoveredSegments = projectToUncoveredSegments(
231
+ candidateEdge,
232
+ overlappingEdges
233
+ );
234
+ this.lastUncoveredSegments = uncoveredSegments;
235
+ this.segmentsWithAdjacentEmptySpace.push(...uncoveredSegments);
236
+ }
237
+ getOutput() {
238
+ return {
239
+ segmentsWithAdjacentEmptySpace: this.segmentsWithAdjacentEmptySpace
152
240
  };
153
- if (lt(r.x, bounds.x) || lt(r.y, bounds.y) || gt(r.x + r.width, bounds.x + bounds.width) || gt(r.y + r.height, bounds.y + bounds.height)) {
154
- continue;
241
+ }
242
+ visualize() {
243
+ const graphics = {
244
+ title: "FindSegmentsWithAdjacentEmptySpace",
245
+ coordinateSystem: "cartesian",
246
+ rects: [],
247
+ points: [],
248
+ lines: [],
249
+ circles: [],
250
+ arrows: [],
251
+ texts: []
252
+ };
253
+ for (const node of this.input.meshNodes) {
254
+ graphics.rects.push({
255
+ center: node.center,
256
+ width: node.width,
257
+ height: node.height,
258
+ stroke: "rgba(0, 0, 0, 0.1)"
259
+ });
155
260
  }
156
- for (const b of blockers) if (overlaps(r, b)) continue STRATS;
157
- let improved = true;
158
- while (improved) {
159
- improved = false;
160
- const eR = maxExpandRight(r, bounds, blockers, maxAspectRatio);
161
- if (eR > 0) {
162
- r = { ...r, width: r.width + eR };
163
- improved = true;
164
- }
165
- const eD = maxExpandDown(r, bounds, blockers, maxAspectRatio);
166
- if (eD > 0) {
167
- r = { ...r, height: r.height + eD };
168
- improved = true;
169
- }
170
- const eL = maxExpandLeft(r, bounds, blockers, maxAspectRatio);
171
- if (eL > 0) {
172
- r = { x: r.x - eL, y: r.y, width: r.width + eL, height: r.height };
173
- improved = true;
174
- }
175
- const eU = maxExpandUp(r, bounds, blockers, maxAspectRatio);
176
- if (eU > 0) {
177
- r = { x: r.x, y: r.y - eU, width: r.width, height: r.height + eU };
178
- improved = true;
179
- }
261
+ for (const unprocessedEdge of this.unprocessedEdges) {
262
+ graphics.lines.push({
263
+ points: visuallyOffsetLine(
264
+ [unprocessedEdge.start, unprocessedEdge.end],
265
+ { dir: unprocessedEdge.facingDirection, amt: -0.1 }
266
+ ),
267
+ strokeColor: "rgba(0, 0, 255, 0.5)",
268
+ strokeDash: "5 5"
269
+ });
180
270
  }
181
- if (r.width + EPS >= minReq.width && r.height + EPS >= minReq.height) {
182
- const area = r.width * r.height;
183
- if (area > bestArea) {
184
- best = r;
185
- bestArea = area;
186
- }
271
+ for (const edge of this.segmentsWithAdjacentEmptySpace) {
272
+ graphics.lines.push({
273
+ points: [edge.start, edge.end],
274
+ strokeColor: "rgba(0,255,0, 0.5)"
275
+ });
187
276
  }
188
- }
189
- return best;
190
- }
191
- function intersect1D(a0, a1, b0, b1) {
192
- const lo = Math.max(a0, b0);
193
- const hi = Math.min(a1, b1);
194
- return hi > lo + EPS ? [lo, hi] : null;
195
- }
196
- function subtractRect2D(A, B) {
197
- if (!overlaps(A, B)) return [A];
198
- const Xi = intersect1D(A.x, A.x + A.width, B.x, B.x + B.width);
199
- const Yi = intersect1D(A.y, A.y + A.height, B.y, B.y + B.height);
200
- if (!Xi || !Yi) return [A];
201
- const [X0, X1] = Xi;
202
- const [Y0, Y1] = Yi;
203
- const out = [];
204
- if (X0 > A.x + EPS) {
205
- out.push({ x: A.x, y: A.y, width: X0 - A.x, height: A.height });
206
- }
207
- if (A.x + A.width > X1 + EPS) {
208
- out.push({ x: X1, y: A.y, width: A.x + A.width - X1, height: A.height });
209
- }
210
- const midW = Math.max(0, X1 - X0);
211
- if (midW > EPS && Y0 > A.y + EPS) {
212
- out.push({ x: X0, y: A.y, width: midW, height: Y0 - A.y });
213
- }
214
- if (midW > EPS && A.y + A.height > Y1 + EPS) {
215
- out.push({ x: X0, y: Y1, width: midW, height: A.y + A.height - Y1 });
216
- }
217
- return out.filter((r) => r.width > EPS && r.height > EPS);
218
- }
219
-
220
- // lib/solvers/rectdiff/candidates.ts
221
- function isFullyOccupiedAllLayers(params) {
222
- const { x, y, layerCount, obstaclesByLayer, placedByLayer } = params;
223
- for (let z = 0; z < layerCount; z++) {
224
- const obs = obstaclesByLayer[z] ?? [];
225
- const placed = placedByLayer[z] ?? [];
226
- const occ = obs.some((b) => containsPoint(b, x, y)) || placed.some((b) => containsPoint(b, x, y));
227
- if (!occ) return false;
228
- }
229
- return true;
230
- }
231
- function computeCandidates3D(params) {
232
- const {
233
- bounds,
234
- gridSize,
235
- layerCount,
236
- obstaclesByLayer,
237
- placedByLayer,
238
- hardPlacedByLayer
239
- } = params;
240
- const out = /* @__PURE__ */ new Map();
241
- for (let x = bounds.x; x < bounds.x + bounds.width; x += gridSize) {
242
- for (let y = bounds.y; y < bounds.y + bounds.height; y += gridSize) {
243
- 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) {
244
- continue;
245
- }
246
- if (isFullyOccupiedAllLayers({
247
- x,
248
- y,
249
- layerCount,
250
- obstaclesByLayer,
251
- placedByLayer
252
- }))
253
- continue;
254
- let bestSpan = [];
255
- let bestZ = 0;
256
- for (let z = 0; z < layerCount; z++) {
257
- const s = longestFreeSpanAroundZ({
258
- x,
259
- y,
260
- z,
261
- layerCount,
262
- minSpan: 1,
263
- maxSpan: void 0,
264
- obstaclesByLayer,
265
- placedByLayer: hardPlacedByLayer
277
+ if (this.lastCandidateEdge) {
278
+ graphics.lines.push({
279
+ points: [this.lastCandidateEdge.start, this.lastCandidateEdge.end],
280
+ strokeColor: "blue"
281
+ });
282
+ }
283
+ if (this.lastOverlappingEdges) {
284
+ for (const edge of this.lastOverlappingEdges) {
285
+ graphics.lines.push({
286
+ points: visuallyOffsetLine([edge.start, edge.end], {
287
+ dir: edge.facingDirection,
288
+ amt: 0.05
289
+ }),
290
+ strokeColor: "red",
291
+ strokeDash: "2 2"
266
292
  });
267
- if (s.length > bestSpan.length) {
268
- bestSpan = s;
269
- bestZ = z;
270
- }
271
293
  }
272
- const anchorZ = bestSpan.length ? bestSpan[Math.floor(bestSpan.length / 2)] : bestZ;
273
- const hardAtZ = [
274
- ...obstaclesByLayer[anchorZ] ?? [],
275
- ...hardPlacedByLayer[anchorZ] ?? []
276
- ];
277
- const d = Math.min(
278
- distancePointToRectEdges(x, y, bounds),
279
- ...hardAtZ.length ? hardAtZ.map((b) => distancePointToRectEdges(x, y, b)) : [Infinity]
280
- );
281
- const k = `${x.toFixed(6)}|${y.toFixed(6)}`;
282
- const cand = {
283
- x,
284
- y,
285
- z: anchorZ,
286
- distance: d,
287
- zSpanLen: bestSpan.length
288
- };
289
- const prev = out.get(k);
290
- if (!prev || cand.zSpanLen > (prev.zSpanLen ?? 0) || cand.zSpanLen === prev.zSpanLen && cand.distance > prev.distance) {
291
- out.set(k, cand);
294
+ }
295
+ if (this.lastUncoveredSegments) {
296
+ for (const edge of this.lastUncoveredSegments) {
297
+ graphics.lines.push({
298
+ points: visuallyOffsetLine([edge.start, edge.end], {
299
+ dir: edge.facingDirection,
300
+ amt: -0.05
301
+ }),
302
+ strokeColor: "green",
303
+ strokeDash: "2 2"
304
+ });
292
305
  }
293
306
  }
307
+ return graphics;
294
308
  }
295
- const arr = Array.from(out.values());
296
- arr.sort((a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance);
297
- return arr;
298
- }
299
- function longestFreeSpanAroundZ(params) {
300
- const {
301
- x,
302
- y,
303
- z,
304
- layerCount,
305
- minSpan,
306
- maxSpan,
307
- obstaclesByLayer,
308
- placedByLayer
309
- } = params;
310
- const isFreeAt = (layer) => {
311
- const blockers = [
312
- ...obstaclesByLayer[layer] ?? [],
313
- ...placedByLayer[layer] ?? []
314
- ];
315
- return !blockers.some((b) => containsPoint(b, x, y));
309
+ };
310
+
311
+ // lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts
312
+ import { BaseSolver as BaseSolver2 } from "@tscircuit/solver-utils";
313
+ import RBush from "rbush";
314
+
315
+ // lib/solvers/GapFillSolver/getBoundsFromCorners.ts
316
+ var getBoundsFromCorners = (corners) => {
317
+ return {
318
+ minX: Math.min(...corners.map((c) => c.x)),
319
+ minY: Math.min(...corners.map((c) => c.y)),
320
+ maxX: Math.max(...corners.map((c) => c.x)),
321
+ maxY: Math.max(...corners.map((c) => c.y))
316
322
  };
317
- let lo = z;
318
- let hi = z;
319
- while (lo - 1 >= 0 && isFreeAt(lo - 1)) lo--;
320
- while (hi + 1 < layerCount && isFreeAt(hi + 1)) hi++;
321
- if (typeof maxSpan === "number") {
322
- const target = clamp(maxSpan, 1, layerCount);
323
- while (hi - lo + 1 > target) {
324
- if (z - lo > hi - z) lo++;
325
- else hi--;
326
- }
327
- }
328
- const res = [];
329
- for (let i = lo; i <= hi; i++) res.push(i);
330
- return res.length >= minSpan ? res : [];
331
- }
332
- function computeDefaultGridSizes(bounds) {
333
- const ref = Math.max(bounds.width, bounds.height);
334
- return [ref / 8, ref / 16, ref / 32];
335
- }
336
- function computeUncoveredSegments(params) {
337
- const { lineStart, lineEnd, coveringIntervals, minSegmentLength } = params;
338
- if (coveringIntervals.length === 0) {
339
- const center = (lineStart + lineEnd) / 2;
340
- return [{ start: lineStart, end: lineEnd, center }];
323
+ };
324
+
325
+ // lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts
326
+ import { segmentToBoxMinDistance } from "@tscircuit/math-utils";
327
+ var EPS3 = 1e-4;
328
+ var ExpandEdgesToEmptySpaceSolver = class extends BaseSolver2 {
329
+ constructor(input) {
330
+ super();
331
+ this.input = input;
332
+ this.unprocessedSegments = [...this.input.segmentsWithAdjacentEmptySpace];
333
+ this.rectSpatialIndex = new RBush();
334
+ this.rectSpatialIndex.load(
335
+ this.input.inputMeshNodes.map((n) => ({
336
+ ...n,
337
+ minX: n.center.x - n.width / 2,
338
+ minY: n.center.y - n.height / 2,
339
+ maxX: n.center.x + n.width / 2,
340
+ maxY: n.center.y + n.height / 2
341
+ }))
342
+ );
341
343
  }
342
- const sorted = [...coveringIntervals].sort((a, b) => a.start - b.start);
343
- const merged = [];
344
- let current = { ...sorted[0] };
345
- for (let i = 1; i < sorted.length; i++) {
346
- const interval = sorted[i];
347
- if (interval.start <= current.end + EPS) {
348
- current.end = Math.max(current.end, interval.end);
349
- } else {
350
- merged.push(current);
351
- current = { ...interval };
344
+ unprocessedSegments = [];
345
+ expandedSegments = [];
346
+ lastSegment = null;
347
+ lastSearchBounds = null;
348
+ lastCollidingNodes = null;
349
+ lastSearchCorner1 = null;
350
+ lastSearchCorner2 = null;
351
+ lastExpandedSegment = null;
352
+ rectSpatialIndex;
353
+ _step() {
354
+ if (this.unprocessedSegments.length === 0) {
355
+ this.solved = true;
356
+ return;
352
357
  }
353
- }
354
- merged.push(current);
355
- const uncovered = [];
356
- if (merged[0].start > lineStart + EPS) {
357
- const start = lineStart;
358
- const end = merged[0].start;
359
- if (end - start >= minSegmentLength) {
360
- uncovered.push({ start, end, center: (start + end) / 2 });
358
+ const segment = this.unprocessedSegments.shift();
359
+ this.lastSegment = segment;
360
+ const { dx, dy } = EDGE_MAP[segment.facingDirection];
361
+ const deltaStartEnd = {
362
+ x: segment.end.x - segment.start.x,
363
+ y: segment.end.y - segment.start.y
364
+ };
365
+ const segLength = Math.sqrt(deltaStartEnd.x ** 2 + deltaStartEnd.y ** 2);
366
+ const normDeltaStartEnd = {
367
+ x: deltaStartEnd.x / segLength,
368
+ y: deltaStartEnd.y / segLength
369
+ };
370
+ let collidingNodes = null;
371
+ let searchDistance = 1;
372
+ const searchCorner1 = {
373
+ x: segment.start.x + dx * EPS3 + normDeltaStartEnd.x * EPS3 * 10,
374
+ y: segment.start.y + dy * EPS3 + normDeltaStartEnd.y * EPS3 * 10
375
+ };
376
+ const searchCorner2 = {
377
+ x: segment.end.x + dx * EPS3 - normDeltaStartEnd.x * EPS3 * 10,
378
+ y: segment.end.y + dy * EPS3 - normDeltaStartEnd.y * EPS3 * 10
379
+ };
380
+ this.lastSearchCorner1 = searchCorner1;
381
+ this.lastSearchCorner2 = searchCorner2;
382
+ while ((!collidingNodes || collidingNodes.length === 0) && searchDistance < 1e3) {
383
+ const searchBounds = getBoundsFromCorners([
384
+ searchCorner1,
385
+ searchCorner2,
386
+ {
387
+ x: searchCorner1.x + dx * searchDistance,
388
+ y: searchCorner1.y + dy * searchDistance
389
+ },
390
+ {
391
+ x: searchCorner2.x + dx * searchDistance,
392
+ y: searchCorner2.y + dy * searchDistance
393
+ }
394
+ ]);
395
+ this.lastSearchBounds = searchBounds;
396
+ collidingNodes = this.rectSpatialIndex.search(searchBounds).filter((n) => n.availableZ.includes(segment.z)).filter(
397
+ (n) => n.capacityMeshNodeId !== segment.parent.capacityMeshNodeId
398
+ );
399
+ searchDistance *= 4;
361
400
  }
362
- }
363
- for (let i = 0; i < merged.length - 1; i++) {
364
- const start = merged[i].end;
365
- const end = merged[i + 1].start;
366
- if (end - start >= minSegmentLength) {
367
- uncovered.push({ start, end, center: (start + end) / 2 });
401
+ if (!collidingNodes || collidingNodes.length === 0) {
402
+ return;
368
403
  }
369
- }
370
- if (merged[merged.length - 1].end < lineEnd - EPS) {
371
- const start = merged[merged.length - 1].end;
372
- const end = lineEnd;
373
- if (end - start >= minSegmentLength) {
374
- uncovered.push({ start, end, center: (start + end) / 2 });
404
+ this.lastCollidingNodes = collidingNodes;
405
+ let smallestDistance = Infinity;
406
+ for (const node of collidingNodes) {
407
+ const distance = segmentToBoxMinDistance(segment.start, segment.end, node);
408
+ if (distance < smallestDistance) {
409
+ smallestDistance = distance;
410
+ }
375
411
  }
376
- }
377
- return uncovered;
378
- }
379
- function computeEdgeCandidates3D(params) {
380
- const {
381
- bounds,
382
- minSize,
383
- layerCount,
384
- obstaclesByLayer,
385
- placedByLayer,
386
- hardPlacedByLayer
387
- } = params;
388
- const out = [];
389
- const \u03B4 = Math.max(minSize * 0.15, EPS * 3);
390
- const dedup = /* @__PURE__ */ new Set();
391
- const key = (x, y, z) => `${z}|${x.toFixed(6)}|${y.toFixed(6)}`;
392
- function fullyOcc(x, y) {
393
- return isFullyOccupiedAllLayers({
394
- x,
395
- y,
396
- layerCount,
397
- obstaclesByLayer,
398
- placedByLayer
399
- });
400
- }
401
- function pushIfFree(x, y, z) {
402
- if (x < bounds.x + EPS || y < bounds.y + EPS || x > bounds.x + bounds.width - EPS || y > bounds.y + bounds.height - EPS)
412
+ const expandDistance = smallestDistance;
413
+ const nodeBounds = getBoundsFromCorners([
414
+ segment.start,
415
+ segment.end,
416
+ {
417
+ x: segment.start.x + dx * expandDistance,
418
+ y: segment.start.y + dy * expandDistance
419
+ },
420
+ {
421
+ x: segment.end.x + dx * expandDistance,
422
+ y: segment.end.y + dy * expandDistance
423
+ }
424
+ ]);
425
+ const nodeCenter = {
426
+ x: (nodeBounds.minX + nodeBounds.maxX) / 2,
427
+ y: (nodeBounds.minY + nodeBounds.maxY) / 2
428
+ };
429
+ const nodeWidth = nodeBounds.maxX - nodeBounds.minX;
430
+ const nodeHeight = nodeBounds.maxY - nodeBounds.minY;
431
+ const expandedSegment = {
432
+ segment,
433
+ newNode: {
434
+ capacityMeshNodeId: `new-${segment.parent.capacityMeshNodeId}-${this.expandedSegments.length}`,
435
+ center: nodeCenter,
436
+ width: nodeWidth,
437
+ height: nodeHeight,
438
+ availableZ: [segment.z],
439
+ layer: segment.parent.layer
440
+ }
441
+ };
442
+ this.lastExpandedSegment = expandedSegment;
443
+ if (nodeWidth < EPS3 || nodeHeight < EPS3) {
403
444
  return;
404
- if (fullyOcc(x, y)) return;
405
- const hard = [
406
- ...obstaclesByLayer[z] ?? [],
407
- ...hardPlacedByLayer[z] ?? []
408
- ];
409
- const d = Math.min(
410
- distancePointToRectEdges(x, y, bounds),
411
- ...hard.length ? hard.map((b) => distancePointToRectEdges(x, y, b)) : [Infinity]
412
- );
413
- const k = key(x, y, z);
414
- if (dedup.has(k)) return;
415
- dedup.add(k);
416
- const span = longestFreeSpanAroundZ({
417
- x,
418
- y,
419
- z,
420
- layerCount,
421
- minSpan: 1,
422
- maxSpan: void 0,
423
- obstaclesByLayer,
424
- placedByLayer: hardPlacedByLayer
445
+ }
446
+ this.expandedSegments.push(expandedSegment);
447
+ this.rectSpatialIndex.insert({
448
+ ...expandedSegment.newNode,
449
+ ...nodeBounds
425
450
  });
426
- out.push({ x, y, z, distance: d, zSpanLen: span.length, isEdgeSeed: true });
427
451
  }
428
- for (let z = 0; z < layerCount; z++) {
429
- const blockers = [
430
- ...obstaclesByLayer[z] ?? [],
431
- ...hardPlacedByLayer[z] ?? []
432
- ];
433
- const corners = [
434
- { x: bounds.x + \u03B4, y: bounds.y + \u03B4 },
435
- // top-left
436
- { x: bounds.x + bounds.width - \u03B4, y: bounds.y + \u03B4 },
437
- // top-right
438
- { x: bounds.x + \u03B4, y: bounds.y + bounds.height - \u03B4 },
439
- // bottom-left
440
- { x: bounds.x + bounds.width - \u03B4, y: bounds.y + bounds.height - \u03B4 }
441
- // bottom-right
442
- ];
443
- for (const corner of corners) {
444
- pushIfFree(corner.x, corner.y, z);
452
+ getOutput() {
453
+ return {
454
+ expandedSegments: this.expandedSegments
455
+ };
456
+ }
457
+ visualize() {
458
+ const graphics = {
459
+ title: "ExpandEdgesToEmptySpace",
460
+ coordinateSystem: "cartesian",
461
+ rects: [],
462
+ points: [],
463
+ lines: [],
464
+ circles: [],
465
+ arrows: [],
466
+ texts: []
467
+ };
468
+ for (const node of this.input.inputMeshNodes) {
469
+ graphics.rects.push({
470
+ center: node.center,
471
+ width: node.width,
472
+ height: node.height,
473
+ stroke: "rgba(0, 0, 0, 0.1)",
474
+ layer: `z${node.availableZ.join(",")}`,
475
+ label: [
476
+ `node ${node.capacityMeshNodeId}`,
477
+ `z:${node.availableZ.join(",")}`
478
+ ].join("\n")
479
+ });
445
480
  }
446
- const topY = bounds.y + \u03B4;
447
- const topCovering = blockers.filter((b) => b.y <= topY && b.y + b.height >= topY).map((b) => ({
448
- start: Math.max(bounds.x, b.x),
449
- end: Math.min(bounds.x + bounds.width, b.x + b.width)
450
- }));
451
- const topUncovered = computeUncoveredSegments({
452
- lineStart: bounds.x + \u03B4,
453
- lineEnd: bounds.x + bounds.width - \u03B4,
454
- coveringIntervals: topCovering,
455
- minSegmentLength: minSize * 0.5
456
- });
457
- for (const seg of topUncovered) {
458
- const segLen = seg.end - seg.start;
459
- if (segLen >= minSize) {
460
- pushIfFree(seg.center, topY, z);
461
- if (segLen > minSize * 1.5) {
462
- pushIfFree(seg.start + minSize * 0.4, topY, z);
463
- pushIfFree(seg.end - minSize * 0.4, topY, z);
464
- }
465
- }
481
+ for (const { newNode } of this.expandedSegments) {
482
+ graphics.rects.push({
483
+ center: newNode.center,
484
+ width: newNode.width,
485
+ height: newNode.height,
486
+ fill: "green",
487
+ label: `expandedSegment (z=${newNode.availableZ.join(",")})`,
488
+ layer: `z${newNode.availableZ.join(",")}`
489
+ });
466
490
  }
467
- const bottomY = bounds.y + bounds.height - \u03B4;
468
- const bottomCovering = blockers.filter((b) => b.y <= bottomY && b.y + b.height >= bottomY).map((b) => ({
469
- start: Math.max(bounds.x, b.x),
470
- end: Math.min(bounds.x + bounds.width, b.x + b.width)
471
- }));
472
- const bottomUncovered = computeUncoveredSegments({
473
- lineStart: bounds.x + \u03B4,
474
- lineEnd: bounds.x + bounds.width - \u03B4,
475
- coveringIntervals: bottomCovering,
476
- minSegmentLength: minSize * 0.5
477
- });
478
- for (const seg of bottomUncovered) {
479
- const segLen = seg.end - seg.start;
480
- if (segLen >= minSize) {
481
- pushIfFree(seg.center, bottomY, z);
482
- if (segLen > minSize * 1.5) {
483
- pushIfFree(seg.start + minSize * 0.4, bottomY, z);
484
- pushIfFree(seg.end - minSize * 0.4, bottomY, z);
485
- }
486
- }
491
+ if (this.lastSegment) {
492
+ graphics.lines.push({
493
+ points: [this.lastSegment.start, this.lastSegment.end],
494
+ strokeColor: "rgba(0, 0, 255, 0.5)"
495
+ });
496
+ }
497
+ if (this.lastSearchBounds) {
498
+ graphics.rects.push({
499
+ center: {
500
+ x: (this.lastSearchBounds.minX + this.lastSearchBounds.maxX) / 2,
501
+ y: (this.lastSearchBounds.minY + this.lastSearchBounds.maxY) / 2
502
+ },
503
+ width: this.lastSearchBounds.maxX - this.lastSearchBounds.minX,
504
+ height: this.lastSearchBounds.maxY - this.lastSearchBounds.minY,
505
+ fill: "rgba(0, 0, 255, 0.25)"
506
+ });
507
+ }
508
+ if (this.lastSearchCorner1 && this.lastSearchCorner2) {
509
+ graphics.points.push({
510
+ x: this.lastSearchCorner1.x,
511
+ y: this.lastSearchCorner1.y,
512
+ color: "rgba(0, 0, 255, 0.5)",
513
+ label: ["searchCorner1", `z=${this.lastSegment?.z}`].join("\n")
514
+ });
515
+ graphics.points.push({
516
+ x: this.lastSearchCorner2.x,
517
+ y: this.lastSearchCorner2.y,
518
+ color: "rgba(0, 0, 255, 0.5)",
519
+ label: ["searchCorner2", `z=${this.lastSegment?.z}`].join("\n")
520
+ });
487
521
  }
488
- const leftX = bounds.x + \u03B4;
489
- const leftCovering = blockers.filter((b) => b.x <= leftX && b.x + b.width >= leftX).map((b) => ({
490
- start: Math.max(bounds.y, b.y),
491
- end: Math.min(bounds.y + bounds.height, b.y + b.height)
492
- }));
493
- const leftUncovered = computeUncoveredSegments({
494
- lineStart: bounds.y + \u03B4,
495
- lineEnd: bounds.y + bounds.height - \u03B4,
496
- coveringIntervals: leftCovering,
497
- minSegmentLength: minSize * 0.5
498
- });
499
- for (const seg of leftUncovered) {
500
- const segLen = seg.end - seg.start;
501
- if (segLen >= minSize) {
502
- pushIfFree(leftX, seg.center, z);
503
- if (segLen > minSize * 1.5) {
504
- pushIfFree(leftX, seg.start + minSize * 0.4, z);
505
- pushIfFree(leftX, seg.end - minSize * 0.4, z);
506
- }
507
- }
522
+ if (this.lastExpandedSegment) {
523
+ graphics.rects.push({
524
+ center: this.lastExpandedSegment.newNode.center,
525
+ width: this.lastExpandedSegment.newNode.width,
526
+ height: this.lastExpandedSegment.newNode.height,
527
+ fill: "purple",
528
+ label: `expandedSegment (z=${this.lastExpandedSegment.segment.z})`
529
+ });
508
530
  }
509
- const rightX = bounds.x + bounds.width - \u03B4;
510
- const rightCovering = blockers.filter((b) => b.x <= rightX && b.x + b.width >= rightX).map((b) => ({
511
- start: Math.max(bounds.y, b.y),
512
- end: Math.min(bounds.y + bounds.height, b.y + b.height)
513
- }));
514
- const rightUncovered = computeUncoveredSegments({
515
- lineStart: bounds.y + \u03B4,
516
- lineEnd: bounds.y + bounds.height - \u03B4,
517
- coveringIntervals: rightCovering,
518
- minSegmentLength: minSize * 0.5
519
- });
520
- for (const seg of rightUncovered) {
521
- const segLen = seg.end - seg.start;
522
- if (segLen >= minSize) {
523
- pushIfFree(rightX, seg.center, z);
524
- if (segLen > minSize * 1.5) {
525
- pushIfFree(rightX, seg.start + minSize * 0.4, z);
526
- pushIfFree(rightX, seg.end - minSize * 0.4, z);
527
- }
531
+ if (this.lastCollidingNodes) {
532
+ for (const node of this.lastCollidingNodes) {
533
+ graphics.rects.push({
534
+ center: node.center,
535
+ width: node.width,
536
+ height: node.height,
537
+ fill: "rgba(255, 0, 0, 0.5)"
538
+ });
528
539
  }
529
540
  }
530
- for (const b of blockers) {
531
- const obLeftX = b.x - \u03B4;
532
- if (obLeftX > bounds.x + EPS && obLeftX < bounds.x + bounds.width - EPS) {
533
- const obLeftCovering = blockers.filter(
534
- (bl) => bl !== b && bl.x <= obLeftX && bl.x + bl.width >= obLeftX
535
- ).map((bl) => ({
536
- start: Math.max(b.y, bl.y),
537
- end: Math.min(b.y + b.height, bl.y + bl.height)
538
- }));
539
- const obLeftUncovered = computeUncoveredSegments({
540
- lineStart: b.y,
541
- lineEnd: b.y + b.height,
542
- coveringIntervals: obLeftCovering,
543
- minSegmentLength: minSize * 0.5
544
- });
545
- for (const seg of obLeftUncovered) {
546
- pushIfFree(obLeftX, seg.center, z);
541
+ return graphics;
542
+ }
543
+ };
544
+
545
+ // lib/solvers/GapFillSolver/GapFillSolverPipeline.ts
546
+ var GapFillSolverPipeline = class extends BasePipelineSolver {
547
+ findSegmentsWithAdjacentEmptySpaceSolver;
548
+ expandEdgesToEmptySpaceSolver;
549
+ pipelineDef = [
550
+ definePipelineStep(
551
+ "findSegmentsWithAdjacentEmptySpaceSolver",
552
+ FindSegmentsWithAdjacentEmptySpaceSolver,
553
+ (gapFillPipeline) => [
554
+ {
555
+ meshNodes: gapFillPipeline.inputProblem.meshNodes
547
556
  }
548
- }
549
- const obRightX = b.x + b.width + \u03B4;
550
- if (obRightX > bounds.x + EPS && obRightX < bounds.x + bounds.width - EPS) {
551
- const obRightCovering = blockers.filter(
552
- (bl) => bl !== b && bl.x <= obRightX && bl.x + bl.width >= obRightX
553
- ).map((bl) => ({
554
- start: Math.max(b.y, bl.y),
555
- end: Math.min(b.y + b.height, bl.y + bl.height)
556
- }));
557
- const obRightUncovered = computeUncoveredSegments({
558
- lineStart: b.y,
559
- lineEnd: b.y + b.height,
560
- coveringIntervals: obRightCovering,
561
- minSegmentLength: minSize * 0.5
562
- });
563
- for (const seg of obRightUncovered) {
564
- pushIfFree(obRightX, seg.center, z);
557
+ ],
558
+ {
559
+ onSolved: () => {
565
560
  }
566
561
  }
567
- const obTopY = b.y - \u03B4;
568
- if (obTopY > bounds.y + EPS && obTopY < bounds.y + bounds.height - EPS) {
569
- const obTopCovering = blockers.filter(
570
- (bl) => bl !== b && bl.y <= obTopY && bl.y + bl.height >= obTopY
571
- ).map((bl) => ({
572
- start: Math.max(b.x, bl.x),
573
- end: Math.min(b.x + b.width, bl.x + bl.width)
574
- }));
575
- const obTopUncovered = computeUncoveredSegments({
576
- lineStart: b.x,
577
- lineEnd: b.x + b.width,
578
- coveringIntervals: obTopCovering,
579
- minSegmentLength: minSize * 0.5
580
- });
581
- for (const seg of obTopUncovered) {
582
- pushIfFree(seg.center, obTopY, z);
562
+ ),
563
+ definePipelineStep(
564
+ "expandEdgesToEmptySpaceSolver",
565
+ ExpandEdgesToEmptySpaceSolver,
566
+ (gapFillPipeline) => [
567
+ {
568
+ inputMeshNodes: gapFillPipeline.inputProblem.meshNodes,
569
+ segmentsWithAdjacentEmptySpace: gapFillPipeline.findSegmentsWithAdjacentEmptySpaceSolver.getOutput().segmentsWithAdjacentEmptySpace
583
570
  }
584
- }
585
- const obBottomY = b.y + b.height + \u03B4;
586
- if (obBottomY > bounds.y + EPS && obBottomY < bounds.y + bounds.height - EPS) {
587
- const obBottomCovering = blockers.filter(
588
- (bl) => bl !== b && bl.y <= obBottomY && bl.y + bl.height >= obBottomY
589
- ).map((bl) => ({
590
- start: Math.max(b.x, bl.x),
591
- end: Math.min(b.x + b.width, bl.x + bl.width)
592
- }));
593
- const obBottomUncovered = computeUncoveredSegments({
594
- lineStart: b.x,
595
- lineEnd: b.x + b.width,
596
- coveringIntervals: obBottomCovering,
597
- minSegmentLength: minSize * 0.5
598
- });
599
- for (const seg of obBottomUncovered) {
600
- pushIfFree(seg.center, obBottomY, z);
571
+ ],
572
+ {
573
+ onSolved: () => {
601
574
  }
602
575
  }
576
+ )
577
+ ];
578
+ getOutput() {
579
+ const expandedSegments = this.expandEdgesToEmptySpaceSolver?.getOutput().expandedSegments ?? [];
580
+ const expandedNodes = expandedSegments.map((es) => es.newNode);
581
+ return {
582
+ outputNodes: [...this.inputProblem.meshNodes, ...expandedNodes]
583
+ };
584
+ }
585
+ initialVisualize() {
586
+ const graphics = {
587
+ title: "GapFillSolverPipeline - Initial",
588
+ coordinateSystem: "cartesian",
589
+ rects: [],
590
+ points: [],
591
+ lines: [],
592
+ circles: [],
593
+ arrows: [],
594
+ texts: []
595
+ };
596
+ for (const node of this.inputProblem.meshNodes) {
597
+ graphics.rects.push({
598
+ center: node.center,
599
+ width: node.width,
600
+ height: node.height,
601
+ stroke: "rgba(0, 0, 0, 0.3)",
602
+ fill: "rgba(100, 100, 100, 0.1)",
603
+ layer: `z${node.availableZ.join(",")}`,
604
+ label: [
605
+ `node ${node.capacityMeshNodeId}`,
606
+ `z:${node.availableZ.join(",")}`
607
+ ].join("\n")
608
+ });
609
+ }
610
+ return graphics;
611
+ }
612
+ finalVisualize() {
613
+ const graphics = {
614
+ title: "GapFillSolverPipeline - Final",
615
+ coordinateSystem: "cartesian",
616
+ rects: [],
617
+ points: [],
618
+ lines: [],
619
+ circles: [],
620
+ arrows: [],
621
+ texts: []
622
+ };
623
+ const { outputNodes } = this.getOutput();
624
+ const expandedSegments = this.expandEdgesToEmptySpaceSolver?.getOutput().expandedSegments ?? [];
625
+ const expandedNodeIds = new Set(
626
+ expandedSegments.map((es) => es.newNode.capacityMeshNodeId)
627
+ );
628
+ for (const node of outputNodes) {
629
+ const isExpanded = expandedNodeIds.has(node.capacityMeshNodeId);
630
+ graphics.rects.push({
631
+ center: node.center,
632
+ width: node.width,
633
+ height: node.height,
634
+ stroke: isExpanded ? "rgba(0, 128, 0, 0.8)" : "rgba(0, 0, 0, 0.3)",
635
+ fill: isExpanded ? "rgba(0, 200, 0, 0.3)" : "rgba(100, 100, 100, 0.1)",
636
+ layer: `z${node.availableZ.join(",")}`,
637
+ label: [
638
+ `${isExpanded ? "[expanded] " : ""}node ${node.capacityMeshNodeId}`,
639
+ `z:${node.availableZ.join(",")}`
640
+ ].join("\n")
641
+ });
603
642
  }
643
+ return graphics;
644
+ }
645
+ };
646
+
647
+ // lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts
648
+ import {
649
+ BasePipelineSolver as BasePipelineSolver2,
650
+ definePipelineStep as definePipelineStep2
651
+ } from "@tscircuit/solver-utils";
652
+
653
+ // lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts
654
+ import { BaseSolver as BaseSolver3 } from "@tscircuit/solver-utils";
655
+
656
+ // lib/utils/rectdiff-geometry.ts
657
+ var EPS4 = 1e-9;
658
+ var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
659
+ var gt = (a, b) => a > b + EPS4;
660
+ var gte = (a, b) => a > b - EPS4;
661
+ var lt = (a, b) => a < b - EPS4;
662
+ var lte = (a, b) => a < b + EPS4;
663
+ function overlaps(a, b) {
664
+ return !(a.x + a.width <= b.x + EPS4 || b.x + b.width <= a.x + EPS4 || a.y + a.height <= b.y + EPS4 || b.y + b.height <= a.y + EPS4);
665
+ }
666
+ function containsPoint(r, p) {
667
+ return p.x >= r.x - EPS4 && p.x <= r.x + r.width + EPS4 && p.y >= r.y - EPS4 && p.y <= r.y + r.height + EPS4;
668
+ }
669
+ function distancePointToRectEdges(p, r) {
670
+ const edges = [
671
+ [r.x, r.y, r.x + r.width, r.y],
672
+ [r.x + r.width, r.y, r.x + r.width, r.y + r.height],
673
+ [r.x + r.width, r.y + r.height, r.x, r.y + r.height],
674
+ [r.x, r.y + r.height, r.x, r.y]
675
+ ];
676
+ let best = Infinity;
677
+ for (const [x1, y1, x2, y2] of edges) {
678
+ const A = p.x - x1, B = p.y - y1, C = x2 - x1, D = y2 - y1;
679
+ const dot = A * C + B * D;
680
+ const lenSq = C * C + D * D;
681
+ let t = lenSq !== 0 ? dot / lenSq : 0;
682
+ t = clamp(t, 0, 1);
683
+ const xx = x1 + t * C;
684
+ const yy = y1 + t * D;
685
+ best = Math.min(best, Math.hypot(p.x - xx, p.y - yy));
686
+ }
687
+ return best;
688
+ }
689
+ function intersect1D(r1, r2) {
690
+ const lo = Math.max(r1[0], r2[0]);
691
+ const hi = Math.min(r1[1], r2[1]);
692
+ return hi > lo + EPS4 ? [lo, hi] : null;
693
+ }
694
+ function subtractRect2D(A, B) {
695
+ if (!overlaps(A, B)) return [A];
696
+ const Xi = intersect1D([A.x, A.x + A.width], [B.x, B.x + B.width]);
697
+ const Yi = intersect1D([A.y, A.y + A.height], [B.y, B.y + B.height]);
698
+ if (!Xi || !Yi) return [A];
699
+ const [X0, X1] = Xi;
700
+ const [Y0, Y1] = Yi;
701
+ const out = [];
702
+ if (X0 > A.x + EPS4) {
703
+ out.push({ x: A.x, y: A.y, width: X0 - A.x, height: A.height });
704
+ }
705
+ if (A.x + A.width > X1 + EPS4) {
706
+ out.push({ x: X1, y: A.y, width: A.x + A.width - X1, height: A.height });
707
+ }
708
+ const midW = Math.max(0, X1 - X0);
709
+ if (midW > EPS4 && Y0 > A.y + EPS4) {
710
+ out.push({ x: X0, y: A.y, width: midW, height: Y0 - A.y });
604
711
  }
605
- out.sort((a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance);
606
- return out;
712
+ if (midW > EPS4 && A.y + A.height > Y1 + EPS4) {
713
+ out.push({ x: X0, y: Y1, width: midW, height: A.y + A.height - Y1 });
714
+ }
715
+ return out.filter((r) => r.width > EPS4 && r.height > EPS4);
607
716
  }
608
717
 
609
- // lib/solvers/rectdiff/geometry/isPointInPolygon.ts
718
+ // lib/solvers/RectDiffSeedingSolver/isPointInPolygon.ts
610
719
  function isPointInPolygon(p, polygon) {
611
720
  let inside = false;
612
721
  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
@@ -618,7 +727,7 @@ function isPointInPolygon(p, polygon) {
618
727
  return inside;
619
728
  }
620
729
 
621
- // lib/solvers/rectdiff/geometry/computeInverseRects.ts
730
+ // lib/solvers/RectDiffSeedingSolver/computeInverseRects.ts
622
731
  function computeInverseRects(bounds, polygon) {
623
732
  if (!polygon || polygon.length < 3) return [];
624
733
  const xs = /* @__PURE__ */ new Set([bounds.x, bounds.x + bounds.width]);
@@ -647,7 +756,7 @@ function computeInverseRects(bounds, polygon) {
647
756
  }
648
757
  const finalRects = [];
649
758
  rawRects.sort((a, b) => {
650
- if (Math.abs(a.y - b.y) > EPS) return a.y - b.y;
759
+ if (Math.abs(a.y - b.y) > EPS4) return a.y - b.y;
651
760
  return a.x - b.x;
652
761
  });
653
762
  let current = null;
@@ -656,9 +765,9 @@ function computeInverseRects(bounds, polygon) {
656
765
  current = r;
657
766
  continue;
658
767
  }
659
- const sameY = Math.abs(current.y - r.y) < EPS;
660
- const sameHeight = Math.abs(current.height - r.height) < EPS;
661
- const touchesX = Math.abs(current.x + current.width - r.x) < EPS;
768
+ const sameY = Math.abs(current.y - r.y) < EPS4;
769
+ const sameHeight = Math.abs(current.height - r.height) < EPS4;
770
+ const touchesX = Math.abs(current.x + current.width - r.x) < EPS4;
662
771
  if (sameY && sameHeight && touchesX) {
663
772
  current.width += r.width;
664
773
  } else {
@@ -668,7 +777,7 @@ function computeInverseRects(bounds, polygon) {
668
777
  }
669
778
  if (current) finalRects.push(current);
670
779
  finalRects.sort((a, b) => {
671
- if (Math.abs(a.x - b.x) > EPS) return a.x - b.x;
780
+ if (Math.abs(a.x - b.x) > EPS4) return a.x - b.x;
672
781
  return a.y - b.y;
673
782
  });
674
783
  const mergedVertical = [];
@@ -678,9 +787,9 @@ function computeInverseRects(bounds, polygon) {
678
787
  current = r;
679
788
  continue;
680
789
  }
681
- const sameX = Math.abs(current.x - r.x) < EPS;
682
- const sameWidth = Math.abs(current.width - r.width) < EPS;
683
- const touchesY = Math.abs(current.y + current.height - r.y) < EPS;
790
+ const sameX = Math.abs(current.x - r.x) < EPS4;
791
+ const sameWidth = Math.abs(current.width - r.width) < EPS4;
792
+ const touchesY = Math.abs(current.y + current.height - r.y) < EPS4;
684
793
  if (sameX && sameWidth && touchesY) {
685
794
  current.height += r.height;
686
795
  } else {
@@ -692,7 +801,7 @@ function computeInverseRects(bounds, polygon) {
692
801
  return mergedVertical;
693
802
  }
694
803
 
695
- // lib/solvers/rectdiff/layers.ts
804
+ // lib/solvers/RectDiffSeedingSolver/layers.ts
696
805
  function layerSortKey(n) {
697
806
  const L = n.toLowerCase();
698
807
  if (L === "top") return -1e6;
@@ -767,1252 +876,1349 @@ function obstacleToXYRect(ob) {
767
876
  return { x: ob.center.x - w / 2, y: ob.center.y - h / 2, width: w, height: h };
768
877
  }
769
878
 
770
- // lib/solvers/rectdiff/engine.ts
771
- function initState(srj, opts) {
772
- const { layerNames, zIndexByName } = buildZIndexMap(srj);
773
- const layerCount = Math.max(1, layerNames.length, srj.layerCount || 1);
774
- const bounds = {
775
- x: srj.bounds.minX,
776
- y: srj.bounds.minY,
777
- width: srj.bounds.maxX - srj.bounds.minX,
778
- height: srj.bounds.maxY - srj.bounds.minY
779
- };
780
- const obstaclesByLayer = Array.from(
781
- { length: layerCount },
782
- () => []
783
- );
784
- let boardVoidRects = [];
785
- if (srj.outline && srj.outline.length > 2) {
786
- boardVoidRects = computeInverseRects(bounds, srj.outline);
787
- for (const voidR of boardVoidRects) {
788
- for (let z = 0; z < layerCount; z++) {
789
- obstaclesByLayer[z].push(voidR);
879
+ // lib/utils/expandRectFromSeed.ts
880
+ function maxExpandRight(params) {
881
+ const { r, bounds, blockers, maxAspect } = params;
882
+ let maxWidth = bounds.x + bounds.width - r.x;
883
+ for (const b of blockers) {
884
+ const verticallyOverlaps = r.y + r.height > b.y + EPS4 && b.y + b.height > r.y + EPS4;
885
+ if (verticallyOverlaps) {
886
+ if (gte(b.x, r.x + r.width)) {
887
+ maxWidth = Math.min(maxWidth, b.x - r.x);
888
+ } else if (b.x + b.width > r.x + r.width - EPS4 && b.x < r.x + r.width + EPS4) {
889
+ return 0;
790
890
  }
791
891
  }
792
892
  }
793
- for (const obstacle of srj.obstacles ?? []) {
794
- const rect = obstacleToXYRect(obstacle);
795
- if (!rect) continue;
796
- const zLayers = obstacleZs(obstacle, zIndexByName);
797
- const invalidZs = zLayers.filter((z) => z < 0 || z >= layerCount);
798
- if (invalidZs.length) {
799
- throw new Error(
800
- `RectDiffSolver: obstacle uses z-layer indices ${invalidZs.join(
801
- ","
802
- )} outside 0-${layerCount - 1}`
803
- );
893
+ let e = Math.max(0, maxWidth - r.width);
894
+ if (e <= 0) return 0;
895
+ if (maxAspect != null) {
896
+ const w = r.width, h = r.height;
897
+ if (w >= h) e = Math.min(e, maxAspect * h - w);
898
+ }
899
+ return Math.max(0, e);
900
+ }
901
+ function maxExpandDown(params) {
902
+ const { r, bounds, blockers, maxAspect } = params;
903
+ let maxHeight = bounds.y + bounds.height - r.y;
904
+ for (const b of blockers) {
905
+ const horizOverlaps = r.x + r.width > b.x + EPS4 && b.x + b.width > r.x + EPS4;
906
+ if (horizOverlaps) {
907
+ if (gte(b.y, r.y + r.height)) {
908
+ maxHeight = Math.min(maxHeight, b.y - r.y);
909
+ } else if (b.y + b.height > r.y + r.height - EPS4 && b.y < r.y + r.height + EPS4) {
910
+ return 0;
911
+ }
804
912
  }
805
- if ((!obstacle.zLayers || obstacle.zLayers.length === 0) && zLayers.length)
806
- obstacle.zLayers = zLayers;
807
- for (const z of zLayers) obstaclesByLayer[z].push(rect);
808
- }
809
- const trace = Math.max(0.01, srj.minTraceWidth || 0.15);
810
- const defaults = {
811
- gridSizes: computeDefaultGridSizes(bounds),
812
- initialCellRatio: 0.2,
813
- maxAspectRatio: 3,
814
- minSingle: { width: 2 * trace, height: 2 * trace },
815
- minMulti: {
816
- width: 4 * trace,
817
- height: 4 * trace,
818
- minLayers: Math.min(2, Math.max(1, srj.layerCount || 1))
819
- },
820
- preferMultiLayer: true,
821
- maxMultiLayerSpan: void 0
822
- };
823
- const options = {
824
- ...defaults,
825
- ...opts,
826
- gridSizes: opts.gridSizes ?? defaults.gridSizes
827
- };
828
- const placedByLayer = Array.from({ length: layerCount }, () => []);
829
- return {
830
- srj,
831
- layerNames,
832
- layerCount,
833
- bounds,
834
- options,
835
- obstaclesByLayer,
836
- boardVoidRects,
837
- phase: "GRID",
838
- gridIndex: 0,
839
- candidates: [],
840
- placed: [],
841
- placedByLayer,
842
- expansionIndex: 0,
843
- edgeAnalysisDone: false,
844
- totalSeedsThisGrid: 0,
845
- consumedSeedsThisGrid: 0
846
- };
913
+ }
914
+ let e = Math.max(0, maxHeight - r.height);
915
+ if (e <= 0) return 0;
916
+ if (maxAspect != null) {
917
+ const w = r.width, h = r.height;
918
+ if (h >= w) e = Math.min(e, maxAspect * w - h);
919
+ }
920
+ return Math.max(0, e);
847
921
  }
848
- function buildHardPlacedByLayer(state) {
849
- const out = Array.from({ length: state.layerCount }, () => []);
850
- for (const p of state.placed) {
851
- if (p.zLayers.length >= state.layerCount) {
852
- for (const z of p.zLayers) out[z].push(p.rect);
922
+ function maxExpandLeft(params) {
923
+ const { r, bounds, blockers, maxAspect } = params;
924
+ let minX = bounds.x;
925
+ for (const b of blockers) {
926
+ const verticallyOverlaps = r.y + r.height > b.y + EPS4 && b.y + b.height > r.y + EPS4;
927
+ if (verticallyOverlaps) {
928
+ if (lte(b.x + b.width, r.x)) {
929
+ minX = Math.max(minX, b.x + b.width);
930
+ } else if (b.x < r.x + EPS4 && b.x + b.width > r.x - EPS4) {
931
+ return 0;
932
+ }
853
933
  }
854
934
  }
855
- return out;
935
+ let e = Math.max(0, r.x - minX);
936
+ if (e <= 0) return 0;
937
+ if (maxAspect != null) {
938
+ const w = r.width, h = r.height;
939
+ if (w >= h) e = Math.min(e, maxAspect * h - w);
940
+ }
941
+ return Math.max(0, e);
856
942
  }
857
- function isFullyOccupiedAtPoint(state, point) {
858
- for (let z = 0; z < state.layerCount; z++) {
859
- const obs = state.obstaclesByLayer[z] ?? [];
860
- const placed = state.placedByLayer[z] ?? [];
861
- const occ = obs.some((b) => containsPoint(b, point.x, point.y)) || placed.some((b) => containsPoint(b, point.x, point.y));
862
- if (!occ) return false;
943
+ function maxExpandUp(params) {
944
+ const { r, bounds, blockers, maxAspect } = params;
945
+ let minY = bounds.y;
946
+ for (const b of blockers) {
947
+ const horizOverlaps = r.x + r.width > b.x + EPS4 && b.x + b.width > r.x + EPS4;
948
+ if (horizOverlaps) {
949
+ if (lte(b.y + b.height, r.y)) {
950
+ minY = Math.max(minY, b.y + b.height);
951
+ } else if (b.y < r.y + EPS4 && b.y + b.height > r.y - EPS4) {
952
+ return 0;
953
+ }
954
+ }
863
955
  }
864
- return true;
956
+ let e = Math.max(0, r.y - minY);
957
+ if (e <= 0) return 0;
958
+ if (maxAspect != null) {
959
+ const w = r.width, h = r.height;
960
+ if (h >= w) e = Math.min(e, maxAspect * w - h);
961
+ }
962
+ return Math.max(0, e);
865
963
  }
866
- function resizeSoftOverlaps(state, newIndex) {
867
- const newcomer = state.placed[newIndex];
868
- const { rect: newR, zLayers: newZs } = newcomer;
869
- const layerCount = state.layerCount;
870
- const removeIdx = [];
871
- const toAdd = [];
872
- for (let i = 0; i < state.placed.length; i++) {
873
- if (i === newIndex) continue;
874
- const old = state.placed[i];
875
- if (old.zLayers.length >= layerCount) continue;
876
- const sharedZ = old.zLayers.filter((z) => newZs.includes(z));
877
- if (sharedZ.length === 0) continue;
878
- if (!overlaps(old.rect, newR)) continue;
879
- const parts = subtractRect2D(old.rect, newR);
880
- removeIdx.push(i);
881
- const unaffectedZ = old.zLayers.filter((z) => !newZs.includes(z));
882
- if (unaffectedZ.length > 0) {
883
- toAdd.push({ rect: old.rect, zLayers: unaffectedZ });
964
+ function expandRectFromSeed(params) {
965
+ const {
966
+ startX,
967
+ startY,
968
+ gridSize,
969
+ bounds,
970
+ blockers,
971
+ initialCellRatio,
972
+ maxAspectRatio,
973
+ minReq
974
+ } = params;
975
+ const minSide = Math.max(1e-9, gridSize * initialCellRatio);
976
+ const initialW = Math.max(minSide, minReq.width);
977
+ const initialH = Math.max(minSide, minReq.height);
978
+ const strategies = [
979
+ { ox: 0, oy: 0 },
980
+ { ox: -initialW, oy: 0 },
981
+ { ox: 0, oy: -initialH },
982
+ { ox: -initialW, oy: -initialH },
983
+ { ox: -initialW / 2, oy: -initialH / 2 }
984
+ ];
985
+ let best = null;
986
+ let bestArea = 0;
987
+ STRATS: for (const s of strategies) {
988
+ let r = {
989
+ x: startX + s.ox,
990
+ y: startY + s.oy,
991
+ width: initialW,
992
+ height: initialH
993
+ };
994
+ if (lt(r.x, bounds.x) || lt(r.y, bounds.y) || gt(r.x + r.width, bounds.x + bounds.width) || gt(r.y + r.height, bounds.y + bounds.height)) {
995
+ continue;
884
996
  }
885
- const minW = Math.min(
886
- state.options.minSingle.width,
887
- state.options.minMulti.width
888
- );
889
- const minH = Math.min(
890
- state.options.minSingle.height,
891
- state.options.minMulti.height
892
- );
893
- for (const p of parts) {
894
- if (p.width + EPS >= minW && p.height + EPS >= minH) {
895
- toAdd.push({ rect: p, zLayers: sharedZ.slice() });
997
+ for (const b of blockers) if (overlaps(r, b)) continue STRATS;
998
+ let improved = true;
999
+ while (improved) {
1000
+ improved = false;
1001
+ const commonParams = { bounds, blockers, maxAspect: maxAspectRatio };
1002
+ const eR = maxExpandRight({ ...commonParams, r });
1003
+ if (eR > 0) {
1004
+ r = { ...r, width: r.width + eR };
1005
+ improved = true;
1006
+ }
1007
+ const eD = maxExpandDown({ ...commonParams, r });
1008
+ if (eD > 0) {
1009
+ r = { ...r, height: r.height + eD };
1010
+ improved = true;
1011
+ }
1012
+ const eL = maxExpandLeft({ ...commonParams, r });
1013
+ if (eL > 0) {
1014
+ r = { x: r.x - eL, y: r.y, width: r.width + eL, height: r.height };
1015
+ improved = true;
1016
+ }
1017
+ const eU = maxExpandUp({ ...commonParams, r });
1018
+ if (eU > 0) {
1019
+ r = { x: r.x, y: r.y - eU, width: r.width, height: r.height + eU };
1020
+ improved = true;
1021
+ }
1022
+ }
1023
+ if (r.width + EPS4 >= minReq.width && r.height + EPS4 >= minReq.height) {
1024
+ const area = r.width * r.height;
1025
+ if (area > bestArea) {
1026
+ best = r;
1027
+ bestArea = area;
896
1028
  }
897
1029
  }
898
1030
  }
899
- removeIdx.sort((a, b) => b - a).forEach((idx) => {
900
- const rem = state.placed.splice(idx, 1)[0];
901
- for (const z of rem.zLayers) {
902
- const arr = state.placedByLayer[z];
903
- const j = arr.findIndex((r) => r === rem.rect);
904
- if (j >= 0) arr.splice(j, 1);
1031
+ return best;
1032
+ }
1033
+
1034
+ // lib/solvers/RectDiffSeedingSolver/computeDefaultGridSizes.ts
1035
+ function computeDefaultGridSizes(bounds) {
1036
+ const ref = Math.max(bounds.width, bounds.height);
1037
+ return [ref / 8, ref / 16, ref / 32];
1038
+ }
1039
+
1040
+ // lib/utils/isFullyOccupiedAtPoint.ts
1041
+ function isFullyOccupiedAtPoint(params, point) {
1042
+ for (let z = 0; z < params.layerCount; z++) {
1043
+ const obs = params.obstaclesByLayer[z] ?? [];
1044
+ const placed = params.placedByLayer[z] ?? [];
1045
+ const occ = obs.some((b) => containsPoint(b, point)) || placed.some((b) => containsPoint(b, point));
1046
+ if (!occ) return false;
1047
+ }
1048
+ return true;
1049
+ }
1050
+
1051
+ // lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts
1052
+ function longestFreeSpanAroundZ(params) {
1053
+ const {
1054
+ x,
1055
+ y,
1056
+ z,
1057
+ layerCount,
1058
+ minSpan,
1059
+ maxSpan,
1060
+ obstaclesByLayer,
1061
+ placedByLayer
1062
+ } = params;
1063
+ const isFreeAt = (layer) => {
1064
+ const blockers = [
1065
+ ...obstaclesByLayer[layer] ?? [],
1066
+ ...placedByLayer[layer] ?? []
1067
+ ];
1068
+ return !blockers.some((b) => containsPoint(b, { x, y }));
1069
+ };
1070
+ let lo = z;
1071
+ let hi = z;
1072
+ while (lo - 1 >= 0 && isFreeAt(lo - 1)) lo--;
1073
+ while (hi + 1 < layerCount && isFreeAt(hi + 1)) hi++;
1074
+ if (typeof maxSpan === "number") {
1075
+ const target = clamp(maxSpan, 1, layerCount);
1076
+ while (hi - lo + 1 > target) {
1077
+ if (z - lo > hi - z) lo++;
1078
+ else hi--;
1079
+ }
1080
+ }
1081
+ const res = [];
1082
+ for (let i = lo; i <= hi; i++) res.push(i);
1083
+ return res.length >= minSpan ? res : [];
1084
+ }
1085
+
1086
+ // lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts
1087
+ function computeCandidates3D(params) {
1088
+ const {
1089
+ bounds,
1090
+ gridSize,
1091
+ layerCount,
1092
+ obstaclesByLayer,
1093
+ placedByLayer,
1094
+ hardPlacedByLayer
1095
+ } = params;
1096
+ const out = /* @__PURE__ */ new Map();
1097
+ for (let x = bounds.x; x < bounds.x + bounds.width; x += gridSize) {
1098
+ for (let y = bounds.y; y < bounds.y + bounds.height; y += gridSize) {
1099
+ if (Math.abs(x - bounds.x) < EPS4 || Math.abs(y - bounds.y) < EPS4 || x > bounds.x + bounds.width - gridSize - EPS4 || y > bounds.y + bounds.height - gridSize - EPS4) {
1100
+ continue;
1101
+ }
1102
+ if (isFullyOccupiedAtPoint(
1103
+ {
1104
+ layerCount,
1105
+ obstaclesByLayer,
1106
+ placedByLayer
1107
+ },
1108
+ { x, y }
1109
+ ))
1110
+ continue;
1111
+ let bestSpan = [];
1112
+ let bestZ = 0;
1113
+ for (let z = 0; z < layerCount; z++) {
1114
+ const s = longestFreeSpanAroundZ({
1115
+ x,
1116
+ y,
1117
+ z,
1118
+ layerCount,
1119
+ minSpan: 1,
1120
+ maxSpan: void 0,
1121
+ obstaclesByLayer,
1122
+ placedByLayer: hardPlacedByLayer
1123
+ });
1124
+ if (s.length > bestSpan.length) {
1125
+ bestSpan = s;
1126
+ bestZ = z;
1127
+ }
1128
+ }
1129
+ const anchorZ = bestSpan.length ? bestSpan[Math.floor(bestSpan.length / 2)] : bestZ;
1130
+ const hardAtZ = [
1131
+ ...obstaclesByLayer[anchorZ] ?? [],
1132
+ ...hardPlacedByLayer[anchorZ] ?? []
1133
+ ];
1134
+ const d = Math.min(
1135
+ distancePointToRectEdges({ x, y }, bounds),
1136
+ ...hardAtZ.length ? hardAtZ.map((b) => distancePointToRectEdges({ x, y }, b)) : [Infinity]
1137
+ );
1138
+ const k = `${x.toFixed(6)}|${y.toFixed(6)}`;
1139
+ const cand = {
1140
+ x,
1141
+ y,
1142
+ z: anchorZ,
1143
+ distance: d,
1144
+ zSpanLen: bestSpan.length
1145
+ };
1146
+ const prev = out.get(k);
1147
+ if (!prev || cand.zSpanLen > (prev.zSpanLen ?? 0) || cand.zSpanLen === prev.zSpanLen && cand.distance > prev.distance) {
1148
+ out.set(k, cand);
1149
+ }
905
1150
  }
906
- });
907
- for (const p of toAdd) {
908
- state.placed.push(p);
909
- for (const z of p.zLayers) state.placedByLayer[z].push(p.rect);
910
1151
  }
1152
+ const arr = Array.from(out.values());
1153
+ arr.sort((a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance);
1154
+ return arr;
911
1155
  }
912
- function stepGrid(state) {
913
- const {
914
- gridSizes,
915
- initialCellRatio,
916
- maxAspectRatio,
917
- minSingle,
918
- minMulti,
919
- preferMultiLayer,
920
- maxMultiLayerSpan
921
- } = state.options;
922
- const grid = gridSizes[state.gridIndex];
923
- const hardPlacedByLayer = buildHardPlacedByLayer(state);
924
- if (state.candidates.length === 0 && state.consumedSeedsThisGrid === 0) {
925
- state.candidates = computeCandidates3D({
926
- bounds: state.bounds,
927
- gridSize: grid,
928
- layerCount: state.layerCount,
929
- obstaclesByLayer: state.obstaclesByLayer,
930
- placedByLayer: state.placedByLayer,
931
- hardPlacedByLayer
932
- });
933
- state.totalSeedsThisGrid = state.candidates.length;
934
- state.consumedSeedsThisGrid = 0;
935
- }
936
- if (state.candidates.length === 0) {
937
- if (state.gridIndex + 1 < gridSizes.length) {
938
- state.gridIndex += 1;
939
- state.totalSeedsThisGrid = 0;
940
- state.consumedSeedsThisGrid = 0;
941
- return;
1156
+
1157
+ // lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts
1158
+ function computeUncoveredSegments(params) {
1159
+ const { lineStart, lineEnd, coveringIntervals, minSegmentLength } = params;
1160
+ if (coveringIntervals.length === 0) {
1161
+ const center = (lineStart + lineEnd) / 2;
1162
+ return [{ start: lineStart, end: lineEnd, center }];
1163
+ }
1164
+ const sorted = [...coveringIntervals].sort((a, b) => a.start - b.start);
1165
+ const merged = [];
1166
+ let current = { ...sorted[0] };
1167
+ for (let i = 1; i < sorted.length; i++) {
1168
+ const interval = sorted[i];
1169
+ if (interval.start <= current.end + EPS4) {
1170
+ current.end = Math.max(current.end, interval.end);
942
1171
  } else {
943
- if (!state.edgeAnalysisDone) {
944
- const minSize = Math.min(minSingle.width, minSingle.height);
945
- state.candidates = computeEdgeCandidates3D({
946
- bounds: state.bounds,
947
- minSize,
948
- layerCount: state.layerCount,
949
- obstaclesByLayer: state.obstaclesByLayer,
950
- placedByLayer: state.placedByLayer,
951
- hardPlacedByLayer
952
- });
953
- state.edgeAnalysisDone = true;
954
- state.totalSeedsThisGrid = state.candidates.length;
955
- state.consumedSeedsThisGrid = 0;
956
- return;
957
- }
958
- state.phase = "EXPANSION";
959
- state.expansionIndex = 0;
960
- return;
1172
+ merged.push(current);
1173
+ current = { ...interval };
961
1174
  }
962
1175
  }
963
- const cand = state.candidates.shift();
964
- state.consumedSeedsThisGrid += 1;
965
- const span = longestFreeSpanAroundZ({
966
- x: cand.x,
967
- y: cand.y,
968
- z: cand.z,
969
- layerCount: state.layerCount,
970
- minSpan: minMulti.minLayers,
971
- maxSpan: maxMultiLayerSpan,
972
- obstaclesByLayer: state.obstaclesByLayer,
973
- placedByLayer: hardPlacedByLayer
974
- });
975
- const attempts = [];
976
- if (span.length >= minMulti.minLayers) {
977
- attempts.push({
978
- kind: "multi",
979
- layers: span,
980
- minReq: { width: minMulti.width, height: minMulti.height }
981
- });
982
- }
983
- attempts.push({
984
- kind: "single",
985
- layers: [cand.z],
986
- minReq: { width: minSingle.width, height: minSingle.height }
987
- });
988
- const ordered = preferMultiLayer ? attempts : attempts.reverse();
989
- for (const attempt of ordered) {
990
- const hardBlockers = [];
991
- for (const z of attempt.layers) {
992
- if (state.obstaclesByLayer[z])
993
- hardBlockers.push(...state.obstaclesByLayer[z]);
994
- if (hardPlacedByLayer[z]) hardBlockers.push(...hardPlacedByLayer[z]);
995
- }
996
- const rect = expandRectFromSeed({
997
- startX: cand.x,
998
- startY: cand.y,
999
- gridSize: grid,
1000
- bounds: state.bounds,
1001
- blockers: hardBlockers,
1002
- initialCellRatio,
1003
- maxAspectRatio,
1004
- minReq: attempt.minReq
1005
- });
1006
- if (!rect) continue;
1007
- const placed = { rect, zLayers: [...attempt.layers] };
1008
- const newIndex = state.placed.push(placed) - 1;
1009
- for (const z of attempt.layers) state.placedByLayer[z].push(rect);
1010
- resizeSoftOverlaps(state, newIndex);
1011
- state.candidates = state.candidates.filter(
1012
- (c) => !isFullyOccupiedAtPoint(state, { x: c.x, y: c.y })
1013
- );
1014
- return;
1015
- }
1016
- }
1017
- function stepExpansion(state) {
1018
- if (state.expansionIndex >= state.placed.length) {
1019
- state.phase = "GAP_FILL";
1020
- return;
1021
- }
1022
- const idx = state.expansionIndex;
1023
- const p = state.placed[idx];
1024
- const lastGrid = state.options.gridSizes[state.options.gridSizes.length - 1];
1025
- const hardPlacedByLayer = buildHardPlacedByLayer(state);
1026
- const hardBlockers = [];
1027
- for (const z of p.zLayers) {
1028
- hardBlockers.push(...state.obstaclesByLayer[z] ?? []);
1029
- hardBlockers.push(...hardPlacedByLayer[z] ?? []);
1030
- }
1031
- const oldRect = p.rect;
1032
- const expanded = expandRectFromSeed({
1033
- startX: p.rect.x + p.rect.width / 2,
1034
- startY: p.rect.y + p.rect.height / 2,
1035
- gridSize: lastGrid,
1036
- bounds: state.bounds,
1037
- blockers: hardBlockers,
1038
- initialCellRatio: 0,
1039
- maxAspectRatio: null,
1040
- minReq: { width: p.rect.width, height: p.rect.height }
1041
- });
1042
- if (expanded) {
1043
- state.placed[idx] = { rect: expanded, zLayers: p.zLayers };
1044
- for (const z of p.zLayers) {
1045
- const arr = state.placedByLayer[z];
1046
- const j = arr.findIndex((r) => r === oldRect);
1047
- if (j >= 0) arr[j] = expanded;
1176
+ merged.push(current);
1177
+ const uncovered = [];
1178
+ if (merged[0].start > lineStart + EPS4) {
1179
+ const start = lineStart;
1180
+ const end = merged[0].start;
1181
+ if (end - start >= minSegmentLength) {
1182
+ uncovered.push({ start, end, center: (start + end) / 2 });
1048
1183
  }
1049
- resizeSoftOverlaps(state, idx);
1050
1184
  }
1051
- state.expansionIndex += 1;
1052
- }
1053
- function finalizeRects(state) {
1054
- const out = state.placed.map((p) => ({
1055
- minX: p.rect.x,
1056
- minY: p.rect.y,
1057
- maxX: p.rect.x + p.rect.width,
1058
- maxY: p.rect.y + p.rect.height,
1059
- zLayers: [...p.zLayers].sort((a, b) => a - b)
1060
- }));
1061
- const layersByObstacleRect = /* @__PURE__ */ new Map();
1062
- state.obstaclesByLayer.forEach((layerObs, z) => {
1063
- for (const rect of layerObs) {
1064
- const layerIndices = layersByObstacleRect.get(rect) ?? [];
1065
- layerIndices.push(z);
1066
- layersByObstacleRect.set(rect, layerIndices);
1185
+ for (let i = 0; i < merged.length - 1; i++) {
1186
+ const start = merged[i].end;
1187
+ const end = merged[i + 1].start;
1188
+ if (end - start >= minSegmentLength) {
1189
+ uncovered.push({ start, end, center: (start + end) / 2 });
1067
1190
  }
1068
- });
1069
- const voidSet = new Set(state.boardVoidRects || []);
1070
- for (const [rect, layerIndices] of layersByObstacleRect.entries()) {
1071
- if (voidSet.has(rect)) continue;
1072
- out.push({
1073
- minX: rect.x,
1074
- minY: rect.y,
1075
- maxX: rect.x + rect.width,
1076
- maxY: rect.y + rect.height,
1077
- zLayers: layerIndices.sort((a, b) => a - b),
1078
- isObstacle: true
1079
- });
1080
- }
1081
- return out;
1082
- }
1083
- function computeProgress(state) {
1084
- const grids = state.options.gridSizes.length;
1085
- if (state.phase === "GRID") {
1086
- const g = state.gridIndex;
1087
- const base = g / (grids + 1);
1088
- const denom = Math.max(1, state.totalSeedsThisGrid);
1089
- const frac = denom ? state.consumedSeedsThisGrid / denom : 1;
1090
- return Math.min(0.999, base + frac * (1 / (grids + 1)));
1091
1191
  }
1092
- if (state.phase === "EXPANSION") {
1093
- const base = grids / (grids + 1);
1094
- const denom = Math.max(1, state.placed.length);
1095
- const frac = denom ? state.expansionIndex / denom : 1;
1096
- return Math.min(0.999, base + frac * (1 / (grids + 1)));
1192
+ if (merged[merged.length - 1].end < lineEnd - EPS4) {
1193
+ const start = merged[merged.length - 1].end;
1194
+ const end = lineEnd;
1195
+ if (end - start >= minSegmentLength) {
1196
+ uncovered.push({ start, end, center: (start + end) / 2 });
1197
+ }
1097
1198
  }
1098
- return 1;
1199
+ return uncovered;
1099
1200
  }
1100
-
1101
- // lib/solvers/rectdiff/rectsToMeshNodes.ts
1102
- function rectsToMeshNodes(rects) {
1103
- let id = 0;
1201
+ function computeEdgeCandidates3D(params) {
1202
+ const {
1203
+ bounds,
1204
+ minSize,
1205
+ layerCount,
1206
+ obstaclesByLayer,
1207
+ placedByLayer,
1208
+ hardPlacedByLayer
1209
+ } = params;
1104
1210
  const out = [];
1105
- for (const r of rects) {
1106
- const w = Math.max(0, r.maxX - r.minX);
1107
- const h = Math.max(0, r.maxY - r.minY);
1108
- if (w <= 0 || h <= 0 || r.zLayers.length === 0) continue;
1109
- out.push({
1110
- capacityMeshNodeId: `cmn_${id++}`,
1111
- center: { x: (r.minX + r.maxX) / 2, y: (r.minY + r.maxY) / 2 },
1112
- width: w,
1113
- height: h,
1114
- layer: "top",
1115
- availableZ: r.zLayers.slice(),
1116
- _containsObstacle: r.isObstacle,
1117
- _containsTarget: r.isObstacle
1118
- });
1119
- }
1120
- return out;
1121
- }
1122
-
1123
- // lib/solvers/RectDiffSolver.ts
1124
- var RectDiffSolver = class extends BaseSolver {
1125
- srj;
1126
- gridOptions;
1127
- state;
1128
- _meshNodes = [];
1129
- constructor(opts) {
1130
- super();
1131
- this.srj = opts.simpleRouteJson;
1132
- this.gridOptions = opts.gridOptions ?? {};
1133
- }
1134
- _setup() {
1135
- this.state = initState(this.srj, this.gridOptions);
1136
- this.stats = {
1137
- phase: this.state.phase,
1138
- gridIndex: this.state.gridIndex
1139
- };
1211
+ const \u03B4 = Math.max(minSize * 0.15, EPS4 * 3);
1212
+ const dedup = /* @__PURE__ */ new Set();
1213
+ const key = (p) => `${p.z}|${p.x.toFixed(6)}|${p.y.toFixed(6)}`;
1214
+ function fullyOcc(p) {
1215
+ return isFullyOccupiedAtPoint(
1216
+ {
1217
+ layerCount,
1218
+ obstaclesByLayer,
1219
+ placedByLayer
1220
+ },
1221
+ p
1222
+ );
1140
1223
  }
1141
- /** Exactly ONE small step per call. */
1142
- _step() {
1143
- if (this.state.phase === "GRID") {
1144
- stepGrid(this.state);
1145
- } else if (this.state.phase === "EXPANSION") {
1146
- stepExpansion(this.state);
1147
- } else if (this.state.phase === "GAP_FILL") {
1148
- this.state.phase = "DONE";
1149
- } else if (this.state.phase === "DONE") {
1150
- if (!this.solved) {
1151
- const rects = finalizeRects(this.state);
1152
- this._meshNodes = rectsToMeshNodes(rects);
1153
- this.solved = true;
1154
- }
1224
+ function pushIfFree(p) {
1225
+ const { x, y, z } = p;
1226
+ if (x < bounds.x + EPS4 || y < bounds.y + EPS4 || x > bounds.x + bounds.width - EPS4 || y > bounds.y + bounds.height - EPS4)
1155
1227
  return;
1156
- }
1157
- this.stats.phase = this.state.phase;
1158
- this.stats.gridIndex = this.state.gridIndex;
1159
- this.stats.placed = this.state.placed.length;
1160
- }
1161
- /** Compute solver progress (0 to 1). */
1162
- computeProgress() {
1163
- if (this.solved || this.state.phase === "DONE") {
1164
- return 1;
1165
- }
1166
- return computeProgress(this.state);
1167
- }
1168
- getOutput() {
1169
- return { meshNodes: this._meshNodes };
1170
- }
1171
- /** Get color based on z layer for visualization. */
1172
- getColorForZLayer(zLayers) {
1173
- const minZ = Math.min(...zLayers);
1174
- const colors = [
1175
- { fill: "#dbeafe", stroke: "#3b82f6" },
1176
- { fill: "#fef3c7", stroke: "#f59e0b" },
1177
- { fill: "#d1fae5", stroke: "#10b981" },
1178
- { fill: "#e9d5ff", stroke: "#a855f7" },
1179
- { fill: "#fed7aa", stroke: "#f97316" },
1180
- { fill: "#fecaca", stroke: "#ef4444" }
1228
+ if (fullyOcc({ x, y })) return;
1229
+ const hard = [
1230
+ ...obstaclesByLayer[z] ?? [],
1231
+ ...hardPlacedByLayer[z] ?? []
1181
1232
  ];
1182
- return colors[minZ % colors.length];
1233
+ const d = Math.min(
1234
+ distancePointToRectEdges({ x, y }, bounds),
1235
+ ...hard.length ? hard.map((b) => distancePointToRectEdges({ x, y }, b)) : [Infinity]
1236
+ );
1237
+ const k = key({ x, y, z });
1238
+ if (dedup.has(k)) return;
1239
+ dedup.add(k);
1240
+ const span = longestFreeSpanAroundZ({
1241
+ x,
1242
+ y,
1243
+ z,
1244
+ layerCount,
1245
+ minSpan: 1,
1246
+ maxSpan: void 0,
1247
+ obstaclesByLayer,
1248
+ placedByLayer: hardPlacedByLayer
1249
+ });
1250
+ out.push({ x, y, z, distance: d, zSpanLen: span.length, isEdgeSeed: true });
1183
1251
  }
1184
- /** Streaming visualization: board + obstacles + current placements. */
1185
- visualize() {
1186
- const rects = [];
1187
- const points = [];
1188
- const lines = [];
1189
- const boardBounds = {
1190
- minX: this.srj.bounds.minX,
1191
- maxX: this.srj.bounds.maxX,
1192
- minY: this.srj.bounds.minY,
1193
- maxY: this.srj.bounds.maxY
1194
- };
1195
- if (this.srj.outline && this.srj.outline.length > 1) {
1196
- lines.push({
1197
- points: [...this.srj.outline, this.srj.outline[0]],
1198
- // Close the loop by adding the first point again
1199
- strokeColor: "#111827",
1200
- strokeWidth: 0.01,
1201
- label: "outline"
1202
- });
1203
- } else {
1204
- rects.push({
1205
- center: {
1206
- x: (boardBounds.minX + boardBounds.maxX) / 2,
1207
- y: (boardBounds.minY + boardBounds.maxY) / 2
1208
- },
1209
- width: boardBounds.maxX - boardBounds.minX,
1210
- height: boardBounds.maxY - boardBounds.minY,
1211
- fill: "none",
1212
- stroke: "#111827",
1213
- label: "board"
1214
- });
1252
+ for (let z = 0; z < layerCount; z++) {
1253
+ const blockers = [
1254
+ ...obstaclesByLayer[z] ?? [],
1255
+ ...hardPlacedByLayer[z] ?? []
1256
+ ];
1257
+ const corners = [
1258
+ { x: bounds.x + \u03B4, y: bounds.y + \u03B4 },
1259
+ // top-left
1260
+ { x: bounds.x + bounds.width - \u03B4, y: bounds.y + \u03B4 },
1261
+ // top-right
1262
+ { x: bounds.x + \u03B4, y: bounds.y + bounds.height - \u03B4 },
1263
+ // bottom-left
1264
+ { x: bounds.x + bounds.width - \u03B4, y: bounds.y + bounds.height - \u03B4 }
1265
+ // bottom-right
1266
+ ];
1267
+ for (const corner of corners) {
1268
+ pushIfFree({ x: corner.x, y: corner.y, z });
1269
+ }
1270
+ const topY = bounds.y + \u03B4;
1271
+ const topCovering = blockers.filter((b) => b.y <= topY && b.y + b.height >= topY).map((b) => ({
1272
+ start: Math.max(bounds.x, b.x),
1273
+ end: Math.min(bounds.x + bounds.width, b.x + b.width)
1274
+ }));
1275
+ const topUncovered = computeUncoveredSegments({
1276
+ lineStart: bounds.x + \u03B4,
1277
+ lineEnd: bounds.x + bounds.width - \u03B4,
1278
+ coveringIntervals: topCovering,
1279
+ minSegmentLength: minSize * 0.5
1280
+ });
1281
+ for (const seg of topUncovered) {
1282
+ const segLen = seg.end - seg.start;
1283
+ if (segLen >= minSize) {
1284
+ pushIfFree({ x: seg.center, y: topY, z });
1285
+ if (segLen > minSize * 1.5) {
1286
+ pushIfFree({ x: seg.start + minSize * 0.4, y: topY, z });
1287
+ pushIfFree({ x: seg.end - minSize * 0.4, y: topY, z });
1288
+ }
1289
+ }
1290
+ }
1291
+ const bottomY = bounds.y + bounds.height - \u03B4;
1292
+ const bottomCovering = blockers.filter((b) => b.y <= bottomY && b.y + b.height >= bottomY).map((b) => ({
1293
+ start: Math.max(bounds.x, b.x),
1294
+ end: Math.min(bounds.x + bounds.width, b.x + b.width)
1295
+ }));
1296
+ const bottomUncovered = computeUncoveredSegments({
1297
+ lineStart: bounds.x + \u03B4,
1298
+ lineEnd: bounds.x + bounds.width - \u03B4,
1299
+ coveringIntervals: bottomCovering,
1300
+ minSegmentLength: minSize * 0.5
1301
+ });
1302
+ for (const seg of bottomUncovered) {
1303
+ const segLen = seg.end - seg.start;
1304
+ if (segLen >= minSize) {
1305
+ pushIfFree({ x: seg.center, y: bottomY, z });
1306
+ if (segLen > minSize * 1.5) {
1307
+ pushIfFree({ x: seg.start + minSize * 0.4, y: bottomY, z });
1308
+ pushIfFree({ x: seg.end - minSize * 0.4, y: bottomY, z });
1309
+ }
1310
+ }
1215
1311
  }
1216
- for (const obstacle of this.srj.obstacles ?? []) {
1217
- if (obstacle.type === "rect" || obstacle.type === "oval") {
1218
- rects.push({
1219
- center: { x: obstacle.center.x, y: obstacle.center.y },
1220
- width: obstacle.width,
1221
- height: obstacle.height,
1222
- fill: "#fee2e2",
1223
- stroke: "#ef4444",
1224
- layer: "obstacle",
1225
- label: "obstacle"
1226
- });
1312
+ const leftX = bounds.x + \u03B4;
1313
+ const leftCovering = blockers.filter((b) => b.x <= leftX && b.x + b.width >= leftX).map((b) => ({
1314
+ start: Math.max(bounds.y, b.y),
1315
+ end: Math.min(bounds.y + bounds.height, b.y + b.height)
1316
+ }));
1317
+ const leftUncovered = computeUncoveredSegments({
1318
+ lineStart: bounds.y + \u03B4,
1319
+ lineEnd: bounds.y + bounds.height - \u03B4,
1320
+ coveringIntervals: leftCovering,
1321
+ minSegmentLength: minSize * 0.5
1322
+ });
1323
+ for (const seg of leftUncovered) {
1324
+ const segLen = seg.end - seg.start;
1325
+ if (segLen >= minSize) {
1326
+ pushIfFree({ x: leftX, y: seg.center, z });
1327
+ if (segLen > minSize * 1.5) {
1328
+ pushIfFree({ x: leftX, y: seg.start + minSize * 0.4, z });
1329
+ pushIfFree({ x: leftX, y: seg.end - minSize * 0.4, z });
1330
+ }
1227
1331
  }
1228
1332
  }
1229
- if (this.state?.boardVoidRects) {
1230
- let outlineBBox = null;
1231
- if (this.srj.outline && this.srj.outline.length > 0) {
1232
- const xs = this.srj.outline.map((p) => p.x);
1233
- const ys = this.srj.outline.map((p) => p.y);
1234
- const minX = Math.min(...xs);
1235
- const minY = Math.min(...ys);
1236
- outlineBBox = {
1237
- x: minX,
1238
- y: minY,
1239
- width: Math.max(...xs) - minX,
1240
- height: Math.max(...ys) - minY
1241
- };
1333
+ const rightX = bounds.x + bounds.width - \u03B4;
1334
+ const rightCovering = blockers.filter((b) => b.x <= rightX && b.x + b.width >= rightX).map((b) => ({
1335
+ start: Math.max(bounds.y, b.y),
1336
+ end: Math.min(bounds.y + bounds.height, b.y + b.height)
1337
+ }));
1338
+ const rightUncovered = computeUncoveredSegments({
1339
+ lineStart: bounds.y + \u03B4,
1340
+ lineEnd: bounds.y + bounds.height - \u03B4,
1341
+ coveringIntervals: rightCovering,
1342
+ minSegmentLength: minSize * 0.5
1343
+ });
1344
+ for (const seg of rightUncovered) {
1345
+ const segLen = seg.end - seg.start;
1346
+ if (segLen >= minSize) {
1347
+ pushIfFree({ x: rightX, y: seg.center, z });
1348
+ if (segLen > minSize * 1.5) {
1349
+ pushIfFree({ x: rightX, y: seg.start + minSize * 0.4, z });
1350
+ pushIfFree({ x: rightX, y: seg.end - minSize * 0.4, z });
1351
+ }
1242
1352
  }
1243
- for (const r of this.state.boardVoidRects) {
1244
- if (outlineBBox && !overlaps(r, outlineBBox)) {
1245
- continue;
1353
+ }
1354
+ for (const b of blockers) {
1355
+ const obLeftX = b.x - \u03B4;
1356
+ if (obLeftX > bounds.x + EPS4 && obLeftX < bounds.x + bounds.width - EPS4) {
1357
+ const obLeftCovering = blockers.filter(
1358
+ (bl) => bl !== b && bl.x <= obLeftX && bl.x + bl.width >= obLeftX
1359
+ ).map((bl) => ({
1360
+ start: Math.max(b.y, bl.y),
1361
+ end: Math.min(b.y + b.height, bl.y + bl.height)
1362
+ }));
1363
+ const obLeftUncovered = computeUncoveredSegments({
1364
+ lineStart: b.y,
1365
+ lineEnd: b.y + b.height,
1366
+ coveringIntervals: obLeftCovering,
1367
+ minSegmentLength: minSize * 0.5
1368
+ });
1369
+ for (const seg of obLeftUncovered) {
1370
+ pushIfFree({ x: obLeftX, y: seg.center, z });
1246
1371
  }
1247
- rects.push({
1248
- center: { x: r.x + r.width / 2, y: r.y + r.height / 2 },
1249
- width: r.width,
1250
- height: r.height,
1251
- fill: "rgba(0, 0, 0, 0.5)",
1252
- stroke: "none",
1253
- label: "void"
1372
+ }
1373
+ const obRightX = b.x + b.width + \u03B4;
1374
+ if (obRightX > bounds.x + EPS4 && obRightX < bounds.x + bounds.width - EPS4) {
1375
+ const obRightCovering = blockers.filter(
1376
+ (bl) => bl !== b && bl.x <= obRightX && bl.x + bl.width >= obRightX
1377
+ ).map((bl) => ({
1378
+ start: Math.max(b.y, bl.y),
1379
+ end: Math.min(b.y + b.height, bl.y + bl.height)
1380
+ }));
1381
+ const obRightUncovered = computeUncoveredSegments({
1382
+ lineStart: b.y,
1383
+ lineEnd: b.y + b.height,
1384
+ coveringIntervals: obRightCovering,
1385
+ minSegmentLength: minSize * 0.5
1254
1386
  });
1387
+ for (const seg of obRightUncovered) {
1388
+ pushIfFree({ x: obRightX, y: seg.center, z });
1389
+ }
1255
1390
  }
1256
- }
1257
- if (this.state?.candidates?.length) {
1258
- for (const cand of this.state.candidates) {
1259
- points.push({
1260
- x: cand.x,
1261
- y: cand.y,
1262
- fill: "#9333ea",
1263
- stroke: "#6b21a8",
1264
- label: `z:${cand.z}`
1391
+ const obTopY = b.y - \u03B4;
1392
+ if (obTopY > bounds.y + EPS4 && obTopY < bounds.y + bounds.height - EPS4) {
1393
+ const obTopCovering = blockers.filter(
1394
+ (bl) => bl !== b && bl.y <= obTopY && bl.y + bl.height >= obTopY
1395
+ ).map((bl) => ({
1396
+ start: Math.max(b.x, bl.x),
1397
+ end: Math.min(b.x + b.width, bl.x + bl.width)
1398
+ }));
1399
+ const obTopUncovered = computeUncoveredSegments({
1400
+ lineStart: b.x,
1401
+ lineEnd: b.x + b.width,
1402
+ coveringIntervals: obTopCovering,
1403
+ minSegmentLength: minSize * 0.5
1265
1404
  });
1405
+ for (const seg of obTopUncovered) {
1406
+ pushIfFree({ x: seg.center, y: obTopY, z });
1407
+ }
1266
1408
  }
1267
- }
1268
- if (this.state?.placed?.length) {
1269
- for (const p of this.state.placed) {
1270
- const colors = this.getColorForZLayer(p.zLayers);
1271
- rects.push({
1272
- center: {
1273
- x: p.rect.x + p.rect.width / 2,
1274
- y: p.rect.y + p.rect.height / 2
1275
- },
1276
- width: p.rect.width,
1277
- height: p.rect.height,
1278
- fill: colors.fill,
1279
- stroke: colors.stroke,
1280
- layer: `z${p.zLayers.join(",")}`,
1281
- label: `free
1282
- z:${p.zLayers.join(",")}`
1409
+ const obBottomY = b.y + b.height + \u03B4;
1410
+ if (obBottomY > bounds.y + EPS4 && obBottomY < bounds.y + bounds.height - EPS4) {
1411
+ const obBottomCovering = blockers.filter(
1412
+ (bl) => bl !== b && bl.y <= obBottomY && bl.y + bl.height >= obBottomY
1413
+ ).map((bl) => ({
1414
+ start: Math.max(b.x, bl.x),
1415
+ end: Math.min(b.x + b.width, bl.x + bl.width)
1416
+ }));
1417
+ const obBottomUncovered = computeUncoveredSegments({
1418
+ lineStart: b.x,
1419
+ lineEnd: b.x + b.width,
1420
+ coveringIntervals: obBottomCovering,
1421
+ minSegmentLength: minSize * 0.5
1283
1422
  });
1423
+ for (const seg of obBottomUncovered) {
1424
+ pushIfFree({ x: seg.center, y: obBottomY, z });
1425
+ }
1284
1426
  }
1285
1427
  }
1286
- return {
1287
- title: `RectDiff (${this.state?.phase ?? "init"})`,
1288
- coordinateSystem: "cartesian",
1289
- rects,
1290
- points,
1291
- lines
1292
- // Include lines in the returned GraphicsObject
1293
- };
1294
- }
1295
- };
1296
-
1297
- // lib/solvers/rectdiff/visualization.ts
1298
- function createBaseVisualization(srj, title = "RectDiff") {
1299
- const rects = [];
1300
- const lines = [];
1301
- const boardBounds = {
1302
- minX: srj.bounds.minX,
1303
- maxX: srj.bounds.maxX,
1304
- minY: srj.bounds.minY,
1305
- maxY: srj.bounds.maxY
1306
- };
1307
- if (srj.outline && srj.outline.length > 1) {
1308
- lines.push({
1309
- points: [...srj.outline, srj.outline[0]],
1310
- strokeColor: "#111827",
1311
- strokeWidth: 0.01,
1312
- label: "outline"
1313
- });
1314
- } else {
1315
- rects.push({
1316
- center: {
1317
- x: (boardBounds.minX + boardBounds.maxX) / 2,
1318
- y: (boardBounds.minY + boardBounds.maxY) / 2
1319
- },
1320
- width: boardBounds.maxX - boardBounds.minX,
1321
- height: boardBounds.maxY - boardBounds.minY,
1322
- fill: "none",
1323
- stroke: "#111827",
1324
- label: "board"
1325
- });
1326
- }
1327
- for (const obstacle of srj.obstacles ?? []) {
1328
- if (obstacle.type === "rect" || obstacle.type === "oval") {
1329
- rects.push({
1330
- center: { x: obstacle.center.x, y: obstacle.center.y },
1331
- width: obstacle.width,
1332
- height: obstacle.height,
1333
- fill: "#fee2e2",
1334
- stroke: "#ef4444",
1335
- layer: "obstacle",
1336
- label: "obstacle"
1337
- });
1338
- }
1339
- }
1340
- return {
1341
- title,
1342
- coordinateSystem: "cartesian",
1343
- rects,
1344
- points: [],
1345
- lines
1346
- };
1347
- }
1348
-
1349
- // lib/solvers/GapFillSolver/GapFillSolverPipeline.ts
1350
- import {
1351
- BasePipelineSolver as BasePipelineSolver2,
1352
- definePipelineStep
1353
- } from "@tscircuit/solver-utils";
1354
-
1355
- // lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts
1356
- import { BaseSolver as BaseSolver2 } from "@tscircuit/solver-utils";
1357
- import Flatbush from "flatbush";
1358
-
1359
- // lib/solvers/GapFillSolver/projectToUncoveredSegments.ts
1360
- var EPS2 = 1e-4;
1361
- function projectToUncoveredSegments(primaryEdge, overlappingEdges) {
1362
- const isHorizontal = Math.abs(primaryEdge.start.y - primaryEdge.end.y) < EPS2;
1363
- const isVertical = Math.abs(primaryEdge.start.x - primaryEdge.end.x) < EPS2;
1364
- if (!isHorizontal && !isVertical) return [];
1365
- const axis = isHorizontal ? "x" : "y";
1366
- const perp = isHorizontal ? "y" : "x";
1367
- const lineCoord = primaryEdge.start[perp];
1368
- const p0 = primaryEdge.start[axis];
1369
- const p1 = primaryEdge.end[axis];
1370
- const pMin = Math.min(p0, p1);
1371
- const pMax = Math.max(p0, p1);
1372
- const clamp2 = (v) => Math.max(pMin, Math.min(pMax, v));
1373
- const intervals = [];
1374
- for (const e of overlappingEdges) {
1375
- if (e === primaryEdge) continue;
1376
- const eIsHorizontal = Math.abs(e.start.y - e.end.y) < EPS2;
1377
- const eIsVertical = Math.abs(e.start.x - e.end.x) < EPS2;
1378
- if (axis === "x" && !eIsHorizontal) continue;
1379
- if (axis === "y" && !eIsVertical) continue;
1380
- if (Math.abs(e.start[perp] - lineCoord) > EPS2) continue;
1381
- const eMin = Math.min(e.start[axis], e.end[axis]);
1382
- const eMax = Math.max(e.start[axis], e.end[axis]);
1383
- const s = clamp2(eMin);
1384
- const t = clamp2(eMax);
1385
- if (t - s > EPS2) intervals.push({ s, e: t });
1386
1428
  }
1387
- if (intervals.length === 0) {
1388
- return [
1389
- {
1390
- ...primaryEdge,
1391
- start: { ...primaryEdge.start },
1392
- end: { ...primaryEdge.end }
1393
- }
1394
- ];
1395
- }
1396
- intervals.sort((a, b) => a.s - b.s);
1397
- const merged = [];
1398
- for (const it of intervals) {
1399
- const last = merged[merged.length - 1];
1400
- if (!last || it.s > last.e + EPS2) merged.push({ ...it });
1401
- else last.e = Math.max(last.e, it.e);
1402
- }
1403
- const uncovered = [];
1404
- let cursor = pMin;
1405
- for (const m of merged) {
1406
- if (m.s > cursor + EPS2) uncovered.push({ s: cursor, e: m.s });
1407
- cursor = Math.max(cursor, m.e);
1408
- if (cursor >= pMax - EPS2) break;
1409
- }
1410
- if (pMax > cursor + EPS2) uncovered.push({ s: cursor, e: pMax });
1411
- if (uncovered.length === 0) return [];
1412
- return uncovered.filter((u) => u.e - u.s > EPS2).map((u) => {
1413
- const start = axis === "x" ? { x: u.s, y: lineCoord } : { x: lineCoord, y: u.s };
1414
- const end = axis === "x" ? { x: u.e, y: lineCoord } : { x: lineCoord, y: u.e };
1415
- return {
1416
- parent: primaryEdge.parent,
1417
- facingDirection: primaryEdge.facingDirection,
1418
- start,
1419
- end,
1420
- z: primaryEdge.z
1421
- };
1422
- });
1429
+ out.sort((a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance);
1430
+ return out;
1423
1431
  }
1424
1432
 
1425
- // lib/solvers/GapFillSolver/edge-constants.ts
1426
- var EDGES = [
1427
- {
1428
- facingDirection: "x-",
1429
- dx: -1,
1430
- dy: 0,
1431
- startX: -0.5,
1432
- startY: 0.5,
1433
- endX: -0.5,
1434
- endY: -0.5
1435
- },
1436
- {
1437
- facingDirection: "x+",
1438
- dx: 1,
1439
- dy: 0,
1440
- startX: 0.5,
1441
- startY: 0.5,
1442
- endX: 0.5,
1443
- endY: -0.5
1444
- },
1445
- {
1446
- facingDirection: "y-",
1447
- dx: 0,
1448
- dy: -1,
1449
- startX: -0.5,
1450
- startY: -0.5,
1451
- endX: 0.5,
1452
- endY: -0.5
1453
- },
1454
- {
1455
- facingDirection: "y+",
1456
- dx: 0,
1457
- dy: 1,
1458
- startX: 0.5,
1459
- startY: 0.5,
1460
- endX: -0.5,
1461
- endY: 0.5
1433
+ // lib/utils/buildHardPlacedByLayer.ts
1434
+ function allLayerNode(params) {
1435
+ const out = Array.from({ length: params.layerCount }, () => []);
1436
+ for (const p of params.placed) {
1437
+ if (p.zLayers.length >= params.layerCount) {
1438
+ for (const z of p.zLayers) out[z].push(p.rect);
1439
+ }
1462
1440
  }
1463
- ];
1464
- var EDGE_MAP = {
1465
- "x-": EDGES.find((e) => e.facingDirection === "x-"),
1466
- "x+": EDGES.find((e) => e.facingDirection === "x+"),
1467
- "y-": EDGES.find((e) => e.facingDirection === "y-"),
1468
- "y+": EDGES.find((e) => e.facingDirection === "y+")
1469
- };
1441
+ return out;
1442
+ }
1470
1443
 
1471
- // lib/solvers/GapFillSolver/visuallyOffsetLine.ts
1472
- var OFFSET_DIR_MAP = {
1473
- "x-": {
1474
- x: -1,
1475
- y: 0
1476
- },
1477
- "x+": {
1478
- x: 1,
1479
- y: 0
1480
- },
1481
- "y-": {
1482
- x: 0,
1483
- y: -1
1484
- },
1485
- "y+": {
1486
- x: 0,
1487
- y: 1
1444
+ // lib/utils/resizeSoftOverlaps.ts
1445
+ function resizeSoftOverlaps(params, newIndex) {
1446
+ const newcomer = params.placed[newIndex];
1447
+ const { rect: newR, zLayers: newZs } = newcomer;
1448
+ const layerCount = params.layerCount;
1449
+ const removeIdx = [];
1450
+ const toAdd = [];
1451
+ for (let i = 0; i < params.placed.length; i++) {
1452
+ if (i === newIndex) continue;
1453
+ const old = params.placed[i];
1454
+ if (old.zLayers.length >= layerCount) continue;
1455
+ const sharedZ = old.zLayers.filter((z) => newZs.includes(z));
1456
+ if (sharedZ.length === 0) continue;
1457
+ if (!overlaps(old.rect, newR)) continue;
1458
+ const parts = subtractRect2D(old.rect, newR);
1459
+ removeIdx.push(i);
1460
+ const unaffectedZ = old.zLayers.filter((z) => !newZs.includes(z));
1461
+ if (unaffectedZ.length > 0) {
1462
+ toAdd.push({ rect: old.rect, zLayers: unaffectedZ });
1463
+ }
1464
+ const minW = Math.min(
1465
+ params.options.minSingle.width,
1466
+ params.options.minMulti.width
1467
+ );
1468
+ const minH = Math.min(
1469
+ params.options.minSingle.height,
1470
+ params.options.minMulti.height
1471
+ );
1472
+ for (const p of parts) {
1473
+ if (p.width + EPS4 >= minW && p.height + EPS4 >= minH) {
1474
+ toAdd.push({ rect: p, zLayers: sharedZ.slice() });
1475
+ }
1476
+ }
1488
1477
  }
1489
- };
1490
- var visuallyOffsetLine = (line, dir, amt) => {
1491
- const offset = OFFSET_DIR_MAP[dir];
1492
- return line.map((p) => ({
1493
- x: p.x + offset.x * amt,
1494
- y: p.y + offset.y * amt
1495
- }));
1496
- };
1478
+ removeIdx.sort((a, b) => b - a).forEach((idx) => {
1479
+ const rem = params.placed.splice(idx, 1)[0];
1480
+ for (const z of rem.zLayers) {
1481
+ const arr = params.placedByLayer[z];
1482
+ const j = arr.findIndex((r) => r === rem.rect);
1483
+ if (j >= 0) arr.splice(j, 1);
1484
+ }
1485
+ });
1486
+ for (const p of toAdd) {
1487
+ params.placed.push(p);
1488
+ for (const z of p.zLayers) params.placedByLayer[z].push(p.rect);
1489
+ }
1490
+ }
1497
1491
 
1498
- // lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts
1499
- import "@tscircuit/math-utils";
1500
- var EPS3 = 1e-4;
1501
- var FindSegmentsWithAdjacentEmptySpaceSolver = class extends BaseSolver2 {
1492
+ // lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts
1493
+ var RectDiffSeedingSolver = class extends BaseSolver3 {
1502
1494
  constructor(input) {
1503
1495
  super();
1504
1496
  this.input = input;
1505
- for (const node of this.input.meshNodes) {
1506
- for (const edge of EDGES) {
1507
- let start = {
1508
- x: node.center.x + node.width * edge.startX,
1509
- y: node.center.y + node.height * edge.startY
1510
- };
1511
- let end = {
1512
- x: node.center.x + node.width * edge.endX,
1513
- y: node.center.y + node.height * edge.endY
1514
- };
1515
- if (start.x > end.x) {
1516
- ;
1517
- [start, end] = [end, start];
1518
- }
1519
- if (Math.abs(start.x - end.x) < EPS3 && start.y > end.y) {
1520
- ;
1521
- [start, end] = [end, start];
1497
+ }
1498
+ // Engine fields (mirrors initState / engine.ts)
1499
+ srj;
1500
+ layerNames;
1501
+ layerCount;
1502
+ bounds;
1503
+ options;
1504
+ obstaclesByLayer;
1505
+ boardVoidRects;
1506
+ gridIndex;
1507
+ candidates;
1508
+ placed;
1509
+ placedByLayer;
1510
+ expansionIndex;
1511
+ edgeAnalysisDone;
1512
+ totalSeedsThisGrid;
1513
+ consumedSeedsThisGrid;
1514
+ _setup() {
1515
+ const srj = this.input.simpleRouteJson;
1516
+ const opts = this.input.gridOptions ?? {};
1517
+ const { layerNames, zIndexByName } = buildZIndexMap(srj);
1518
+ const layerCount = Math.max(1, layerNames.length, srj.layerCount || 1);
1519
+ const bounds = {
1520
+ x: srj.bounds.minX,
1521
+ y: srj.bounds.minY,
1522
+ width: srj.bounds.maxX - srj.bounds.minX,
1523
+ height: srj.bounds.maxY - srj.bounds.minY
1524
+ };
1525
+ const obstaclesByLayer = Array.from(
1526
+ { length: layerCount },
1527
+ () => []
1528
+ );
1529
+ let boardVoidRects = [];
1530
+ if (srj.outline && srj.outline.length > 2) {
1531
+ boardVoidRects = computeInverseRects(bounds, srj.outline);
1532
+ for (const voidR of boardVoidRects) {
1533
+ for (let z = 0; z < layerCount; z++) {
1534
+ obstaclesByLayer[z].push(voidR);
1522
1535
  }
1523
- for (const z of node.availableZ) {
1524
- this.unprocessedEdges.push({
1525
- parent: node,
1526
- start,
1527
- end,
1528
- facingDirection: edge.facingDirection,
1529
- z
1536
+ }
1537
+ }
1538
+ for (const obstacle of srj.obstacles ?? []) {
1539
+ const rect = obstacleToXYRect(obstacle);
1540
+ if (!rect) continue;
1541
+ const zLayers = obstacleZs(obstacle, zIndexByName);
1542
+ const invalidZs = zLayers.filter((z) => z < 0 || z >= layerCount);
1543
+ if (invalidZs.length) {
1544
+ throw new Error(
1545
+ `RectDiff: obstacle uses z-layer indices ${invalidZs.join(",")} outside 0-${layerCount - 1}`
1546
+ );
1547
+ }
1548
+ if ((!obstacle.zLayers || obstacle.zLayers.length === 0) && zLayers.length) {
1549
+ obstacle.zLayers = zLayers;
1550
+ }
1551
+ for (const z of zLayers) obstaclesByLayer[z].push(rect);
1552
+ }
1553
+ const trace = Math.max(0.01, srj.minTraceWidth || 0.15);
1554
+ const defaults = {
1555
+ gridSizes: [],
1556
+ initialCellRatio: 0.2,
1557
+ maxAspectRatio: 3,
1558
+ minSingle: { width: 2 * trace, height: 2 * trace },
1559
+ minMulti: {
1560
+ width: 4 * trace,
1561
+ height: 4 * trace,
1562
+ minLayers: Math.min(2, Math.max(1, srj.layerCount || 1))
1563
+ },
1564
+ preferMultiLayer: true,
1565
+ maxMultiLayerSpan: void 0
1566
+ };
1567
+ const options = {
1568
+ ...defaults,
1569
+ ...opts,
1570
+ gridSizes: opts.gridSizes ?? // re-use the helper that was previously in engine
1571
+ computeDefaultGridSizes(bounds)
1572
+ };
1573
+ const placedByLayer = Array.from(
1574
+ { length: layerCount },
1575
+ () => []
1576
+ );
1577
+ this.srj = srj;
1578
+ this.layerNames = layerNames;
1579
+ this.layerCount = layerCount;
1580
+ this.bounds = bounds;
1581
+ this.options = options;
1582
+ this.obstaclesByLayer = obstaclesByLayer;
1583
+ this.boardVoidRects = boardVoidRects;
1584
+ this.gridIndex = 0;
1585
+ this.candidates = [];
1586
+ this.placed = [];
1587
+ this.placedByLayer = placedByLayer;
1588
+ this.expansionIndex = 0;
1589
+ this.edgeAnalysisDone = false;
1590
+ this.totalSeedsThisGrid = 0;
1591
+ this.consumedSeedsThisGrid = 0;
1592
+ this.stats = {
1593
+ gridIndex: this.gridIndex
1594
+ };
1595
+ }
1596
+ /** Exactly ONE grid candidate step per call. */
1597
+ _step() {
1598
+ this._stepGrid();
1599
+ this.stats.gridIndex = this.gridIndex;
1600
+ this.stats.placed = this.placed.length;
1601
+ }
1602
+ /**
1603
+ * One micro-step during the GRID phase: handle exactly one candidate.
1604
+ */
1605
+ _stepGrid() {
1606
+ const {
1607
+ gridSizes,
1608
+ initialCellRatio,
1609
+ maxAspectRatio,
1610
+ minSingle,
1611
+ minMulti,
1612
+ preferMultiLayer,
1613
+ maxMultiLayerSpan
1614
+ } = this.options;
1615
+ const grid = gridSizes[this.gridIndex];
1616
+ const hardPlacedByLayer = allLayerNode({
1617
+ layerCount: this.layerCount,
1618
+ placed: this.placed
1619
+ });
1620
+ if (this.candidates.length === 0 && this.consumedSeedsThisGrid === 0) {
1621
+ this.candidates = computeCandidates3D({
1622
+ bounds: this.bounds,
1623
+ gridSize: grid,
1624
+ layerCount: this.layerCount,
1625
+ obstaclesByLayer: this.obstaclesByLayer,
1626
+ placedByLayer: this.placedByLayer,
1627
+ hardPlacedByLayer
1628
+ });
1629
+ this.totalSeedsThisGrid = this.candidates.length;
1630
+ this.consumedSeedsThisGrid = 0;
1631
+ }
1632
+ if (this.candidates.length === 0) {
1633
+ if (this.gridIndex + 1 < gridSizes.length) {
1634
+ this.gridIndex += 1;
1635
+ this.totalSeedsThisGrid = 0;
1636
+ this.consumedSeedsThisGrid = 0;
1637
+ return;
1638
+ } else {
1639
+ if (!this.edgeAnalysisDone) {
1640
+ const minSize = Math.min(minSingle.width, minSingle.height);
1641
+ this.candidates = computeEdgeCandidates3D({
1642
+ bounds: this.bounds,
1643
+ minSize,
1644
+ layerCount: this.layerCount,
1645
+ obstaclesByLayer: this.obstaclesByLayer,
1646
+ placedByLayer: this.placedByLayer,
1647
+ hardPlacedByLayer
1530
1648
  });
1649
+ this.edgeAnalysisDone = true;
1650
+ this.totalSeedsThisGrid = this.candidates.length;
1651
+ this.consumedSeedsThisGrid = 0;
1652
+ return;
1531
1653
  }
1654
+ this.solved = true;
1655
+ this.expansionIndex = 0;
1656
+ return;
1532
1657
  }
1533
1658
  }
1534
- this.allEdges = [...this.unprocessedEdges];
1535
- this.edgeSpatialIndex = new Flatbush(this.allEdges.length);
1536
- for (const edge of this.allEdges) {
1537
- this.edgeSpatialIndex.add(
1538
- edge.start.x,
1539
- edge.start.y,
1540
- edge.end.x,
1541
- edge.end.y
1659
+ const cand = this.candidates.shift();
1660
+ this.consumedSeedsThisGrid += 1;
1661
+ const span = longestFreeSpanAroundZ({
1662
+ x: cand.x,
1663
+ y: cand.y,
1664
+ z: cand.z,
1665
+ layerCount: this.layerCount,
1666
+ minSpan: minMulti.minLayers,
1667
+ maxSpan: maxMultiLayerSpan,
1668
+ obstaclesByLayer: this.obstaclesByLayer,
1669
+ placedByLayer: hardPlacedByLayer
1670
+ });
1671
+ const attempts = [];
1672
+ if (span.length >= minMulti.minLayers) {
1673
+ attempts.push({
1674
+ kind: "multi",
1675
+ layers: span,
1676
+ minReq: { width: minMulti.width, height: minMulti.height }
1677
+ });
1678
+ }
1679
+ attempts.push({
1680
+ kind: "single",
1681
+ layers: [cand.z],
1682
+ minReq: { width: minSingle.width, height: minSingle.height }
1683
+ });
1684
+ const ordered = preferMultiLayer ? attempts : attempts.reverse();
1685
+ for (const attempt of ordered) {
1686
+ const hardBlockers = [];
1687
+ for (const z of attempt.layers) {
1688
+ if (this.obstaclesByLayer[z])
1689
+ hardBlockers.push(...this.obstaclesByLayer[z]);
1690
+ if (hardPlacedByLayer[z]) hardBlockers.push(...hardPlacedByLayer[z]);
1691
+ }
1692
+ const rect = expandRectFromSeed({
1693
+ startX: cand.x,
1694
+ startY: cand.y,
1695
+ gridSize: grid,
1696
+ bounds: this.bounds,
1697
+ blockers: hardBlockers,
1698
+ initialCellRatio,
1699
+ maxAspectRatio,
1700
+ minReq: attempt.minReq
1701
+ });
1702
+ if (!rect) continue;
1703
+ const placed = { rect, zLayers: [...attempt.layers] };
1704
+ const newIndex = this.placed.push(placed) - 1;
1705
+ for (const z of attempt.layers) this.placedByLayer[z].push(rect);
1706
+ resizeSoftOverlaps(
1707
+ {
1708
+ layerCount: this.layerCount,
1709
+ placed: this.placed,
1710
+ placedByLayer: this.placedByLayer,
1711
+ options: this.options
1712
+ },
1713
+ newIndex
1714
+ );
1715
+ this.candidates = this.candidates.filter(
1716
+ (c) => !isFullyOccupiedAtPoint(
1717
+ {
1718
+ layerCount: this.layerCount,
1719
+ obstaclesByLayer: this.obstaclesByLayer,
1720
+ placedByLayer: this.placedByLayer
1721
+ },
1722
+ { x: c.x, y: c.y }
1723
+ )
1542
1724
  );
1725
+ return;
1543
1726
  }
1544
- this.edgeSpatialIndex.finish();
1545
1727
  }
1546
- allEdges;
1547
- unprocessedEdges = [];
1548
- segmentsWithAdjacentEmptySpace = [];
1549
- edgeSpatialIndex;
1550
- lastCandidateEdge = null;
1551
- lastOverlappingEdges = null;
1552
- lastUncoveredSegments = null;
1553
- _step() {
1554
- if (this.unprocessedEdges.length === 0) {
1555
- this.solved = true;
1556
- this.lastCandidateEdge = null;
1557
- this.lastOverlappingEdges = null;
1558
- this.lastUncoveredSegments = null;
1559
- return;
1728
+ /** Compute solver progress (0 to 1) during GRID phase. */
1729
+ computeProgress() {
1730
+ if (this.solved) {
1731
+ return 1;
1560
1732
  }
1561
- const candidateEdge = this.unprocessedEdges.shift();
1562
- this.lastCandidateEdge = candidateEdge;
1563
- const nearbyEdges = this.edgeSpatialIndex.search(
1564
- candidateEdge.start.x - EPS3,
1565
- candidateEdge.start.y - EPS3,
1566
- candidateEdge.end.x + EPS3,
1567
- candidateEdge.end.y + EPS3
1568
- );
1569
- const overlappingEdges = nearbyEdges.map((i) => this.allEdges[i]).filter((e) => e.z === candidateEdge.z);
1570
- this.lastOverlappingEdges = overlappingEdges;
1571
- const uncoveredSegments = projectToUncoveredSegments(
1572
- candidateEdge,
1573
- overlappingEdges
1574
- );
1575
- this.lastUncoveredSegments = uncoveredSegments;
1576
- this.segmentsWithAdjacentEmptySpace.push(...uncoveredSegments);
1733
+ const grids = this.options.gridSizes.length;
1734
+ const g = this.gridIndex;
1735
+ const base = g / (grids + 1);
1736
+ const denom = Math.max(1, this.totalSeedsThisGrid);
1737
+ const frac = denom ? this.consumedSeedsThisGrid / denom : 1;
1738
+ return Math.min(0.999, base + frac * (1 / (grids + 1)));
1577
1739
  }
1740
+ /**
1741
+ * Output the intermediate RectDiff engine data to feed into the
1742
+ * expansion phase solver.
1743
+ */
1578
1744
  getOutput() {
1579
1745
  return {
1580
- segmentsWithAdjacentEmptySpace: this.segmentsWithAdjacentEmptySpace
1746
+ srj: this.srj,
1747
+ layerNames: this.layerNames,
1748
+ layerCount: this.layerCount,
1749
+ bounds: this.bounds,
1750
+ options: this.options,
1751
+ obstaclesByLayer: this.obstaclesByLayer,
1752
+ boardVoidRects: this.boardVoidRects,
1753
+ gridIndex: this.gridIndex,
1754
+ candidates: this.candidates,
1755
+ placed: this.placed,
1756
+ placedByLayer: this.placedByLayer,
1757
+ expansionIndex: this.expansionIndex,
1758
+ edgeAnalysisDone: this.edgeAnalysisDone,
1759
+ totalSeedsThisGrid: this.totalSeedsThisGrid,
1760
+ consumedSeedsThisGrid: this.consumedSeedsThisGrid
1581
1761
  };
1582
1762
  }
1763
+ /** Get color based on z layer for visualization. */
1764
+ getColorForZLayer(zLayers) {
1765
+ const minZ = Math.min(...zLayers);
1766
+ const colors = [
1767
+ { fill: "#dbeafe", stroke: "#3b82f6" },
1768
+ { fill: "#fef3c7", stroke: "#f59e0b" },
1769
+ { fill: "#d1fae5", stroke: "#10b981" },
1770
+ { fill: "#e9d5ff", stroke: "#a855f7" },
1771
+ { fill: "#fed7aa", stroke: "#f97316" },
1772
+ { fill: "#fecaca", stroke: "#ef4444" }
1773
+ ];
1774
+ return colors[minZ % colors.length];
1775
+ }
1776
+ /** Visualization focused on the grid seeding phase. */
1583
1777
  visualize() {
1584
- const graphics = {
1585
- title: "FindSegmentsWithAdjacentEmptySpace",
1586
- coordinateSystem: "cartesian",
1587
- rects: [],
1588
- points: [],
1589
- lines: [],
1590
- circles: [],
1591
- arrows: [],
1592
- texts: []
1778
+ const rects = [];
1779
+ const points = [];
1780
+ const lines = [];
1781
+ const srj = this.srj ?? this.input.simpleRouteJson;
1782
+ const boardBounds = {
1783
+ minX: srj.bounds.minX,
1784
+ maxX: srj.bounds.maxX,
1785
+ minY: srj.bounds.minY,
1786
+ maxY: srj.bounds.maxY
1593
1787
  };
1594
- for (const node of this.input.meshNodes) {
1595
- graphics.rects.push({
1596
- center: node.center,
1597
- width: node.width,
1598
- height: node.height,
1599
- stroke: "rgba(0, 0, 0, 0.1)"
1788
+ if (srj.outline && srj.outline.length > 1) {
1789
+ lines.push({
1790
+ points: [...srj.outline, srj.outline[0]],
1791
+ strokeColor: "#111827",
1792
+ strokeWidth: 0.01,
1793
+ label: "outline"
1600
1794
  });
1601
- }
1602
- for (const unprocessedEdge of this.unprocessedEdges) {
1603
- graphics.lines.push({
1604
- points: visuallyOffsetLine(
1605
- [unprocessedEdge.start, unprocessedEdge.end],
1606
- unprocessedEdge.facingDirection,
1607
- -0.1
1608
- ),
1609
- strokeColor: "rgba(0, 0, 255, 0.5)",
1610
- strokeDash: "5 5"
1795
+ } else {
1796
+ rects.push({
1797
+ center: {
1798
+ x: (boardBounds.minX + boardBounds.maxX) / 2,
1799
+ y: (boardBounds.minY + boardBounds.maxY) / 2
1800
+ },
1801
+ width: boardBounds.maxX - boardBounds.minX,
1802
+ height: boardBounds.maxY - boardBounds.minY,
1803
+ fill: "none",
1804
+ stroke: "#111827",
1805
+ label: "board"
1611
1806
  });
1612
1807
  }
1613
- for (const edge of this.segmentsWithAdjacentEmptySpace) {
1614
- graphics.lines.push({
1615
- points: [edge.start, edge.end],
1616
- strokeColor: "rgba(0,255,0, 0.5)"
1617
- });
1808
+ for (const obstacle of srj.obstacles ?? []) {
1809
+ if (obstacle.type === "rect" || obstacle.type === "oval") {
1810
+ rects.push({
1811
+ center: { x: obstacle.center.x, y: obstacle.center.y },
1812
+ width: obstacle.width,
1813
+ height: obstacle.height,
1814
+ fill: "#fee2e2",
1815
+ stroke: "#ef4444",
1816
+ layer: "obstacle",
1817
+ label: "obstacle"
1818
+ });
1819
+ }
1618
1820
  }
1619
- if (this.lastCandidateEdge) {
1620
- graphics.lines.push({
1621
- points: [this.lastCandidateEdge.start, this.lastCandidateEdge.end],
1622
- strokeColor: "blue"
1623
- });
1821
+ if (this.boardVoidRects) {
1822
+ let outlineBBox = null;
1823
+ if (srj.outline && srj.outline.length > 0) {
1824
+ const xs = srj.outline.map((p) => p.x);
1825
+ const ys = srj.outline.map((p) => p.y);
1826
+ const minX = Math.min(...xs);
1827
+ const minY = Math.min(...ys);
1828
+ outlineBBox = {
1829
+ x: minX,
1830
+ y: minY,
1831
+ width: Math.max(...xs) - minX,
1832
+ height: Math.max(...ys) - minY
1833
+ };
1834
+ }
1835
+ for (const r of this.boardVoidRects) {
1836
+ if (outlineBBox && !overlaps(r, outlineBBox)) {
1837
+ continue;
1838
+ }
1839
+ rects.push({
1840
+ center: { x: r.x + r.width / 2, y: r.y + r.height / 2 },
1841
+ width: r.width,
1842
+ height: r.height,
1843
+ fill: "rgba(0, 0, 0, 0.5)",
1844
+ stroke: "none",
1845
+ label: "void"
1846
+ });
1847
+ }
1624
1848
  }
1625
- if (this.lastOverlappingEdges) {
1626
- for (const edge of this.lastOverlappingEdges) {
1627
- graphics.lines.push({
1628
- points: visuallyOffsetLine(
1629
- [edge.start, edge.end],
1630
- edge.facingDirection,
1631
- 0.05
1632
- ),
1633
- strokeColor: "red",
1634
- strokeDash: "2 2"
1849
+ if (this.candidates?.length) {
1850
+ for (const cand of this.candidates) {
1851
+ points.push({
1852
+ x: cand.x,
1853
+ y: cand.y,
1854
+ fill: "#9333ea",
1855
+ stroke: "#6b21a8",
1856
+ label: `z:${cand.z}`
1635
1857
  });
1636
1858
  }
1637
1859
  }
1638
- if (this.lastUncoveredSegments) {
1639
- for (const edge of this.lastUncoveredSegments) {
1640
- graphics.lines.push({
1641
- points: visuallyOffsetLine(
1642
- [edge.start, edge.end],
1643
- edge.facingDirection,
1644
- -0.05
1645
- ),
1646
- strokeColor: "green",
1647
- strokeDash: "2 2"
1860
+ if (this.placed?.length) {
1861
+ for (const placement of this.placed) {
1862
+ const colors = this.getColorForZLayer(placement.zLayers);
1863
+ rects.push({
1864
+ center: {
1865
+ x: placement.rect.x + placement.rect.width / 2,
1866
+ y: placement.rect.y + placement.rect.height / 2
1867
+ },
1868
+ width: placement.rect.width,
1869
+ height: placement.rect.height,
1870
+ fill: colors.fill,
1871
+ stroke: colors.stroke,
1872
+ layer: `z${placement.zLayers.join(",")}`,
1873
+ label: `free
1874
+ z:${placement.zLayers.join(",")}`
1648
1875
  });
1649
1876
  }
1650
1877
  }
1651
- return graphics;
1878
+ return {
1879
+ title: "RectDiff Grid",
1880
+ coordinateSystem: "cartesian",
1881
+ rects,
1882
+ points,
1883
+ lines
1884
+ };
1652
1885
  }
1653
1886
  };
1654
1887
 
1655
- // lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts
1656
- import { BaseSolver as BaseSolver3 } from "@tscircuit/solver-utils";
1657
- import RBush from "rbush";
1888
+ // lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts
1889
+ import { BaseSolver as BaseSolver4 } from "@tscircuit/solver-utils";
1658
1890
 
1659
- // lib/solvers/GapFillSolver/getBoundsFromCorners.ts
1660
- var getBoundsFromCorners = (corners) => {
1661
- return {
1662
- minX: Math.min(...corners.map((c) => c.x)),
1663
- minY: Math.min(...corners.map((c) => c.y)),
1664
- maxX: Math.max(...corners.map((c) => c.x)),
1665
- maxY: Math.max(...corners.map((c) => c.y))
1666
- };
1667
- };
1891
+ // lib/utils/finalizeRects.ts
1892
+ function finalizeRects(params) {
1893
+ const out = params.placed.map((p) => ({
1894
+ minX: p.rect.x,
1895
+ minY: p.rect.y,
1896
+ maxX: p.rect.x + p.rect.width,
1897
+ maxY: p.rect.y + p.rect.height,
1898
+ zLayers: [...p.zLayers].sort((a, b) => a - b)
1899
+ }));
1900
+ const layersByObstacleRect = /* @__PURE__ */ new Map();
1901
+ params.obstaclesByLayer.forEach((layerObs, z) => {
1902
+ for (const rect of layerObs) {
1903
+ const layerIndices = layersByObstacleRect.get(rect) ?? [];
1904
+ layerIndices.push(z);
1905
+ layersByObstacleRect.set(rect, layerIndices);
1906
+ }
1907
+ });
1908
+ const voidSet = new Set(params.boardVoidRects || []);
1909
+ for (const [rect, layerIndices] of layersByObstacleRect.entries()) {
1910
+ if (voidSet.has(rect)) continue;
1911
+ out.push({
1912
+ minX: rect.x,
1913
+ minY: rect.y,
1914
+ maxX: rect.x + rect.width,
1915
+ maxY: rect.y + rect.height,
1916
+ zLayers: layerIndices.sort((a, b) => a - b),
1917
+ isObstacle: true
1918
+ });
1919
+ }
1920
+ return out;
1921
+ }
1668
1922
 
1669
- // lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts
1670
- import { segmentToBoxMinDistance } from "@tscircuit/math-utils";
1671
- var EPS4 = 1e-4;
1672
- var ExpandEdgesToEmptySpaceSolver = class extends BaseSolver3 {
1923
+ // lib/solvers/RectDiffExpansionSolver/rectsToMeshNodes.ts
1924
+ function rectsToMeshNodes(rects) {
1925
+ let id = 0;
1926
+ const out = [];
1927
+ for (const r of rects) {
1928
+ const w = Math.max(0, r.maxX - r.minX);
1929
+ const h = Math.max(0, r.maxY - r.minY);
1930
+ if (w <= 0 || h <= 0 || r.zLayers.length === 0) continue;
1931
+ out.push({
1932
+ capacityMeshNodeId: `cmn_${id++}`,
1933
+ center: { x: (r.minX + r.maxX) / 2, y: (r.minY + r.maxY) / 2 },
1934
+ width: w,
1935
+ height: h,
1936
+ layer: "top",
1937
+ availableZ: r.zLayers.slice(),
1938
+ _containsObstacle: r.isObstacle,
1939
+ _containsTarget: r.isObstacle
1940
+ });
1941
+ }
1942
+ return out;
1943
+ }
1944
+
1945
+ // lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts
1946
+ var RectDiffExpansionSolver = class extends BaseSolver4 {
1673
1947
  constructor(input) {
1674
1948
  super();
1675
1949
  this.input = input;
1676
- this.unprocessedSegments = [...this.input.segmentsWithAdjacentEmptySpace];
1677
- this.rectSpatialIndex = new RBush();
1678
- this.rectSpatialIndex.load(
1679
- this.input.inputMeshNodes.map((n) => ({
1680
- ...n,
1681
- minX: n.center.x - n.width / 2,
1682
- minY: n.center.y - n.height / 2,
1683
- maxX: n.center.x + n.width / 2,
1684
- maxY: n.center.y + n.height / 2
1685
- }))
1686
- );
1950
+ Object.assign(this, this.input.initialSnapshot);
1687
1951
  }
1688
- unprocessedSegments = [];
1689
- expandedSegments = [];
1690
- lastSegment = null;
1691
- lastSearchBounds = null;
1692
- lastCollidingNodes = null;
1693
- lastSearchCorner1 = null;
1694
- lastSearchCorner2 = null;
1695
- lastExpandedSegment = null;
1696
- rectSpatialIndex;
1697
- _step() {
1698
- if (this.unprocessedSegments.length === 0) {
1699
- this.solved = true;
1700
- return;
1701
- }
1702
- const segment = this.unprocessedSegments.shift();
1703
- this.lastSegment = segment;
1704
- const { dx, dy } = EDGE_MAP[segment.facingDirection];
1705
- const deltaStartEnd = {
1706
- x: segment.end.x - segment.start.x,
1707
- y: segment.end.y - segment.start.y
1708
- };
1709
- const segLength = Math.sqrt(deltaStartEnd.x ** 2 + deltaStartEnd.y ** 2);
1710
- const normDeltaStartEnd = {
1711
- x: deltaStartEnd.x / segLength,
1712
- y: deltaStartEnd.y / segLength
1713
- };
1714
- let collidingNodes = null;
1715
- let searchDistance = 1;
1716
- const searchCorner1 = {
1717
- x: segment.start.x + dx * EPS4 + normDeltaStartEnd.x * EPS4 * 10,
1718
- y: segment.start.y + dy * EPS4 + normDeltaStartEnd.y * EPS4 * 10
1719
- };
1720
- const searchCorner2 = {
1721
- x: segment.end.x + dx * EPS4 - normDeltaStartEnd.x * EPS4 * 10,
1722
- y: segment.end.y + dy * EPS4 - normDeltaStartEnd.y * EPS4 * 10
1952
+ // Engine fields (same shape used by rectdiff/engine.ts)
1953
+ srj;
1954
+ layerNames;
1955
+ layerCount;
1956
+ bounds;
1957
+ options;
1958
+ obstaclesByLayer;
1959
+ boardVoidRects;
1960
+ gridIndex;
1961
+ candidates;
1962
+ placed;
1963
+ placedByLayer;
1964
+ expansionIndex;
1965
+ edgeAnalysisDone;
1966
+ totalSeedsThisGrid;
1967
+ consumedSeedsThisGrid;
1968
+ _meshNodes = [];
1969
+ _setup() {
1970
+ this.stats = {
1971
+ gridIndex: this.gridIndex
1723
1972
  };
1724
- this.lastSearchCorner1 = searchCorner1;
1725
- this.lastSearchCorner2 = searchCorner2;
1726
- while ((!collidingNodes || collidingNodes.length === 0) && searchDistance < 1e3) {
1727
- const searchBounds = getBoundsFromCorners([
1728
- searchCorner1,
1729
- searchCorner2,
1730
- {
1731
- x: searchCorner1.x + dx * searchDistance,
1732
- y: searchCorner1.y + dy * searchDistance
1733
- },
1734
- {
1735
- x: searchCorner2.x + dx * searchDistance,
1736
- y: searchCorner2.y + dy * searchDistance
1737
- }
1738
- ]);
1739
- this.lastSearchBounds = searchBounds;
1740
- collidingNodes = this.rectSpatialIndex.search(searchBounds).filter((n) => n.availableZ.includes(segment.z)).filter(
1741
- (n) => n.capacityMeshNodeId !== segment.parent.capacityMeshNodeId
1742
- );
1743
- searchDistance *= 4;
1973
+ }
1974
+ _step() {
1975
+ if (this.solved) return;
1976
+ this._stepExpansion();
1977
+ this.stats.gridIndex = this.gridIndex;
1978
+ this.stats.placed = this.placed.length;
1979
+ if (this.expansionIndex >= this.placed.length) {
1980
+ this.finalizeIfNeeded();
1744
1981
  }
1745
- if (!collidingNodes || collidingNodes.length === 0) {
1982
+ }
1983
+ _stepExpansion() {
1984
+ if (this.expansionIndex >= this.placed.length) {
1746
1985
  return;
1747
1986
  }
1748
- this.lastCollidingNodes = collidingNodes;
1749
- let smallestDistance = Infinity;
1750
- for (const node of collidingNodes) {
1751
- const distance = segmentToBoxMinDistance(segment.start, segment.end, node);
1752
- if (distance < smallestDistance) {
1753
- smallestDistance = distance;
1754
- }
1755
- }
1756
- const expandDistance = smallestDistance;
1757
- const nodeBounds = getBoundsFromCorners([
1758
- segment.start,
1759
- segment.end,
1760
- {
1761
- x: segment.start.x + dx * expandDistance,
1762
- y: segment.start.y + dy * expandDistance
1763
- },
1764
- {
1765
- x: segment.end.x + dx * expandDistance,
1766
- y: segment.end.y + dy * expandDistance
1767
- }
1768
- ]);
1769
- const nodeCenter = {
1770
- x: (nodeBounds.minX + nodeBounds.maxX) / 2,
1771
- y: (nodeBounds.minY + nodeBounds.maxY) / 2
1772
- };
1773
- const nodeWidth = nodeBounds.maxX - nodeBounds.minX;
1774
- const nodeHeight = nodeBounds.maxY - nodeBounds.minY;
1775
- const expandedSegment = {
1776
- segment,
1777
- newNode: {
1778
- capacityMeshNodeId: `new-${segment.parent.capacityMeshNodeId}-${this.expandedSegments.length}`,
1779
- center: nodeCenter,
1780
- width: nodeWidth,
1781
- height: nodeHeight,
1782
- availableZ: [segment.z],
1783
- layer: segment.parent.layer
1987
+ const idx = this.expansionIndex;
1988
+ const p = this.placed[idx];
1989
+ const lastGrid = this.options.gridSizes[this.options.gridSizes.length - 1];
1990
+ const hardPlacedByLayer = allLayerNode({
1991
+ layerCount: this.layerCount,
1992
+ placed: this.placed
1993
+ });
1994
+ const hardBlockers = [];
1995
+ for (const z of p.zLayers) {
1996
+ hardBlockers.push(...this.obstaclesByLayer[z] ?? []);
1997
+ hardBlockers.push(...hardPlacedByLayer[z] ?? []);
1998
+ }
1999
+ const oldRect = p.rect;
2000
+ const expanded = expandRectFromSeed({
2001
+ startX: p.rect.x + p.rect.width / 2,
2002
+ startY: p.rect.y + p.rect.height / 2,
2003
+ gridSize: lastGrid,
2004
+ bounds: this.bounds,
2005
+ blockers: hardBlockers,
2006
+ initialCellRatio: 0,
2007
+ maxAspectRatio: null,
2008
+ minReq: { width: p.rect.width, height: p.rect.height }
2009
+ });
2010
+ if (expanded) {
2011
+ this.placed[idx] = { rect: expanded, zLayers: p.zLayers };
2012
+ for (const z of p.zLayers) {
2013
+ const arr = this.placedByLayer[z];
2014
+ const j = arr.findIndex((r) => r === oldRect);
2015
+ if (j >= 0) arr[j] = expanded;
1784
2016
  }
1785
- };
1786
- this.lastExpandedSegment = expandedSegment;
1787
- if (nodeWidth < EPS4 || nodeHeight < EPS4) {
1788
- return;
2017
+ resizeSoftOverlaps(
2018
+ {
2019
+ layerCount: this.layerCount,
2020
+ placed: this.placed,
2021
+ placedByLayer: this.placedByLayer,
2022
+ options: this.options
2023
+ },
2024
+ idx
2025
+ );
1789
2026
  }
1790
- this.expandedSegments.push(expandedSegment);
1791
- this.rectSpatialIndex.insert({
1792
- ...expandedSegment.newNode,
1793
- ...nodeBounds
2027
+ this.expansionIndex += 1;
2028
+ }
2029
+ finalizeIfNeeded() {
2030
+ if (this.solved) return;
2031
+ const rects = finalizeRects({
2032
+ placed: this.placed,
2033
+ obstaclesByLayer: this.obstaclesByLayer,
2034
+ boardVoidRects: this.boardVoidRects
1794
2035
  });
2036
+ this._meshNodes = rectsToMeshNodes(rects);
2037
+ this.solved = true;
2038
+ }
2039
+ computeProgress() {
2040
+ if (this.solved) return 1;
2041
+ const grids = this.options.gridSizes.length;
2042
+ const base = grids / (grids + 1);
2043
+ const denom = Math.max(1, this.placed.length);
2044
+ const frac = denom ? this.expansionIndex / denom : 1;
2045
+ return Math.min(0.999, base + frac * (1 / (grids + 1)));
1795
2046
  }
1796
2047
  getOutput() {
1797
- return {
1798
- expandedSegments: this.expandedSegments
1799
- };
2048
+ if (!this.solved && this._meshNodes.length === 0) {
2049
+ this.finalizeIfNeeded();
2050
+ }
2051
+ return { meshNodes: this._meshNodes };
1800
2052
  }
2053
+ /** Simple visualization of expanded placements. */
1801
2054
  visualize() {
1802
- const graphics = {
1803
- title: "ExpandEdgesToEmptySpace",
1804
- coordinateSystem: "cartesian",
1805
- rects: [],
1806
- points: [],
1807
- lines: [],
1808
- circles: [],
1809
- arrows: [],
1810
- texts: []
1811
- };
1812
- for (const node of this.input.inputMeshNodes) {
1813
- graphics.rects.push({
1814
- center: node.center,
1815
- width: node.width,
1816
- height: node.height,
1817
- stroke: "rgba(0, 0, 0, 0.1)",
1818
- layer: `z${node.availableZ.join(",")}`,
1819
- label: [
1820
- `node ${node.capacityMeshNodeId}`,
1821
- `z:${node.availableZ.join(",")}`
1822
- ].join("\n")
1823
- });
1824
- }
1825
- for (const { newNode } of this.expandedSegments) {
1826
- graphics.rects.push({
1827
- center: newNode.center,
1828
- width: newNode.width,
1829
- height: newNode.height,
1830
- fill: "green",
1831
- label: `expandedSegment (z=${newNode.availableZ.join(",")})`,
1832
- layer: `z${newNode.availableZ.join(",")}`
1833
- });
1834
- }
1835
- if (this.lastSegment) {
1836
- graphics.lines.push({
1837
- points: [this.lastSegment.start, this.lastSegment.end],
1838
- strokeColor: "rgba(0, 0, 255, 0.5)"
1839
- });
1840
- }
1841
- if (this.lastSearchBounds) {
1842
- graphics.rects.push({
2055
+ const rects = [];
2056
+ for (const placement of this.placed ?? []) {
2057
+ rects.push({
1843
2058
  center: {
1844
- x: (this.lastSearchBounds.minX + this.lastSearchBounds.maxX) / 2,
1845
- y: (this.lastSearchBounds.minY + this.lastSearchBounds.maxY) / 2
2059
+ x: placement.rect.x + placement.rect.width / 2,
2060
+ y: placement.rect.y + placement.rect.height / 2
1846
2061
  },
1847
- width: this.lastSearchBounds.maxX - this.lastSearchBounds.minX,
1848
- height: this.lastSearchBounds.maxY - this.lastSearchBounds.minY,
1849
- fill: "rgba(0, 0, 255, 0.25)"
1850
- });
1851
- }
1852
- if (this.lastSearchCorner1 && this.lastSearchCorner2) {
1853
- graphics.points.push({
1854
- x: this.lastSearchCorner1.x,
1855
- y: this.lastSearchCorner1.y,
1856
- color: "rgba(0, 0, 255, 0.5)",
1857
- label: ["searchCorner1", `z=${this.lastSegment?.z}`].join("\n")
1858
- });
1859
- graphics.points.push({
1860
- x: this.lastSearchCorner2.x,
1861
- y: this.lastSearchCorner2.y,
1862
- color: "rgba(0, 0, 255, 0.5)",
1863
- label: ["searchCorner2", `z=${this.lastSegment?.z}`].join("\n")
1864
- });
1865
- }
1866
- if (this.lastExpandedSegment) {
1867
- graphics.rects.push({
1868
- center: this.lastExpandedSegment.newNode.center,
1869
- width: this.lastExpandedSegment.newNode.width,
1870
- height: this.lastExpandedSegment.newNode.height,
1871
- fill: "purple",
1872
- label: `expandedSegment (z=${this.lastExpandedSegment.segment.z})`
2062
+ width: placement.rect.width,
2063
+ height: placement.rect.height,
2064
+ stroke: "rgba(37, 99, 235, 0.9)",
2065
+ fill: "rgba(191, 219, 254, 0.5)",
2066
+ layer: `z${placement.zLayers.join(",")}`,
2067
+ label: `expanded
2068
+ z:${placement.zLayers.join(",")}`
1873
2069
  });
1874
2070
  }
1875
- if (this.lastCollidingNodes) {
1876
- for (const node of this.lastCollidingNodes) {
1877
- graphics.rects.push({
1878
- center: node.center,
1879
- width: node.width,
1880
- height: node.height,
1881
- fill: "rgba(255, 0, 0, 0.5)"
1882
- });
1883
- }
1884
- }
1885
- return graphics;
2071
+ return {
2072
+ title: "RectDiff Expansion",
2073
+ coordinateSystem: "cartesian",
2074
+ rects,
2075
+ points: [],
2076
+ lines: []
2077
+ };
1886
2078
  }
1887
2079
  };
1888
2080
 
1889
- // lib/solvers/GapFillSolver/GapFillSolverPipeline.ts
1890
- var GapFillSolverPipeline = class extends BasePipelineSolver2 {
1891
- findSegmentsWithAdjacentEmptySpaceSolver;
1892
- expandEdgesToEmptySpaceSolver;
2081
+ // lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts
2082
+ var RectDiffGridSolverPipeline = class extends BasePipelineSolver2 {
2083
+ rectDiffSeedingSolver;
2084
+ rectDiffExpansionSolver;
1893
2085
  pipelineDef = [
1894
- definePipelineStep(
1895
- "findSegmentsWithAdjacentEmptySpaceSolver",
1896
- FindSegmentsWithAdjacentEmptySpaceSolver,
1897
- (gapFillPipeline) => [
2086
+ definePipelineStep2(
2087
+ "rectDiffSeedingSolver",
2088
+ RectDiffSeedingSolver,
2089
+ (pipeline) => [
1898
2090
  {
1899
- meshNodes: gapFillPipeline.inputProblem.meshNodes
1900
- }
1901
- ],
1902
- {
1903
- onSolved: () => {
2091
+ simpleRouteJson: pipeline.inputProblem.simpleRouteJson,
2092
+ gridOptions: pipeline.inputProblem.gridOptions
1904
2093
  }
1905
- }
2094
+ ]
1906
2095
  ),
1907
- definePipelineStep(
1908
- "expandEdgesToEmptySpace",
1909
- ExpandEdgesToEmptySpaceSolver,
1910
- (gapFillPipeline) => [
2096
+ definePipelineStep2(
2097
+ "rectDiffExpansionSolver",
2098
+ RectDiffExpansionSolver,
2099
+ (pipeline) => [
1911
2100
  {
1912
- inputMeshNodes: gapFillPipeline.inputProblem.meshNodes,
1913
- segmentsWithAdjacentEmptySpace: gapFillPipeline.findSegmentsWithAdjacentEmptySpaceSolver.getOutput().segmentsWithAdjacentEmptySpace
1914
- }
1915
- ],
1916
- {
1917
- onSolved: () => {
2101
+ initialSnapshot: pipeline.rectDiffSeedingSolver.getOutput()
1918
2102
  }
1919
- }
2103
+ ]
1920
2104
  )
1921
2105
  ];
1922
- getOutput() {
1923
- const expandedSegments = this.expandEdgesToEmptySpaceSolver?.getOutput().expandedSegments ?? [];
1924
- const expandedNodes = expandedSegments.map((es) => es.newNode);
1925
- return {
1926
- outputNodes: [...this.inputProblem.meshNodes, ...expandedNodes]
1927
- };
2106
+ getConstructorParams() {
2107
+ return [this.inputProblem];
1928
2108
  }
1929
- initialVisualize() {
1930
- const graphics = {
1931
- title: "GapFillSolverPipeline - Initial",
1932
- coordinateSystem: "cartesian",
1933
- rects: [],
1934
- points: [],
1935
- lines: [],
1936
- circles: [],
1937
- arrows: [],
1938
- texts: []
1939
- };
1940
- for (const node of this.inputProblem.meshNodes) {
1941
- graphics.rects.push({
1942
- center: node.center,
1943
- width: node.width,
1944
- height: node.height,
1945
- stroke: "rgba(0, 0, 0, 0.3)",
1946
- fill: "rgba(100, 100, 100, 0.1)",
1947
- layer: `z${node.availableZ.join(",")}`,
1948
- label: [
1949
- `node ${node.capacityMeshNodeId}`,
1950
- `z:${node.availableZ.join(",")}`
1951
- ].join("\n")
1952
- });
2109
+ getOutput() {
2110
+ if (this.rectDiffExpansionSolver) {
2111
+ return this.rectDiffExpansionSolver.getOutput();
2112
+ }
2113
+ if (this.rectDiffSeedingSolver) {
2114
+ const snapshot = this.rectDiffSeedingSolver.getOutput();
2115
+ const meshNodes = snapshot.placed.map(
2116
+ (placement, idx) => ({
2117
+ capacityMeshNodeId: `grid-${idx}`,
2118
+ center: {
2119
+ x: placement.rect.x + placement.rect.width / 2,
2120
+ y: placement.rect.y + placement.rect.height / 2
2121
+ },
2122
+ width: placement.rect.width,
2123
+ height: placement.rect.height,
2124
+ availableZ: placement.zLayers,
2125
+ layer: `z${placement.zLayers.join(",")}`
2126
+ })
2127
+ );
2128
+ return { meshNodes };
1953
2129
  }
1954
- return graphics;
2130
+ return { meshNodes: [] };
1955
2131
  }
1956
- finalVisualize() {
1957
- const graphics = {
1958
- title: "GapFillSolverPipeline - Final",
2132
+ visualize() {
2133
+ if (this.rectDiffExpansionSolver) {
2134
+ return this.rectDiffExpansionSolver.visualize();
2135
+ }
2136
+ if (this.rectDiffSeedingSolver) {
2137
+ return this.rectDiffSeedingSolver.visualize();
2138
+ }
2139
+ return {
2140
+ title: "RectDiff Grid Pipeline",
1959
2141
  coordinateSystem: "cartesian",
1960
2142
  rects: [],
1961
2143
  points: [],
1962
- lines: [],
1963
- circles: [],
1964
- arrows: [],
1965
- texts: []
2144
+ lines: []
1966
2145
  };
1967
- const { outputNodes } = this.getOutput();
1968
- const expandedSegments = this.expandEdgesToEmptySpaceSolver?.getOutput().expandedSegments ?? [];
1969
- const expandedNodeIds = new Set(
1970
- expandedSegments.map((es) => es.newNode.capacityMeshNodeId)
1971
- );
1972
- for (const node of outputNodes) {
1973
- const isExpanded = expandedNodeIds.has(node.capacityMeshNodeId);
1974
- graphics.rects.push({
1975
- center: node.center,
1976
- width: node.width,
1977
- height: node.height,
1978
- stroke: isExpanded ? "rgba(0, 128, 0, 0.8)" : "rgba(0, 0, 0, 0.3)",
1979
- fill: isExpanded ? "rgba(0, 200, 0, 0.3)" : "rgba(100, 100, 100, 0.1)",
1980
- layer: `z${node.availableZ.join(",")}`,
1981
- label: [
1982
- `${isExpanded ? "[expanded] " : ""}node ${node.capacityMeshNodeId}`,
1983
- `z:${node.availableZ.join(",")}`
1984
- ].join("\n")
2146
+ }
2147
+ };
2148
+
2149
+ // lib/rectdiff-visualization.ts
2150
+ function createBaseVisualization(srj, title = "RectDiff") {
2151
+ const rects = [];
2152
+ const lines = [];
2153
+ const boardBounds = {
2154
+ minX: srj.bounds.minX,
2155
+ maxX: srj.bounds.maxX,
2156
+ minY: srj.bounds.minY,
2157
+ maxY: srj.bounds.maxY
2158
+ };
2159
+ if (srj.outline && srj.outline.length > 1) {
2160
+ lines.push({
2161
+ points: [...srj.outline, srj.outline[0]],
2162
+ strokeColor: "#111827",
2163
+ strokeWidth: 0.01,
2164
+ label: "outline"
2165
+ });
2166
+ } else {
2167
+ rects.push({
2168
+ center: {
2169
+ x: (boardBounds.minX + boardBounds.maxX) / 2,
2170
+ y: (boardBounds.minY + boardBounds.maxY) / 2
2171
+ },
2172
+ width: boardBounds.maxX - boardBounds.minX,
2173
+ height: boardBounds.maxY - boardBounds.minY,
2174
+ fill: "none",
2175
+ stroke: "#111827",
2176
+ label: "board"
2177
+ });
2178
+ }
2179
+ for (const obstacle of srj.obstacles ?? []) {
2180
+ if (obstacle.type === "rect" || obstacle.type === "oval") {
2181
+ rects.push({
2182
+ center: { x: obstacle.center.x, y: obstacle.center.y },
2183
+ width: obstacle.width,
2184
+ height: obstacle.height,
2185
+ fill: "#fee2e2",
2186
+ stroke: "#ef4444",
2187
+ layer: "obstacle",
2188
+ label: "obstacle"
1985
2189
  });
1986
2190
  }
1987
- return graphics;
1988
2191
  }
1989
- };
2192
+ return {
2193
+ title,
2194
+ coordinateSystem: "cartesian",
2195
+ rects,
2196
+ points: [],
2197
+ lines
2198
+ };
2199
+ }
1990
2200
 
1991
2201
  // lib/RectDiffPipeline.ts
1992
2202
  var RectDiffPipeline = class extends BasePipelineSolver3 {
1993
- rectDiffSolver;
2203
+ rectDiffGridSolverPipeline;
1994
2204
  gapFillSolver;
1995
2205
  pipelineDef = [
1996
- definePipelineStep2(
1997
- "rectDiffSolver",
1998
- RectDiffSolver,
2206
+ definePipelineStep3(
2207
+ "rectDiffGridSolverPipeline",
2208
+ RectDiffGridSolverPipeline,
1999
2209
  (rectDiffPipeline) => [
2000
2210
  {
2001
2211
  simpleRouteJson: rectDiffPipeline.inputProblem.simpleRouteJson,
2002
2212
  gridOptions: rectDiffPipeline.inputProblem.gridOptions
2003
2213
  }
2004
- ],
2005
- {
2006
- onSolved: () => {
2007
- }
2008
- }
2214
+ ]
2009
2215
  ),
2010
- definePipelineStep2(
2216
+ definePipelineStep3(
2011
2217
  "gapFillSolver",
2012
2218
  GapFillSolverPipeline,
2013
2219
  (rectDiffPipeline) => [
2014
2220
  {
2015
- meshNodes: rectDiffPipeline.rectDiffSolver?.getOutput().meshNodes ?? []
2221
+ meshNodes: rectDiffPipeline.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? []
2016
2222
  }
2017
2223
  ]
2018
2224
  )
@@ -2025,7 +2231,10 @@ var RectDiffPipeline = class extends BasePipelineSolver3 {
2025
2231
  if (gapFillOutput) {
2026
2232
  return { meshNodes: gapFillOutput.outputNodes };
2027
2233
  }
2028
- return this.rectDiffSolver.getOutput();
2234
+ if (this.rectDiffGridSolverPipeline) {
2235
+ return this.rectDiffGridSolverPipeline.getOutput();
2236
+ }
2237
+ return { meshNodes: [] };
2029
2238
  }
2030
2239
  initialVisualize() {
2031
2240
  console.log("RectDiffPipeline - initialVisualize");
@@ -2033,7 +2242,7 @@ var RectDiffPipeline = class extends BasePipelineSolver3 {
2033
2242
  this.inputProblem.simpleRouteJson,
2034
2243
  "RectDiffPipeline - Initial"
2035
2244
  );
2036
- const initialNodes = this.rectDiffSolver?.getOutput().meshNodes ?? [];
2245
+ const initialNodes = this.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? [];
2037
2246
  for (const node of initialNodes) {
2038
2247
  graphics.rects.push({
2039
2248
  center: node.center,
@@ -2057,7 +2266,7 @@ var RectDiffPipeline = class extends BasePipelineSolver3 {
2057
2266
  );
2058
2267
  const { meshNodes: outputNodes } = this.getOutput();
2059
2268
  const initialNodeIds = new Set(
2060
- (this.rectDiffSolver?.getOutput().meshNodes ?? []).map(
2269
+ (this.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? []).map(
2061
2270
  (n) => n.capacityMeshNodeId
2062
2271
  )
2063
2272
  );