diff --git a/index.html b/index.html index 53a8428..f3e2e47 100644 --- a/index.html +++ b/index.html @@ -9,7 +9,10 @@ // Apply saved theme before first paint to avoid flash (function() { const t = localStorage.getItem('stlt-theme'); - if (t !== 'dark') document.documentElement.setAttribute('data-theme', 'light'); + // If user never picked a theme, respect their OS preference + const prefersDark = t ? t === 'dark' + : window.matchMedia('(prefers-color-scheme: dark)').matches; + if (!prefersDark) document.documentElement.setAttribute('data-theme', 'light'); })(); // Apply saved language before first paint to avoid flash (function() { diff --git a/js/decimation.js b/js/decimation.js index 6b5c23a..eda0b12 100644 --- a/js/decimation.js +++ b/js/decimation.js @@ -48,8 +48,8 @@ const CREASE_WEIGHT = 1e4; // quadric penalty weight for crease edges // Module-level scratch arrays for hasLinkViolation — avoids new Map() per call. // Size 128 exceeds the maximum practical vertex valence in any STL mesh. -const _hlvHi = new Float64Array(128); -const _hlvLo = new Int32Array(128); +const _hlvHi = new Float64Array(512); +const _hlvLo = new Int32Array(512); // ── Public API ─────────────────────────────────────────────────────────────── @@ -97,12 +97,23 @@ export async function decimate(geometry, targetTriangles, onProgress) { const initFaces = activeFaces; const toRemove = initFaces - targetTriangles; let lastProg = 0; - let collapses = 0; + let iterations = 0; while (activeFaces > targetTriangles && heap.size() > 0) { const idx = heap.pop(); if (idx < 0) break; + // Yield periodically based on total iterations (including rejections) + // to keep the UI responsive. Critical for flat / low-displacement + // surfaces where most collapses are rejected by the safety guards. + if ((++iterations & 4095) === 0) { + await new Promise(r => setTimeout(r, 0)); + if (onProgress) { + const p = Math.min(1, (initFaces - activeFaces) / toRemove); + if (p - lastProg > 0.005) { onProgress(p); lastProg = p; } + } + } + const v1 = heap.getV1(idx), v2 = heap.getV2(idx); const ver1 = heap.getVer1(idx), ver2 = heap.getVer2(idx); const px = heap.getPx(idx), py = heap.getPy(idx), pz = heap.getPz(idx); @@ -170,13 +181,7 @@ export async function decimate(geometry, targetTriangles, onProgress) { } } - if (onProgress && (++collapses & 511) === 0) { - const p = Math.min(1, (initFaces - activeFaces) / toRemove); - if (p - lastProg > 0.015) { - onProgress(p); lastProg = p; - await new Promise(r => setTimeout(r, 0)); - } - } + } if (onProgress) onProgress(1); @@ -498,14 +503,25 @@ function pushEdge(heap, quadrics, positions, version, v1, v2) { const e1 = evalQSum(quadrics, v1, v2, positions[v1*3], positions[v1*3+1], positions[v1*3+2]); const e2 = evalQSum(quadrics, v1, v2, positions[v2*3], positions[v2*3+1], positions[v2*3+2]); const em = evalQSum(quadrics, v1, v2, mx, my, mz); - if (e1 <= e2 && e1 <= em) { px = positions[v1*3]; py = positions[v1*3+1]; pz = positions[v1*3+2]; } - else if (e2 <= em) { px = positions[v2*3]; py = positions[v2*3+1]; pz = positions[v2*3+2]; } - else { px = mx; py = my; pz = mz; } + // Prefer midpoint when costs are near-equal (degenerate / flat surfaces). + // Midpoint minimises displacement of adjacent triangles, reducing normal + // flips and preventing the collapse loop from stalling on coplanar geometry. + const eMin = Math.min(e1, e2, em); + const eTol = eMin * 1e-2 + 1e-12; + if (em <= eMin + eTol) { px = mx; py = my; pz = mz; } + else if (e1 <= e2) { px = positions[v1*3]; py = positions[v1*3+1]; pz = positions[v1*3+2]; } + else { px = positions[v2*3]; py = positions[v2*3+1]; pz = positions[v2*3+2]; } } const cost = evalQSum(quadrics, v1, v2, px, py, pz); - // Snapshot both vertices' versions so the pop-side check can detect staleness - heap.push(cost, v1, v2, version[v1], version[v2], px, py, pz); + // Tiny edge-length tiebreaker: on degenerate (flat) surfaces where QEM + // costs are ~0, prefer collapsing shorter edges first for better triangle + // quality and fewer guard rejections. + const dx = positions[v2*3] - positions[v1*3]; + const dy = positions[v2*3+1] - positions[v1*3+1]; + const dz = positions[v2*3+2] - positions[v1*3+2]; + heap.push(cost + (dx*dx + dy*dy + dz*dz) * 1e-8, + v1, v2, version[v1], version[v2], px, py, pz); } // ── Indexed <-> Non-indexed conversion ────────────────────────────────────── diff --git a/js/presetTextures.js b/js/presetTextures.js index af7ad4b..7f6509a 100644 --- a/js/presetTextures.js +++ b/js/presetTextures.js @@ -34,6 +34,7 @@ const IMAGE_PRESETS = [ { name: 'Bubble', url: 'textures/bubble.jpg' }, { name: 'Carbon Fiber', url: 'textures/carbonFiber.jpg' }, { name: 'Crystal', url: 'textures/crystal.jpg' }, + { name: 'Dots', url: 'textures/dots.jpg' }, { name: 'Grip Surface', url: 'textures/gripSurface.jpg' }, { name: 'Hexagons', url: 'textures/hexagons.jpg' }, { name: 'Knitting', url: 'textures/knitting.jpg' }, diff --git a/textures/dots.jpg b/textures/dots.jpg new file mode 100644 index 0000000..91c0e59 Binary files /dev/null and b/textures/dots.jpg differ diff --git a/textures/hexagons.jpg b/textures/hexagons.jpg index 003da8c..3213be6 100644 Binary files a/textures/hexagons.jpg and b/textures/hexagons.jpg differ