@tscircuit/rectdiff 0.0.12 → 0.0.14

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 (37) hide show
  1. package/dist/index.d.ts +169 -27
  2. package/dist/index.js +2012 -1672
  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/visuallyOffsetLine.ts +5 -2
  8. package/lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts +252 -0
  9. package/lib/solvers/{rectdiff → RectDiffExpansionSolver}/rectsToMeshNodes.ts +1 -2
  10. package/lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts +106 -0
  11. package/lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts +70 -0
  12. package/lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts +487 -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} +44 -225
  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 +60 -0
  19. package/lib/types/capacity-mesh-types.ts +9 -0
  20. package/lib/utils/buildHardPlacedByLayer.ts +14 -0
  21. package/lib/{solvers/rectdiff/geometry.ts → utils/expandRectFromSeed.ts} +21 -120
  22. package/lib/utils/finalizeRects.ts +54 -0
  23. package/lib/utils/isFullyOccupiedAtPoint.ts +28 -0
  24. package/lib/utils/rectToTree.ts +10 -0
  25. package/lib/utils/rectdiff-geometry.ts +94 -0
  26. package/lib/utils/resizeSoftOverlaps.ts +103 -0
  27. package/lib/utils/sameTreeRect.ts +7 -0
  28. package/package.json +1 -1
  29. package/tests/board-outline.test.ts +2 -1
  30. package/tests/examples/example01.test.tsx +18 -1
  31. package/tests/obstacle-extra-layers.test.ts +1 -1
  32. package/tests/obstacle-zlayers.test.ts +1 -1
  33. package/utils/rectsEqual.ts +2 -2
  34. package/utils/rectsOverlap.ts +2 -2
  35. package/lib/solvers/RectDiffSolver.ts +0 -231
  36. package/lib/solvers/rectdiff/engine.ts +0 -481
  37. /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;
357
+ }
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;
352
400
  }
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 });
401
+ if (!collidingNodes || collidingNodes.length === 0) {
402
+ return;
361
403
  }
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 });
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
+ }
368
411
  }
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 });
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) {
444
+ return;
375
445
  }
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
446
+ this.expandedSegments.push(expandedSegment);
447
+ this.rectSpatialIndex.insert({
448
+ ...expandedSegment.newNode,
449
+ ...nodeBounds
399
450
  });
400
451
  }
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)
403
- 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
425
- });
426
- out.push({ x, y, z, distance: d, zSpanLen: span.length, isEdgeSeed: true });
452
+ getOutput() {
453
+ return {
454
+ expandedSegments: this.expandedSegments
455
+ };
427
456
  }
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);
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
+ });
487
496
  }
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
- }
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
+ });
508
521
  }
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
- }
528
- }
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
+ });
529
530
  }
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
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)"
544
538
  });
545
- for (const seg of obLeftUncovered) {
546
- pushIfFree(obLeftX, seg.center, z);
547
- }
548
539
  }
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);
540
+ }
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
565
556
  }
566
- }
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);
557
+ ],
558
+ {
559
+ onSolved: () => {
583
560
  }
584
561
  }
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);
562
+ ),
563
+ definePipelineStep(
564
+ "expandEdgesToEmptySpaceSolver",
565
+ ExpandEdgesToEmptySpaceSolver,
566
+ (gapFillPipeline) => [
567
+ {
568
+ inputMeshNodes: gapFillPipeline.inputProblem.meshNodes,
569
+ segmentsWithAdjacentEmptySpace: gapFillPipeline.findSegmentsWithAdjacentEmptySpaceSolver.getOutput().segmentsWithAdjacentEmptySpace
570
+ }
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
+ });
603
609
  }
610
+ return graphics;
604
611
  }
605
- out.sort((a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance);
606
- return out;
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
+ });
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 });
711
+ }
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,1480 @@ 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
- );
804
- }
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
- };
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);
847
900
  }
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);
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
+ }
853
912
  }
854
913
  }
855
- return out;
856
- }
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;
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);
863
919
  }
864
- return true;
920
+ return Math.max(0, e);
865
921
  }
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 });
884
- }
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() });
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;
896
932
  }
897
933
  }
898
934
  }
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);
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);
942
+ }
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
+ }
905
954
  }
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
955
  }
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);
911
963
  }
912
- function stepGrid(state) {
964
+ function expandRectFromSeed(params) {
913
965
  const {
914
- gridSizes,
966
+ startX,
967
+ startY,
968
+ gridSize,
969
+ bounds,
970
+ blockers,
915
971
  initialCellRatio,
916
972
  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;
942
- } 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;
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;
996
+ }
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;
957
1028
  }
958
- state.phase = "EXPANSION";
959
- state.expansionIndex = 0;
960
- return;
961
1029
  }
962
1030
  }
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
- });
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
+ import "rbush";
1042
+ function isFullyOccupiedAtPoint(params) {
1043
+ const query = {
1044
+ minX: params.point.x,
1045
+ minY: params.point.y,
1046
+ maxX: params.point.x,
1047
+ maxY: params.point.y
1048
+ };
1049
+ for (let z = 0; z < params.layerCount; z++) {
1050
+ const obstacleIdx = params.obstacleIndexByLayer[z];
1051
+ const hasObstacle = !!obstacleIdx && obstacleIdx.search(query).length > 0;
1052
+ const placedIdx = params.placedIndexByLayer[z];
1053
+ const hasPlaced = !!placedIdx && placedIdx.search(query).length > 0;
1054
+ if (!hasObstacle && !hasPlaced) return false;
982
1055
  }
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;
1056
+ return true;
1057
+ }
1058
+
1059
+ // lib/solvers/RectDiffSeedingSolver/longestFreeSpanAroundZ.ts
1060
+ function longestFreeSpanAroundZ(params) {
1061
+ const {
1062
+ x,
1063
+ y,
1064
+ z,
1065
+ layerCount,
1066
+ minSpan,
1067
+ maxSpan,
1068
+ obstacleIndexByLayer,
1069
+ additionalBlockersByLayer
1070
+ } = params;
1071
+ const isFreeAt = (layer) => {
1072
+ const query = {
1073
+ minX: x,
1074
+ minY: y,
1075
+ maxX: x,
1076
+ maxY: y
1077
+ };
1078
+ const obstacleIdx = obstacleIndexByLayer[layer];
1079
+ if (obstacleIdx && obstacleIdx.search(query).length > 0) return false;
1080
+ const extras = additionalBlockersByLayer?.[layer] ?? [];
1081
+ return !extras.some((b) => containsPoint(b, { x, y }));
1082
+ };
1083
+ let lo = z;
1084
+ let hi = z;
1085
+ while (lo - 1 >= 0 && isFreeAt(lo - 1)) lo--;
1086
+ while (hi + 1 < layerCount && isFreeAt(hi + 1)) hi++;
1087
+ if (typeof maxSpan === "number") {
1088
+ const target = clamp(maxSpan, 1, layerCount);
1089
+ while (hi - lo + 1 > target) {
1090
+ if (z - lo > hi - z) lo++;
1091
+ else hi--;
1092
+ }
1093
+ }
1094
+ const res = [];
1095
+ for (let i = lo; i <= hi; i++) res.push(i);
1096
+ return res.length >= minSpan ? res : [];
1097
+ }
1098
+
1099
+ // lib/solvers/RectDiffSeedingSolver/computeCandidates3D.ts
1100
+ function computeCandidates3D(params) {
1101
+ const {
1102
+ bounds,
1103
+ gridSize,
1104
+ layerCount,
1105
+ obstacleIndexByLayer,
1106
+ placedIndexByLayer,
1107
+ hardPlacedByLayer
1108
+ } = params;
1109
+ const out = /* @__PURE__ */ new Map();
1110
+ for (let x = bounds.x; x < bounds.x + bounds.width; x += gridSize) {
1111
+ for (let y = bounds.y; y < bounds.y + bounds.height; y += gridSize) {
1112
+ 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) {
1113
+ continue;
1114
+ }
1115
+ if (isFullyOccupiedAtPoint({
1116
+ layerCount,
1117
+ obstacleIndexByLayer,
1118
+ placedIndexByLayer,
1119
+ point: { x, y }
1120
+ }))
1121
+ continue;
1122
+ let bestSpan = [];
1123
+ let bestZ = 0;
1124
+ for (let z = 0; z < layerCount; z++) {
1125
+ const s = longestFreeSpanAroundZ({
1126
+ x,
1127
+ y,
1128
+ z,
1129
+ layerCount,
1130
+ minSpan: 1,
1131
+ maxSpan: void 0,
1132
+ obstacleIndexByLayer,
1133
+ additionalBlockersByLayer: hardPlacedByLayer
1134
+ });
1135
+ if (s.length > bestSpan.length) {
1136
+ bestSpan = s;
1137
+ bestZ = z;
1138
+ }
1139
+ }
1140
+ const anchorZ = bestSpan.length ? bestSpan[Math.floor(bestSpan.length / 2)] : bestZ;
1141
+ const hardAtZ = [
1142
+ ...obstacleIndexByLayer[anchorZ]?.all() ?? [],
1143
+ ...hardPlacedByLayer[anchorZ] ?? []
1144
+ ];
1145
+ const d = Math.min(
1146
+ distancePointToRectEdges({ x, y }, bounds),
1147
+ ...hardAtZ.length ? hardAtZ.map((b) => distancePointToRectEdges({ x, y }, b)) : [Infinity]
1148
+ );
1149
+ const k = `${x.toFixed(6)}|${y.toFixed(6)}`;
1150
+ const cand = {
1151
+ x,
1152
+ y,
1153
+ z: anchorZ,
1154
+ distance: d,
1155
+ zSpanLen: bestSpan.length
1156
+ };
1157
+ const prev = out.get(k);
1158
+ if (!prev || cand.zSpanLen > (prev.zSpanLen ?? 0) || cand.zSpanLen === prev.zSpanLen && cand.distance > prev.distance) {
1159
+ out.set(k, cand);
1160
+ }
1161
+ }
1015
1162
  }
