mirror of
https://github.com/CNCKitchen/stlTexturizer.git
synced 2026-04-07 22:11:32 +00:00
initial commit
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* 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 0–1)
|
||||
* @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;
|
||||
}
|
||||
Reference in New Issue
Block a user