Files
archived-stlTexturizer/js/subdivision.js
T
2026-03-16 20:37:32 +01:00

187 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Edge-based adaptive mesh subdivision.
*
* @param {THREE.BufferGeometry} geometry non-indexed input from STLLoader
* @param {number} maxEdgeLength maximum allowed edge length (same unit as STL)
* @param {number} maxTriangles hard cap on output triangle count
* @param {function} [onProgress] optional callback(fraction 01)
* @returns {{ geometry: THREE.BufferGeometry, limitReached: boolean }}
*/
import * as THREE from 'three';
const QUANTISE = 1e4;
// ── Public entry point ───────────────────────────────────────────────────────
export function subdivide(geometry, maxEdgeLength, maxTriangles, onProgress) {
const { positions, normals, indices } = toIndexed(geometry);
const maxIterations = 12;
let currentIndices = indices;
let limitReached = false;
for (let iter = 0; iter < maxIterations; iter++) {
const triCount = currentIndices.length / 3;
if (triCount >= maxTriangles) {
limitReached = true;
break;
}
const { newIndices, changed } = subdividePass(
positions, normals, currentIndices, maxEdgeLength, maxTriangles
);
currentIndices = newIndices;
// Check if the pass itself hit the limit
if (newIndices.length / 3 >= maxTriangles) {
limitReached = true;
}
if (onProgress) onProgress(Math.min(0.95, (iter + 1) / maxIterations));
if (!changed || limitReached) break;
}
return { geometry: toNonIndexed(positions, normals, currentIndices), limitReached };
}
// ── One subdivision pass ──────────────────────────────────────────────────────
function subdividePass(positions, normals, indices, maxEdgeLength, maxTriangles) {
const maxSq = maxEdgeLength * maxEdgeLength;
const midCache = new Map();
const nextIndices = [];
let changed = false;
for (let t = 0; t < indices.length; t += 3) {
// Hard stop: don't add more triangles once the cap is reached
if (nextIndices.length / 3 >= maxTriangles) {
// Push remaining unsplit triangles as-is
for (let r = t; r < indices.length; r++) nextIndices.push(indices[r]);
break;
}
const a = indices[t];
const b = indices[t + 1];
const c = indices[t + 2];
const ab = edgeLenSq(positions, a, b);
const bc = edgeLenSq(positions, b, c);
const ca = edgeLenSq(positions, c, a);
const longest = Math.max(ab, bc, ca);
if (longest <= maxSq) {
// Triangle is fine keep as is
nextIndices.push(a, b, c);
continue;
}
changed = true;
// Split the longest edge
if (longest === ab) {
const m = getMidpoint(positions, normals, midCache, a, b);
nextIndices.push(a, m, c, m, b, c);
} else if (longest === bc) {
const m = getMidpoint(positions, normals, midCache, b, c);
nextIndices.push(a, b, m, a, m, c);
} else {
const m = getMidpoint(positions, normals, midCache, c, a);
nextIndices.push(a, b, m, m, b, c);
}
}
return { newIndices: nextIndices, changed };
}
// ── Helpers ──────────────────────────────────────────────────────────────────
function edgeLenSq(pos, a, b) {
const dx = pos[a*3] - pos[b*3];
const dy = pos[a*3+1] - pos[b*3+1];
const dz = pos[a*3+2] - pos[b*3+2];
return dx*dx + dy*dy + dz*dz;
}
function getMidpoint(positions, normals, cache, a, b) {
const key = a < b ? `${a}:${b}` : `${b}:${a}`;
if (cache.has(key)) return cache.get(key);
// Midpoint position
const mx = (positions[a*3] + positions[b*3]) / 2;
const my = (positions[a*3+1] + positions[b*3+1]) / 2;
const mz = (positions[a*3+2] + positions[b*3+2]) / 2;
// Midpoint normal (average + normalise)
const nx = normals[a*3] + normals[b*3];
const ny = normals[a*3+1] + normals[b*3+1];
const nz = normals[a*3+2] + normals[b*3+2];
const nl = Math.sqrt(nx*nx + ny*ny + nz*nz) || 1;
const idx = (positions.length / 3) | 0;
positions.push(mx, my, mz);
normals.push(nx / nl, ny / nl, nz / nl);
cache.set(key, idx);
return idx;
}
// ── Non-indexed → indexed conversion ────────────────────────────────────────
function toIndexed(geometry) {
const posAttr = geometry.attributes.position;
const nrmAttr = geometry.attributes.normal;
const positions = [];
const normals = [];
const indices = [];
const vertMap = new Map();
const n = posAttr.count;
for (let i = 0; i < n; i++) {
const px = posAttr.getX(i);
const py = posAttr.getY(i);
const pz = posAttr.getZ(i);
const nx_ = nrmAttr ? nrmAttr.getX(i) : 0;
const ny_ = nrmAttr ? nrmAttr.getY(i) : 0;
const nz_ = nrmAttr ? nrmAttr.getZ(i) : 1;
const key = `${Math.round(px * QUANTISE)}_${Math.round(py * QUANTISE)}_${Math.round(pz * QUANTISE)}`;
let idx = vertMap.get(key);
if (idx === undefined) {
idx = positions.length / 3;
positions.push(px, py, pz);
normals.push(nx_, ny_, nz_);
vertMap.set(key, idx);
}
indices.push(idx);
}
return { positions, normals, indices };
}
// ── Indexed → non-indexed ────────────────────────────────────────────────────
function toNonIndexed(positions, normals, indices) {
const triCount = indices.length / 3;
const posArray = new Float32Array(triCount * 9);
const nrmArray = new Float32Array(triCount * 9);
for (let t = 0; t < triCount; t++) {
for (let v = 0; v < 3; v++) {
const vidx = indices[t * 3 + v];
posArray[t * 9 + v * 3] = positions[vidx * 3];
posArray[t * 9 + v * 3 + 1] = positions[vidx * 3 + 1];
posArray[t * 9 + v * 3 + 2] = positions[vidx * 3 + 2];
nrmArray[t * 9 + v * 3] = normals[vidx * 3];
nrmArray[t * 9 + v * 3 + 1] = normals[vidx * 3 + 1];
nrmArray[t * 9 + v * 3 + 2] = normals[vidx * 3 + 2];
}
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
geo.setAttribute('normal', new THREE.BufferAttribute(nrmArray, 3));
return geo;
}