1163
+ const arr = Array.from(out.values());
1164
+ arr.sort((a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance);
1165
+ return arr;
1016
1166
  }
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;
1167
+
1168
+ // lib/solvers/RectDiffSeedingSolver/computeEdgeCandidates3D.ts
1169
+ function computeUncoveredSegments(params) {
1170
+ const { lineStart, lineEnd, coveringIntervals, minSegmentLength } = params;
1171
+ if (coveringIntervals.length === 0) {
1172
+ const center = (lineStart + lineEnd) / 2;
1173
+ return [{ start: lineStart, end: lineEnd, center }];
1174
+ }
1175
+ const sorted = [...coveringIntervals].sort((a, b) => a.start - b.start);
1176
+ const merged = [];
1177
+ let current = { ...sorted[0] };
1178
+ for (let i = 1; i < sorted.length; i++) {
1179
+ const interval = sorted[i];
1180
+ if (interval.start <= current.end + EPS4) {
1181
+ current.end = Math.max(current.end, interval.end);
1182
+ } else {
1183
+ merged.push(current);
1184
+ current = { ...interval };
1048
1185
  }
1049
- resizeSoftOverlaps(state, idx);
1050
1186
  }
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);
1187
+ merged.push(current);
1188
+ const uncovered = [];
1189
+ if (merged[0].start > lineStart + EPS4) {
1190
+ const start = lineStart;
1191
+ const end = merged[0].start;
1192
+ if (end - start >= minSegmentLength) {
1193
+ uncovered.push({ start, end, center: (start + end) / 2 });
1067
1194
  }
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
1195
  }
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)));
1196
+ for (let i = 0; i < merged.length - 1; i++) {
1197
+ const start = merged[i].end;
1198
+ const end = merged[i + 1].start;
1199
+ if (end - start >= minSegmentLength) {
1200
+ uncovered.push({ start, end, center: (start + end) / 2 });
1201
+ }
1091
1202
  }
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)));
1203
+ if (merged[merged.length - 1].end < lineEnd - EPS4) {
1204
+ const start = merged[merged.length - 1].end;
1205
+ const end = lineEnd;
1206
+ if (end - start >= minSegmentLength) {
1207
+ uncovered.push({ start, end, center: (start + end) / 2 });
1208
+ }
1097
1209
  }
1098
- return 1;
1210
+ return uncovered;
1099
1211
  }
