@tscircuit/rectdiff 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +9 -0
- package/.github/workflows/bun-formatcheck.yml +26 -0
- package/.github/workflows/bun-pver-release.yml +71 -0
- package/.github/workflows/bun-test.yml +31 -0
- package/.github/workflows/bun-typecheck.yml +26 -0
- package/CLAUDE.md +23 -0
- package/README.md +5 -0
- package/biome.json +93 -0
- package/bun.lock +29 -0
- package/bunfig.toml +5 -0
- package/components/SolverDebugger3d.tsx +833 -0
- package/cosmos.config.json +6 -0
- package/cosmos.decorator.tsx +21 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +921 -0
- package/experiments/rect-fill-2d.tsx +983 -0
- package/experiments/rect3d_visualizer.html +640 -0
- package/global.d.ts +4 -0
- package/index.html +12 -0
- package/lib/index.ts +1 -0
- package/lib/solvers/RectDiffSolver.ts +158 -0
- package/lib/solvers/rectdiff/candidates.ts +397 -0
- package/lib/solvers/rectdiff/engine.ts +355 -0
- package/lib/solvers/rectdiff/geometry.ts +284 -0
- package/lib/solvers/rectdiff/layers.ts +48 -0
- package/lib/solvers/rectdiff/rectsToMeshNodes.ts +22 -0
- package/lib/solvers/rectdiff/types.ts +63 -0
- package/lib/types/capacity-mesh-types.ts +33 -0
- package/lib/types/srj-types.ts +37 -0
- package/package.json +33 -0
- package/pages/example01.page.tsx +11 -0
- package/test-assets/example01.json +933 -0
- package/tests/__snapshots__/svg.snap.svg +3 -0
- package/tests/examples/__snapshots__/example01.snap.svg +121 -0
- package/tests/examples/example01.test.tsx +65 -0
- package/tests/fixtures/preload.ts +1 -0
- package/tests/incremental-solver.test.ts +100 -0
- package/tests/rect-diff-solver.test.ts +154 -0
- package/tests/svg.test.ts +12 -0
- package/tsconfig.json +30 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
// lib/solvers/RectDiffSolver.ts
|
|
2
|
+
import { BaseSolver } from "@tscircuit/solver-utils";
|
|
3
|
+
|
|
4
|
+
// lib/solvers/rectdiff/geometry.ts
|
|
5
|
+
var EPS = 1e-9;
|
|
6
|
+
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
7
|
+
var gt = (a, b) => a > b + EPS;
|
|
8
|
+
var gte = (a, b) => a > b - EPS;
|
|
9
|
+
var lt = (a, b) => a < b - EPS;
|
|
10
|
+
var lte = (a, b) => a < b + EPS;
|
|
11
|
+
function overlaps(a, b) {
|
|
12
|
+
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);
|
|
13
|
+
}
|
|
14
|
+
function containsPoint(r, x, y) {
|
|
15
|
+
return x >= r.x - EPS && x <= r.x + r.width + EPS && y >= r.y - EPS && y <= r.y + r.height + EPS;
|
|
16
|
+
}
|
|
17
|
+
function distancePointToRectEdges(px, py, r) {
|
|
18
|
+
const edges = [
|
|
19
|
+
[r.x, r.y, r.x + r.width, r.y],
|
|
20
|
+
[r.x + r.width, r.y, r.x + r.width, r.y + r.height],
|
|
21
|
+
[r.x + r.width, r.y + r.height, r.x, r.y + r.height],
|
|
22
|
+
[r.x, r.y + r.height, r.x, r.y]
|
|
23
|
+
];
|
|
24
|
+
let best = Infinity;
|
|
25
|
+
for (const [x1, y1, x2, y2] of edges) {
|
|
26
|
+
const A = px - x1, B = py - y1, C = x2 - x1, D = y2 - y1;
|
|
27
|
+
const dot = A * C + B * D;
|
|
28
|
+
const lenSq = C * C + D * D;
|
|
29
|
+
let t = lenSq !== 0 ? dot / lenSq : 0;
|
|
30
|
+
t = clamp(t, 0, 1);
|
|
31
|
+
const xx = x1 + t * C;
|
|
32
|
+
const yy = y1 + t * D;
|
|
33
|
+
best = Math.min(best, Math.hypot(px - xx, py - yy));
|
|
34
|
+
}
|
|
35
|
+
return best;
|
|
36
|
+
}
|
|
37
|
+
function maxExpandRight(r, bounds, blockers, maxAspect) {
|
|
38
|
+
let maxWidth = bounds.x + bounds.width - r.x;
|
|
39
|
+
for (const b of blockers) {
|
|
40
|
+
const verticallyOverlaps = r.y + r.height > b.y + EPS && b.y + b.height > r.y + EPS;
|
|
41
|
+
if (verticallyOverlaps) {
|
|
42
|
+
if (gte(b.x, r.x + r.width)) {
|
|
43
|
+
maxWidth = Math.min(maxWidth, b.x - r.x);
|
|
44
|
+
} else if (b.x + b.width > r.x + r.width - EPS && b.x < r.x + r.width + EPS) {
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let e = Math.max(0, maxWidth - r.width);
|
|
50
|
+
if (e <= 0) return 0;
|
|
51
|
+
if (maxAspect != null) {
|
|
52
|
+
const w = r.width, h = r.height;
|
|
53
|
+
if (w >= h) e = Math.min(e, maxAspect * h - w);
|
|
54
|
+
}
|
|
55
|
+
return Math.max(0, e);
|
|
56
|
+
}
|
|
57
|
+
function maxExpandDown(r, bounds, blockers, maxAspect) {
|
|
58
|
+
let maxHeight = bounds.y + bounds.height - r.y;
|
|
59
|
+
for (const b of blockers) {
|
|
60
|
+
const horizOverlaps = r.x + r.width > b.x + EPS && b.x + b.width > r.x + EPS;
|
|
61
|
+
if (horizOverlaps) {
|
|
62
|
+
if (gte(b.y, r.y + r.height)) {
|
|
63
|
+
maxHeight = Math.min(maxHeight, b.y - r.y);
|
|
64
|
+
} else if (b.y + b.height > r.y + r.height - EPS && b.y < r.y + r.height + EPS) {
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
let e = Math.max(0, maxHeight - r.height);
|
|
70
|
+
if (e <= 0) return 0;
|
|
71
|
+
if (maxAspect != null) {
|
|
72
|
+
const w = r.width, h = r.height;
|
|
73
|
+
if (h >= w) e = Math.min(e, maxAspect * w - h);
|
|
74
|
+
}
|
|
75
|
+
return Math.max(0, e);
|
|
76
|
+
}
|
|
77
|
+
function maxExpandLeft(r, bounds, blockers, maxAspect) {
|
|
78
|
+
let minX = bounds.x;
|
|
79
|
+
for (const b of blockers) {
|
|
80
|
+
const verticallyOverlaps = r.y + r.height > b.y + EPS && b.y + b.height > r.y + EPS;
|
|
81
|
+
if (verticallyOverlaps) {
|
|
82
|
+
if (lte(b.x + b.width, r.x)) {
|
|
83
|
+
minX = Math.max(minX, b.x + b.width);
|
|
84
|
+
} else if (b.x < r.x + EPS && b.x + b.width > r.x - EPS) {
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
let e = Math.max(0, r.x - minX);
|
|
90
|
+
if (e <= 0) return 0;
|
|
91
|
+
if (maxAspect != null) {
|
|
92
|
+
const w = r.width, h = r.height;
|
|
93
|
+
if (w >= h) e = Math.min(e, maxAspect * h - w);
|
|
94
|
+
}
|
|
95
|
+
return Math.max(0, e);
|
|
96
|
+
}
|
|
97
|
+
function maxExpandUp(r, bounds, blockers, maxAspect) {
|
|
98
|
+
let minY = bounds.y;
|
|
99
|
+
for (const b of blockers) {
|
|
100
|
+
const horizOverlaps = r.x + r.width > b.x + EPS && b.x + b.width > r.x + EPS;
|
|
101
|
+
if (horizOverlaps) {
|
|
102
|
+
if (lte(b.y + b.height, r.y)) {
|
|
103
|
+
minY = Math.max(minY, b.y + b.height);
|
|
104
|
+
} else if (b.y < r.y + EPS && b.y + b.height > r.y - EPS) {
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
let e = Math.max(0, r.y - minY);
|
|
110
|
+
if (e <= 0) return 0;
|
|
111
|
+
if (maxAspect != null) {
|
|
112
|
+
const w = r.width, h = r.height;
|
|
113
|
+
if (h >= w) e = Math.min(e, maxAspect * w - h);
|
|
114
|
+
}
|
|
115
|
+
return Math.max(0, e);
|
|
116
|
+
}
|
|
117
|
+
function expandRectFromSeed(startX, startY, gridSize, bounds, blockers, initialCellRatio, maxAspectRatio, minReq) {
|
|
118
|
+
const minSide = Math.max(1e-9, gridSize * initialCellRatio);
|
|
119
|
+
const initialW = Math.max(minSide, minReq.width);
|
|
120
|
+
const initialH = Math.max(minSide, minReq.height);
|
|
121
|
+
const strategies = [
|
|
122
|
+
{ ox: 0, oy: 0 },
|
|
123
|
+
{ ox: -initialW, oy: 0 },
|
|
124
|
+
{ ox: 0, oy: -initialH },
|
|
125
|
+
{ ox: -initialW, oy: -initialH },
|
|
126
|
+
{ ox: -initialW / 2, oy: -initialH / 2 }
|
|
127
|
+
];
|
|
128
|
+
let best = null;
|
|
129
|
+
let bestArea = 0;
|
|
130
|
+
STRATS: for (const s of strategies) {
|
|
131
|
+
let r = { x: startX + s.ox, y: startY + s.oy, width: initialW, height: initialH };
|
|
132
|
+
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)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
for (const b of blockers) if (overlaps(r, b)) continue STRATS;
|
|
136
|
+
let improved = true;
|
|
137
|
+
while (improved) {
|
|
138
|
+
improved = false;
|
|
139
|
+
const eR = maxExpandRight(r, bounds, blockers, maxAspectRatio);
|
|
140
|
+
if (eR > 0) {
|
|
141
|
+
r = { ...r, width: r.width + eR };
|
|
142
|
+
improved = true;
|
|
143
|
+
}
|
|
144
|
+
const eD = maxExpandDown(r, bounds, blockers, maxAspectRatio);
|
|
145
|
+
if (eD > 0) {
|
|
146
|
+
r = { ...r, height: r.height + eD };
|
|
147
|
+
improved = true;
|
|
148
|
+
}
|
|
149
|
+
const eL = maxExpandLeft(r, bounds, blockers, maxAspectRatio);
|
|
150
|
+
if (eL > 0) {
|
|
151
|
+
r = { x: r.x - eL, y: r.y, width: r.width + eL, height: r.height };
|
|
152
|
+
improved = true;
|
|
153
|
+
}
|
|
154
|
+
const eU = maxExpandUp(r, bounds, blockers, maxAspectRatio);
|
|
155
|
+
if (eU > 0) {
|
|
156
|
+
r = { x: r.x, y: r.y - eU, width: r.width, height: r.height + eU };
|
|
157
|
+
improved = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (r.width + EPS >= minReq.width && r.height + EPS >= minReq.height) {
|
|
161
|
+
const area = r.width * r.height;
|
|
162
|
+
if (area > bestArea) {
|
|
163
|
+
best = r;
|
|
164
|
+
bestArea = area;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return best;
|
|
169
|
+
}
|
|
170
|
+
function intersect1D(a0, a1, b0, b1) {
|
|
171
|
+
const lo = Math.max(a0, b0);
|
|
172
|
+
const hi = Math.min(a1, b1);
|
|
173
|
+
return hi > lo + EPS ? [lo, hi] : null;
|
|
174
|
+
}
|
|
175
|
+
function subtractRect2D(A, B) {
|
|
176
|
+
if (!overlaps(A, B)) return [A];
|
|
177
|
+
const Xi = intersect1D(A.x, A.x + A.width, B.x, B.x + B.width);
|
|
178
|
+
const Yi = intersect1D(A.y, A.y + A.height, B.y, B.y + B.height);
|
|
179
|
+
if (!Xi || !Yi) return [A];
|
|
180
|
+
const [X0, X1] = Xi;
|
|
181
|
+
const [Y0, Y1] = Yi;
|
|
182
|
+
const out = [];
|
|
183
|
+
if (X0 > A.x + EPS) {
|
|
184
|
+
out.push({ x: A.x, y: A.y, width: X0 - A.x, height: A.height });
|
|
185
|
+
}
|
|
186
|
+
if (A.x + A.width > X1 + EPS) {
|
|
187
|
+
out.push({ x: X1, y: A.y, width: A.x + A.width - X1, height: A.height });
|
|
188
|
+
}
|
|
189
|
+
const midW = Math.max(0, X1 - X0);
|
|
190
|
+
if (midW > EPS && Y0 > A.y + EPS) {
|
|
191
|
+
out.push({ x: X0, y: A.y, width: midW, height: Y0 - A.y });
|
|
192
|
+
}
|
|
193
|
+
if (midW > EPS && A.y + A.height > Y1 + EPS) {
|
|
194
|
+
out.push({ x: X0, y: Y1, width: midW, height: A.y + A.height - Y1 });
|
|
195
|
+
}
|
|
196
|
+
return out.filter((r) => r.width > EPS && r.height > EPS);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// lib/solvers/rectdiff/candidates.ts
|
|
200
|
+
function isFullyOccupiedAllLayers(x, y, layerCount, obstaclesByLayer, placedByLayer) {
|
|
201
|
+
for (let z = 0; z < layerCount; z++) {
|
|
202
|
+
const obs = obstaclesByLayer[z] ?? [];
|
|
203
|
+
const placed = placedByLayer[z] ?? [];
|
|
204
|
+
const occ = obs.some((b) => containsPoint(b, x, y)) || placed.some((b) => containsPoint(b, x, y));
|
|
205
|
+
if (!occ) return false;
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
function computeCandidates3D(bounds, gridSize, layerCount, obstaclesByLayer, placedByLayer, hardPlacedByLayer) {
|
|
210
|
+
const out = /* @__PURE__ */ new Map();
|
|
211
|
+
for (let x = bounds.x; x < bounds.x + bounds.width; x += gridSize) {
|
|
212
|
+
for (let y = bounds.y; y < bounds.y + bounds.height; y += gridSize) {
|
|
213
|
+
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) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (isFullyOccupiedAllLayers(x, y, layerCount, obstaclesByLayer, placedByLayer)) continue;
|
|
217
|
+
let bestSpan = [];
|
|
218
|
+
let bestZ = 0;
|
|
219
|
+
for (let z = 0; z < layerCount; z++) {
|
|
220
|
+
const s = longestFreeSpanAroundZ(
|
|
221
|
+
x,
|
|
222
|
+
y,
|
|
223
|
+
z,
|
|
224
|
+
layerCount,
|
|
225
|
+
1,
|
|
226
|
+
void 0,
|
|
227
|
+
// no cap here
|
|
228
|
+
obstaclesByLayer,
|
|
229
|
+
hardPlacedByLayer
|
|
230
|
+
// IMPORTANT: ignore soft nodes
|
|
231
|
+
);
|
|
232
|
+
if (s.length > bestSpan.length) {
|
|
233
|
+
bestSpan = s;
|
|
234
|
+
bestZ = z;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const anchorZ = bestSpan.length ? bestSpan[Math.floor(bestSpan.length / 2)] : bestZ;
|
|
238
|
+
const hardAtZ = [
|
|
239
|
+
...obstaclesByLayer[anchorZ] ?? [],
|
|
240
|
+
...hardPlacedByLayer[anchorZ] ?? []
|
|
241
|
+
];
|
|
242
|
+
const d = Math.min(
|
|
243
|
+
distancePointToRectEdges(x, y, bounds),
|
|
244
|
+
...hardAtZ.length ? hardAtZ.map((b) => distancePointToRectEdges(x, y, b)) : [Infinity]
|
|
245
|
+
);
|
|
246
|
+
const k = `${x.toFixed(6)}|${y.toFixed(6)}`;
|
|
247
|
+
const cand = {
|
|
248
|
+
x,
|
|
249
|
+
y,
|
|
250
|
+
z: anchorZ,
|
|
251
|
+
distance: d,
|
|
252
|
+
zSpanLen: bestSpan.length
|
|
253
|
+
};
|
|
254
|
+
const prev = out.get(k);
|
|
255
|
+
if (!prev || cand.zSpanLen > (prev.zSpanLen ?? 0) || cand.zSpanLen === prev.zSpanLen && cand.distance > prev.distance) {
|
|
256
|
+
out.set(k, cand);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const arr = Array.from(out.values());
|
|
261
|
+
arr.sort((a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance);
|
|
262
|
+
return arr;
|
|
263
|
+
}
|
|
264
|
+
function longestFreeSpanAroundZ(x, y, z, layerCount, minSpan, maxSpan, obstaclesByLayer, placedByLayer) {
|
|
265
|
+
const isFreeAt = (layer) => {
|
|
266
|
+
const blockers = [...obstaclesByLayer[layer] ?? [], ...placedByLayer[layer] ?? []];
|
|
267
|
+
return !blockers.some((b) => containsPoint(b, x, y));
|
|
268
|
+
};
|
|
269
|
+
let lo = z;
|
|
270
|
+
let hi = z;
|
|
271
|
+
while (lo - 1 >= 0 && isFreeAt(lo - 1)) lo--;
|
|
272
|
+
while (hi + 1 < layerCount && isFreeAt(hi + 1)) hi++;
|
|
273
|
+
if (typeof maxSpan === "number") {
|
|
274
|
+
const target = clamp(maxSpan, 1, layerCount);
|
|
275
|
+
while (hi - lo + 1 > target) {
|
|
276
|
+
if (z - lo > hi - z) lo++;
|
|
277
|
+
else hi--;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const res = [];
|
|
281
|
+
for (let i = lo; i <= hi; i++) res.push(i);
|
|
282
|
+
return res.length >= minSpan ? res : [];
|
|
283
|
+
}
|
|
284
|
+
function computeDefaultGridSizes(bounds) {
|
|
285
|
+
const ref = Math.max(bounds.width, bounds.height);
|
|
286
|
+
return [ref / 8, ref / 16, ref / 32];
|
|
287
|
+
}
|
|
288
|
+
function computeUncoveredSegments(lineStart, lineEnd, coveringIntervals, minSegmentLength) {
|
|
289
|
+
if (coveringIntervals.length === 0) {
|
|
290
|
+
const center = (lineStart + lineEnd) / 2;
|
|
291
|
+
return [{ start: lineStart, end: lineEnd, center }];
|
|
292
|
+
}
|
|
293
|
+
const sorted = [...coveringIntervals].sort((a, b) => a.start - b.start);
|
|
294
|
+
const merged = [];
|
|
295
|
+
let current = { ...sorted[0] };
|
|
296
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
297
|
+
const interval = sorted[i];
|
|
298
|
+
if (interval.start <= current.end + EPS) {
|
|
299
|
+
current.end = Math.max(current.end, interval.end);
|
|
300
|
+
} else {
|
|
301
|
+
merged.push(current);
|
|
302
|
+
current = { ...interval };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
merged.push(current);
|
|
306
|
+
const uncovered = [];
|
|
307
|
+
if (merged[0].start > lineStart + EPS) {
|
|
308
|
+
const start = lineStart;
|
|
309
|
+
const end = merged[0].start;
|
|
310
|
+
if (end - start >= minSegmentLength) {
|
|
311
|
+
uncovered.push({ start, end, center: (start + end) / 2 });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for (let i = 0; i < merged.length - 1; i++) {
|
|
315
|
+
const start = merged[i].end;
|
|
316
|
+
const end = merged[i + 1].start;
|
|
317
|
+
if (end - start >= minSegmentLength) {
|
|
318
|
+
uncovered.push({ start, end, center: (start + end) / 2 });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (merged[merged.length - 1].end < lineEnd - EPS) {
|
|
322
|
+
const start = merged[merged.length - 1].end;
|
|
323
|
+
const end = lineEnd;
|
|
324
|
+
if (end - start >= minSegmentLength) {
|
|
325
|
+
uncovered.push({ start, end, center: (start + end) / 2 });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return uncovered;
|
|
329
|
+
}
|
|
330
|
+
function computeEdgeCandidates3D(bounds, minSize, layerCount, obstaclesByLayer, placedByLayer, hardPlacedByLayer) {
|
|
331
|
+
const out = [];
|
|
332
|
+
const \u03B4 = Math.max(minSize * 0.15, EPS * 3);
|
|
333
|
+
const dedup = /* @__PURE__ */ new Set();
|
|
334
|
+
const key = (x, y, z) => `${z}|${x.toFixed(6)}|${y.toFixed(6)}`;
|
|
335
|
+
function fullyOcc(x, y) {
|
|
336
|
+
return isFullyOccupiedAllLayers(x, y, layerCount, obstaclesByLayer, placedByLayer);
|
|
337
|
+
}
|
|
338
|
+
function pushIfFree(x, y, z) {
|
|
339
|
+
if (x < bounds.x + EPS || y < bounds.y + EPS || x > bounds.x + bounds.width - EPS || y > bounds.y + bounds.height - EPS) return;
|
|
340
|
+
if (fullyOcc(x, y)) return;
|
|
341
|
+
const hard = [
|
|
342
|
+
...obstaclesByLayer[z] ?? [],
|
|
343
|
+
...hardPlacedByLayer[z] ?? []
|
|
344
|
+
];
|
|
345
|
+
const d = Math.min(
|
|
346
|
+
distancePointToRectEdges(x, y, bounds),
|
|
347
|
+
...hard.length ? hard.map((b) => distancePointToRectEdges(x, y, b)) : [Infinity]
|
|
348
|
+
);
|
|
349
|
+
const k = key(x, y, z);
|
|
350
|
+
if (dedup.has(k)) return;
|
|
351
|
+
dedup.add(k);
|
|
352
|
+
const span = longestFreeSpanAroundZ(x, y, z, layerCount, 1, void 0, obstaclesByLayer, hardPlacedByLayer);
|
|
353
|
+
out.push({ x, y, z, distance: d, zSpanLen: span.length, isEdgeSeed: true });
|
|
354
|
+
}
|
|
355
|
+
for (let z = 0; z < layerCount; z++) {
|
|
356
|
+
const blockers = [...obstaclesByLayer[z] ?? [], ...hardPlacedByLayer[z] ?? []];
|
|
357
|
+
const corners = [
|
|
358
|
+
{ x: bounds.x + \u03B4, y: bounds.y + \u03B4 },
|
|
359
|
+
// top-left
|
|
360
|
+
{ x: bounds.x + bounds.width - \u03B4, y: bounds.y + \u03B4 },
|
|
361
|
+
// top-right
|
|
362
|
+
{ x: bounds.x + \u03B4, y: bounds.y + bounds.height - \u03B4 },
|
|
363
|
+
// bottom-left
|
|
364
|
+
{ x: bounds.x + bounds.width - \u03B4, y: bounds.y + bounds.height - \u03B4 }
|
|
365
|
+
// bottom-right
|
|
366
|
+
];
|
|
367
|
+
for (const corner of corners) {
|
|
368
|
+
pushIfFree(corner.x, corner.y, z);
|
|
369
|
+
}
|
|
370
|
+
const topY = bounds.y + \u03B4;
|
|
371
|
+
const topCovering = blockers.filter((b) => b.y <= topY && b.y + b.height >= topY).map((b) => ({ start: Math.max(bounds.x, b.x), end: Math.min(bounds.x + bounds.width, b.x + b.width) }));
|
|
372
|
+
const topUncovered = computeUncoveredSegments(bounds.x + \u03B4, bounds.x + bounds.width - \u03B4, topCovering, minSize * 0.5);
|
|
373
|
+
for (const seg of topUncovered) {
|
|
374
|
+
const segLen = seg.end - seg.start;
|
|
375
|
+
if (segLen >= minSize) {
|
|
376
|
+
pushIfFree(seg.center, topY, z);
|
|
377
|
+
if (segLen > minSize * 1.5) {
|
|
378
|
+
pushIfFree(seg.start + minSize * 0.4, topY, z);
|
|
379
|
+
pushIfFree(seg.end - minSize * 0.4, topY, z);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const bottomY = bounds.y + bounds.height - \u03B4;
|
|
384
|
+
const bottomCovering = blockers.filter((b) => b.y <= bottomY && b.y + b.height >= bottomY).map((b) => ({ start: Math.max(bounds.x, b.x), end: Math.min(bounds.x + bounds.width, b.x + b.width) }));
|
|
385
|
+
const bottomUncovered = computeUncoveredSegments(bounds.x + \u03B4, bounds.x + bounds.width - \u03B4, bottomCovering, minSize * 0.5);
|
|
386
|
+
for (const seg of bottomUncovered) {
|
|
387
|
+
const segLen = seg.end - seg.start;
|
|
388
|
+
if (segLen >= minSize) {
|
|
389
|
+
pushIfFree(seg.center, bottomY, z);
|
|
390
|
+
if (segLen > minSize * 1.5) {
|
|
391
|
+
pushIfFree(seg.start + minSize * 0.4, bottomY, z);
|
|
392
|
+
pushIfFree(seg.end - minSize * 0.4, bottomY, z);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const leftX = bounds.x + \u03B4;
|
|
397
|
+
const leftCovering = blockers.filter((b) => b.x <= leftX && b.x + b.width >= leftX).map((b) => ({ start: Math.max(bounds.y, b.y), end: Math.min(bounds.y + bounds.height, b.y + b.height) }));
|
|
398
|
+
const leftUncovered = computeUncoveredSegments(bounds.y + \u03B4, bounds.y + bounds.height - \u03B4, leftCovering, minSize * 0.5);
|
|
399
|
+
for (const seg of leftUncovered) {
|
|
400
|
+
const segLen = seg.end - seg.start;
|
|
401
|
+
if (segLen >= minSize) {
|
|
402
|
+
pushIfFree(leftX, seg.center, z);
|
|
403
|
+
if (segLen > minSize * 1.5) {
|
|
404
|
+
pushIfFree(leftX, seg.start + minSize * 0.4, z);
|
|
405
|
+
pushIfFree(leftX, seg.end - minSize * 0.4, z);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const rightX = bounds.x + bounds.width - \u03B4;
|
|
410
|
+
const rightCovering = blockers.filter((b) => b.x <= rightX && b.x + b.width >= rightX).map((b) => ({ start: Math.max(bounds.y, b.y), end: Math.min(bounds.y + bounds.height, b.y + b.height) }));
|
|
411
|
+
const rightUncovered = computeUncoveredSegments(bounds.y + \u03B4, bounds.y + bounds.height - \u03B4, rightCovering, minSize * 0.5);
|
|
412
|
+
for (const seg of rightUncovered) {
|
|
413
|
+
const segLen = seg.end - seg.start;
|
|
414
|
+
if (segLen >= minSize) {
|
|
415
|
+
pushIfFree(rightX, seg.center, z);
|
|
416
|
+
if (segLen > minSize * 1.5) {
|
|
417
|
+
pushIfFree(rightX, seg.start + minSize * 0.4, z);
|
|
418
|
+
pushIfFree(rightX, seg.end - minSize * 0.4, z);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
for (const b of blockers) {
|
|
423
|
+
const obLeftX = b.x - \u03B4;
|
|
424
|
+
if (obLeftX > bounds.x + EPS && obLeftX < bounds.x + bounds.width - EPS) {
|
|
425
|
+
const obLeftCovering = blockers.filter((bl) => bl !== b && bl.x <= obLeftX && bl.x + bl.width >= obLeftX).map((bl) => ({ start: Math.max(b.y, bl.y), end: Math.min(b.y + b.height, bl.y + bl.height) }));
|
|
426
|
+
const obLeftUncovered = computeUncoveredSegments(b.y, b.y + b.height, obLeftCovering, minSize * 0.5);
|
|
427
|
+
for (const seg of obLeftUncovered) {
|
|
428
|
+
pushIfFree(obLeftX, seg.center, z);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const obRightX = b.x + b.width + \u03B4;
|
|
432
|
+
if (obRightX > bounds.x + EPS && obRightX < bounds.x + bounds.width - EPS) {
|
|
433
|
+
const obRightCovering = blockers.filter((bl) => bl !== b && bl.x <= obRightX && bl.x + bl.width >= obRightX).map((bl) => ({ start: Math.max(b.y, bl.y), end: Math.min(b.y + b.height, bl.y + bl.height) }));
|
|
434
|
+
const obRightUncovered = computeUncoveredSegments(b.y, b.y + b.height, obRightCovering, minSize * 0.5);
|
|
435
|
+
for (const seg of obRightUncovered) {
|
|
436
|
+
pushIfFree(obRightX, seg.center, z);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const obTopY = b.y - \u03B4;
|
|
440
|
+
if (obTopY > bounds.y + EPS && obTopY < bounds.y + bounds.height - EPS) {
|
|
441
|
+
const obTopCovering = blockers.filter((bl) => bl !== b && bl.y <= obTopY && bl.y + bl.height >= obTopY).map((bl) => ({ start: Math.max(b.x, bl.x), end: Math.min(b.x + b.width, bl.x + bl.width) }));
|
|
442
|
+
const obTopUncovered = computeUncoveredSegments(b.x, b.x + b.width, obTopCovering, minSize * 0.5);
|
|
443
|
+
for (const seg of obTopUncovered) {
|
|
444
|
+
pushIfFree(seg.center, obTopY, z);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const obBottomY = b.y + b.height + \u03B4;
|
|
448
|
+
if (obBottomY > bounds.y + EPS && obBottomY < bounds.y + bounds.height - EPS) {
|
|
449
|
+
const obBottomCovering = blockers.filter((bl) => bl !== b && bl.y <= obBottomY && bl.y + bl.height >= obBottomY).map((bl) => ({ start: Math.max(b.x, bl.x), end: Math.min(b.x + b.width, bl.x + bl.width) }));
|
|
450
|
+
const obBottomUncovered = computeUncoveredSegments(b.x, b.x + b.width, obBottomCovering, minSize * 0.5);
|
|
451
|
+
for (const seg of obBottomUncovered) {
|
|
452
|
+
pushIfFree(seg.center, obBottomY, z);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
out.sort((a, b) => b.zSpanLen - a.zSpanLen || b.distance - a.distance);
|
|
458
|
+
return out;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// lib/solvers/rectdiff/layers.ts
|
|
462
|
+
function layerSortKey(n) {
|
|
463
|
+
const L = n.toLowerCase();
|
|
464
|
+
if (L === "top") return -1e6;
|
|
465
|
+
if (L === "bottom") return 1e6;
|
|
466
|
+
const m = /^inner(\d+)$/i.exec(L);
|
|
467
|
+
if (m) return parseInt(m[1], 10) || 0;
|
|
468
|
+
return 100 + L.charCodeAt(0);
|
|
469
|
+
}
|
|
470
|
+
function canonicalizeLayerOrder(names) {
|
|
471
|
+
return Array.from(new Set(names)).sort((a, b) => {
|
|
472
|
+
const ka = layerSortKey(a);
|
|
473
|
+
const kb = layerSortKey(b);
|
|
474
|
+
if (ka !== kb) return ka - kb;
|
|
475
|
+
return a.localeCompare(b);
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
function buildZIndexMap(srj) {
|
|
479
|
+
const names = canonicalizeLayerOrder((srj.obstacles ?? []).flatMap((o) => o.layers ?? []));
|
|
480
|
+
const fallback = Array.from(
|
|
481
|
+
{ length: Math.max(1, srj.layerCount || 1) },
|
|
482
|
+
(_, i) => i === 0 ? "top" : i === (srj.layerCount || 1) - 1 ? "bottom" : `inner${i}`
|
|
483
|
+
);
|
|
484
|
+
const layerNames = names.length ? names : fallback;
|
|
485
|
+
const map = /* @__PURE__ */ new Map();
|
|
486
|
+
layerNames.forEach((n, i) => map.set(n, i));
|
|
487
|
+
return { layerNames, zIndexByName: map };
|
|
488
|
+
}
|
|
489
|
+
function obstacleZs(ob, zIndexByName) {
|
|
490
|
+
if (ob.zLayers?.length) return Array.from(new Set(ob.zLayers)).sort((a, b) => a - b);
|
|
491
|
+
const fromNames = (ob.layers ?? []).map((n) => zIndexByName.get(n)).filter((v) => typeof v === "number");
|
|
492
|
+
return Array.from(new Set(fromNames)).sort((a, b) => a - b);
|
|
493
|
+
}
|
|
494
|
+
function obstacleToXYRect(ob) {
|
|
495
|
+
const w = ob.width;
|
|
496
|
+
const h = ob.height;
|
|
497
|
+
if (typeof w !== "number" || typeof h !== "number") return null;
|
|
498
|
+
return { x: ob.center.x - w / 2, y: ob.center.y - h / 2, width: w, height: h };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// lib/solvers/rectdiff/engine.ts
|
|
502
|
+
function initState(srj, opts) {
|
|
503
|
+
const { layerNames, zIndexByName } = buildZIndexMap(srj);
|
|
504
|
+
const layerCount = Math.max(1, layerNames.length, srj.layerCount || 1);
|
|
505
|
+
const bounds = {
|
|
506
|
+
x: srj.bounds.minX,
|
|
507
|
+
y: srj.bounds.minY,
|
|
508
|
+
width: srj.bounds.maxX - srj.bounds.minX,
|
|
509
|
+
height: srj.bounds.maxY - srj.bounds.minY
|
|
510
|
+
};
|
|
511
|
+
const obstaclesByLayer = Array.from({ length: layerCount }, () => []);
|
|
512
|
+
for (const ob of srj.obstacles ?? []) {
|
|
513
|
+
const r = obstacleToXYRect(ob);
|
|
514
|
+
if (!r) continue;
|
|
515
|
+
const zs = obstacleZs(ob, zIndexByName);
|
|
516
|
+
for (const z of zs) if (z >= 0 && z < layerCount) obstaclesByLayer[z].push(r);
|
|
517
|
+
}
|
|
518
|
+
const trace = Math.max(0.01, srj.minTraceWidth || 0.15);
|
|
519
|
+
const defaults = {
|
|
520
|
+
gridSizes: computeDefaultGridSizes(bounds),
|
|
521
|
+
initialCellRatio: 0.2,
|
|
522
|
+
maxAspectRatio: 3,
|
|
523
|
+
minSingle: { width: 2 * trace, height: 2 * trace },
|
|
524
|
+
minMulti: {
|
|
525
|
+
width: 4 * trace,
|
|
526
|
+
height: 4 * trace,
|
|
527
|
+
minLayers: Math.min(2, Math.max(1, srj.layerCount || 1))
|
|
528
|
+
},
|
|
529
|
+
preferMultiLayer: true,
|
|
530
|
+
maxMultiLayerSpan: void 0
|
|
531
|
+
};
|
|
532
|
+
const options = { ...defaults, ...opts, gridSizes: opts.gridSizes ?? defaults.gridSizes };
|
|
533
|
+
const placedByLayer = Array.from({ length: layerCount }, () => []);
|
|
534
|
+
return {
|
|
535
|
+
srj,
|
|
536
|
+
layerNames,
|
|
537
|
+
layerCount,
|
|
538
|
+
bounds,
|
|
539
|
+
options,
|
|
540
|
+
obstaclesByLayer,
|
|
541
|
+
phase: "GRID",
|
|
542
|
+
gridIndex: 0,
|
|
543
|
+
candidates: [],
|
|
544
|
+
placed: [],
|
|
545
|
+
placedByLayer,
|
|
546
|
+
expansionIndex: 0,
|
|
547
|
+
edgeAnalysisDone: false,
|
|
548
|
+
totalSeedsThisGrid: 0,
|
|
549
|
+
consumedSeedsThisGrid: 0
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function buildHardPlacedByLayer(state) {
|
|
553
|
+
const out = Array.from({ length: state.layerCount }, () => []);
|
|
554
|
+
for (const p of state.placed) {
|
|
555
|
+
if (p.zLayers.length >= state.layerCount) {
|
|
556
|
+
for (const z of p.zLayers) out[z].push(p.rect);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return out;
|
|
560
|
+
}
|
|
561
|
+
function isFullyOccupiedAtPoint(state, x, y) {
|
|
562
|
+
for (let z = 0; z < state.layerCount; z++) {
|
|
563
|
+
const obs = state.obstaclesByLayer[z] ?? [];
|
|
564
|
+
const placed = state.placedByLayer[z] ?? [];
|
|
565
|
+
const occ = obs.some((b) => containsPoint(b, x, y)) || placed.some((b) => containsPoint(b, x, y));
|
|
566
|
+
if (!occ) return false;
|
|
567
|
+
}
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
function resizeSoftOverlaps(state, newIndex) {
|
|
571
|
+
const newcomer = state.placed[newIndex];
|
|
572
|
+
const { rect: newR, zLayers: newZs } = newcomer;
|
|
573
|
+
const layerCount = state.layerCount;
|
|
574
|
+
const removeIdx = [];
|
|
575
|
+
const toAdd = [];
|
|
576
|
+
for (let i = 0; i < state.placed.length; i++) {
|
|
577
|
+
if (i === newIndex) continue;
|
|
578
|
+
const old = state.placed[i];
|
|
579
|
+
if (old.zLayers.length >= layerCount) continue;
|
|
580
|
+
const sharedZ = old.zLayers.filter((z) => newZs.includes(z));
|
|
581
|
+
if (sharedZ.length === 0) continue;
|
|
582
|
+
if (!overlaps(old.rect, newR)) continue;
|
|
583
|
+
const parts = subtractRect2D(old.rect, newR);
|
|
584
|
+
removeIdx.push(i);
|
|
585
|
+
const unaffectedZ = old.zLayers.filter((z) => !newZs.includes(z));
|
|
586
|
+
if (unaffectedZ.length > 0) {
|
|
587
|
+
toAdd.push({ rect: old.rect, zLayers: unaffectedZ });
|
|
588
|
+
}
|
|
589
|
+
const minW = Math.min(state.options.minSingle.width, state.options.minMulti.width);
|
|
590
|
+
const minH = Math.min(state.options.minSingle.height, state.options.minMulti.height);
|
|
591
|
+
for (const p of parts) {
|
|
592
|
+
if (p.width + EPS >= minW && p.height + EPS >= minH) {
|
|
593
|
+
toAdd.push({ rect: p, zLayers: sharedZ.slice() });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
removeIdx.sort((a, b) => b - a).forEach((idx) => {
|
|
598
|
+
const rem = state.placed.splice(idx, 1)[0];
|
|
599
|
+
for (const z of rem.zLayers) {
|
|
600
|
+
const arr = state.placedByLayer[z];
|
|
601
|
+
const j = arr.findIndex((r) => r === rem.rect);
|
|
602
|
+
if (j >= 0) arr.splice(j, 1);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
for (const p of toAdd) {
|
|
606
|
+
state.placed.push(p);
|
|
607
|
+
for (const z of p.zLayers) state.placedByLayer[z].push(p.rect);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function stepGrid(state) {
|
|
611
|
+
const {
|
|
612
|
+
gridSizes,
|
|
613
|
+
initialCellRatio,
|
|
614
|
+
maxAspectRatio,
|
|
615
|
+
minSingle,
|
|
616
|
+
minMulti,
|
|
617
|
+
preferMultiLayer,
|
|
618
|
+
maxMultiLayerSpan
|
|
619
|
+
} = state.options;
|
|
620
|
+
const grid = gridSizes[state.gridIndex];
|
|
621
|
+
const hardPlacedByLayer = buildHardPlacedByLayer(state);
|
|
622
|
+
if (state.candidates.length === 0 && state.consumedSeedsThisGrid === 0) {
|
|
623
|
+
state.candidates = computeCandidates3D(
|
|
624
|
+
state.bounds,
|
|
625
|
+
grid,
|
|
626
|
+
state.layerCount,
|
|
627
|
+
state.obstaclesByLayer,
|
|
628
|
+
state.placedByLayer,
|
|
629
|
+
// all nodes (soft + hard) for fully-occupied test
|
|
630
|
+
hardPlacedByLayer
|
|
631
|
+
// hard blockers for ranking/span
|
|
632
|
+
);
|
|
633
|
+
state.totalSeedsThisGrid = state.candidates.length;
|
|
634
|
+
state.consumedSeedsThisGrid = 0;
|
|
635
|
+
}
|
|
636
|
+
if (state.candidates.length === 0) {
|
|
637
|
+
if (state.gridIndex + 1 < gridSizes.length) {
|
|
638
|
+
state.gridIndex += 1;
|
|
639
|
+
state.totalSeedsThisGrid = 0;
|
|
640
|
+
state.consumedSeedsThisGrid = 0;
|
|
641
|
+
return;
|
|
642
|
+
} else {
|
|
643
|
+
if (!state.edgeAnalysisDone) {
|
|
644
|
+
const minSize = Math.min(minSingle.width, minSingle.height);
|
|
645
|
+
state.candidates = computeEdgeCandidates3D(
|
|
646
|
+
state.bounds,
|
|
647
|
+
minSize,
|
|
648
|
+
state.layerCount,
|
|
649
|
+
state.obstaclesByLayer,
|
|
650
|
+
state.placedByLayer,
|
|
651
|
+
// for fully-occupied test
|
|
652
|
+
hardPlacedByLayer
|
|
653
|
+
);
|
|
654
|
+
state.edgeAnalysisDone = true;
|
|
655
|
+
state.totalSeedsThisGrid = state.candidates.length;
|
|
656
|
+
state.consumedSeedsThisGrid = 0;
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
state.phase = "EXPANSION";
|
|
660
|
+
state.expansionIndex = 0;
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const cand = state.candidates.shift();
|
|
665
|
+
state.consumedSeedsThisGrid += 1;
|
|
666
|
+
const span = longestFreeSpanAroundZ(
|
|
667
|
+
cand.x,
|
|
668
|
+
cand.y,
|
|
669
|
+
cand.z,
|
|
670
|
+
state.layerCount,
|
|
671
|
+
minMulti.minLayers,
|
|
672
|
+
maxMultiLayerSpan,
|
|
673
|
+
state.obstaclesByLayer,
|
|
674
|
+
hardPlacedByLayer
|
|
675
|
+
// ignore soft nodes for span
|
|
676
|
+
);
|
|
677
|
+
const attempts = [];
|
|
678
|
+
if (span.length >= minMulti.minLayers) {
|
|
679
|
+
attempts.push({ kind: "multi", layers: span, minReq: { width: minMulti.width, height: minMulti.height } });
|
|
680
|
+
}
|
|
681
|
+
attempts.push({ kind: "single", layers: [cand.z], minReq: { width: minSingle.width, height: minSingle.height } });
|
|
682
|
+
const ordered = preferMultiLayer ? attempts : attempts.reverse();
|
|
683
|
+
for (const attempt of ordered) {
|
|
684
|
+
const hardBlockers = [];
|
|
685
|
+
for (const z of attempt.layers) {
|
|
686
|
+
if (state.obstaclesByLayer[z]) hardBlockers.push(...state.obstaclesByLayer[z]);
|
|
687
|
+
if (hardPlacedByLayer[z]) hardBlockers.push(...hardPlacedByLayer[z]);
|
|
688
|
+
}
|
|
689
|
+
const rect = expandRectFromSeed(
|
|
690
|
+
cand.x,
|
|
691
|
+
cand.y,
|
|
692
|
+
grid,
|
|
693
|
+
state.bounds,
|
|
694
|
+
hardBlockers,
|
|
695
|
+
// soft nodes DO NOT block expansion
|
|
696
|
+
initialCellRatio,
|
|
697
|
+
maxAspectRatio,
|
|
698
|
+
attempt.minReq
|
|
699
|
+
);
|
|
700
|
+
if (!rect) continue;
|
|
701
|
+
const placed = { rect, zLayers: [...attempt.layers] };
|
|
702
|
+
const newIndex = state.placed.push(placed) - 1;
|
|
703
|
+
for (const z of attempt.layers) state.placedByLayer[z].push(rect);
|
|
704
|
+
resizeSoftOverlaps(state, newIndex);
|
|
705
|
+
state.candidates = state.candidates.filter((c) => !isFullyOccupiedAtPoint(state, c.x, c.y));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function stepExpansion(state) {
|
|
710
|
+
if (state.expansionIndex >= state.placed.length) {
|
|
711
|
+
state.phase = "DONE";
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const idx = state.expansionIndex;
|
|
715
|
+
const p = state.placed[idx];
|
|
716
|
+
const lastGrid = state.options.gridSizes[state.options.gridSizes.length - 1];
|
|
717
|
+
const hardPlacedByLayer = buildHardPlacedByLayer(state);
|
|
718
|
+
const hardBlockers = [];
|
|
719
|
+
for (const z of p.zLayers) {
|
|
720
|
+
hardBlockers.push(...state.obstaclesByLayer[z] ?? []);
|
|
721
|
+
hardBlockers.push(...hardPlacedByLayer[z] ?? []);
|
|
722
|
+
}
|
|
723
|
+
const oldRect = p.rect;
|
|
724
|
+
const expanded = expandRectFromSeed(
|
|
725
|
+
p.rect.x + p.rect.width / 2,
|
|
726
|
+
p.rect.y + p.rect.height / 2,
|
|
727
|
+
lastGrid,
|
|
728
|
+
state.bounds,
|
|
729
|
+
hardBlockers,
|
|
730
|
+
0,
|
|
731
|
+
// seed bias off
|
|
732
|
+
null,
|
|
733
|
+
// no aspect cap in expansion pass
|
|
734
|
+
{ width: p.rect.width, height: p.rect.height }
|
|
735
|
+
);
|
|
736
|
+
if (expanded) {
|
|
737
|
+
state.placed[idx] = { rect: expanded, zLayers: p.zLayers };
|
|
738
|
+
for (const z of p.zLayers) {
|
|
739
|
+
const arr = state.placedByLayer[z];
|
|
740
|
+
const j = arr.findIndex((r) => r === oldRect);
|
|
741
|
+
if (j >= 0) arr[j] = expanded;
|
|
742
|
+
}
|
|
743
|
+
resizeSoftOverlaps(state, idx);
|
|
744
|
+
}
|
|
745
|
+
state.expansionIndex += 1;
|
|
746
|
+
}
|
|
747
|
+
function finalizeRects(state) {
|
|
748
|
+
return state.placed.map((p) => ({
|
|
749
|
+
minX: p.rect.x,
|
|
750
|
+
minY: p.rect.y,
|
|
751
|
+
maxX: p.rect.x + p.rect.width,
|
|
752
|
+
maxY: p.rect.y + p.rect.height,
|
|
753
|
+
zLayers: [...p.zLayers].sort((a, b) => a - b)
|
|
754
|
+
}));
|
|
755
|
+
}
|
|
756
|
+
function computeProgress(state) {
|
|
757
|
+
const grids = state.options.gridSizes.length;
|
|
758
|
+
if (state.phase === "GRID") {
|
|
759
|
+
const g = state.gridIndex;
|
|
760
|
+
const base = g / (grids + 1);
|
|
761
|
+
const denom = Math.max(1, state.totalSeedsThisGrid);
|
|
762
|
+
const frac = denom ? state.consumedSeedsThisGrid / denom : 1;
|
|
763
|
+
return Math.min(0.999, base + frac * (1 / (grids + 1)));
|
|
764
|
+
}
|
|
765
|
+
if (state.phase === "EXPANSION") {
|
|
766
|
+
const base = grids / (grids + 1);
|
|
767
|
+
const denom = Math.max(1, state.placed.length);
|
|
768
|
+
const frac = denom ? state.expansionIndex / denom : 1;
|
|
769
|
+
return Math.min(0.999, base + frac * (1 / (grids + 1)));
|
|
770
|
+
}
|
|
771
|
+
return 1;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// lib/solvers/rectdiff/rectsToMeshNodes.ts
|
|
775
|
+
function rectsToMeshNodes(rects) {
|
|
776
|
+
let id = 0;
|
|
777
|
+
const out = [];
|
|
778
|
+
for (const r of rects) {
|
|
779
|
+
const w = Math.max(0, r.maxX - r.minX);
|
|
780
|
+
const h = Math.max(0, r.maxY - r.minY);
|
|
781
|
+
if (w <= 0 || h <= 0 || r.zLayers.length === 0) continue;
|
|
782
|
+
out.push({
|
|
783
|
+
capacityMeshNodeId: `cmn_${id++}`,
|
|
784
|
+
center: { x: (r.minX + r.maxX) / 2, y: (r.minY + r.maxY) / 2 },
|
|
785
|
+
width: w,
|
|
786
|
+
height: h,
|
|
787
|
+
layer: "top",
|
|
788
|
+
availableZ: r.zLayers.slice()
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
return out;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// lib/solvers/RectDiffSolver.ts
|
|
795
|
+
var RectDiffSolver = class extends BaseSolver {
|
|
796
|
+
srj;
|
|
797
|
+
mode;
|
|
798
|
+
gridOptions;
|
|
799
|
+
state;
|
|
800
|
+
_meshNodes = [];
|
|
801
|
+
constructor(opts) {
|
|
802
|
+
super();
|
|
803
|
+
this.srj = opts.simpleRouteJson;
|
|
804
|
+
this.mode = opts.mode ?? "grid";
|
|
805
|
+
this.gridOptions = opts.gridOptions ?? {};
|
|
806
|
+
}
|
|
807
|
+
_setup() {
|
|
808
|
+
this.state = initState(this.srj, this.gridOptions);
|
|
809
|
+
this.stats = {
|
|
810
|
+
phase: this.state.phase,
|
|
811
|
+
gridIndex: this.state.gridIndex
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
/** IMPORTANT: exactly ONE small step per call */
|
|
815
|
+
_step() {
|
|
816
|
+
if (this.state.phase === "GRID") {
|
|
817
|
+
stepGrid(this.state);
|
|
818
|
+
} else if (this.state.phase === "EXPANSION") {
|
|
819
|
+
stepExpansion(this.state);
|
|
820
|
+
} else if (this.state.phase === "DONE") {
|
|
821
|
+
if (!this.solved) {
|
|
822
|
+
const rects = finalizeRects(this.state);
|
|
823
|
+
this._meshNodes = rectsToMeshNodes(rects);
|
|
824
|
+
this.solved = true;
|
|
825
|
+
}
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
this.stats.phase = this.state.phase;
|
|
829
|
+
this.stats.gridIndex = this.state.gridIndex;
|
|
830
|
+
this.stats.placed = this.state.placed.length;
|
|
831
|
+
}
|
|
832
|
+
// Let BaseSolver update this.progress automatically if present.
|
|
833
|
+
computeProgress() {
|
|
834
|
+
return computeProgress(this.state);
|
|
835
|
+
}
|
|
836
|
+
getOutput() {
|
|
837
|
+
return { meshNodes: this._meshNodes };
|
|
838
|
+
}
|
|
839
|
+
// Helper to get color based on z layer
|
|
840
|
+
getColorForZLayer(zLayers) {
|
|
841
|
+
const minZ = Math.min(...zLayers);
|
|
842
|
+
const colors = [
|
|
843
|
+
{ fill: "#dbeafe", stroke: "#3b82f6" },
|
|
844
|
+
// blue (z=0)
|
|
845
|
+
{ fill: "#fef3c7", stroke: "#f59e0b" },
|
|
846
|
+
// amber (z=1)
|
|
847
|
+
{ fill: "#d1fae5", stroke: "#10b981" },
|
|
848
|
+
// green (z=2)
|
|
849
|
+
{ fill: "#e9d5ff", stroke: "#a855f7" },
|
|
850
|
+
// purple (z=3)
|
|
851
|
+
{ fill: "#fed7aa", stroke: "#f97316" },
|
|
852
|
+
// orange (z=4)
|
|
853
|
+
{ fill: "#fecaca", stroke: "#ef4444" }
|
|
854
|
+
// red (z=5)
|
|
855
|
+
];
|
|
856
|
+
return colors[minZ % colors.length];
|
|
857
|
+
}
|
|
858
|
+
// Streaming visualization: board + obstacles + current placements.
|
|
859
|
+
visualize() {
|
|
860
|
+
const rects = [];
|
|
861
|
+
const points = [];
|
|
862
|
+
rects.push({
|
|
863
|
+
center: {
|
|
864
|
+
x: (this.srj.bounds.minX + this.srj.bounds.maxX) / 2,
|
|
865
|
+
y: (this.srj.bounds.minY + this.srj.bounds.maxY) / 2
|
|
866
|
+
},
|
|
867
|
+
width: this.srj.bounds.maxX - this.srj.bounds.minX,
|
|
868
|
+
height: this.srj.bounds.maxY - this.srj.bounds.minY,
|
|
869
|
+
fill: "none",
|
|
870
|
+
stroke: "#111827",
|
|
871
|
+
label: "board"
|
|
872
|
+
});
|
|
873
|
+
for (const ob of this.srj.obstacles ?? []) {
|
|
874
|
+
if (ob.type === "rect" || ob.type === "oval") {
|
|
875
|
+
rects.push({
|
|
876
|
+
center: { x: ob.center.x, y: ob.center.y },
|
|
877
|
+
width: ob.width,
|
|
878
|
+
height: ob.height,
|
|
879
|
+
fill: "#fee2e2",
|
|
880
|
+
stroke: "#ef4444",
|
|
881
|
+
layer: "obstacle",
|
|
882
|
+
label: "obstacle"
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (this.state?.candidates?.length) {
|
|
887
|
+
for (const cand of this.state.candidates) {
|
|
888
|
+
points.push({
|
|
889
|
+
x: cand.x,
|
|
890
|
+
y: cand.y,
|
|
891
|
+
fill: "#9333ea",
|
|
892
|
+
stroke: "#6b21a8",
|
|
893
|
+
label: `z:${cand.z}`
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (this.state?.placed?.length) {
|
|
898
|
+
for (const p of this.state.placed) {
|
|
899
|
+
const colors = this.getColorForZLayer(p.zLayers);
|
|
900
|
+
rects.push({
|
|
901
|
+
center: { x: p.rect.x + p.rect.width / 2, y: p.rect.y + p.rect.height / 2 },
|
|
902
|
+
width: p.rect.width,
|
|
903
|
+
height: p.rect.height,
|
|
904
|
+
fill: colors.fill,
|
|
905
|
+
stroke: colors.stroke,
|
|
906
|
+
label: `free
|
|
907
|
+
z:${p.zLayers.join(",")}`
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
title: "RectDiff (incremental)",
|
|
913
|
+
coordinateSystem: "cartesian",
|
|
914
|
+
rects,
|
|
915
|
+
points
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
export {
|
|
920
|
+
RectDiffSolver
|
|
921
|
+
};
|