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;
|
||||
}
|
||||
|
||||
// ── Guard 2: Link condition (non-manifold / pinch prevention) ────────────────
|
||||
// The set of common neighbours of v1 and v2 must equal exactly the apex
|
||||
// vertices of their shared faces. Extra common neighbours mean collapsing
|
||||
// would fuse disconnected regions → non-manifold edge.
|
||||
// ── Guard 2: Duplicate-face / pinch prevention ───────────────────────────────
|
||||
// After collapsing v2 into v1, every face of v2 that survives (i.e. does not
|
||||
// share v1) gets v2 replaced by v1. If any such remapped face is identical to
|
||||
// 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) {
|
||||
// Collect apex vertices of the shared faces
|
||||
const apices = new Set();
|
||||
// Build a set of face "signatures" already incident to v1 (excluding shared faces).
|
||||
// A signature is the sorted triple of vertex indices, encoded as a string.
|
||||
const v1Sigs = new Set();
|
||||
for (const f of vertFaces[v1]) {
|
||||
if (faces[f * 3] < 0) continue;
|
||||
const fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
|
||||
if (fa === v2 || fb === v2 || fc === v2) {
|
||||
if (fa !== v1 && fa !== v2) apices.add(fa);
|
||||
if (fb !== v1 && fb !== v2) apices.add(fb);
|
||||
if (fc !== v1 && fc !== v2) apices.add(fc);
|
||||
}
|
||||
if (fa === v2 || fb === v2 || fc === v2) continue; // shared face, will be deleted
|
||||
const arr = [fa, fb, fc].sort((a, b) => a - b);
|
||||
v1Sigs.add(`${arr[0]},${arr[1]},${arr[2]}`);
|
||||
}
|
||||
|
||||
// Build neighbour sets for v1 and v2 from active faces
|
||||
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); }
|
||||
}
|
||||
// Check every surviving face of v2 (after remapping v2→v1) for duplicates.
|
||||
for (const f of vertFaces[v2]) {
|
||||
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); }
|
||||
}
|
||||
|
||||
// Check common neighbours match apices exactly
|
||||
for (const nb of nb1) {
|
||||
if (nb !== v2 && nb2.has(nb) && !apices.has(nb)) return true;
|
||||
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
|
||||
const ra = fa === v2 ? v1 : fa;
|
||||
const rb = fb === v2 ? v1 : fb;
|
||||
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;
|
||||
}
|
||||
|
||||
+108
-24
@@ -48,12 +48,43 @@ export function subdivide(geometry, maxEdgeLength, onProgress) {
|
||||
}
|
||||
|
||||
// ── 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) {
|
||||
const maxSq = maxEdgeLength * maxEdgeLength;
|
||||
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 = [];
|
||||
let changed = false;
|
||||
|
||||
for (let t = 0; t < indices.length; t += 3) {
|
||||
// Safety cap: stop splitting, carry remaining triangles as-is
|
||||
@@ -62,41 +93,94 @@ function subdividePass(positions, normals, indices, maxEdgeLength, safetyCap) {
|
||||
break;
|
||||
}
|
||||
|
||||
const a = indices[t];
|
||||
const b = indices[t + 1];
|
||||
const c = indices[t + 2];
|
||||
const a = indices[t], b = indices[t + 1], c = indices[t + 2];
|
||||
const sAB = splitEdges.has(edgeKey(a, b));
|
||||
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);
|
||||
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
|
||||
if (n === 0) {
|
||||
// ── 0-split: keep triangle ─────────────────────────────────────────
|
||||
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 {
|
||||
const m = getMidpoint(positions, normals, midCache, c, a);
|
||||
nextIndices.push(a, b, m, m, b, c);
|
||||
// ── 2-split: 3 sub-triangles, fan from the untouched-edge vertex ───
|
||||
//
|
||||
// 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 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/** 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) {
|
||||
const dx = pos[a*3] - pos[b*3];
|
||||
const dy = pos[a*3+1] - pos[b*3+1];
|
||||
|
||||
Reference in New Issue
Block a user