1100
-
1101
- // lib/solvers/rectdiff/rectsToMeshNodes.ts
1102
- function rectsToMeshNodes(rects) {
1103
- let id = 0;
1212
+ function computeEdgeCandidates3D(params) {
1213
+ const {
1214
+ bounds,
1215
+ minSize,
1216
+ layerCount,
1217
+ obstacleIndexByLayer,
1218
+ placedIndexByLayer,
1219
+ hardPlacedByLayer
1220
+ } = params;
1104
1221
  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
1222
+ const \u03B4 = Math.max(minSize * 0.15, EPS4 * 3);
1223
+ const dedup = /* @__PURE__ */ new Set();
1224
+ const key = (p) => `${p.z}|${p.x.toFixed(6)}|${p.y.toFixed(6)}`;
1225
+ function fullyOcc(p) {
1226
+ return isFullyOccupiedAtPoint({
1227
+ layerCount,
1228
+ obstacleIndexByLayer,
1229
+ placedIndexByLayer,
1230
+ point: p
1118
1231
  });
1119
1232
  }
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
- };
1140
- }
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
- }
1233
+ function pushIfFree(p) {
1234
+ const { x, y, z } = p;
1235
+ if (x < bounds.x + EPS4 || y < bounds.y + EPS4 || x > bounds.x + bounds.width - EPS4 || y > bounds.y + bounds.height - EPS4)
1155
1236
  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" }
1237
+ if (fullyOcc({ x, y })) return;
1238
+ const hard = [
1239
+ ...obstacleIndexByLayer[z]?.all() ?? [],
1240
+ ...hardPlacedByLayer[z] ?? []
1181
1241
  ];
1182
- return colors[minZ % colors.length];
1242
+ const d = Math.min(
1243
+ distancePointToRectEdges({ x, y }, bounds),
1244
+ ...hard.length ? hard.map((b) => distancePointToRectEdges({ x, y }, b)) : [Infinity]
1245
+ );
1246
+ const k = key({ x, y, z });
1247
+ if (dedup.has(k)) return;
1248
+ dedup.add(k);
1249
+ const span = longestFreeSpanAroundZ({
1250
+ x,
1251
+ y,
1252
+ z,
1253
+ layerCount,
1254
+ minSpan: 1,
1255
+ maxSpan: void 0,
1256
+ obstacleIndexByLayer,
1257
+ additionalBlockersByLayer: hardPlacedByLayer
1258
+ });
1259
+ out.push({ x, y, z, distance: d, zSpanLen: span.length, isEdgeSeed: true });
1183
1260
  }
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
- });
1261
+ for (let z = 0; z < layerCount; z++) {
1262
+ const blockers = [
1263
+ ...obstacleIndexByLayer[z]?.all() ?? [],
1264
+ ...hardPlacedByLayer[z] ?? []
1265
+ ];
1266
+ const corners = [
1267
+ { x: bounds.x + \u03B4, y: bounds.y + \u03B4 },
1268
+ // top-left
1269
+ { x: bounds.x + bounds.width - \u03B4, y: bounds.y + \u03B4 },
1270
+ // top-right
1271
+ { x: bounds.x + \u03B4, y: bounds.y + bounds.height - \u03B4 },
1272
+ // bottom-left
1273
+ { x: bounds.x + bounds.width - \u03B4, y: bounds.y + bounds.height - \u03B4 }
1274
+ // bottom-right
1275
+ ];
1276
+ for (const corner of corners) {
1277
+ pushIfFree({ x: corner.x, y: corner.y, z });
1215
1278
  }
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
- });
1279
+ const topY = bounds.y + \u03B4;
1280
+ const topCovering = blockers.filter((b) => b.y <= topY && b.y + b.height >= topY).map((b) => ({
1281
+ start: Math.max(bounds.x, b.x),
1282
+ end: Math.min(bounds.x + bounds.width, b.x + b.width)
1283
+ }));
1284
+ const topUncovered = computeUncoveredSegments({
1285
+ lineStart: bounds.x + \u03B4,
1286
+ lineEnd: bounds.x + bounds.width - \u03B4,
1287
+ coveringIntervals: topCovering,
1288
+ minSegmentLength: minSize * 0.5
1289
+ });
1290
+ for (const seg of topUncovered) {
1291
+ const segLen = seg.end - seg.start;
1292
+ if (segLen >= minSize) {
1293
+ pushIfFree({ x: seg.center, y: topY, z });
1294
+ if (segLen > minSize * 1.5) {
1295
+ pushIfFree({ x: seg.start + minSize * 0.4, y: topY, z });
1296
+ pushIfFree({ x: seg.end - minSize * 0.4, y: topY, z });
1297
+ }
1227
1298
  }
1228
1299
  }
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
- };
1300
+ const bottomY = bounds.y + bounds.height - \u03B4;
1301
+ const bottomCovering = blockers.filter((b) => b.y <= bottomY && b.y + b.height >= bottomY).map((b) => ({
1302
+ start: Math.max(bounds.x, b.x),
1303
+ end: Math.min(bounds.x + bounds.width, b.x + b.width)
1304
+ }));
1305
+ const bottomUncovered = computeUncoveredSegments({
1306
+ lineStart: bounds.x + \u03B4,
1307
+ lineEnd: bounds.x + bounds.width - \u03B4,
1308
+ coveringIntervals: bottomCovering,
1309
+ minSegmentLength: minSize * 0.5
1310
+ });
1311
+ for (const seg of bottomUncovered) {
1312
+ const segLen = seg.end - seg.start;
1313
+ if (segLen >= minSize) {
1314
+ pushIfFree({ x: seg.center, y: bottomY, z });
1315
+ if (segLen > minSize * 1.5) {
1316
+ pushIfFree({ x: seg.start + minSize * 0.4, y: bottomY, z });
1317
+ pushIfFree({ x: seg.end - minSize * 0.4, y: bottomY, z });
1318
+ }
1319
+ }
1320
+ }
1321
+ const leftX = bounds.x + \u03B4;
1322
+ const leftCovering = blockers.filter((b) => b.x <= leftX && b.x + b.width >= leftX).map((b) => ({
1323
+ start: Math.max(bounds.y, b.y),
1324
+ end: Math.min(bounds.y + bounds.height, b.y + b.height)
1325
+ }));
1326
+ const leftUncovered = computeUncoveredSegments({
1327
+ lineStart: bounds.y + \u03B4,
1328
+ lineEnd: bounds.y + bounds.height - \u03B4,
1329
+ coveringIntervals: leftCovering,
1330
+ minSegmentLength: minSize * 0.5
1331
+ });
1332
+ for (const seg of leftUncovered) {
1333
+ const segLen = seg.end - seg.start;
1334
+ if (segLen >= minSize) {
1335
+ pushIfFree({ x: leftX, y: seg.center, z });
1336
+ if (segLen > minSize * 1.5) {
1337
+ pushIfFree({ x: leftX, y: seg.start + minSize * 0.4, z });
1338
+ pushIfFree({ x: leftX, y: seg.end - minSize * 0.4, z });
1339
+ }
1340
+ }
1341
+ }
1342
+ const rightX = bounds.x + bounds.width - \u03B4;
1343
+ const rightCovering = blockers.filter((b) => b.x <= rightX && b.x + b.width >= rightX).map((b) => ({
1344
+ start: Math.max(bounds.y, b.y),
1345
+ end: Math.min(bounds.y + bounds.height, b.y + b.height)
1346
+ }));
1347
+ const rightUncovered = computeUncoveredSegments({
1348
+ lineStart: bounds.y + \u03B4,
1349
+ lineEnd: bounds.y + bounds.height - \u03B4,
1350
+ coveringIntervals: rightCovering,
1351
+ minSegmentLength: minSize * 0.5
1352
+ });
1353
+ for (const seg of rightUncovered) {
1354
+ const segLen = seg.end - seg.start;
1355
+ if (segLen >= minSize) {
1356
+ pushIfFree({ x: rightX, y: seg.center, z });
1357
+ if (segLen > minSize * 1.5) {
1358
+ pushIfFree({ x: rightX, y: seg.start + minSize * 0.4, z });
1359
+ pushIfFree({ x: rightX, y: seg.end - minSize * 0.4, z });
1360
+ }
1242
1361
  }
1243
- for (const r of this.state.boardVoidRects) {
1244
- if (outlineBBox && !overlaps(r, outlineBBox)) {
1245
- continue;
1362
+ }
1363
+ for (const b of blockers) {
1364
+ const obLeftX = b.x - \u03B4;
1365
+ if (obLeftX > bounds.x + EPS4 && obLeftX < bounds.x + bounds.width - EPS4) {
1366
+ const obLeftCovering = blockers.filter(
1367
+ (bl) => bl !== b && bl.x <= obLeftX && bl.x + bl.width >= obLeftX
1368
+ ).map((bl) => ({
1369
+ start: Math.max(b.y, bl.y),
1370
+ end: Math.min(b.y + b.height, bl.y + bl.height)
1371
+ }));
1372
+ const obLeftUncovered = computeUncoveredSegments({
1373
+ lineStart: b.y,
1374
+ lineEnd: b.y + b.height,
1375
+ coveringIntervals: obLeftCovering,
1376
+ minSegmentLength: minSize * 0.5
1377
+ });
1378
+ for (const seg of obLeftUncovered) {
1379
+ pushIfFree({ x: obLeftX, y: seg.center, z });
1246
1380
  }
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"
1381
+ }
1382
+ const obRightX = b.x + b.width + \u03B4;
1383
+ if (obRightX > bounds.x + EPS4 && obRightX < bounds.x + bounds.width - EPS4) {
1384
+ const obRightCovering = blockers.filter(
1385
+ (bl) => bl !== b && bl.x <= obRightX && bl.x + bl.width >= obRightX
1386
+ ).map((bl) => ({
1387
+ start: Math.max(b.y, bl.y),
1388
+ end: Math.min(b.y + b.height, bl.y + bl.height)
1389
+ }));
1390
+ const obRightUncovered = computeUncoveredSegments({
1391
+ lineStart: b.y,
1392
+ lineEnd: b.y + b.height,
1393
+ coveringIntervals: obRightCovering,
1394
+ minSegmentLength: minSize * 0.5
1254
1395
  });
1396
+ for (const seg of obRightUncovered) {
1397
+ pushIfFree({ x: obRightX, y: seg.center, z });
1398
+ }
1255
1399
  }
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}`
1400
+ const obTopY = b.y - \u03B4;
1401
+ if (obTopY > bounds.y + EPS4 && obTopY < bounds.y + bounds.height - EPS4) {
1402
+ const obTopCovering = blockers.filter(
1403
+ (bl) => bl !== b && bl.y <= obTopY && bl.y + bl.height >= obTopY
1404
+ ).map((bl) => ({
1405
+ start: Math.max(b.x, bl.x),
1406
+ end: Math.min(b.x + b.width, bl.x + bl.width)
1407
+ }));
1408
+ const obTopUncovered = computeUncoveredSegments({
1409
+ lineStart: b.x,
1410
+ lineEnd: b.x + b.width,
1411
+ coveringIntervals: obTopCovering,
1412
+ minSegmentLength: minSize * 0.5
1265
1413
  });
1414
+ for (const seg of obTopUncovered) {
1415
+ pushIfFree({ x: seg.center, y: obTopY, z });
1416
+ }
1266
1417
  }
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(",")}`
1418
+ const obBottomY = b.y + b.height + \u03B4;
1419
+ if (obBottomY > bounds.y + EPS4 && obBottomY < bounds.y + bounds.height - EPS4) {
1420
+ const obBottomCovering = blockers.filter(
1421
+ (bl) => bl !== b && bl.y <= obBottomY && bl.y + bl.height >= obBottomY
1422
+ ).map((bl) => ({
1423
+ start: Math.max(b.x, bl.x),
1424
+ end: Math.min(b.x + b.width, bl.x + bl.width)
1425
+ }));
1426
+ const obBottomUncovered = computeUncoveredSegments({
1427
+ lineStart: b.x,
1428
+ lineEnd: b.x + b.width,
1429
+ coveringIntervals: obBottomCovering,
1430
+ minSegmentLength: minSize * 0.5
1283
1431
  });
1432
+ for (const seg of obBottomUncovered) {
1433
+ pushIfFree({ x: seg.center, y: obBottomY, z });
1434
+ }
1284
1435
  }
1285
1436
  }
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
- }
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
- });
1423
- }
1424
-
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
1462
1437
  }
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
- };
1438
+ out.sort((a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance);
1439
+ return out;
1440
+ }
1470
1441
 
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
1442
+ // lib/utils/buildHardPlacedByLayer.ts
1443
+ function allLayerNode(params) {
1444
+ const out = Array.from({ length: params.layerCount }, () => []);
1445
+ for (const p of params.placed) {
1446
+ if (p.zLayers.length >= params.layerCount) {
1447
+ for (const z of p.zLayers) out[z].push(p.rect);
1448
+ }
1488
1449
  }
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
- };
1450
+ return out;
1451
+ }
1497
1452
 
