mirror of
https://github.com/CNCKitchen/stlTexturizer.git
synced 2026-04-07 22:11:32 +00:00
Enhance subdivision algorithm to eliminate T-junctions with a two-step edge splitting approach
This commit is contained in:
+21
-23
@@ -138,38 +138,36 @@ function isBoundaryEdge(faces, vertFaces, v1, v2) {
|
|||||||
return shared < 2;
|
return shared < 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Guard 2: Link condition (non-manifold / pinch prevention) ────────────────
|
// ── Guard 2: Duplicate-face / pinch prevention ───────────────────────────────
|
||||||
// The set of common neighbours of v1 and v2 must equal exactly the apex
|
// After collapsing v2 into v1, every face of v2 that survives (i.e. does not
|
||||||
// vertices of their shared faces. Extra common neighbours mean collapsing
|
// share v1) gets v2 replaced by v1. If any such remapped face is identical to
|
||||||
// would fuse disconnected regions → non-manifold edge.
|
// a face already incident to v1, the collapse would create a duplicate → reject.
|
||||||
|
// This is the actual harm the link condition guards against, without the
|
||||||
|
// false-positives that the strict set-equality test produces on interior edges.
|
||||||
|
|
||||||
function hasLinkViolation(faces, vertFaces, v1, v2) {
|
function hasLinkViolation(faces, vertFaces, v1, v2) {
|
||||||
// Collect apex vertices of the shared faces
|
// Build a set of face "signatures" already incident to v1 (excluding shared faces).
|
||||||
const apices = new Set();
|
// A signature is the sorted triple of vertex indices, encoded as a string.
|
||||||
|
const v1Sigs = new Set();
|
||||||
for (const f of vertFaces[v1]) {
|
for (const f of vertFaces[v1]) {
|
||||||
if (faces[f * 3] < 0) continue;
|
if (faces[f * 3] < 0) continue;
|
||||||
const fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
|
const fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
|
||||||
if (fa === v2 || fb === v2 || fc === v2) {
|
if (fa === v2 || fb === v2 || fc === v2) continue; // shared face, will be deleted
|
||||||
if (fa !== v1 && fa !== v2) apices.add(fa);
|
const arr = [fa, fb, fc].sort((a, b) => a - b);
|
||||||
if (fb !== v1 && fb !== v2) apices.add(fb);
|
v1Sigs.add(`${arr[0]},${arr[1]},${arr[2]}`);
|
||||||
if (fc !== v1 && fc !== v2) apices.add(fc);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build neighbour sets for v1 and v2 from active faces
|
// Check every surviving face of v2 (after remapping v2→v1) for duplicates.
|
||||||
const nb1 = new Set(), nb2 = new Set();
|
|
||||||
for (const f of vertFaces[v1]) {
|
|
||||||
if (faces[f * 3] < 0) continue;
|
|
||||||
for (let k = 0; k < 3; k++) { const nb = faces[f * 3 + k]; if (nb !== v1) nb1.add(nb); }
|
|
||||||
}
|
|
||||||
for (const f of vertFaces[v2]) {
|
for (const f of vertFaces[v2]) {
|
||||||
if (faces[f * 3] < 0) continue;
|
if (faces[f * 3] < 0) continue;
|
||||||
for (let k = 0; k < 3; k++) { const nb = faces[f * 3 + k]; if (nb !== v2) nb2.add(nb); }
|
const fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
|
||||||
}
|
if (fa === v1 || fb === v1 || fc === v1) continue; // shared face, will be deleted
|
||||||
|
// Remap v2 → v1
|
||||||
// Check common neighbours match apices exactly
|
const ra = fa === v2 ? v1 : fa;
|
||||||
for (const nb of nb1) {
|
const rb = fb === v2 ? v1 : fb;
|
||||||
if (nb !== v2 && nb2.has(nb) && !apices.has(nb)) return true;
|
const rc = fc === v2 ? v1 : fc;
|
||||||
|
const arr = [ra, rb, rc].sort((a, b) => a - b);
|
||||||
|
if (v1Sigs.has(`${arr[0]},${arr[1]},${arr[2]}`)) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-24
@@ -48,12 +48,43 @@ export function subdivide(geometry, maxEdgeLength, onProgress) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── One subdivision pass ──────────────────────────────────────────────────────
|
// ── One subdivision pass ──────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Uses a two-step approach to eliminate T-junctions:
|
||||||
|
//
|
||||||
|
// Step 1 – scan ALL triangles and mark every edge whose squared length
|
||||||
|
// exceeds maxSq. Because this is global, both triangles that
|
||||||
|
// share an edge always agree on whether to split it.
|
||||||
|
//
|
||||||
|
// Step 2 – rebuild the index list. Each triangle is handled according to
|
||||||
|
// how many of its three edges are marked:
|
||||||
|
//
|
||||||
|
// 0 edges → keep as-is
|
||||||
|
// 1 edge → 2 sub-triangles (bisect the one long edge)
|
||||||
|
// 2 edges → 3 sub-triangles (fan from the vertex opposite the short edge)
|
||||||
|
// 3 edges → 4 sub-triangles (classic 1→4 midpoint subdivision – most regular)
|
||||||
|
//
|
||||||
|
// The 2- and 3-edge cases are new compared to the old single-edge split and
|
||||||
|
// produce significantly more regular results. Thin slivers with one very
|
||||||
|
// long edge still produce chains of thin children (unavoidable without moving
|
||||||
|
// vertices off the surface), but the mesh is now crack-free in all cases.
|
||||||
|
|
||||||
function subdividePass(positions, normals, indices, maxEdgeLength, safetyCap) {
|
function subdividePass(positions, normals, indices, maxEdgeLength, safetyCap) {
|
||||||
const maxSq = maxEdgeLength * maxEdgeLength;
|
const maxSq = maxEdgeLength * maxEdgeLength;
|
||||||
const midCache = new Map();
|
const midCache = new Map();
|
||||||
|
|
||||||
|
// ── Step 1: globally mark edges that need splitting ─────────────────────
|
||||||
|
const splitEdges = new Set();
|
||||||
|
for (let t = 0; t < indices.length; t += 3) {
|
||||||
|
const a = indices[t], b = indices[t + 1], c = indices[t + 2];
|
||||||
|
if (edgeLenSq(positions, a, b) > maxSq) splitEdges.add(edgeKey(a, b));
|
||||||
|
if (edgeLenSq(positions, b, c) > maxSq) splitEdges.add(edgeKey(b, c));
|
||||||
|
if (edgeLenSq(positions, c, a) > maxSq) splitEdges.add(edgeKey(c, a));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (splitEdges.size === 0) return { newIndices: indices, changed: false };
|
||||||
|
|
||||||
|
// ── Step 2: rebuild index list ───────────────────────────────────────────
|
||||||
const nextIndices = [];
|
const nextIndices = [];
|
||||||
let changed = false;
|
|
||||||
|
|
||||||
for (let t = 0; t < indices.length; t += 3) {
|
for (let t = 0; t < indices.length; t += 3) {
|
||||||
// Safety cap: stop splitting, carry remaining triangles as-is
|
// Safety cap: stop splitting, carry remaining triangles as-is
|
||||||
@@ -62,41 +93,94 @@ function subdividePass(positions, normals, indices, maxEdgeLength, safetyCap) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const a = indices[t];
|
const a = indices[t], b = indices[t + 1], c = indices[t + 2];
|
||||||
const b = indices[t + 1];
|
const sAB = splitEdges.has(edgeKey(a, b));
|
||||||
const c = indices[t + 2];
|
const sBC = splitEdges.has(edgeKey(b, c));
|
||||||
|
const sCA = splitEdges.has(edgeKey(c, a));
|
||||||
|
const n = (sAB ? 1 : 0) + (sBC ? 1 : 0) + (sCA ? 1 : 0);
|
||||||
|
|
||||||
const ab = edgeLenSq(positions, a, b);
|
if (n === 0) {
|
||||||
const bc = edgeLenSq(positions, b, c);
|
// ── 0-split: keep triangle ─────────────────────────────────────────
|
||||||
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);
|
nextIndices.push(a, b, c);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
changed = true;
|
} else if (n === 3) {
|
||||||
|
// ── 3-split: 1→4 regular midpoint subdivision ──────────────────────
|
||||||
|
//
|
||||||
|
// a
|
||||||
|
// / \
|
||||||
|
// mCA─mAB
|
||||||
|
// / \ / \
|
||||||
|
// c─mBC───b
|
||||||
|
//
|
||||||
|
const mAB = getMidpoint(positions, normals, midCache, a, b);
|
||||||
|
const mBC = getMidpoint(positions, normals, midCache, b, c);
|
||||||
|
const mCA = getMidpoint(positions, normals, midCache, c, a);
|
||||||
|
nextIndices.push(
|
||||||
|
a, mAB, mCA,
|
||||||
|
mAB, b, mBC,
|
||||||
|
mCA, mBC, c,
|
||||||
|
mAB, mBC, mCA,
|
||||||
|
);
|
||||||
|
|
||||||
|
} else if (n === 1) {
|
||||||
|
// ── 1-split: bisect the one marked edge → 2 sub-triangles ──────────
|
||||||
|
if (sAB) {
|
||||||
|
const m = getMidpoint(positions, normals, midCache, a, b);
|
||||||
|
nextIndices.push(a, m, c, m, b, c);
|
||||||
|
} else if (sBC) {
|
||||||
|
const m = getMidpoint(positions, normals, midCache, b, c);
|
||||||
|
nextIndices.push(a, b, m, a, m, c);
|
||||||
|
} else { // sCA
|
||||||
|
const m = getMidpoint(positions, normals, midCache, c, a);
|
||||||
|
nextIndices.push(a, b, m, m, b, c);
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
} else {
|
||||||
const m = getMidpoint(positions, normals, midCache, c, a);
|
// ── 2-split: 3 sub-triangles, fan from the untouched-edge vertex ───
|
||||||
nextIndices.push(a, b, m, m, b, c);
|
//
|
||||||
|
// For each case the unsplit-edge vertex forms a small corner triangle
|
||||||
|
// with its two adjacent midpoints; the remaining quadrilateral is
|
||||||
|
// split along the diagonal that connects those two midpoints to the
|
||||||
|
// opposite vertices, preserving consistent CCW winding throughout.
|
||||||
|
|
||||||
|
if (!sAB) { // sBC + sCA: fan from C
|
||||||
|
const mBC = getMidpoint(positions, normals, midCache, b, c);
|
||||||
|
const mCA = getMidpoint(positions, normals, midCache, c, a);
|
||||||
|
nextIndices.push(
|
||||||
|
a, b, mBC,
|
||||||
|
a, mBC, mCA,
|
||||||
|
c, mCA, mBC,
|
||||||
|
);
|
||||||
|
} else if (!sBC) { // sAB + sCA: fan from A
|
||||||
|
const mAB = getMidpoint(positions, normals, midCache, a, b);
|
||||||
|
const mCA = getMidpoint(positions, normals, midCache, c, a);
|
||||||
|
nextIndices.push(
|
||||||
|
a, mAB, mCA,
|
||||||
|
mAB, b, c,
|
||||||
|
mAB, c, mCA,
|
||||||
|
);
|
||||||
|
} else { // sAB + sBC: fan from B
|
||||||
|
const mAB = getMidpoint(positions, normals, midCache, a, b);
|
||||||
|
const mBC = getMidpoint(positions, normals, midCache, b, c);
|
||||||
|
nextIndices.push(
|
||||||
|
b, mBC, mAB,
|
||||||
|
a, mAB, mBC,
|
||||||
|
a, mBC, c,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { newIndices: nextIndices, changed };
|
return { newIndices: nextIndices, changed: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 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];
|
||||||
|
|||||||
Reference in New Issue
Block a user