mirror of
https://github.com/CNCKitchen/stlTexturizer.git
synced 2026-04-07 22:11:32 +00:00
perf: replace string-key maps with numeric flat arrays in hot paths
Displacement, subdivision and exclusion all used template-string keys for vertex dedup and edge lookup maps. Replaced with a single numeric dedup pass + flat typed arrays (Float64Array / Uint8Array), cutting displacement time by ~2.5x and subdivision by ~2.4x on a 68k tri STL. - displacement.js: vertex dedup → flat arrays for smooth normals, zone areas, masked fractions, displacement cache, excluded set - subdivision.js: numeric edge keys (a*maxV+b) instead of template strings - exclusion.js: numeric edge keys, BFS queue.shift() → index pointer, adjacency Map → Array, TypedArray.set() for overlay copy - mapping.js: Math.pow(x,4) → x²·x², cos/sin cached per computeUV call, applyTransform signature changed to accept precomputed cos/sin
This commit is contained in:
+76
-77
@@ -45,8 +45,6 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
|||||||
const settingsWithAspect = { ...settings, textureAspectU: aspectU, textureAspectV: aspectV };
|
const settingsWithAspect = { ...settings, textureAspectU: aspectU, textureAspectV: aspectV };
|
||||||
|
|
||||||
const QUANT = 1e4;
|
const QUANT = 1e4;
|
||||||
const posKey = (x, y, z) =>
|
|
||||||
`${Math.round(x * QUANT)}_${Math.round(y * QUANT)}_${Math.round(z * QUANT)}`;
|
|
||||||
|
|
||||||
// ── WHY GAPS HAPPEN ───────────────────────────────────────────────────────
|
// ── WHY GAPS HAPPEN ───────────────────────────────────────────────────────
|
||||||
// The mesh is non-indexed (unrolled): every triangle has its own copy of
|
// The mesh is non-indexed (unrolled): every triangle has its own copy of
|
||||||
@@ -64,33 +62,47 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
|||||||
// underlying geometry is still faceted (the subdivision didn't change it),
|
// underlying geometry is still faceted (the subdivision didn't change it),
|
||||||
// so printed edges remain sharp.
|
// so printed edges remain sharp.
|
||||||
|
|
||||||
|
// ── Vertex dedup pass: position → numeric ID via one-time string-map pass ─
|
||||||
|
const _dedupMap = new Map();
|
||||||
|
let _nextId = 0;
|
||||||
|
const vertexId = new Uint32Array(count);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const x = posAttr.getX(i), y = posAttr.getY(i), z = posAttr.getZ(i);
|
||||||
|
const key = `${Math.round(x * QUANT)}_${Math.round(y * QUANT)}_${Math.round(z * QUANT)}`;
|
||||||
|
let id = _dedupMap.get(key);
|
||||||
|
if (id === undefined) { id = _nextId++; _dedupMap.set(key, id); }
|
||||||
|
vertexId[i] = id;
|
||||||
|
}
|
||||||
|
const uniqueCount = _nextId;
|
||||||
|
|
||||||
// ── Pass 1: accumulate area-weighted smooth normals per unique position ───
|
// ── Pass 1: accumulate area-weighted smooth normals per unique position ───
|
||||||
// Map: posKey → [nx, ny, nz] (unnormalised sum)
|
// Flat arrays indexed by vertex dedup ID (replaces Map<string, ...>)
|
||||||
const smoothNrmMap = new Map();
|
const smoothNrmX = new Float64Array(uniqueCount);
|
||||||
// zoneAreaMap: posKey → [xArea, yArea, zArea] (cubic mapping only)
|
const smoothNrmY = new Float64Array(uniqueCount);
|
||||||
// Tracks the total adjacent face area in each cubic projection zone (X/Y/Z dominant).
|
const smoothNrmZ = new Float64Array(uniqueCount);
|
||||||
// Seam-edge vertices that border two zones get a blend proportional to face area,
|
|
||||||
// eliminating the mixed-projection artefact on seam-crossing triangles.
|
// zoneArea: per-axis face area for cubic mapping (replaces zoneAreaMap)
|
||||||
const zoneAreaMap = new Map();
|
const zoneAreaX = new Float64Array(uniqueCount);
|
||||||
// maskedFracMap: posKey → [maskedArea, totalArea]
|
const zoneAreaY = new Float64Array(uniqueCount);
|
||||||
// Tracks the fraction of surrounding face area that is masked so boundary
|
const zoneAreaZ = new Float64Array(uniqueCount);
|
||||||
// vertices get a smooth displacement blend instead of a hard on/off cutoff.
|
|
||||||
const maskedFracMap = new Map();
|
// maskedFrac: [maskedArea, totalArea] per unique vertex (replaces maskedFracMap)
|
||||||
|
const maskedFracMasked = new Float64Array(uniqueCount);
|
||||||
|
const maskedFracTotal = new Float64Array(uniqueCount);
|
||||||
|
|
||||||
// Optional per-vertex exclusion weights threaded through by subdivision.js.
|
// Optional per-vertex exclusion weights threaded through by subdivision.js.
|
||||||
// A face's user-exclusion flag = average of its 3 vertex weights > 0.99.
|
// A face's user-exclusion flag = average of its 3 vertex weights > 0.99.
|
||||||
const ewAttr = geometry.attributes.excludeWeight || null;
|
const ewAttr = geometry.attributes.excludeWeight || null;
|
||||||
// Per-face user-exclusion flag: stored separately from maskedFracMap so that
|
// Per-face user-exclusion flag: stored separately from maskedFrac so that
|
||||||
// user-excluded faces do NOT bleed reduced displacement into adjacent faces
|
// user-excluded faces do NOT bleed reduced displacement into adjacent faces
|
||||||
// via shared vertices (maskedFracMap is only for angle-based blending).
|
// via shared vertices (maskedFrac is only for angle-based blending).
|
||||||
const userExcludedFaces = ewAttr ? new Uint8Array(count / 3) : null;
|
const userExcludedFaces = ewAttr ? new Uint8Array(count / 3) : null;
|
||||||
// Positions that belong to at least one user-excluded face. Any included-face
|
// Positions that belong to at least one user-excluded face (replaces excludedPosSet).
|
||||||
// vertex whose original position is in this set sits on the seam boundary; we
|
const excludedPos = ewAttr ? new Uint8Array(uniqueCount) : null;
|
||||||
// pin it to zero displacement so both sides of the seam end up at the same
|
|
||||||
// final position. Without this the mesh has an open crack at the mask
|
// Displacement cache: one sample per unique vertex (replaces dispCache Map)
|
||||||
// boundary, which causes the QEM decimator to treat the excluded patch as an
|
const dispCacheVal = new Float64Array(uniqueCount);
|
||||||
// isolated open-mesh island and collapse it to nothing (missing triangles).
|
const dispCacheSet = new Uint8Array(uniqueCount);
|
||||||
const excludedPosSet = ewAttr ? new Set() : null;
|
|
||||||
|
|
||||||
for (let t = 0; t < count; t += 3) {
|
for (let t = 0; t < count; t += 3) {
|
||||||
vA.fromBufferAttribute(posAttr, t);
|
vA.fromBufferAttribute(posAttr, t);
|
||||||
@@ -141,9 +153,8 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let v = 0; v < 3; v++) {
|
for (let v = 0; v < 3; v++) {
|
||||||
tmpPos.fromBufferAttribute(posAttr, t + v);
|
const vid = vertexId[t + v];
|
||||||
const k = posKey(tmpPos.x, tmpPos.y, tmpPos.z);
|
if (userExcluded && excludedPos) excludedPos[vid] = 1;
|
||||||
if (userExcluded && excludedPosSet) excludedPosSet.add(k);
|
|
||||||
// Use the buffer normal (from subdivision) weighted by face area.
|
// Use the buffer normal (from subdivision) weighted by face area.
|
||||||
// The subdivision pipeline splits indexed vertices at sharp dihedral
|
// The subdivision pipeline splits indexed vertices at sharp dihedral
|
||||||
// edges (>30°), so the interpolated buffer normals are smooth across
|
// edges (>30°), so the interpolated buffer normals are smooth across
|
||||||
@@ -151,44 +162,33 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
|||||||
// This eliminates visible faceting steps on round surfaces while still
|
// This eliminates visible faceting steps on round surfaces while still
|
||||||
// preserving hard edges.
|
// preserving hard edges.
|
||||||
tmpNrm.fromBufferAttribute(nrmAttr, t + v);
|
tmpNrm.fromBufferAttribute(nrmAttr, t + v);
|
||||||
const existing = smoothNrmMap.get(k);
|
smoothNrmX[vid] += tmpNrm.x * faceArea;
|
||||||
if (existing) {
|
smoothNrmY[vid] += tmpNrm.y * faceArea;
|
||||||
existing[0] += tmpNrm.x * faceArea;
|
smoothNrmZ[vid] += tmpNrm.z * faceArea;
|
||||||
existing[1] += tmpNrm.y * faceArea;
|
|
||||||
existing[2] += tmpNrm.z * faceArea;
|
|
||||||
} else {
|
|
||||||
smoothNrmMap.set(k, [tmpNrm.x * faceArea, tmpNrm.y * faceArea, tmpNrm.z * faceArea]);
|
|
||||||
}
|
|
||||||
if (czX > 1e-12 || czY > 1e-12 || czZ > 1e-12) {
|
if (czX > 1e-12 || czY > 1e-12 || czZ > 1e-12) {
|
||||||
const za = zoneAreaMap.get(k);
|
zoneAreaX[vid] += czX;
|
||||||
if (za) { za[0] += czX; za[1] += czY; za[2] += czZ; }
|
zoneAreaY[vid] += czY;
|
||||||
else { zoneAreaMap.set(k, [czX, czY, czZ]); }
|
zoneAreaZ[vid] += czZ;
|
||||||
}
|
|
||||||
const mf = maskedFracMap.get(k);
|
|
||||||
if (mf) {
|
|
||||||
if (faceMasked) mf[0] += faceArea;
|
|
||||||
mf[1] += faceArea;
|
|
||||||
} else {
|
|
||||||
maskedFracMap.set(k, [faceMasked ? faceArea : 0, faceArea]);
|
|
||||||
}
|
}
|
||||||
|
if (faceMasked) maskedFracMasked[vid] += faceArea;
|
||||||
|
maskedFracTotal[vid] += faceArea;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalise each accumulated normal
|
// Normalise each accumulated normal
|
||||||
smoothNrmMap.forEach((n) => {
|
for (let id = 0; id < uniqueCount; id++) {
|
||||||
const len = Math.sqrt(n[0]*n[0] + n[1]*n[1] + n[2]*n[2]) || 1;
|
const len = Math.sqrt(smoothNrmX[id]*smoothNrmX[id] + smoothNrmY[id]*smoothNrmY[id] + smoothNrmZ[id]*smoothNrmZ[id]) || 1;
|
||||||
n[0] /= len; n[1] /= len; n[2] /= len;
|
smoothNrmX[id] /= len; smoothNrmY[id] /= len; smoothNrmZ[id] /= len;
|
||||||
});
|
}
|
||||||
|
|
||||||
// ── Pass 2: sample displacement texture once per unique position ──────────
|
// ── Pass 2: sample displacement texture once per unique position ──────────
|
||||||
const dispCache = new Map(); // posKey → grey [0, 1]
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
tmpPos.fromBufferAttribute(posAttr, i);
|
const vid = vertexId[i];
|
||||||
const k = posKey(tmpPos.x, tmpPos.y, tmpPos.z);
|
if (dispCacheSet[vid]) continue;
|
||||||
if (dispCache.has(k)) continue;
|
dispCacheSet[vid] = 1;
|
||||||
|
|
||||||
const sn = smoothNrmMap.get(k);
|
tmpPos.fromBufferAttribute(posAttr, i);
|
||||||
|
|
||||||
// Cubic: zone-area-weighted sampling with a stable per-face dominant axis.
|
// Cubic: zone-area-weighted sampling with a stable per-face dominant axis.
|
||||||
// Non-seam vertices use their single zone purely; seam-edge vertices that
|
// Non-seam vertices use their single zone purely; seam-edge vertices that
|
||||||
@@ -201,39 +201,39 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
|||||||
// top (0,0,1) and bottom (0,0,-1) face normals cancel at shared edge vertices,
|
// top (0,0,1) and bottom (0,0,-1) face normals cancel at shared edge vertices,
|
||||||
// leaving a horizontal smooth normal. computeUV would then pick the wrong
|
// leaving a horizontal smooth normal. computeUV would then pick the wrong
|
||||||
// cubic projection axis, making those faces appear untextured. The face-
|
// cubic projection axis, making those faces appear untextured. The face-
|
||||||
// normal-based zoneAreaMap is immune to this because it classifies faces by
|
// normal-based zoneArea arrays are immune to this because they classify faces
|
||||||
// their geometric cross-product normal, not the averaged vertex normal.
|
// by their geometric cross-product normal, not the averaged vertex normal.
|
||||||
if (settings.mappingMode === 6 /* MODE_CUBIC */) {
|
if (settings.mappingMode === 6 /* MODE_CUBIC */) {
|
||||||
const za = zoneAreaMap.get(k);
|
const zaX = zoneAreaX[vid], zaY = zoneAreaY[vid], zaZ = zoneAreaZ[vid];
|
||||||
const total = za ? za[0] + za[1] + za[2] : 0;
|
const total = zaX + zaY + zaZ;
|
||||||
if (total > 0) {
|
if (total > 0) {
|
||||||
const md = Math.max(bounds.size.x, bounds.size.y, bounds.size.z, 1e-6);
|
const md = Math.max(bounds.size.x, bounds.size.y, bounds.size.z, 1e-6);
|
||||||
const rotRad = (settings.rotation ?? 0) * Math.PI / 180;
|
const rotRad = (settings.rotation ?? 0) * Math.PI / 180;
|
||||||
let grey = 0;
|
let grey = 0;
|
||||||
if (za[0] > 0) { // X-dominant zone → YZ projection
|
if (zaX > 0) { // X-dominant zone → YZ projection
|
||||||
let rawU = (tmpPos.y-bounds.min.y)/md;
|
let rawU = (tmpPos.y-bounds.min.y)/md;
|
||||||
if (sn[0] < 0) rawU = -rawU; // flip U for -X faces
|
if (smoothNrmX[vid] < 0) rawU = -rawU; // flip U for -X faces
|
||||||
const uv = _cubicUV(rawU, (tmpPos.z-bounds.min.z)/md, settings, rotRad, aspectU, aspectV);
|
const uv = _cubicUV(rawU, (tmpPos.z-bounds.min.z)/md, settings, rotRad, aspectU, aspectV);
|
||||||
grey += sampleBilinear(imageData.data, imgWidth, imgHeight, uv.u, uv.v) * (za[0]/total);
|
grey += sampleBilinear(imageData.data, imgWidth, imgHeight, uv.u, uv.v) * (zaX/total);
|
||||||
}
|
}
|
||||||
if (za[1] > 0) { // Y-dominant zone → XZ projection
|
if (zaY > 0) { // Y-dominant zone → XZ projection
|
||||||
let rawU = (tmpPos.x-bounds.min.x)/md;
|
let rawU = (tmpPos.x-bounds.min.x)/md;
|
||||||
if (sn[1] > 0) rawU = -rawU; // flip U for +Y faces
|
if (smoothNrmY[vid] > 0) rawU = -rawU; // flip U for +Y faces
|
||||||
const uv = _cubicUV(rawU, (tmpPos.z-bounds.min.z)/md, settings, rotRad, aspectU, aspectV);
|
const uv = _cubicUV(rawU, (tmpPos.z-bounds.min.z)/md, settings, rotRad, aspectU, aspectV);
|
||||||
grey += sampleBilinear(imageData.data, imgWidth, imgHeight, uv.u, uv.v) * (za[1]/total);
|
grey += sampleBilinear(imageData.data, imgWidth, imgHeight, uv.u, uv.v) * (zaY/total);
|
||||||
}
|
}
|
||||||
if (za[2] > 0) { // Z-dominant zone → XY projection
|
if (zaZ > 0) { // Z-dominant zone → XY projection
|
||||||
let rawU = (tmpPos.x-bounds.min.x)/md;
|
let rawU = (tmpPos.x-bounds.min.x)/md;
|
||||||
if (sn[2] < 0) rawU = -rawU; // flip U for -Z faces
|
if (smoothNrmZ[vid] < 0) rawU = -rawU; // flip U for -Z faces
|
||||||
const uv = _cubicUV(rawU, (tmpPos.y-bounds.min.y)/md, settings, rotRad, aspectU, aspectV);
|
const uv = _cubicUV(rawU, (tmpPos.y-bounds.min.y)/md, settings, rotRad, aspectU, aspectV);
|
||||||
grey += sampleBilinear(imageData.data, imgWidth, imgHeight, uv.u, uv.v) * (za[2]/total);
|
grey += sampleBilinear(imageData.data, imgWidth, imgHeight, uv.u, uv.v) * (zaZ/total);
|
||||||
}
|
}
|
||||||
dispCache.set(k, grey);
|
dispCacheVal[vid] = grey;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpNrm.set(sn[0], sn[1], sn[2]);
|
tmpNrm.set(smoothNrmX[vid], smoothNrmY[vid], smoothNrmZ[vid]);
|
||||||
|
|
||||||
const uvResult = computeUV(tmpPos, tmpNrm, settings.mappingMode, settingsWithAspect, bounds);
|
const uvResult = computeUV(tmpPos, tmpNrm, settings.mappingMode, settingsWithAspect, bounds);
|
||||||
let grey;
|
let grey;
|
||||||
@@ -245,7 +245,7 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
|||||||
} else {
|
} else {
|
||||||
grey = sampleBilinear(imageData.data, imgWidth, imgHeight, uvResult.u, uvResult.v);
|
grey = sampleBilinear(imageData.data, imgWidth, imgHeight, uvResult.u, uvResult.v);
|
||||||
}
|
}
|
||||||
dispCache.set(k, grey);
|
dispCacheVal[vid] = grey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Pass 3: displace every vertex copy by the same vector ─────────────────
|
// ── Pass 3: displace every vertex copy by the same vector ─────────────────
|
||||||
@@ -258,9 +258,8 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
|||||||
tmpPos.fromBufferAttribute(posAttr, i);
|
tmpPos.fromBufferAttribute(posAttr, i);
|
||||||
tmpNrm.fromBufferAttribute(nrmAttr, i);
|
tmpNrm.fromBufferAttribute(nrmAttr, i);
|
||||||
|
|
||||||
const k = posKey(tmpPos.x, tmpPos.y, tmpPos.z);
|
const vid = vertexId[i];
|
||||||
const sn = smoothNrmMap.get(k);
|
const grey = dispCacheVal[vid];
|
||||||
const grey = dispCache.get(k);
|
|
||||||
|
|
||||||
// User-excluded faces get zero displacement; only angle-based masking uses
|
// User-excluded faces get zero displacement; only angle-based masking uses
|
||||||
// the smooth per-vertex blend so neighbours are never unintentionally dimmed.
|
// the smooth per-vertex blend so neighbours are never unintentionally dimmed.
|
||||||
@@ -268,15 +267,15 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett
|
|||||||
// Pin included-face vertices that share a position with an excluded face.
|
// Pin included-face vertices that share a position with an excluded face.
|
||||||
// This seals the open crack at the mask boundary so the mesh stays watertight
|
// This seals the open crack at the mask boundary so the mesh stays watertight
|
||||||
// and the decimator cannot collapse the excluded patch to zero faces.
|
// and the decimator cannot collapse the excluded patch to zero faces.
|
||||||
const isSealedBoundary = !isFaceExcluded && excludedPosSet && excludedPosSet.has(k);
|
const isSealedBoundary = !isFaceExcluded && excludedPos && excludedPos[vid] === 1;
|
||||||
const mf = maskedFracMap.get(k) || [0, 1];
|
const mfTotal = maskedFracTotal[vid];
|
||||||
const maskedFrac = mf[1] > 0 ? mf[0] / mf[1] : 0;
|
const maskedFrac = mfTotal > 0 ? maskedFracMasked[vid] / mfTotal : 0;
|
||||||
const centeredGrey = settings.symmetricDisplacement ? (grey - 0.5) : grey;
|
const centeredGrey = settings.symmetricDisplacement ? (grey - 0.5) : grey;
|
||||||
const disp = (isFaceExcluded || isSealedBoundary) ? 0 : (1 - maskedFrac) * centeredGrey * settings.amplitude;
|
const disp = (isFaceExcluded || isSealedBoundary) ? 0 : (1 - maskedFrac) * centeredGrey * settings.amplitude;
|
||||||
|
|
||||||
const newX = tmpPos.x + sn[0] * disp;
|
const newX = tmpPos.x + smoothNrmX[vid] * disp;
|
||||||
const newY = tmpPos.y + sn[1] * disp;
|
const newY = tmpPos.y + smoothNrmY[vid] * disp;
|
||||||
let newZ = tmpPos.z + sn[2] * disp;
|
let newZ = tmpPos.z + smoothNrmZ[vid] * disp;
|
||||||
|
|
||||||
// Prevent boundary vertices from poking through the masked surface in Z.
|
// Prevent boundary vertices from poking through the masked surface in Z.
|
||||||
// Only triggers for vertices that are partly masked (maskedFrac > 0) and
|
// Only triggers for vertices that are partly masked (maskedFrac > 0) and
|
||||||
|
|||||||
+33
-28
@@ -23,7 +23,7 @@ const quantKey = (x, y, z) =>
|
|||||||
*
|
*
|
||||||
* @param {THREE.BufferGeometry} geometry – non-indexed
|
* @param {THREE.BufferGeometry} geometry – non-indexed
|
||||||
* @returns {{
|
* @returns {{
|
||||||
* adjacency: Map<number, Array<{neighbor:number, angle:number}>>,
|
* adjacency: Array<Array<{neighbor:number, angle:number}>>,
|
||||||
* centroids: Float32Array (triCount × 3, world-space centroid per triangle)
|
* centroids: Float32Array (triCount × 3, world-space centroid per triangle)
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
@@ -71,24 +71,27 @@ export function buildAdjacency(geometry) {
|
|||||||
|
|
||||||
// Build edge → triangle list (two triangles share an edge iff they share two
|
// Build edge → triangle list (two triangles share an edge iff they share two
|
||||||
// vertex positions after quantization-based deduplication).
|
// vertex positions after quantization-based deduplication).
|
||||||
|
// Vertex-dedup pass: assign a numeric ID to each unique quantised position.
|
||||||
|
const posToId = new Map();
|
||||||
|
let nextId = 0;
|
||||||
|
const vertId = new Uint32Array(triCount * 3);
|
||||||
|
for (let i = 0; i < triCount * 3; i++) {
|
||||||
|
const x = posAttr.getX(i), y = posAttr.getY(i), z = posAttr.getZ(i);
|
||||||
|
const key = `${Math.round(x*QUANT)}_${Math.round(y*QUANT)}_${Math.round(z*QUANT)}`;
|
||||||
|
let id = posToId.get(key);
|
||||||
|
if (id === undefined) { id = nextId++; posToId.set(key, id); }
|
||||||
|
vertId[i] = id;
|
||||||
|
}
|
||||||
|
// nextId^2 < MAX_SAFE_INTEGER → safe up to ~94M unique vertices
|
||||||
|
const numEdgeKey = (a, b) => a < b ? a * nextId + b : b * nextId + a;
|
||||||
|
|
||||||
const edgeMap = new Map();
|
const edgeMap = new Map();
|
||||||
const makeEdgeKey = (ax, ay, az, bx, by, bz) => {
|
const edgePairs = [0, 1, 0, 2, 1, 2]; // vertex-index pairs within triangle
|
||||||
const ka = quantKey(ax, ay, az);
|
|
||||||
const kb = quantKey(bx, by, bz);
|
|
||||||
return ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let t = 0; t < triCount; t++) {
|
for (let t = 0; t < triCount; t++) {
|
||||||
const i = t * 3;
|
const base = t * 3;
|
||||||
vA.fromBufferAttribute(posAttr, i);
|
for (let e = 0; e < 6; e += 2) {
|
||||||
vB.fromBufferAttribute(posAttr, i + 1);
|
const ek = numEdgeKey(vertId[base + edgePairs[e]], vertId[base + edgePairs[e + 1]]);
|
||||||
vC.fromBufferAttribute(posAttr, i + 2);
|
|
||||||
|
|
||||||
const ekAB = makeEdgeKey(vA.x, vA.y, vA.z, vB.x, vB.y, vB.z);
|
|
||||||
const ekBC = makeEdgeKey(vB.x, vB.y, vB.z, vC.x, vC.y, vC.z);
|
|
||||||
const ekCA = makeEdgeKey(vC.x, vC.y, vC.z, vA.x, vA.y, vA.z);
|
|
||||||
|
|
||||||
for (const ek of [ekAB, ekBC, ekCA]) {
|
|
||||||
const entry = edgeMap.get(ek);
|
const entry = edgeMap.get(ek);
|
||||||
if (entry) entry.push(t);
|
if (entry) entry.push(t);
|
||||||
else edgeMap.set(ek, [t]);
|
else edgeMap.set(ek, [t]);
|
||||||
@@ -96,8 +99,9 @@ export function buildAdjacency(geometry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert edge map to adjacency list with per-edge dihedral angle
|
// Convert edge map to adjacency list with per-edge dihedral angle
|
||||||
const adjacency = new Map();
|
// Array from buildAdjacency
|
||||||
for (let t = 0; t < triCount; t++) adjacency.set(t, []);
|
const adjacency = new Array(triCount);
|
||||||
|
for (let t = 0; t < triCount; t++) adjacency[t] = [];
|
||||||
|
|
||||||
for (const [, tris] of edgeMap) {
|
for (const [, tris] of edgeMap) {
|
||||||
if (tris.length !== 2) continue;
|
if (tris.length !== 2) continue;
|
||||||
@@ -106,8 +110,8 @@ export function buildAdjacency(geometry) {
|
|||||||
const nBx = faceNormals[b * 3], nBy = faceNormals[b * 3 + 1], nBz = faceNormals[b * 3 + 2];
|
const nBx = faceNormals[b * 3], nBy = faceNormals[b * 3 + 1], nBz = faceNormals[b * 3 + 2];
|
||||||
const dot = Math.max(-1, Math.min(1, nAx * nBx + nAy * nBy + nAz * nBz));
|
const dot = Math.max(-1, Math.min(1, nAx * nBx + nAy * nBy + nAz * nBz));
|
||||||
const angleDeg = Math.acos(dot) * (180 / Math.PI);
|
const angleDeg = Math.acos(dot) * (180 / Math.PI);
|
||||||
adjacency.get(a).push({ neighbor: b, angle: angleDeg });
|
adjacency[a].push({ neighbor: b, angle: angleDeg });
|
||||||
adjacency.get(b).push({ neighbor: a, angle: angleDeg });
|
adjacency[b].push({ neighbor: a, angle: angleDeg });
|
||||||
}
|
}
|
||||||
|
|
||||||
return { adjacency, centroids, boundRadii };
|
return { adjacency, centroids, boundRadii };
|
||||||
@@ -120,16 +124,17 @@ export function buildAdjacency(geometry) {
|
|||||||
* Spreads across edges whose dihedral angle ≤ thresholdDeg.
|
* Spreads across edges whose dihedral angle ≤ thresholdDeg.
|
||||||
*
|
*
|
||||||
* @param {number} seedTriIdx
|
* @param {number} seedTriIdx
|
||||||
* @param {Map<number, Array<{neighbor:number, angle:number}>>} adjacency
|
* @param {Array<Array<{neighbor:number, angle:number}>>} adjacency
|
||||||
* @param {number} thresholdDeg
|
* @param {number} thresholdDeg
|
||||||
* @returns {Set<number>} set of triangle indices in the filled region
|
* @returns {Set<number>} set of triangle indices in the filled region
|
||||||
*/
|
*/
|
||||||
export function bucketFill(seedTriIdx, adjacency, thresholdDeg) {
|
export function bucketFill(seedTriIdx, adjacency, thresholdDeg) {
|
||||||
const visited = new Set([seedTriIdx]);
|
const visited = new Set([seedTriIdx]);
|
||||||
const queue = [seedTriIdx];
|
const queue = [seedTriIdx];
|
||||||
while (queue.length > 0) {
|
let head = 0;
|
||||||
const cur = queue.shift();
|
while (head < queue.length) {
|
||||||
const neighbors = adjacency.get(cur);
|
const cur = queue[head++];
|
||||||
|
const neighbors = adjacency[cur];
|
||||||
if (!neighbors) continue;
|
if (!neighbors) continue;
|
||||||
for (const { neighbor, angle } of neighbors) {
|
for (const { neighbor, angle } of neighbors) {
|
||||||
if (!visited.has(neighbor) && angle <= thresholdDeg) {
|
if (!visited.has(neighbor) && angle <= thresholdDeg) {
|
||||||
@@ -163,15 +168,15 @@ export function buildExclusionOverlayGeo(geometry, faceSet, invert = false) {
|
|||||||
for (let t = 0; t < total; t++) {
|
for (let t = 0; t < total; t++) {
|
||||||
if (faceSet.has(t)) continue;
|
if (faceSet.has(t)) continue;
|
||||||
const src = t * 9;
|
const src = t * 9;
|
||||||
for (let i = 0; i < 9; i++) outPos[dst + i] = srcPos[src + i];
|
outPos.set(srcPos.subarray(src, src + 9), dst);
|
||||||
if (outNrm) for (let i = 0; i < 9; i++) outNrm[dst + i] = srcNrm[src + i];
|
if (outNrm) outNrm.set(srcNrm.subarray(src, src + 9), dst);
|
||||||
dst += 9;
|
dst += 9;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const t of faceSet) {
|
for (const t of faceSet) {
|
||||||
const src = t * 9;
|
const src = t * 9;
|
||||||
for (let i = 0; i < 9; i++) outPos[dst + i] = srcPos[src + i];
|
outPos.set(srcPos.subarray(src, src + 9), dst);
|
||||||
if (outNrm) for (let i = 0; i < 9; i++) outNrm[dst + i] = srcNrm[src + i];
|
if (outNrm) outNrm.set(srcNrm.subarray(src, src + 9), dst);
|
||||||
dst += 9;
|
dst += 9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-22
@@ -112,6 +112,8 @@ export function computeUV(pos, normal, mode, settings, bounds) {
|
|||||||
const scaleV = (settings.scaleV) / aV;
|
const scaleV = (settings.scaleV) / aV;
|
||||||
const { offsetU, offsetV } = settings;
|
const { offsetU, offsetV } = settings;
|
||||||
const rotRad = (settings.rotation ?? 0) * Math.PI / 180;
|
const rotRad = (settings.rotation ?? 0) * Math.PI / 180;
|
||||||
|
const cosR = Math.cos(rotRad);
|
||||||
|
const sinR = Math.sin(rotRad);
|
||||||
const maxDim = Math.max(size.x, size.y, size.z);
|
const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
const md = Math.max(maxDim, 1e-6);
|
const md = Math.max(maxDim, 1e-6);
|
||||||
|
|
||||||
@@ -161,14 +163,14 @@ export function computeUV(pos, normal, mode, settings, bounds) {
|
|||||||
const d = uRaw < 0.5 ? uRaw : uRaw - 1.0;
|
const d = uRaw < 0.5 ? uRaw : uRaw - 1.0;
|
||||||
const tRaw = (d + seamBand) / (2.0 * seamBand);
|
const tRaw = (d + seamBand) / (2.0 * seamBand);
|
||||||
const t = tRaw * tRaw * (3 - 2 * tRaw); // smoothstep
|
const t = tRaw * tRaw * (3 - 2 * tRaw); // smoothstep
|
||||||
const tLeft = applyTransform(1.0 + d, vSide, scaleU, scaleV, offsetU, offsetV, rotRad);
|
const tLeft = applyTransform(1.0 + d, vSide, scaleU, scaleV, offsetU, offsetV, cosR, sinR);
|
||||||
const tRight = applyTransform(d, vSide, scaleU, scaleV, offsetU, offsetV, rotRad);
|
const tRight = applyTransform(d, vSide, scaleU, scaleV, offsetU, offsetV, cosR, sinR);
|
||||||
sideSamples = [
|
sideSamples = [
|
||||||
{ u: tRight.u, v: tRight.v, w: t },
|
{ u: tRight.u, v: tRight.v, w: t },
|
||||||
{ u: tLeft.u, v: tLeft.v, w: 1 - t },
|
{ u: tLeft.u, v: tLeft.v, w: 1 - t },
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
const tSide = applyTransform(uRaw, vSide, scaleU, scaleV, offsetU, offsetV, rotRad);
|
const tSide = applyTransform(uRaw, vSide, scaleU, scaleV, offsetU, offsetV, cosR, sinR);
|
||||||
sideSamples = [{ u: tSide.u, v: tSide.v, w: 1 }];
|
sideSamples = [{ u: tSide.u, v: tSide.v, w: 1 }];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +191,7 @@ export function computeUV(pos, normal, mode, settings, bounds) {
|
|||||||
|
|
||||||
const uCap = rx / C + 0.5;
|
const uCap = rx / C + 0.5;
|
||||||
const vCap = ry / C + 0.5;
|
const vCap = ry / C + 0.5;
|
||||||
const tCap = applyTransform(uCap, vCap, scaleU, scaleV, offsetU, offsetV, rotRad);
|
const tCap = applyTransform(uCap, vCap, scaleU, scaleV, offsetU, offsetV, cosR, sinR);
|
||||||
|
|
||||||
if (capW >= 1) {
|
if (capW >= 1) {
|
||||||
return tCap;
|
return tCap;
|
||||||
@@ -218,8 +220,8 @@ export function computeUV(pos, normal, mode, settings, bounds) {
|
|||||||
const d = uRaw < 0.5 ? uRaw : uRaw - 1.0;
|
const d = uRaw < 0.5 ? uRaw : uRaw - 1.0;
|
||||||
const tRaw = (d + seamBand) / (2.0 * seamBand);
|
const tRaw = (d + seamBand) / (2.0 * seamBand);
|
||||||
const t = tRaw * tRaw * (3 - 2 * tRaw); // smoothstep
|
const t = tRaw * tRaw * (3 - 2 * tRaw); // smoothstep
|
||||||
const tLeft = applyTransform(1.0 + d, vRaw, scaleU, scaleV, offsetU, offsetV, rotRad);
|
const tLeft = applyTransform(1.0 + d, vRaw, scaleU, scaleV, offsetU, offsetV, cosR, sinR);
|
||||||
const tRight = applyTransform(d, vRaw, scaleU, scaleV, offsetU, offsetV, rotRad);
|
const tRight = applyTransform(d, vRaw, scaleU, scaleV, offsetU, offsetV, cosR, sinR);
|
||||||
return {
|
return {
|
||||||
triplanar: true,
|
triplanar: true,
|
||||||
samples: [
|
samples: [
|
||||||
@@ -244,9 +246,9 @@ export function computeUV(pos, normal, mode, settings, bounds) {
|
|||||||
if (normal.y > 0) xzU = -xzU;
|
if (normal.y > 0) xzU = -xzU;
|
||||||
let xyU = (pos.x - min.x) / md;
|
let xyU = (pos.x - min.x) / md;
|
||||||
if (normal.z < 0) xyU = -xyU;
|
if (normal.z < 0) xyU = -xyU;
|
||||||
const tYZ = applyTransform(yzU, (pos.z - min.z) / md, scaleU, scaleV, offsetU, offsetV, rotRad);
|
const tYZ = applyTransform(yzU, (pos.z - min.z) / md, scaleU, scaleV, offsetU, offsetV, cosR, sinR);
|
||||||
const tXZ = applyTransform(xzU, (pos.z - min.z) / md, scaleU, scaleV, offsetU, offsetV, rotRad);
|
const tXZ = applyTransform(xzU, (pos.z - min.z) / md, scaleU, scaleV, offsetU, offsetV, cosR, sinR);
|
||||||
const tXY = applyTransform(xyU, (pos.y - min.y) / md, scaleU, scaleV, offsetU, offsetV, rotRad);
|
const tXY = applyTransform(xyU, (pos.y - min.y) / md, scaleU, scaleV, offsetU, offsetV, cosR, sinR);
|
||||||
|
|
||||||
if (weights.x > 0.999) return tYZ;
|
if (weights.x > 0.999) return tYZ;
|
||||||
if (weights.y > 0.999) return tXZ;
|
if (weights.y > 0.999) return tXZ;
|
||||||
@@ -268,10 +270,10 @@ export function computeUV(pos, normal, mode, settings, bounds) {
|
|||||||
const ax = Math.abs(normal.x);
|
const ax = Math.abs(normal.x);
|
||||||
const ay = Math.abs(normal.y);
|
const ay = Math.abs(normal.y);
|
||||||
const az = Math.abs(normal.z);
|
const az = Math.abs(normal.z);
|
||||||
const pw = 4.0;
|
const ax2 = ax * ax, ay2 = ay * ay, az2 = az * az;
|
||||||
const bx = Math.pow(ax, pw);
|
const bx = ax2 * ax2;
|
||||||
const by = Math.pow(ay, pw);
|
const by = ay2 * ay2;
|
||||||
const bz = Math.pow(az, pw);
|
const bz = az2 * az2;
|
||||||
const sum = bx + by + bz + 1e-6;
|
const sum = bx + by + bz + 1e-6;
|
||||||
const wx = bx / sum;
|
const wx = bx / sum;
|
||||||
const wy = by / sum;
|
const wy = by / sum;
|
||||||
@@ -304,25 +306,24 @@ export function computeUV(pos, normal, mode, settings, bounds) {
|
|||||||
return {
|
return {
|
||||||
triplanar: true,
|
triplanar: true,
|
||||||
samples: [
|
samples: [
|
||||||
{ ...applyTransform(uvXY.u, uvXY.v, scaleU, scaleV, offsetU, offsetV, rotRad), w: uvXY.w },
|
{ ...applyTransform(uvXY.u, uvXY.v, scaleU, scaleV, offsetU, offsetV, cosR, sinR), w: uvXY.w },
|
||||||
{ ...applyTransform(uvXZ.u, uvXZ.v, scaleU, scaleV, offsetU, offsetV, rotRad), w: uvXZ.w },
|
{ ...applyTransform(uvXZ.u, uvXZ.v, scaleU, scaleV, offsetU, offsetV, cosR, sinR), w: uvXZ.w },
|
||||||
{ ...applyTransform(uvYZ.u, uvYZ.v, scaleU, scaleV, offsetU, offsetV, rotRad), w: uvYZ.w },
|
{ ...applyTransform(uvYZ.u, uvYZ.v, scaleU, scaleV, offsetU, offsetV, cosR, sinR), w: uvYZ.w },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return applyTransform(u, v, scaleU, scaleV, offsetU, offsetV, rotRad);
|
return applyTransform(u, v, scaleU, scaleV, offsetU, offsetV, cosR, sinR);
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyTransform(u, v, scaleU, scaleV, offsetU, offsetV, rotRad) {
|
function applyTransform(u, v, scaleU, scaleV, offsetU, offsetV, cosR, sinR) {
|
||||||
let uu = u / scaleU + offsetU;
|
let uu = u / scaleU + offsetU;
|
||||||
let vv = v / scaleV + offsetV;
|
let vv = v / scaleV + offsetV;
|
||||||
if (rotRad !== 0) {
|
if (cosR !== 1 || sinR !== 0) {
|
||||||
const c = Math.cos(rotRad), s = Math.sin(rotRad);
|
|
||||||
uu -= 0.5; vv -= 0.5;
|
uu -= 0.5; vv -= 0.5;
|
||||||
const ru = c * uu - s * vv;
|
const ru = cosR * uu - sinR * vv;
|
||||||
const rv = s * uu + c * vv;
|
const rv = sinR * uu + cosR * vv;
|
||||||
uu = ru + 0.5; vv = rv + 0.5;
|
uu = ru + 0.5; vv = rv + 0.5;
|
||||||
}
|
}
|
||||||
return { triplanar: false, u: fract(uu), v: fract(vv) };
|
return { triplanar: false, u: fract(uu), v: fract(vv) };
|
||||||
|
|||||||
+10
-8
@@ -119,13 +119,17 @@ export async function subdivide(geometry, maxEdgeLength, onProgress, faceWeights
|
|||||||
function subdividePass(positions, normals, weights, indices, maxEdgeLength, safetyCap, faceExcluded = null, canonIdx = null, posCanonMap = null, faceParentId = null) {
|
function subdividePass(positions, normals, weights, indices, maxEdgeLength, safetyCap, faceExcluded = null, canonIdx = null, posCanonMap = null, faceParentId = null) {
|
||||||
const maxSq = maxEdgeLength * maxEdgeLength;
|
const maxSq = maxEdgeLength * maxEdgeLength;
|
||||||
const midCache = new Map();
|
const midCache = new Map();
|
||||||
|
midCache._maxV = positions.length / 3; // set before any getMidpoint calls
|
||||||
|
|
||||||
// When canonIdx is available (accurate/export mode), use position-canonical
|
// When canonIdx is available (accurate/export mode), use position-canonical
|
||||||
// edge keys so split-vertex faces on both sides of a sharp edge see the same
|
// edge keys so split-vertex faces on both sides of a sharp edge see the same
|
||||||
// split decision. Otherwise (fast/preview mode) use simple index-based keys.
|
// split decision. Otherwise (fast/preview mode) use simple index-based keys.
|
||||||
|
// NOTE: _maxV is computed once at pass start. During the pass, positions grows
|
||||||
|
// via getMidpoint, but splitEdges and midCache only use indices < _maxV.
|
||||||
|
const _maxV = positions.length / 3;
|
||||||
const _edgeKey = canonIdx
|
const _edgeKey = canonIdx
|
||||||
? (a, b) => { const ca = canonIdx[a], cb = canonIdx[b]; return ca < cb ? `${ca}:${cb}` : `${cb}:${ca}`; }
|
? (a, b) => { const ca = canonIdx[a], cb = canonIdx[b]; return ca < cb ? ca * _maxV + cb : cb * _maxV + ca; }
|
||||||
: (a, b) => a < b ? `${a}:${b}` : `${b}:${a}`;
|
: (a, b) => a < b ? a * _maxV + b : b * _maxV + a;
|
||||||
|
|
||||||
// ── Step 1: globally mark edges that need splitting ─────────────────────
|
// ── Step 1: globally mark edges that need splitting ─────────────────────
|
||||||
// Excluded triangles do NOT proactively mark their own edges – their
|
// Excluded triangles do NOT proactively mark their own edges – their
|
||||||
@@ -264,11 +268,6 @@ function subdividePass(positions, normals, weights, indices, maxEdgeLength, safe
|
|||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Canonical order key for an undirected edge – matches the getMidpoint cache key. */
|
|
||||||
function edgeKey(a, b) {
|
|
||||||
return a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function edgeLenSq(pos, a, b) {
|
function edgeLenSq(pos, a, b) {
|
||||||
const dx = pos[a*3] - pos[b*3];
|
const dx = pos[a*3] - pos[b*3];
|
||||||
const dy = pos[a*3+1] - pos[b*3+1];
|
const dy = pos[a*3+1] - pos[b*3+1];
|
||||||
@@ -277,7 +276,10 @@ function edgeLenSq(pos, a, b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMidpoint(positions, normals, weights, cache, a, b, canonIdx, posCanonMap) {
|
function getMidpoint(positions, normals, weights, cache, a, b, canonIdx, posCanonMap) {
|
||||||
const key = a < b ? `${a}:${b}` : `${b}:${a}`;
|
// Numeric edge key: uses _maxV set on the cache by subdividePass at pass start.
|
||||||
|
// All lookups use indices < _maxV, so this is safe even as positions grows.
|
||||||
|
const _mv = cache._maxV;
|
||||||
|
const key = a < b ? a * _mv + b : b * _mv + a;
|
||||||
if (cache.has(key)) return cache.get(key);
|
if (cache.has(key)) return cache.get(key);
|
||||||
|
|
||||||
// Midpoint position
|
// Midpoint position
|
||||||
|
|||||||
Reference in New Issue
Block a user