1498
- // lib/solvers/GapFillSolver/FindSegmentsWithAdjacentEmptySpaceSolver.ts
1499
- import "@tscircuit/math-utils";
1500
- var EPS3 = 1e-4;
1501
- var FindSegmentsWithAdjacentEmptySpaceSolver = class extends BaseSolver2 {
1453
+ // lib/utils/resizeSoftOverlaps.ts
1454
+ function resizeSoftOverlaps(params, newIndex) {
1455
+ const newcomer = params.placed[newIndex];
1456
+ const { rect: newR, zLayers: newZs } = newcomer;
1457
+ const layerCount = params.layerCount;
1458
+ const removeIdx = [];
1459
+ const toAdd = [];
1460
+ for (let i = 0; i < params.placed.length; i++) {
1461
+ if (i === newIndex) continue;
1462
+ const old = params.placed[i];
1463
+ if (old.zLayers.length >= layerCount) continue;
1464
+ const sharedZ = old.zLayers.filter((z) => newZs.includes(z));
1465
+ if (sharedZ.length === 0) continue;
1466
+ if (!overlaps(old.rect, newR)) continue;
1467
+ const parts = subtractRect2D(old.rect, newR);
1468
+ removeIdx.push(i);
1469
+ const unaffectedZ = old.zLayers.filter((z) => !newZs.includes(z));
1470
+ if (unaffectedZ.length > 0) {
1471
+ toAdd.push({ rect: old.rect, zLayers: unaffectedZ });
1472
+ }
1473
+ const minW = Math.min(
1474
+ params.options.minSingle.width,
1475
+ params.options.minMulti.width
1476
+ );
1477
+ const minH = Math.min(
1478
+ params.options.minSingle.height,
1479
+ params.options.minMulti.height
1480
+ );
1481
+ for (const p of parts) {
1482
+ if (p.width + EPS4 >= minW && p.height + EPS4 >= minH) {
1483
+ toAdd.push({ rect: p, zLayers: sharedZ.slice() });
1484
+ }
1485
+ }
1486
+ }
1487
+ const rectToTree2 = (rect) => ({
1488
+ ...rect,
1489
+ minX: rect.x,
1490
+ minY: rect.y,
1491
+ maxX: rect.x + rect.width,
1492
+ maxY: rect.y + rect.height
1493
+ });
1494
+ const sameRect = (a, b) => a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
1495
+ removeIdx.sort((a, b) => b - a).forEach((idx) => {
1496
+ const rem = params.placed.splice(idx, 1)[0];
1497
+ if (params.placedIndexByLayer) {
1498
+ for (const z of rem.zLayers) {
1499
+ const tree = params.placedIndexByLayer[z];
1500
+ if (tree) tree.remove(rectToTree2(rem.rect), sameRect);
1501
+ }
1502
+ }
1503
+ });
1504
+ for (const p of toAdd) {
1505
+ params.placed.push(p);
1506
+ for (const z of p.zLayers) {
1507
+ if (params.placedIndexByLayer) {
1508
+ const idx = params.placedIndexByLayer[z];
1509
+ if (idx) {
1510
+ idx.insert({
1511
+ ...p.rect,
1512
+ minX: p.rect.x,
1513
+ minY: p.rect.y,
1514
+ maxX: p.rect.x + p.rect.width,
1515
+ maxY: p.rect.y + p.rect.height
1516
+ });
1517
+ }
1518
+ }
1519
+ }
1520
+ }
1521
+ }
1522
+
1523
+ // lib/solvers/RectDiffSeedingSolver/RectDiffSeedingSolver.ts
1524
+ import RBush3 from "rbush";
1525
+ var RectDiffSeedingSolver = class extends BaseSolver3 {
1502
1526
  constructor(input) {
1503
1527
  super();
1504
1528
  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];
1522
- }
1523
- for (const z of node.availableZ) {
1524
- this.unprocessedEdges.push({
1525
- parent: node,
1526
- start,
1527
- end,
1528
- facingDirection: edge.facingDirection,
1529
- z
1529
+ }
1530
+ // Engine fields (mirrors initState / engine.ts)
1531
+ srj;
1532
+ layerNames;
1533
+ layerCount;
1534
+ bounds;
1535
+ options;
1536
+ boardVoidRects;
1537
+ gridIndex;
1538
+ candidates;
1539
+ placed;
1540
+ placedIndexByLayer;
1541
+ expansionIndex;
1542
+ edgeAnalysisDone;
1543
+ totalSeedsThisGrid;
1544
+ consumedSeedsThisGrid;
1545
+ _setup() {
1546
+ const srj = this.input.simpleRouteJson;
1547
+ const opts = this.input.gridOptions ?? {};
1548
+ const { layerNames, zIndexByName } = buildZIndexMap(srj);
1549
+ const layerCount = Math.max(1, layerNames.length, srj.layerCount || 1);
1550
+ const bounds = {
1551
+ x: srj.bounds.minX,
1552
+ y: srj.bounds.minY,
1553
+ width: srj.bounds.maxX - srj.bounds.minX,
1554
+ height: srj.bounds.maxY - srj.bounds.minY
1555
+ };
1556
+ const trace = Math.max(0.01, srj.minTraceWidth || 0.15);
1557
+ const defaults = {
1558
+ gridSizes: [],
1559
+ initialCellRatio: 0.2,
1560
+ maxAspectRatio: 3,
1561
+ minSingle: { width: 2 * trace, height: 2 * trace },
1562
+ minMulti: {
1563
+ width: 4 * trace,
1564
+ height: 4 * trace,
1565
+ minLayers: Math.min(2, Math.max(1, srj.layerCount || 1))
1566
+ },
1567
+ preferMultiLayer: true,
1568
+ maxMultiLayerSpan: void 0
1569
+ };
1570
+ const options = {
1571
+ ...defaults,
1572
+ ...opts,
1573
+ gridSizes: opts.gridSizes ?? // re-use the helper that was previously in engine
1574
+ computeDefaultGridSizes(bounds)
1575
+ };
1576
+ this.srj = srj;
1577
+ this.layerNames = layerNames;
1578
+ this.layerCount = layerCount;
1579
+ this.bounds = bounds;
1580
+ this.options = options;
1581
+ this.boardVoidRects = this.input.boardVoidRects;
1582
+ this.gridIndex = 0;
1583
+ this.candidates = [];
1584
+ this.placed = [];
1585
+ this.placedIndexByLayer = Array.from(
1586
+ { length: layerCount },
1587
+ () => new RBush3()
1588
+ );
1589
+ this.expansionIndex = 0;
1590
+ this.edgeAnalysisDone = false;
1591
+ this.totalSeedsThisGrid = 0;
1592
+ this.consumedSeedsThisGrid = 0;
1593
+ this.stats = {
1594
+ gridIndex: this.gridIndex
1595
+ };
1596
+ }
1597
+ /** Exactly ONE grid candidate step per call. */
1598
+ _step() {
1599
+ this._stepGrid();
1600
+ this.stats.gridIndex = this.gridIndex;
1601
+ this.stats.placed = this.placed.length;
1602
+ }
1603
+ /**
1604
+ * One micro-step during the GRID phase: handle exactly one candidate.
1605
+ */
1606
+ _stepGrid() {
1607
+ const {
1608
+ gridSizes,
1609
+ initialCellRatio,
1610
+ maxAspectRatio,
1611
+ minSingle,
1612
+ minMulti,
1613
+ preferMultiLayer,
1614
+ maxMultiLayerSpan
1615
+ } = this.options;
1616
+ const grid = gridSizes[this.gridIndex];
1617
+ const hardPlacedByLayer = allLayerNode({
1618
+ layerCount: this.layerCount,
1619
+ placed: this.placed
1620
+ });
1621
+ if (this.candidates.length === 0 && this.consumedSeedsThisGrid === 0) {
1622
+ this.candidates = computeCandidates3D({
1623
+ bounds: this.bounds,
1624
+ gridSize: grid,
1625
+ layerCount: this.layerCount,
1626
+ hardPlacedByLayer,
1627
+ obstacleIndexByLayer: this.input.obstacleIndexByLayer,
1628
+ placedIndexByLayer: this.placedIndexByLayer
1629
+ });
1630
+ this.totalSeedsThisGrid = this.candidates.length;
1631
+ this.consumedSeedsThisGrid = 0;
1632
+ }
1633
+ if (this.candidates.length === 0) {
1634
+ if (this.gridIndex + 1 < gridSizes.length) {
1635
+ this.gridIndex += 1;
1636
+ this.totalSeedsThisGrid = 0;
1637
+ this.consumedSeedsThisGrid = 0;
1638
+ return;
1639
+ } else {
1640
+ if (!this.edgeAnalysisDone) {
1641
+ const minSize = Math.min(minSingle.width, minSingle.height);
1642
+ this.candidates = computeEdgeCandidates3D({
1643
+ bounds: this.bounds,
1644
+ minSize,
1645
+ layerCount: this.layerCount,
1646
+ obstacleIndexByLayer: this.input.obstacleIndexByLayer,
1647
+ placedIndexByLayer: this.placedIndexByLayer,
1648
+ hardPlacedByLayer
1530
1649
  });
1650
+ this.edgeAnalysisDone = true;
1651
+ this.totalSeedsThisGrid = this.candidates.length;
1652
+ this.consumedSeedsThisGrid = 0;
1653
+ return;
1531
1654
  }
1655
+ this.solved = true;
1656
+ this.expansionIndex = 0;
1657
+ return;
1532
1658
  }
1533
1659
  }
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
1660
+ const cand = this.candidates.shift();
1661
+ this.consumedSeedsThisGrid += 1;
1662
+ const span = longestFreeSpanAroundZ({
1663
+ x: cand.x,
1664
+ y: cand.y,
1665
+ z: cand.z,
1666
+ layerCount: this.layerCount,
1667
+ minSpan: minMulti.minLayers,
1668
+ maxSpan: maxMultiLayerSpan,
1669
+ obstacleIndexByLayer: this.input.obstacleIndexByLayer,
1670
+ additionalBlockersByLayer: hardPlacedByLayer
1671
+ });
1672
+ const attempts = [];
1673
+ if (span.length >= minMulti.minLayers) {
1674
+ attempts.push({
1675
+ kind: "multi",
1676
+ layers: span,
1677
+ minReq: { width: minMulti.width, height: minMulti.height }
1678
+ });
1679
+ }
1680
+ attempts.push({
1681
+ kind: "single",
1682
+ layers: [cand.z],
1683
+ minReq: { width: minSingle.width, height: minSingle.height }
1684
+ });
1685
+ const ordered = preferMultiLayer ? attempts : attempts.reverse();
1686
+ for (const attempt of ordered) {
1687
+ const hardBlockers = [];
1688
+ for (const z of attempt.layers) {
1689
+ const obstacleLayer = this.input.obstacleIndexByLayer[z];
1690
+ if (obstacleLayer) hardBlockers.push(...obstacleLayer.all());
1691
+ if (hardPlacedByLayer[z]) hardBlockers.push(...hardPlacedByLayer[z]);
1692
+ }
1693
+ const rect = expandRectFromSeed({
1694
+ startX: cand.x,
1695
+ startY: cand.y,
1696
+ gridSize: grid,
1697
+ bounds: this.bounds,
1698
+ blockers: hardBlockers,
1699
+ initialCellRatio,
1700
+ maxAspectRatio,
1701
+ minReq: attempt.minReq
1702
+ });
1703
+ if (!rect) continue;
1704
+ const placed = { rect, zLayers: [...attempt.layers] };
1705
+ const newIndex = this.placed.push(placed) - 1;
1706
+ for (const z of attempt.layers) {
1707
+ const idx = this.placedIndexByLayer[z];
1708
+ if (idx) {
1709
+ idx.insert({
1710
+ ...rect,
1711
+ minX: rect.x,
1712
+ minY: rect.y,
1713
+ maxX: rect.x + rect.width,
1714
+ maxY: rect.y + rect.height
1715
+ });
1716
+ }
1717
+ }
1718
+ resizeSoftOverlaps(
1719
+ {
1720
+ layerCount: this.layerCount,
1721
+ placed: this.placed,
1722
+ options: this.options,
1723
+ placedIndexByLayer: this.placedIndexByLayer
1724
+ },
1725
+ newIndex
1726
+ );
1727
+ this.candidates = this.candidates.filter(
1728
+ (c) => !isFullyOccupiedAtPoint({
1729
+ layerCount: this.layerCount,
1730
+ obstacleIndexByLayer: this.input.obstacleIndexByLayer,
1731
+ placedIndexByLayer: this.placedIndexByLayer,
1732
+ point: { x: c.x, y: c.y }
1733
+ })
1542
1734
  );
1735
+ return;
1543
1736
  }
1544
- this.edgeSpatialIndex.finish();
1545
1737
  }
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;
1738
+ /** Compute solver progress (0 to 1) during GRID phase. */
1739
+ computeProgress() {
1740
+ if (this.solved) {
1741
+ return 1;
1560
1742
  }
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);
1743
+ const grids = this.options.gridSizes.length;
1744
+ const g = this.gridIndex;
1745
+ const base = g / (grids + 1);
1746
+ const denom = Math.max(1, this.totalSeedsThisGrid);
1747
+ const frac = denom ? this.consumedSeedsThisGrid / denom : 1;
1748
+ return Math.min(0.999, base + frac * (1 / (grids + 1)));
1577
1749
  }
1750
+ /**
1751
+ * Output the intermediate RectDiff engine data to feed into the
1752
+ * expansion phase solver.
1753
+ */
1578
1754
  getOutput() {
1579
1755
  return {
1580
- segmentsWithAdjacentEmptySpace: this.segmentsWithAdjacentEmptySpace
1756
+ srj: this.srj,
1757
+ layerNames: this.layerNames,
1758
+ layerCount: this.layerCount,
1759
+ bounds: this.bounds,
1760
+ options: this.options,
1761
+ boardVoidRects: this.boardVoidRects,
1762
+ gridIndex: this.gridIndex,
1763
+ candidates: this.candidates,
1764
+ placed: this.placed,
1765
+ expansionIndex: this.expansionIndex,
1766
+ edgeAnalysisDone: this.edgeAnalysisDone,
1767
+ totalSeedsThisGrid: this.totalSeedsThisGrid,
1768
+ consumedSeedsThisGrid: this.consumedSeedsThisGrid
1581
1769
  };
1582
1770
  }
1771
+ /** Get color based on z layer for visualization. */
1772
+ getColorForZLayer(zLayers) {
1773
+ const minZ = Math.min(...zLayers);
1774
+ const colors = [
1775
+ { fill: "#dbeafe", stroke: "#3b82f6" },
1776
+ { fill: "#fef3c7", stroke: "#f59e0b" },
1777
+ { fill: "#d1fae5", stroke: "#10b981" },
1778
+ { fill: "#e9d5ff", stroke: "#a855f7" },
1779
+ { fill: "#fed7aa", stroke: "#f97316" },
1780
+ { fill: "#fecaca", stroke: "#ef4444" }
1781
+ ];
1782
+ return colors[minZ % colors.length];
1783
+ }
1784
+ /** Visualization focused on the grid seeding phase. */
1583
1785
  visualize() {
1584
- const graphics = {
1585
- title: "FindSegmentsWithAdjacentEmptySpace",
1586
- coordinateSystem: "cartesian",
1587
- rects: [],
1588
- points: [],
1589
- lines: [],
1590
- circles: [],
1591
- arrows: [],
1592
- texts: []
1786
+ const rects = [];
1787
+ const points = [];
1788
+ const lines = [];
1789
+ const srj = this.srj ?? this.input.simpleRouteJson;
1790
+ const boardBounds = {
1791
+ minX: srj.bounds.minX,
1792
+ maxX: srj.bounds.maxX,
1793
+ minY: srj.bounds.minY,
1794
+ maxY: srj.bounds.maxY
1593
1795
  };
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)"
1796
+ if (srj.outline && srj.outline.length > 1) {
1797
+ lines.push({
1798
+ points: [...srj.outline, srj.outline[0]],
1799
+ strokeColor: "#111827",
1800
+ strokeWidth: 0.01,
1801
+ label: "outline"
1600
1802
  });
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"
1803
+ } else {
1804
+ rects.push({
1805
+ center: {
1806
+ x: (boardBounds.minX + boardBounds.maxX) / 2,
1807
+ y: (boardBounds.minY + boardBounds.maxY) / 2
1808
+ },
1809
+ width: boardBounds.maxX - boardBounds.minX,
1810
+ height: boardBounds.maxY - boardBounds.minY,
1811
+ fill: "none",
1812
+ stroke: "#111827",
1813
+ label: "board"
1611
1814
  });
1612
1815
  }
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
- });
1816
+ for (const obstacle of srj.obstacles ?? []) {
1817
+ if (obstacle.type === "rect" || obstacle.type === "oval") {
1818
+ rects.push({
1819
+ center: { x: obstacle.center.x, y: obstacle.center.y },
1820
+ width: obstacle.width,
1821
+ height: obstacle.height,
1822
+ fill: "#fee2e2",
1823
+ stroke: "#ef4444",
1824
+ layer: "obstacle",
1825
+ label: "obstacle"
1826
+ });
1827
+ }
1618
1828
  }
1619
- if (this.lastCandidateEdge) {
1620
- graphics.lines.push({
1621
- points: [this.lastCandidateEdge.start, this.lastCandidateEdge.end],
1622
- strokeColor: "blue"
1623
- });
1829
+ if (this.boardVoidRects) {
1830
+ let outlineBBox = null;
1831
+ if (srj.outline && srj.outline.length > 0) {
1832
+ const xs = srj.outline.map((p) => p.x);
1833
+ const ys = srj.outline.map((p) => p.y);
1834
+ const minX = Math.min(...xs);
1835
+ const minY = Math.min(...ys);
1836
+ outlineBBox = {
1837
+ x: minX,
1838
+ y: minY,
1839
+ width: Math.max(...xs) - minX,
1840
+ height: Math.max(...ys) - minY
1841
+ };
1842
+ }
1843
+ for (const r of this.boardVoidRects) {
1844
+ if (outlineBBox && !overlaps(r, outlineBBox)) {
1845
+ continue;
1846
+ }
1847
+ rects.push({
1848
+ center: { x: r.x + r.width / 2, y: r.y + r.height / 2 },
1849
+ width: r.width,
1850
+ height: r.height,
1851
+ fill: "rgba(0, 0, 0, 0.5)",
1852
+ stroke: "none",
1853
+ label: "void"
1854
+ });
1855
+ }
1624
1856
  }
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"
1857
+ if (this.candidates?.length) {
1858
+ for (const cand of this.candidates) {
1859
+ points.push({
1860
+ x: cand.x,
1861
+ y: cand.y,
1862
+ fill: "#9333ea",
1863
+ stroke: "#6b21a8",
1864
+ label: `z:${cand.z}`
1635
1865
  });
1636
1866
  }
1637
1867
  }
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"
1868
+ if (this.placed?.length) {
1869
+ for (const placement of this.placed) {
1870
+ const colors = this.getColorForZLayer(placement.zLayers);
1871
+ rects.push({
1872
+ center: {
1873
+ x: placement.rect.x + placement.rect.width / 2,
1874
+ y: placement.rect.y + placement.rect.height / 2
1875
+ },
1876
+ width: placement.rect.width,
1877
+ height: placement.rect.height,
1878
+ fill: colors.fill,
1879
+ stroke: colors.stroke,
1880
+ layer: `z${placement.zLayers.join(",")}`,
1881
+ label: `free
1882
+ z:${placement.zLayers.join(",")}`
1648
1883
  });
1649
1884
  }
1650
1885
  }
1651
- return graphics;
1886
+ return {
1887
+ title: "RectDiff Grid",
1888
+ coordinateSystem: "cartesian",
1889
+ rects,
1890
+ points,
1891
+ lines
1892
+ };
1652
1893
  }
1653
1894
  };
