@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
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>3D Rect Difference & Coalescing Visualizer (No Discretization)</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
7
|
+
<style>
|
|
8
|
+
html, body { height: 100%; margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, 'Helvetica Neue', Arial, 'Noto Sans', 'Apple Color Emoji', 'Segoe UI Emoji'; }
|
|
9
|
+
#app { display: grid; grid-template-columns: 360px 1fr; height: 100%; }
|
|
10
|
+
#controls { padding: 12px 14px; overflow: auto; border-right: 1px solid #e6e6e6; background: #fafafa; }
|
|
11
|
+
#controls h1 { font-size: 18px; margin: 8px 0 10px; }
|
|
12
|
+
#controls h2 { font-size: 14px; margin: 16px 0 8px; color: #333; }
|
|
13
|
+
#controls label { display:block; font-size: 12px; margin: 6px 0 2px; color:#333; }
|
|
14
|
+
#controls input[type="number"], #controls select, #controls textarea { width:100%; box-sizing:border-box; font: inherit; padding: 6px 8px; }
|
|
15
|
+
#controls textarea { height: 180px; resize: vertical; }
|
|
16
|
+
#controls .row { display:flex; gap:8px; }
|
|
17
|
+
#controls .row > div { flex:1; }
|
|
18
|
+
#controls .btns { display:flex; gap:8px; margin: 10px 0 12px; }
|
|
19
|
+
button { padding: 8px 10px; font: inherit; cursor: pointer; border-radius: 6px; border:1px solid #ccc; background:#fff; }
|
|
20
|
+
button.primary { background: #2563eb; color: white; border-color: #2563eb; }
|
|
21
|
+
button:disabled { opacity:.6; cursor: not-allowed; }
|
|
22
|
+
#stats { font-size:12px; background:#fff; border:1px solid #e5e5e5; border-radius:8px; padding:8px; }
|
|
23
|
+
#stats b { font-weight:600; }
|
|
24
|
+
#toggles { display:flex; gap:8px; margin-top:8px; }
|
|
25
|
+
#view { position: relative; }
|
|
26
|
+
#view canvas { display:block; width:100%; height:100%; }
|
|
27
|
+
.small { font-size: 11px; color:#666; }
|
|
28
|
+
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono"; }
|
|
29
|
+
.warn { color:#92400e; background: #fff7ed; border: 1px solid #fed7aa; padding: 6px 8px; border-radius:8px; font-size:12px; }
|
|
30
|
+
.ok { color:#065f46; background: #ecfdf5; border: 1px solid #a7f3d0; padding: 6px 8px; border-radius:8px; font-size:12px; }
|
|
31
|
+
</style>
|
|
32
|
+
<!-- Three.js from CDN -->
|
|
33
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
34
|
+
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
|
|
35
|
+
</head>
|
|
36
|
+
<body>
|
|
37
|
+
<div id="app">
|
|
38
|
+
<div id="controls">
|
|
39
|
+
<h1>3D Rect Difference & Coalescing (No Discretization)</h1>
|
|
40
|
+
<div class="small">Subtract axis-aligned 3D cutouts from a root box (with discrete Z layers), then merge adjacent boxes to produce large non-overlapping prisms. No rasterization/discretization is used—only exact splits along cutout faces and exact coalescing.</div>
|
|
41
|
+
<h2>Input</h2>
|
|
42
|
+
<label>Problem JSON (<span class="mono">InputProblem</span>)</label>
|
|
43
|
+
<textarea id="inputArea" spellcheck="false"></textarea>
|
|
44
|
+
<div class="row">
|
|
45
|
+
<div>
|
|
46
|
+
<label>Z layer thickness</label>
|
|
47
|
+
<input id="thickness" type="number" step="0.01" value="1" />
|
|
48
|
+
</div>
|
|
49
|
+
<div>
|
|
50
|
+
<label>Score exponent p ( > 1 )</label>
|
|
51
|
+
<input id="exponent" type="number" step="0.1" value="2" />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="row">
|
|
55
|
+
<div>
|
|
56
|
+
<label>Axis order</label>
|
|
57
|
+
<select id="axisOrder">
|
|
58
|
+
<option value="AUTO" selected>Auto (try all 6)</option>
|
|
59
|
+
<option value="X,Y,Z">X → Y → Z</option>
|
|
60
|
+
<option value="X,Z,Y">X → Z → Y</option>
|
|
61
|
+
<option value="Y,X,Z">Y → X → Z</option>
|
|
62
|
+
<option value="Y,Z,X">Y → Z → X</option>
|
|
63
|
+
<option value="Z,X,Y">Z → X → Y</option>
|
|
64
|
+
<option value="Z,Y,X">Z → Y → X</option>
|
|
65
|
+
</select>
|
|
66
|
+
</div>
|
|
67
|
+
<div>
|
|
68
|
+
<label>Max merge cycles</label>
|
|
69
|
+
<input id="maxCycles" type="number" step="1" value="6" />
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="row">
|
|
73
|
+
<div>
|
|
74
|
+
<label>EPS tolerance</label>
|
|
75
|
+
<input id="eps" type="number" step="0.000000001" value="1e-9" />
|
|
76
|
+
</div>
|
|
77
|
+
<div>
|
|
78
|
+
<label>Seed (cutout order)</label>
|
|
79
|
+
<input id="seed" type="number" step="1" value="0" />
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="btns">
|
|
83
|
+
<button id="runBtn" class="primary">Run</button>
|
|
84
|
+
<button id="exampleBtn">Load example</button>
|
|
85
|
+
<button id="clearBtn">Clear scene</button>
|
|
86
|
+
</div>
|
|
87
|
+
<div id="stats">
|
|
88
|
+
<div id="status" class="ok">Ready.</div>
|
|
89
|
+
<div style="margin-top:6px;">
|
|
90
|
+
<div><b>Boxes:</b> <span id="boxCount">–</span></div>
|
|
91
|
+
<div><b>Total free volume:</b> <span id="freeVol">–</span></div>
|
|
92
|
+
<div><b>Score (∑vol^p):</b> <span id="scoreVal">–</span></div>
|
|
93
|
+
<div><b>Axis order:</b> <span id="orderUsed">–</span></div>
|
|
94
|
+
</div>
|
|
95
|
+
<div id="notes" class="small" style="margin-top:6px;"></div>
|
|
96
|
+
</div>
|
|
97
|
+
<h2>View</h2>
|
|
98
|
+
<div id="toggles">
|
|
99
|
+
<label><input id="showRoot" type="checkbox" checked /> Root</label>
|
|
100
|
+
<label><input id="showCutouts" type="checkbox" checked /> Cutouts</label>
|
|
101
|
+
<label><input id="showOutput" type="checkbox" checked /> Output</label>
|
|
102
|
+
<label><input id="wireframeOutput" type="checkbox" /> Wireframe</label>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="small" style="margin-top:8px;">
|
|
105
|
+
Controls: drag to orbit, wheel to zoom, right-drag to pan.
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div id="view"></div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<script>
|
|
112
|
+
(() => {
|
|
113
|
+
// ---------- Geometry & algorithm (no discretization) ----------
|
|
114
|
+
|
|
115
|
+
const EPS_DEFAULT = 1e-9;
|
|
116
|
+
|
|
117
|
+
function almostEq(a, b, eps) { return Math.abs(a - b) <= eps; }
|
|
118
|
+
function gt(a,b,eps){ return a > b + eps; }
|
|
119
|
+
function gte(a,b,eps){ return a > b - eps; }
|
|
120
|
+
function lt(a,b,eps){ return a < b - eps; }
|
|
121
|
+
function lte(a,b,eps){ return a < b + eps; }
|
|
122
|
+
|
|
123
|
+
function ensureContiguous(layers) {
|
|
124
|
+
const z = [...layers].sort((a,b)=>a-b);
|
|
125
|
+
for (let i=1;i<z.length;i++) if (z[i] !== z[i-1] + 1) {
|
|
126
|
+
throw new Error('zLayers must be contiguous integers: ' + JSON.stringify(layers));
|
|
127
|
+
}
|
|
128
|
+
return z;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Box with z stored as integer half-open [z0,z1)
|
|
132
|
+
function toBoxFromRoot(root) {
|
|
133
|
+
const z = ensureContiguous(root.zLayers);
|
|
134
|
+
return {
|
|
135
|
+
minX: root.minX, minY: root.minY, maxX: root.maxX, maxY: root.maxY,
|
|
136
|
+
z0: z[0], z1: z[z.length-1] + 1
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function toBoxFromCutout(c, rootZs) {
|
|
140
|
+
const z = ensureContiguous(c.zLayers).filter(zl => rootZs.has(zl));
|
|
141
|
+
if (z.length === 0) return null;
|
|
142
|
+
return {
|
|
143
|
+
minX: c.minX, minY: c.minY, maxX: c.maxX, maxY: c.maxY,
|
|
144
|
+
z0: z[0], z1: z[z.length-1] + 1
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function nonEmptyBox(b, eps) {
|
|
149
|
+
return gt(b.maxX, b.minX, eps) && gt(b.maxY, b.minY, eps) && (b.z1 - b.z0) > 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function intersect1D(a0,a1,b0,b1,eps){
|
|
153
|
+
const lo = Math.max(a0,b0);
|
|
154
|
+
const hi = Math.min(a1,b1);
|
|
155
|
+
return gt(hi, lo, eps) ? [lo, hi] : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function intersects(a, b, eps){
|
|
159
|
+
return intersect1D(a.minX,a.maxX,b.minX,b.maxX,eps)
|
|
160
|
+
&& intersect1D(a.minY,a.maxY,b.minY,b.maxY,eps)
|
|
161
|
+
&& (Math.min(a.z1,b.z1) > Math.max(a.z0,b.z0));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Subtract B from A; return up to 6 boxes (A \ B).
|
|
165
|
+
function subtractBox(A, B, eps) {
|
|
166
|
+
if (!intersects(A,B,eps)) return [A];
|
|
167
|
+
|
|
168
|
+
const Xi = intersect1D(A.minX,A.maxX,B.minX,B.maxX,eps);
|
|
169
|
+
const Yi = intersect1D(A.minY,A.maxY,B.minY,B.maxY,eps);
|
|
170
|
+
const Z0 = Math.max(A.z0, B.z0);
|
|
171
|
+
const Z1 = Math.min(A.z1, B.z1);
|
|
172
|
+
if (!Xi || !Yi || !(Z1>Z0)) return [A];
|
|
173
|
+
|
|
174
|
+
const [X0,X1] = Xi, [Y0,Y1] = Yi;
|
|
175
|
+
const out = [];
|
|
176
|
+
|
|
177
|
+
// Left slab
|
|
178
|
+
if (gt(X0, A.minX, eps)) out.push({ minX: A.minX, maxX: X0, minY: A.minY, maxY: A.maxY, z0: A.z0, z1: A.z1 });
|
|
179
|
+
// Right slab
|
|
180
|
+
if (gt(A.maxX, X1, eps)) out.push({ minX: X1, maxX: A.maxX, minY: A.minY, maxY: A.maxY, z0: A.z0, z1: A.z1 });
|
|
181
|
+
|
|
182
|
+
// Middle X range: further split along Y
|
|
183
|
+
const midX0 = Math.max(A.minX, X0);
|
|
184
|
+
const midX1 = Math.min(A.maxX, X1);
|
|
185
|
+
|
|
186
|
+
// Front (lower Y)
|
|
187
|
+
if (gt(Y0, A.minY, eps)) out.push({ minX: midX0, maxX: midX1, minY: A.minY, maxY: Y0, z0: A.z0, z1: A.z1 });
|
|
188
|
+
// Back (upper Y)
|
|
189
|
+
if (gt(A.maxY, Y1, eps)) out.push({ minX: midX0, maxX: midX1, minY: Y1, maxY: A.maxY, z0: A.z0, z1: A.z1 });
|
|
190
|
+
|
|
191
|
+
// Center X,Y range: split along Z
|
|
192
|
+
const midY0 = Math.max(A.minY, Y0);
|
|
193
|
+
const midY1 = Math.min(A.maxY, Y1);
|
|
194
|
+
|
|
195
|
+
if (Z0 > A.z0) out.push({ minX: midX0, maxX: midX1, minY: midY0, maxY: midY1, z0: A.z0, z1: Z0 });
|
|
196
|
+
if (A.z1 > Z1) out.push({ minX: midX0, maxX: midX1, minY: midY0, maxY: midY1, z0: Z1, z1: A.z1 });
|
|
197
|
+
|
|
198
|
+
return out.filter(b => nonEmptyBox(b, eps));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Subtract a single cutout from a list of boxes
|
|
202
|
+
function subtractCutoutFromList(boxes, cutout, eps) {
|
|
203
|
+
const out = [];
|
|
204
|
+
for (const b of boxes) {
|
|
205
|
+
if (intersects(b, cutout, eps)) {
|
|
206
|
+
const parts = subtractBox(b, cutout, eps);
|
|
207
|
+
for (const p of parts) out.push(p);
|
|
208
|
+
} else {
|
|
209
|
+
out.push(b);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Deterministic subtraction of all cutouts (sorted for stability)
|
|
216
|
+
function subtractAll(rootBox, cutoutBoxes, eps, seed=0) {
|
|
217
|
+
// Deterministic sort: by z0,z1, then minY,minX, then maxX,maxY
|
|
218
|
+
const rnd = mulberry32(seed);
|
|
219
|
+
const cuts = [...cutoutBoxes].sort((a,b)=> (a.z0-b.z0) || (a.z1-b.z1) || (a.minY-b.minY) || (a.minX-b.minX) || (a.maxX-b.maxX) || (a.maxY-b.maxY) );
|
|
220
|
+
// Optional stable shuffle by seed to let user stress-test; keep deterministic
|
|
221
|
+
if (seed !== 0) {
|
|
222
|
+
for (let i=cuts.length-1;i>0;i--) {
|
|
223
|
+
const j = Math.floor(rnd()* (i+1));
|
|
224
|
+
const tmp = cuts[i]; cuts[i]=cuts[j]; cuts[j]=tmp;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
let free = [rootBox];
|
|
228
|
+
for (const c of cuts) {
|
|
229
|
+
free = subtractCutoutFromList(free, c, eps);
|
|
230
|
+
}
|
|
231
|
+
return free;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Merge along an axis where the other two extents exactly match (within EPS)
|
|
235
|
+
function mergeAlongAxis(boxes, axis, eps) {
|
|
236
|
+
if (boxes.length <= 1) return boxes;
|
|
237
|
+
const groups = new Map();
|
|
238
|
+
const R = (v)=> v.toFixed(12); // key rounding
|
|
239
|
+
|
|
240
|
+
function keyX(b){ return `y:${R(b.minY)}-${R(b.maxY)}|z:${b.z0}-${b.z1}`; }
|
|
241
|
+
function keyY(b){ return `x:${R(b.minX)}-${R(b.maxX)}|z:${b.z0}-${b.z1}`; }
|
|
242
|
+
function keyZ(b){ return `x:${R(b.minX)}-${R(b.maxX)}|y:${R(b.minY)}-${R(b.maxY)}`; }
|
|
243
|
+
|
|
244
|
+
const keyFn = axis==='X' ? keyX : axis==='Y' ? keyY : keyZ;
|
|
245
|
+
|
|
246
|
+
for (const b of boxes) {
|
|
247
|
+
const k = keyFn(b);
|
|
248
|
+
const arr = groups.get(k);
|
|
249
|
+
if (arr) arr.push(b); else groups.set(k, [b]);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const out = [];
|
|
253
|
+
for (const arr of groups.values()) {
|
|
254
|
+
if (axis === 'X') {
|
|
255
|
+
arr.sort((a,b)=> (a.minX-b.minX) || (a.maxX-b.maxX));
|
|
256
|
+
let cur = arr[0];
|
|
257
|
+
for (let i=1;i<arr.length;i++) {
|
|
258
|
+
const n = arr[i];
|
|
259
|
+
if (almostEq(cur.maxX, n.minX, eps)) {
|
|
260
|
+
cur = { ...cur, maxX: n.maxX };
|
|
261
|
+
} else {
|
|
262
|
+
out.push(cur); cur = n;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
out.push(cur);
|
|
266
|
+
} else if (axis === 'Y') {
|
|
267
|
+
arr.sort((a,b)=> (a.minY-b.minY) || (a.maxY-b.maxY));
|
|
268
|
+
let cur = arr[0];
|
|
269
|
+
for (let i=1;i<arr.length;i++) {
|
|
270
|
+
const n = arr[i];
|
|
271
|
+
if (almostEq(cur.maxY, n.minY, eps)) {
|
|
272
|
+
cur = { ...cur, maxY: n.maxY };
|
|
273
|
+
} else {
|
|
274
|
+
out.push(cur); cur = n;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
out.push(cur);
|
|
278
|
+
} else { // Z
|
|
279
|
+
arr.sort((a,b)=> (a.z0-b.z0) || (a.z1-b.z1));
|
|
280
|
+
let cur = arr[0];
|
|
281
|
+
for (let i=1;i<arr.length;i++) {
|
|
282
|
+
const n = arr[i];
|
|
283
|
+
if (cur.z1 === n.z0) {
|
|
284
|
+
cur = { ...cur, z1: n.z1 };
|
|
285
|
+
} else {
|
|
286
|
+
out.push(cur); cur = n;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
out.push(cur);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Apply merges in an axis order; iterate cycles until no change or maxCycles
|
|
296
|
+
function coalesce(boxes, order, eps, maxCycles=4) {
|
|
297
|
+
let prevLen = -1;
|
|
298
|
+
let cur = boxes.slice();
|
|
299
|
+
for (let cycle=0; cycle<maxCycles; cycle++) {
|
|
300
|
+
prevLen = cur.length;
|
|
301
|
+
for (const ax of order) {
|
|
302
|
+
cur = mergeAlongAxis(cur, ax, eps);
|
|
303
|
+
}
|
|
304
|
+
if (cur.length === prevLen) break;
|
|
305
|
+
}
|
|
306
|
+
return cur;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function permutations(arr) {
|
|
310
|
+
const res = [];
|
|
311
|
+
const used = Array(arr.length).fill(false);
|
|
312
|
+
const curr = [];
|
|
313
|
+
function backtrack(){
|
|
314
|
+
if (curr.length===arr.length){ res.push(curr.slice()); return; }
|
|
315
|
+
for(let i=0;i<arr.length;i++){
|
|
316
|
+
if (used[i]) continue;
|
|
317
|
+
used[i]=true; curr.push(arr[i]);
|
|
318
|
+
backtrack();
|
|
319
|
+
curr.pop(); used[i]=false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
backtrack();
|
|
323
|
+
return res;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function scoreBoxes(boxes, thickness, p) {
|
|
327
|
+
let s = 0;
|
|
328
|
+
for (const b of boxes) {
|
|
329
|
+
const dx = b.maxX - b.minX;
|
|
330
|
+
const dy = b.maxY - b.minY;
|
|
331
|
+
const dz = (b.z1 - b.z0) * thickness;
|
|
332
|
+
const vol = dx * dy * dz;
|
|
333
|
+
s += Math.pow(vol, p);
|
|
334
|
+
}
|
|
335
|
+
return s;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function totalVolume(boxes, thickness) {
|
|
339
|
+
let v=0;
|
|
340
|
+
for (const b of boxes) {
|
|
341
|
+
v += (b.maxX-b.minX)*(b.maxY-b.minY)*((b.z1-b.z0)*thickness);
|
|
342
|
+
}
|
|
343
|
+
return v;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function boxToRect3d(b) {
|
|
347
|
+
const zLayers = [];
|
|
348
|
+
for (let z=b.z0; z<b.z1; z++) zLayers.push(z);
|
|
349
|
+
return { minX:b.minX, minY:b.minY, maxX:b.maxX, maxY:b.maxY, zLayers };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function solveNoDiscretization(problem, options) {
|
|
353
|
+
const { Z_LAYER_THICKNESS, p, order, maxCycles, eps, seed } = options;
|
|
354
|
+
const rootZs = new Set(problem.rootRect.zLayers);
|
|
355
|
+
const rootBox = toBoxFromRoot(problem.rootRect);
|
|
356
|
+
const cutouts = problem.cutouts
|
|
357
|
+
.map(c => toBoxFromCutout(c, rootZs))
|
|
358
|
+
.filter(Boolean);
|
|
359
|
+
|
|
360
|
+
if (!nonEmptyBox(rootBox, eps)) throw new Error('Root box is empty.');
|
|
361
|
+
|
|
362
|
+
// Subtract all cutouts (exact; no rasterization)
|
|
363
|
+
const diffBoxes = subtractAll(rootBox, cutouts, eps, seed);
|
|
364
|
+
|
|
365
|
+
const orders = order === 'AUTO' ? permutations(['X','Y','Z']) : [order.split(',')];
|
|
366
|
+
|
|
367
|
+
let best = null, bestScore = -Infinity, bestOrder = null;
|
|
368
|
+
for (const ord of orders) {
|
|
369
|
+
const merged = coalesce(diffBoxes, ord, eps, maxCycles);
|
|
370
|
+
const sc = scoreBoxes(merged, Z_LAYER_THICKNESS, p);
|
|
371
|
+
if (sc > bestScore) { best = merged; bestScore = sc; bestOrder = ord.join(','); }
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
boxes: best,
|
|
375
|
+
rects: best.map(boxToRect3d),
|
|
376
|
+
score: bestScore,
|
|
377
|
+
orderUsed: bestOrder || 'X,Y,Z',
|
|
378
|
+
totalFreeVolume: totalVolume(best, Z_LAYER_THICKNESS)
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Simple deterministic PRNG for seed-based order stress testing
|
|
383
|
+
function mulberry32(a) {
|
|
384
|
+
return function() {
|
|
385
|
+
var t = a += 0x6D2B79F5;
|
|
386
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
387
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
388
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ---------- Three.js setup ----------
|
|
393
|
+
|
|
394
|
+
let renderer, scene, camera, controls;
|
|
395
|
+
let rootGroup, cutoutsGroup, outputGroup, axesHelper;
|
|
396
|
+
|
|
397
|
+
function initThree(container) {
|
|
398
|
+
const w = container.clientWidth;
|
|
399
|
+
const h = container.clientHeight;
|
|
400
|
+
|
|
401
|
+
renderer = new THREE.WebGLRenderer({ antialias:true });
|
|
402
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
403
|
+
renderer.setSize(w, h);
|
|
404
|
+
container.innerHTML = '';
|
|
405
|
+
container.appendChild(renderer.domElement);
|
|
406
|
+
|
|
407
|
+
scene = new THREE.Scene();
|
|
408
|
+
scene.background = new THREE.Color(0xf7f8fa);
|
|
409
|
+
|
|
410
|
+
camera = new THREE.PerspectiveCamera(45, w/h, 0.1, 10000);
|
|
411
|
+
camera.position.set(80, 80, 120);
|
|
412
|
+
|
|
413
|
+
controls = new THREE.OrbitControls(camera, renderer.domElement);
|
|
414
|
+
controls.enableDamping = true;
|
|
415
|
+
|
|
416
|
+
rootGroup = new THREE.Group();
|
|
417
|
+
cutoutsGroup = new THREE.Group();
|
|
418
|
+
outputGroup = new THREE.Group();
|
|
419
|
+
scene.add(rootGroup);
|
|
420
|
+
scene.add(cutoutsGroup);
|
|
421
|
+
scene.add(outputGroup);
|
|
422
|
+
|
|
423
|
+
axesHelper = new THREE.AxesHelper(50);
|
|
424
|
+
scene.add(axesHelper);
|
|
425
|
+
|
|
426
|
+
const amb = new THREE.AmbientLight(0xffffff, 0.9);
|
|
427
|
+
scene.add(amb);
|
|
428
|
+
const dir = new THREE.DirectionalLight(0xffffff, 0.6);
|
|
429
|
+
dir.position.set(1,2,3);
|
|
430
|
+
scene.add(dir);
|
|
431
|
+
|
|
432
|
+
window.addEventListener('resize', onResize);
|
|
433
|
+
function onResize(){
|
|
434
|
+
const w = container.clientWidth;
|
|
435
|
+
const h = container.clientHeight;
|
|
436
|
+
camera.aspect = w/h;
|
|
437
|
+
camera.updateProjectionMatrix();
|
|
438
|
+
renderer.setSize(w,h);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function animate(){ requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); }
|
|
442
|
+
animate();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Map problem XYZ -> Three.js (X, Y-up, Z-depth).
|
|
446
|
+
// Our problem Z (layers) maps to Y (up) in Three.
|
|
447
|
+
function makeMeshForBox(b, thickness, color, wire=false, opacity=0.45) {
|
|
448
|
+
const dx = b.maxX - b.minX;
|
|
449
|
+
const dz = b.maxY - b.minY; // map Y -> Three Z
|
|
450
|
+
const dh = (b.z1 - b.z0) * thickness; // height
|
|
451
|
+
const cx = (b.minX + b.maxX)/2;
|
|
452
|
+
const cz = (b.minY + b.maxY)/2;
|
|
453
|
+
const cy = ((b.z0 + b.z1)/2) * thickness;
|
|
454
|
+
|
|
455
|
+
const geom = new THREE.BoxGeometry(dx, dh, dz);
|
|
456
|
+
let mat;
|
|
457
|
+
if (wire) {
|
|
458
|
+
const edges = new THREE.EdgesGeometry(geom);
|
|
459
|
+
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color }));
|
|
460
|
+
line.position.set(cx, cy, cz);
|
|
461
|
+
return line;
|
|
462
|
+
} else {
|
|
463
|
+
mat = new THREE.MeshPhongMaterial({ color, opacity, transparent: true });
|
|
464
|
+
const mesh = new THREE.Mesh(geom, mat);
|
|
465
|
+
mesh.position.set(cx, cy, cz);
|
|
466
|
+
return mesh;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function clearGroups() {
|
|
471
|
+
for (const g of [rootGroup, cutoutsGroup, outputGroup]) {
|
|
472
|
+
while (g.children.length) g.remove(g.children[0]);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function fitCameraToBox(root, thickness) {
|
|
477
|
+
const dx = root.maxX - root.minX;
|
|
478
|
+
const dz = root.maxY - root.minY;
|
|
479
|
+
const dh = (Math.max(...root.zLayers) - Math.min(...root.zLayers) + 1) * thickness;
|
|
480
|
+
const size = Math.max(dx, dz, dh);
|
|
481
|
+
const dist = size * 2.0;
|
|
482
|
+
camera.position.set(root.maxX + dist*0.6, dh + dist, root.maxY + dist*0.6);
|
|
483
|
+
camera.near = Math.max(0.1, size/100);
|
|
484
|
+
camera.far = dist*10 + size*10;
|
|
485
|
+
camera.updateProjectionMatrix();
|
|
486
|
+
controls.target.set((root.minX+root.maxX)/2, dh/2, (root.minY+root.maxY)/2);
|
|
487
|
+
controls.update();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ---------- UI glue ----------
|
|
491
|
+
|
|
492
|
+
const elInput = document.getElementById('inputArea');
|
|
493
|
+
const elThick = document.getElementById('thickness');
|
|
494
|
+
const elExp = document.getElementById('exponent');
|
|
495
|
+
const elOrder = document.getElementById('axisOrder');
|
|
496
|
+
const elMaxCycles = document.getElementById('maxCycles');
|
|
497
|
+
const elEps = document.getElementById('eps');
|
|
498
|
+
const elSeed = document.getElementById('seed');
|
|
499
|
+
|
|
500
|
+
const elRun = document.getElementById('runBtn');
|
|
501
|
+
const elExample = document.getElementById('exampleBtn');
|
|
502
|
+
const elClear = document.getElementById('clearBtn');
|
|
503
|
+
|
|
504
|
+
const elStatus = document.getElementById('status');
|
|
505
|
+
const elBoxCount = document.getElementById('boxCount');
|
|
506
|
+
const elFreeVol = document.getElementById('freeVol');
|
|
507
|
+
const elScore = document.getElementById('scoreVal');
|
|
508
|
+
const elOrderUsed = document.getElementById('orderUsed');
|
|
509
|
+
const elNotes = document.getElementById('notes');
|
|
510
|
+
|
|
511
|
+
const elShowRoot = document.getElementById('showRoot');
|
|
512
|
+
const elShowCutouts = document.getElementById('showCutouts');
|
|
513
|
+
const elShowOutput = document.getElementById('showOutput');
|
|
514
|
+
const elWireOutput = document.getElementById('wireframeOutput');
|
|
515
|
+
|
|
516
|
+
function setStatus(text, ok=true) {
|
|
517
|
+
elStatus.textContent = text;
|
|
518
|
+
elStatus.className = ok ? 'ok' : 'warn';
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function loadExample() {
|
|
522
|
+
const example = {
|
|
523
|
+
rootRect: { minX: 0, minY: 0, maxX: 100, maxY: 60, zLayers: [0,1,2,3] },
|
|
524
|
+
cutouts: [
|
|
525
|
+
{ minX: 10, minY: 10, maxX: 40, maxY: 50, zLayers: [0,1] },
|
|
526
|
+
{ minX: 20, minY: 0, maxX: 30, maxY: 20, zLayers: [2,3] },
|
|
527
|
+
{ minX: 55, minY: 5, maxX: 95, maxY: 20, zLayers: [0,1,2,3] },
|
|
528
|
+
{ minX: 45, minY: 25, maxX: 70, maxY: 35, zLayers: [1,2] }
|
|
529
|
+
]
|
|
530
|
+
};
|
|
531
|
+
elInput.value = JSON.stringify(example, null, 2);
|
|
532
|
+
setStatus('Loaded example. Click Run.', true);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function parseProblemFromTextarea() {
|
|
536
|
+
let obj;
|
|
537
|
+
try { obj = JSON.parse(elInput.value); }
|
|
538
|
+
catch (e) { throw new Error('Invalid JSON: ' + e.message); }
|
|
539
|
+
if (!obj || !obj.rootRect || !Array.isArray(obj.cutouts)) {
|
|
540
|
+
throw new Error('Expected object: { rootRect, cutouts }');
|
|
541
|
+
}
|
|
542
|
+
const r = obj.rootRect;
|
|
543
|
+
const checkRect = (t) => {
|
|
544
|
+
if (typeof t.minX!=='number'||typeof t.maxX!=='number'||typeof t.minY!=='number'||typeof t.maxY!=='number'||!Array.isArray(t.zLayers)) {
|
|
545
|
+
throw new Error('Rect3d requires minX, minY, maxX, maxY, zLayers:number[]');
|
|
546
|
+
}
|
|
547
|
+
if (!(t.minX < t.maxX && t.minY < t.maxY)) throw new Error('Invalid bounds in a rect.');
|
|
548
|
+
};
|
|
549
|
+
checkRect(r);
|
|
550
|
+
obj.cutouts.forEach(checkRect);
|
|
551
|
+
return obj;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function renderResult(problem, result, options) {
|
|
555
|
+
clearGroups();
|
|
556
|
+
const thickness = options.Z_LAYER_THICKNESS;
|
|
557
|
+
|
|
558
|
+
// Root wireframe
|
|
559
|
+
if (elShowRoot.checked) {
|
|
560
|
+
const rootBox = toBoxFromRoot(problem.rootRect);
|
|
561
|
+
const rootWire = makeMeshForBox(rootBox, thickness, 0x111827, true);
|
|
562
|
+
rootGroup.add(rootWire);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Cutouts (red translucent)
|
|
566
|
+
if (elShowCutouts.checked) {
|
|
567
|
+
const rootZs = new Set(problem.rootRect.zLayers);
|
|
568
|
+
for (const c of problem.cutouts) {
|
|
569
|
+
const b = toBoxFromCutout(c, rootZs);
|
|
570
|
+
if (!b || !nonEmptyBox(b, EPS_DEFAULT)) continue;
|
|
571
|
+
const mesh = makeMeshForBox(b, thickness, 0xef4444, false, 0.35);
|
|
572
|
+
cutoutsGroup.add(mesh);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Output boxes (green/blue)
|
|
577
|
+
if (elShowOutput.checked) {
|
|
578
|
+
const wire = elWireOutput.checked;
|
|
579
|
+
for (const b of result.boxes) {
|
|
580
|
+
const mesh = makeMeshForBox(b, thickness, wire ? 0x2563eb : 0x10b981, wire, wire ? 1.0 : 0.50);
|
|
581
|
+
outputGroup.add(mesh);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Fit camera
|
|
586
|
+
fitCameraToBox(problem.rootRect, thickness);
|
|
587
|
+
|
|
588
|
+
// Update stats
|
|
589
|
+
elBoxCount.textContent = String(result.boxes.length);
|
|
590
|
+
elFreeVol.textContent = result.totalFreeVolume.toFixed(3);
|
|
591
|
+
elScore.textContent = result.score.toFixed(3);
|
|
592
|
+
elOrderUsed.textContent = result.orderUsed;
|
|
593
|
+
elNotes.textContent = `Returned ${result.rects.length} Rect3d objects. Toggle “Wireframe” if rendering many boxes.`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function runOnce() {
|
|
597
|
+
try {
|
|
598
|
+
const problem = parseProblemFromTextarea();
|
|
599
|
+
const thickness = parseFloat(elThick.value);
|
|
600
|
+
const p = parseFloat(elExp.value);
|
|
601
|
+
const axisVal = elOrder.value;
|
|
602
|
+
const maxCycles = Math.max(1, parseInt(elMaxCycles.value,10) || 4);
|
|
603
|
+
const eps = parseFloat(elEps.value) || EPS_DEFAULT;
|
|
604
|
+
const seed = parseInt(elSeed.value, 10) || 0;
|
|
605
|
+
|
|
606
|
+
if (!(thickness>0)) throw new Error('Z layer thickness must be > 0');
|
|
607
|
+
if (!(p>1)) throw new Error('Exponent p must be > 1');
|
|
608
|
+
|
|
609
|
+
const result = solveNoDiscretization(problem, {
|
|
610
|
+
Z_LAYER_THICKNESS: thickness,
|
|
611
|
+
p, order: axisVal, maxCycles, eps, seed
|
|
612
|
+
});
|
|
613
|
+
renderResult(problem, result, { Z_LAYER_THICKNESS: thickness });
|
|
614
|
+
|
|
615
|
+
setStatus('Success. Use the controls to adjust parameters.', true);
|
|
616
|
+
console.log('Output Rect3d[]:', result.rects);
|
|
617
|
+
window.__lastResult = result; // for console access
|
|
618
|
+
} catch (err) {
|
|
619
|
+
setStatus(String(err.message || err), false);
|
|
620
|
+
console.error(err);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Initialize
|
|
625
|
+
initThree(document.getElementById('view'));
|
|
626
|
+
elRun.addEventListener('click', runOnce);
|
|
627
|
+
elExample.addEventListener('click', loadExample);
|
|
628
|
+
elClear.addEventListener('click', () => { clearGroups(); setStatus('Scene cleared.'); });
|
|
629
|
+
|
|
630
|
+
elShowRoot.addEventListener('change', runOnce);
|
|
631
|
+
elShowCutouts.addEventListener('change', runOnce);
|
|
632
|
+
elShowOutput.addEventListener('change', runOnce);
|
|
633
|
+
elWireOutput.addEventListener('change', runOnce);
|
|
634
|
+
|
|
635
|
+
// Load example initially
|
|
636
|
+
loadExample();
|
|
637
|
+
})();
|
|
638
|
+
</script>
|
|
639
|
+
</body>
|
|
640
|
+
</html>
|
package/global.d.ts
ADDED
package/index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>React Cosmos Vite Renderer</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
package/lib/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./solvers/RectDiffSolver"
|