1654
1895
 
1655
- // lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts
1656
- import { BaseSolver as BaseSolver3 } from "@tscircuit/solver-utils";
1657
- import RBush from "rbush";
1896
+ // lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts
1897
+ import { BaseSolver as BaseSolver4 } from "@tscircuit/solver-utils";
1658
1898
 
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
- };
1899
+ // lib/utils/finalizeRects.ts
1900
+ function finalizeRects(params) {
1901
+ const out = params.placed.map((p) => ({
1902
+ minX: p.rect.x,
1903
+ minY: p.rect.y,
1904
+ maxX: p.rect.x + p.rect.width,
1905
+ maxY: p.rect.y + p.rect.height,
1906
+ zLayers: [...p.zLayers].sort((a, b) => a - b)
1907
+ }));
1908
+ const { zIndexByName } = buildZIndexMap(params.srj);
1909
+ const layersByKey = /* @__PURE__ */ new Map();
1910
+ for (const obstacle of params.srj.obstacles ?? []) {
1911
+ const rect = obstacleToXYRect(obstacle);
1912
+ if (!rect) continue;
1913
+ const zLayers = obstacle.zLayers?.length && obstacle.zLayers.length > 0 ? obstacle.zLayers : obstacleZs(obstacle, zIndexByName);
1914
+ const key = `${rect.x}:${rect.y}:${rect.width}:${rect.height}`;
1915
+ let entry = layersByKey.get(key);
1916
+ if (!entry) {
1917
+ entry = { rect, layers: /* @__PURE__ */ new Set() };
1918
+ layersByKey.set(key, entry);
1919
+ }
1920
+ zLayers.forEach((layer) => entry.layers.add(layer));
1921
+ }
1922
+ for (const { rect, layers } of layersByKey.values()) {
1923
+ out.push({
1924
+ minX: rect.x,
1925
+ minY: rect.y,
1926
+ maxX: rect.x + rect.width,
1927
+ maxY: rect.y + rect.height,
1928
+ zLayers: Array.from(layers).sort((a, b) => a - b),
1929
+ isObstacle: true
1930
+ });
1931
+ }
1932
+ return out;
1933
+ }
1668
1934
 
1669
- // lib/solvers/GapFillSolver/ExpandEdgesToEmptySpaceSolver.ts
1670
- import { segmentToBoxMinDistance } from "@tscircuit/math-utils";
1671
- var EPS4 = 1e-4;
1672
- var ExpandEdgesToEmptySpaceSolver = class extends BaseSolver3 {
1935
+ // lib/solvers/RectDiffExpansionSolver/rectsToMeshNodes.ts
1936
+ function rectsToMeshNodes(rects) {
1937
+ let id = 0;
1938
+ const out = [];
1939
+ for (const r of rects) {
1940
+ const w = Math.max(0, r.maxX - r.minX);
1941
+ const h = Math.max(0, r.maxY - r.minY);
1942
+ if (w <= 0 || h <= 0 || r.zLayers.length === 0) continue;
1943
+ out.push({
1944
+ capacityMeshNodeId: `cmn_${id++}`,
1945
+ center: { x: (r.minX + r.maxX) / 2, y: (r.minY + r.maxY) / 2 },
1946
+ width: w,
1947
+ height: h,
1948
+ layer: "top",
1949
+ availableZ: r.zLayers.slice(),
1950
+ _containsObstacle: r.isObstacle,
1951
+ _containsTarget: r.isObstacle
1952
+ });
1953
+ }
1954
+ return out;
1955
+ }
1956
+
1957
+ // lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts
1958
+ import RBush4 from "rbush";
1959
+
1960
+ // lib/utils/rectToTree.ts
1961
+ var rectToTree = (rect) => ({
1962
+ ...rect,
1963
+ minX: rect.x,
1964
+ minY: rect.y,
1965
+ maxX: rect.x + rect.width,
1966
+ maxY: rect.y + rect.height
1967
+ });
1968
+
1969
+ // lib/utils/sameTreeRect.ts
1970
+ var sameTreeRect = (a, b) => a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
1971
+
1972
+ // lib/solvers/RectDiffExpansionSolver/RectDiffExpansionSolver.ts
1973
+ var RectDiffExpansionSolver = class extends BaseSolver4 {
1673
1974
  constructor(input) {
1674
1975
  super();
1675
1976
  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
- );
1977
+ Object.assign(this, this.input.initialSnapshot);
1687
1978
  }
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
1979
+ // Engine fields (same shape used by rectdiff/engine.ts)
1980
+ srj;
1981
+ layerNames;
1982
+ layerCount;
1983
+ bounds;
1984
+ options;
1985
+ boardVoidRects;
1986
+ gridIndex;
1987
+ candidates;
1988
+ placed;
1989
+ placedIndexByLayer;
1990
+ expansionIndex;
1991
+ edgeAnalysisDone;
1992
+ totalSeedsThisGrid;
1993
+ consumedSeedsThisGrid;
1994
+ _meshNodes = [];
1995
+ _setup() {
1996
+ this.stats = {
1997
+ gridIndex: this.gridIndex
1723
1998
  };
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
1999
+ if (this.input.obstacleIndexByLayer) {
2000
+ } else {
2001
+ const { zIndexByName } = buildZIndexMap(this.srj);
2002
+ this.input.obstacleIndexByLayer = Array.from(
2003
+ { length: this.layerCount },
2004
+ () => new RBush4()
1742
2005
  );
1743
- searchDistance *= 4;
1744
- }
1745
- if (!collidingNodes || collidingNodes.length === 0) {
1746
- return;
1747
- }
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;
2006
+ const insertObstacle = (rect, z) => {
2007
+ const tree = this.input.obstacleIndexByLayer[z];
2008
+ if (tree) tree.insert(rectToTree(rect));
2009
+ };
2010
+ for (const voidRect of this.boardVoidRects ?? []) {
2011
+ for (let z = 0; z < this.layerCount; z++) insertObstacle(voidRect, z);
1754
2012
  }
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
2013
+ for (const obstacle of this.srj.obstacles ?? []) {
2014
+ const rect = obstacleToXYRect(obstacle);
2015
+ if (!rect) continue;
2016
+ const zLayers = obstacle.zLayers?.length && obstacle.zLayers.length > 0 ? obstacle.zLayers : obstacleZs(obstacle, zIndexByName);
2017
+ zLayers.forEach((z) => {
2018
+ if (z >= 0 && z < this.layerCount) insertObstacle(rect, z);
2019
+ });
1767
2020
  }
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
2021
+ }
2022
+ this.placedIndexByLayer = Array.from(
2023
+ { length: this.layerCount },
2024
+ () => new RBush4()
2025
+ );
2026
+ for (const placement of this.placed ?? []) {
2027
+ for (const z of placement.zLayers) {
2028
+ const tree = this.placedIndexByLayer[z];
2029
+ if (tree) tree.insert(rectToTree(placement.rect));
1784
2030
  }
1785
- };
1786
- this.lastExpandedSegment = expandedSegment;
1787
- if (nodeWidth < EPS4 || nodeHeight < EPS4) {
1788
- return;
1789
2031
  }
1790
- this.expandedSegments.push(expandedSegment);
1791
- this.rectSpatialIndex.insert({
1792
- ...expandedSegment.newNode,
1793
- ...nodeBounds
1794
- });
1795
2032
  }
1796
- getOutput() {
1797
- return {
1798
- expandedSegments: this.expandedSegments
1799
- };
2033
+ _step() {
2034
+ if (this.solved) return;
2035
+ this._stepExpansion();
2036
+ this.stats.gridIndex = this.gridIndex;
2037
+ this.stats.placed = this.placed.length;
2038
+ if (this.expansionIndex >= this.placed.length) {
2039
+ this.finalizeIfNeeded();
2040
+ }
1800
2041
  }
1801
- 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
- });
2042
+ _stepExpansion() {
2043
+ if (this.expansionIndex >= this.placed.length) {
2044
+ return;
1824
2045
  }
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
- });
2046
+ const idx = this.expansionIndex;
2047
+ const p = this.placed[idx];
2048
+ const lastGrid = this.options.gridSizes[this.options.gridSizes.length - 1];
2049
+ const hardPlacedByLayer = allLayerNode({
2050
+ layerCount: this.layerCount,
2051
+ placed: this.placed
2052
+ });
2053
+ const hardBlockers = [];
2054
+ for (const z of p.zLayers) {
2055
+ const obstacleTree = this.input.obstacleIndexByLayer[z];
2056
+ if (obstacleTree) hardBlockers.push(...obstacleTree.all());
2057
+ hardBlockers.push(...hardPlacedByLayer[z] ?? []);
2058
+ }
2059
+ const oldRect = p.rect;
2060
+ const expanded = expandRectFromSeed({
2061
+ startX: p.rect.x + p.rect.width / 2,
2062
+ startY: p.rect.y + p.rect.height / 2,
2063
+ gridSize: lastGrid,
2064
+ bounds: this.bounds,
2065
+ blockers: hardBlockers,
2066
+ initialCellRatio: 0,
2067
+ maxAspectRatio: null,
2068
+ minReq: { width: p.rect.width, height: p.rect.height }
2069
+ });
2070
+ if (expanded) {
2071
+ this.placed[idx] = { rect: expanded, zLayers: p.zLayers };
2072
+ for (const z of p.zLayers) {
2073
+ const tree = this.placedIndexByLayer[z];
2074
+ if (tree) {
2075
+ tree.remove(rectToTree(oldRect), sameTreeRect);
2076
+ tree.insert(rectToTree(expanded));
2077
+ }
2078
+ }
2079
+ resizeSoftOverlaps(
2080
+ {
2081
+ layerCount: this.layerCount,
2082
+ placed: this.placed,
2083
+ options: this.options,
2084
+ placedIndexByLayer: this.placedIndexByLayer
2085
+ },
2086
+ idx
2087
+ );
1834
2088
  }
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
- });
2089
+ this.expansionIndex += 1;
2090
+ }
2091
+ finalizeIfNeeded() {
2092
+ if (this.solved) return;
2093
+ const rects = finalizeRects({
2094
+ placed: this.placed,
2095
+ srj: this.srj,
2096
+ boardVoidRects: this.boardVoidRects
2097
+ });
2098
+ this._meshNodes = rectsToMeshNodes(rects);
2099
+ this.solved = true;
2100
+ }
2101
+ computeProgress() {
2102
+ if (this.solved) return 1;
2103
+ const grids = this.options.gridSizes.length;
2104
+ const base = grids / (grids + 1);
2105
+ const denom = Math.max(1, this.placed.length);
2106
+ const frac = denom ? this.expansionIndex / denom : 1;
2107
+ return Math.min(0.999, base + frac * (1 / (grids + 1)));
2108
+ }
2109
+ getOutput() {
2110
+ if (!this.solved && this._meshNodes.length === 0) {
2111
+ this.finalizeIfNeeded();
1840
2112
  }
1841
- if (this.lastSearchBounds) {
1842
- graphics.rects.push({
2113
+ return { meshNodes: this._meshNodes };
2114
+ }
2115
+ /** Simple visualization of expanded placements. */
2116
+ visualize() {
2117
+ const rects = [];
2118
+ for (const placement of this.placed ?? []) {
2119
+ rects.push({
1843
2120
  center: {
1844
- x: (this.lastSearchBounds.minX + this.lastSearchBounds.maxX) / 2,
1845
- y: (this.lastSearchBounds.minY + this.lastSearchBounds.maxY) / 2
2121
+ x: placement.rect.x + placement.rect.width / 2,
2122
+ y: placement.rect.y + placement.rect.height / 2
1846
2123
  },
1847
- width: this.lastSearchBounds.maxX - this.lastSearchBounds.minX,
1848
- height: this.lastSearchBounds.maxY - this.lastSearchBounds.minY,
1849
- fill: "rgba(0, 0, 255, 0.25)"
2124
+ width: placement.rect.width,
2125
+ height: placement.rect.height,
2126
+ stroke: "rgba(37, 99, 235, 0.9)",
2127
+ fill: "rgba(191, 219, 254, 0.5)",
2128
+ layer: `z${placement.zLayers.join(",")}`,
2129
+ label: `expanded
2130
+ z:${placement.zLayers.join(",")}`
1850
2131
  });
1851
2132
  }
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
- });
2133
+ return {
2134
+ title: "RectDiff Expansion",
2135
+ coordinateSystem: "cartesian",
2136
+ rects,
2137
+ points: [],
2138
+ lines: []
2139
+ };
2140
+ }
2141
+ };
2142
+
2143
+ // lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts
2144
+ import "rbush";
2145
+
2146
+ // lib/solvers/RectDiffGridSolverPipeline/buildObstacleIndexes.ts
2147
+ import RBush5 from "rbush";
2148
+ var buildObstacleIndexes = (srj) => {
2149
+ const { layerNames, zIndexByName } = buildZIndexMap(srj);
2150
+ const layerCount = Math.max(1, layerNames.length, srj.layerCount || 1);
2151
+ const bounds = {
2152
+ x: srj.bounds.minX,
2153
+ y: srj.bounds.minY,
2154
+ width: srj.bounds.maxX - srj.bounds.minX,
2155
+ height: srj.bounds.maxY - srj.bounds.minY
2156
+ };
2157
+ const obstacleIndexByLayer = Array.from(
2158
+ { length: layerCount },
2159
+ () => new RBush5()
2160
+ );
2161
+ const insertObstacle = (rect, z) => {
2162
+ const treeRect = {
2163
+ ...rect,
2164
+ minX: rect.x,
2165
+ minY: rect.y,
2166
+ maxX: rect.x + rect.width,
2167
+ maxY: rect.y + rect.height
2168
+ };
2169
+ obstacleIndexByLayer[z]?.insert(treeRect);
2170
+ };
2171
+ let boardVoidRects = [];
2172
+ if (srj.outline && srj.outline.length > 2) {
2173
+ boardVoidRects = computeInverseRects(bounds, srj.outline);
2174
+ for (const voidRect of boardVoidRects) {
2175
+ for (let z = 0; z < layerCount; z++) insertObstacle(voidRect, z);
1865
2176
  }
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})`
1873
- });
2177
+ }
2178
+ for (const obstacle of srj.obstacles ?? []) {
2179
+ const rect = obstacleToXYRect(obstacle);
2180
+ if (!rect) continue;
2181
+ const zLayers = obstacleZs(obstacle, zIndexByName);
2182
+ const invalidZs = zLayers.filter((z) => z < 0 || z >= layerCount);
2183
+ if (invalidZs.length) {
2184
+ throw new Error(
2185
+ `RectDiff: obstacle uses z-layer indices ${invalidZs.join(",")} outside 0-${layerCount - 1}`
2186
+ );
1874
2187
  }
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
- }
2188
+ if ((!obstacle.zLayers || obstacle.zLayers.length === 0) && zLayers.length) {
2189
+ obstacle.zLayers = zLayers;
1884
2190
  }
1885
- return graphics;
2191
+ for (const z of zLayers) insertObstacle(rect, z);
1886
2192
  }
2193
+ return { obstacleIndexByLayer, boardVoidRects };
1887
2194
  };
1888
2195
 
1889
- // lib/solvers/GapFillSolver/GapFillSolverPipeline.ts
1890
- var GapFillSolverPipeline = class extends BasePipelineSolver2 {
1891
- findSegmentsWithAdjacentEmptySpaceSolver;
1892
- expandEdgesToEmptySpaceSolver;
2196
+ // lib/solvers/RectDiffGridSolverPipeline/RectDiffGridSolverPipeline.ts
2197
+ var RectDiffGridSolverPipeline = class extends BasePipelineSolver2 {
2198
+ rectDiffSeedingSolver;
2199
+ rectDiffExpansionSolver;
2200
+ boardVoidRects;
2201
+ obstacleIndexByLayer;
2202
+ constructor(inputProblem) {
2203
+ super(inputProblem);
2204
+ const { obstacleIndexByLayer, boardVoidRects } = buildObstacleIndexes(
2205
+ inputProblem.simpleRouteJson
2206
+ );
2207
+ this.obstacleIndexByLayer = obstacleIndexByLayer;
2208
+ this.boardVoidRects = boardVoidRects;
2209
+ }
1893
2210
  pipelineDef = [
1894
- definePipelineStep(
1895
- "findSegmentsWithAdjacentEmptySpaceSolver",
1896
- FindSegmentsWithAdjacentEmptySpaceSolver,
1897
- (gapFillPipeline) => [
2211
+ definePipelineStep2(
2212
+ "rectDiffSeedingSolver",
2213
+ RectDiffSeedingSolver,
2214
+ (pipeline) => [
1898
2215
  {
1899
- meshNodes: gapFillPipeline.inputProblem.meshNodes
1900
- }
1901
- ],
1902
- {
1903
- onSolved: () => {
2216
+ simpleRouteJson: pipeline.inputProblem.simpleRouteJson,
2217
+ gridOptions: pipeline.inputProblem.gridOptions,
2218
+ obstacleIndexByLayer: pipeline.obstacleIndexByLayer,
2219
+ boardVoidRects: pipeline.boardVoidRects
1904
2220
  }
1905
- }
2221
+ ]
1906
2222
  ),
1907
- definePipelineStep(
1908
- "expandEdgesToEmptySpaceSolver",
1909
- ExpandEdgesToEmptySpaceSolver,
1910
- (gapFillPipeline) => [
2223
+ definePipelineStep2(
2224
+ "rectDiffExpansionSolver",
2225
+ RectDiffExpansionSolver,
2226
+ (pipeline) => [
1911
2227
  {
1912
- inputMeshNodes: gapFillPipeline.inputProblem.meshNodes,
1913
- segmentsWithAdjacentEmptySpace: gapFillPipeline.findSegmentsWithAdjacentEmptySpaceSolver.getOutput().segmentsWithAdjacentEmptySpace
1914
- }
1915
- ],
1916
- {
1917
- onSolved: () => {
2228
+ initialSnapshot: {
2229
+ ...pipeline.rectDiffSeedingSolver.getOutput(),
2230
+ boardVoidRects: pipeline.boardVoidRects ?? []
2231
+ },
2232
+ obstacleIndexByLayer: pipeline.obstacleIndexByLayer
1918
2233
  }
1919
- }
2234
+ ]
1920
2235
  )
1921
2236
  ];
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
- };
2237
+ getConstructorParams() {
2238
+ return [this.inputProblem];
1928
2239
  }
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
- });
2240
+ getOutput() {
2241
+ if (this.rectDiffExpansionSolver) {
2242
+ return this.rectDiffExpansionSolver.getOutput();
2243
+ }
2244
+ if (this.rectDiffSeedingSolver) {
2245
+ const snapshot = this.rectDiffSeedingSolver.getOutput();
2246
+ const meshNodes = snapshot.placed.map(
2247
+ (placement, idx) => ({
2248
+ capacityMeshNodeId: `grid-${idx}`,
2249
+ center: {
2250
+ x: placement.rect.x + placement.rect.width / 2,
2251
+ y: placement.rect.y + placement.rect.height / 2
2252
+ },
2253
+ width: placement.rect.width,
2254
+ height: placement.rect.height,
2255
+ availableZ: placement.zLayers,
2256
+ layer: `z${placement.zLayers.join(",")}`
2257
+ })
2258
+ );
2259
+ return { meshNodes };
1953
2260
  }
1954
- return graphics;
2261
+ return { meshNodes: [] };
1955
2262
  }
1956
- finalVisualize() {
1957
- const graphics = {
1958
- title: "GapFillSolverPipeline - Final",
2263
+ visualize() {
2264
+ if (this.rectDiffExpansionSolver) {
2265
+ return this.rectDiffExpansionSolver.visualize();
2266
+ }
2267
+ if (this.rectDiffSeedingSolver) {
2268
+ return this.rectDiffSeedingSolver.visualize();
2269
+ }
2270
+ return {
2271
+ title: "RectDiff Grid Pipeline",
1959
2272
  coordinateSystem: "cartesian",
1960
2273
  rects: [],
1961
2274
  points: [],
1962
- lines: [],
1963
- circles: [],
1964
- arrows: [],
1965
- texts: []
2275
+ lines: []
1966
2276
  };
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")
2277
+ }
2278
+ };
2279
+
2280
+ // lib/rectdiff-visualization.ts
2281
+ function createBaseVisualization(srj, title = "RectDiff") {
2282
+ const rects = [];
2283
+ const lines = [];
2284
+ const boardBounds = {
2285
+ minX: srj.bounds.minX,
2286
+ maxX: srj.bounds.maxX,
2287
+ minY: srj.bounds.minY,
2288
+ maxY: srj.bounds.maxY
2289
+ };
2290
+ if (srj.outline && srj.outline.length > 1) {
2291
+ lines.push({
2292
+ points: [...srj.outline, srj.outline[0]],
2293
+ strokeColor: "#111827",
2294
+ strokeWidth: 0.01,
2295
+ label: "outline"
2296
+ });
2297
+ } else {
2298
+ rects.push({
2299
+ center: {
2300
+ x: (boardBounds.minX + boardBounds.maxX) / 2,
2301
+ y: (boardBounds.minY + boardBounds.maxY) / 2
2302
+ },
2303
+ width: boardBounds.maxX - boardBounds.minX,
2304
+ height: boardBounds.maxY - boardBounds.minY,
2305
+ fill: "none",
2306
+ stroke: "#111827",
2307
+ label: "board"
2308
+ });
2309
+ }
2310
+ for (const obstacle of srj.obstacles ?? []) {
2311
+ if (obstacle.type === "rect" || obstacle.type === "oval") {
2312
+ rects.push({
2313
+ center: { x: obstacle.center.x, y: obstacle.center.y },
2314
+ width: obstacle.width,
2315
+ height: obstacle.height,
2316
+ fill: "#fee2e2",
2317
+ stroke: "#ef4444",
2318
+ layer: "obstacle",
2319
+ label: "obstacle"
1985
2320
  });
1986
2321
  }
1987
- return graphics;
1988
2322
  }
1989
- };
2323
+ return {
2324
+ title,
2325
+ coordinateSystem: "cartesian",
2326
+ rects,
2327
+ points: [],
2328
+ lines
2329
+ };
2330
+ }
1990
2331
 
1991
2332
  // lib/RectDiffPipeline.ts
1992
2333
  var RectDiffPipeline = class extends BasePipelineSolver3 {
1993
- rectDiffSolver;
2334
+ rectDiffGridSolverPipeline;
1994
2335
  gapFillSolver;
1995
2336
  pipelineDef = [
1996
- definePipelineStep2(
1997
- "rectDiffSolver",
1998
- RectDiffSolver,
2337
+ definePipelineStep3(
2338
+ "rectDiffGridSolverPipeline",
2339
+ RectDiffGridSolverPipeline,
1999
2340
  (rectDiffPipeline) => [
2000
2341
  {
2001
2342
  simpleRouteJson: rectDiffPipeline.inputProblem.simpleRouteJson,
2002
2343
  gridOptions: rectDiffPipeline.inputProblem.gridOptions
2003
2344
  }
2004
- ],
2005
- {
2006
- onSolved: () => {
2007
- }
2008
- }
2345
+ ]
2009
2346
  ),
2010
- definePipelineStep2(
2347
+ definePipelineStep3(
2011
2348
  "gapFillSolver",
2012
2349
  GapFillSolverPipeline,
2013
2350
  (rectDiffPipeline) => [
2014
2351
  {
2015
- meshNodes: rectDiffPipeline.rectDiffSolver?.getOutput().meshNodes ?? []
2352
+ meshNodes: rectDiffPipeline.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? []
2016
2353
  }
2017
2354
  ]
2018
2355
  )
@@ -2025,7 +2362,10 @@ var RectDiffPipeline = class extends BasePipelineSolver3 {
2025
2362
  if (gapFillOutput) {
2026
2363
  return { meshNodes: gapFillOutput.outputNodes };
2027
2364
  }
2028
- return this.rectDiffSolver.getOutput();
2365
+ if (this.rectDiffGridSolverPipeline) {
2366
+ return this.rectDiffGridSolverPipeline.getOutput();
2367
+ }
2368
+ return { meshNodes: [] };
2029
2369
  }
2030
2370
  initialVisualize() {
2031
2371
  console.log("RectDiffPipeline - initialVisualize");
@@ -2033,7 +2373,7 @@ var RectDiffPipeline = class extends BasePipelineSolver3 {
2033
2373
  this.inputProblem.simpleRouteJson,
2034
2374
  "RectDiffPipeline - Initial"
2035
2375
  );
2036
- const initialNodes = this.rectDiffSolver?.getOutput().meshNodes ?? [];
2376
+ const initialNodes = this.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? [];
2037
2377
  for (const node of initialNodes) {
2038
2378
  graphics.rects.push({
2039
2379
  center: node.center,
@@ -2057,7 +2397,7 @@ var RectDiffPipeline = class extends BasePipelineSolver3 {
2057
2397
  );
2058
2398
  const { meshNodes: outputNodes } = this.getOutput();
2059
2399
  const initialNodeIds = new Set(
2060
- (this.rectDiffSolver?.getOutput().meshNodes ?? []).map(
2400
+ (this.rectDiffGridSolverPipeline?.getOutput().meshNodes ?? []).map(
2061
2401
  (n) => n.capacityMeshNodeId
2062
2402
  )
2063
2403
  );