Files
archived-stlTexturizer/js/exclusion.js
T
CNCKitchen 1d3e756245 feat: add surface exclusions panel and functionality
- Introduced a new section in the UI for surface exclusions, allowing users to exclude triangles from displacement using brush and bucket fill tools.
- Implemented brush type switching (single and radius) and radius control for the brush tool.
- Added functionality for bucket fill with a threshold angle to control the fill area.
- Integrated exclusion weights into the displacement algorithm to ensure excluded faces are handled correctly during subdivision.
- Created adjacency and centroid calculations for triangles to support the bucket fill operation.
- Developed overlay geometries for visual feedback on excluded faces and hover previews.
- Enhanced the CSS for the new exclusion tools and their layout in the UI.
2026-03-17 14:35:45 +01:00

182 lines
6.8 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.
/**
* exclusion.js — per-face exclusion masking
*
* Provides three capabilities:
* 1. buildAdjacency builds an inter-triangle adjacency list with dihedral
* angles and precomputes per-triangle centroids.
* 2. bucketFill BFS flood fill that respects a max dihedral-angle
* threshold (stops at "sharp" edges).
* 3. buildExclusionOverlayGeo compact geometry for the orange preview overlay.
* 4. buildFaceWeights per-vertex exclusion weights for the subdivision pass.
*/
import * as THREE from 'three';
const QUANT = 1e4;
const quantKey = (x, y, z) =>
`${Math.round(x * QUANT)}_${Math.round(y * QUANT)}_${Math.round(z * QUANT)}`;
// ── Adjacency & centroids ─────────────────────────────────────────────────────
/**
* Build inter-triangle adjacency data for a non-indexed BufferGeometry.
*
* @param {THREE.BufferGeometry} geometry non-indexed
* @returns {{
* adjacency: Map<number, Array<{neighbor:number, angle:number}>>,
* centroids: Float32Array (triCount × 3, world-space centroid per triangle)
* }}
*/
export function buildAdjacency(geometry) {
const posAttr = geometry.attributes.position;
const triCount = posAttr.count / 3;
// Pre-allocate face normals and centroids
const faceNormals = new Float32Array(triCount * 3);
const centroids = new Float32Array(triCount * 3);
const vA = new THREE.Vector3();
const vB = new THREE.Vector3();
const vC = new THREE.Vector3();
const e1 = new THREE.Vector3();
const e2 = new THREE.Vector3();
const fn = new THREE.Vector3();
for (let t = 0; t < triCount; t++) {
const i = t * 3;
vA.fromBufferAttribute(posAttr, i);
vB.fromBufferAttribute(posAttr, i + 1);
vC.fromBufferAttribute(posAttr, i + 2);
e1.subVectors(vB, vA);
e2.subVectors(vC, vA);
fn.crossVectors(e1, e2).normalize();
faceNormals[i] = fn.x;
faceNormals[i + 1] = fn.y;
faceNormals[i + 2] = fn.z;
centroids[i] = (vA.x + vB.x + vC.x) / 3;
centroids[i + 1] = (vA.y + vB.y + vC.y) / 3;
centroids[i + 2] = (vA.z + vB.z + vC.z) / 3;
}
// Build edge → triangle list (two triangles share an edge iff they share two
// vertex positions after quantization-based deduplication).
const edgeMap = new Map();
const makeEdgeKey = (ax, ay, az, bx, by, bz) => {
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++) {
const i = t * 3;
vA.fromBufferAttribute(posAttr, i);
vB.fromBufferAttribute(posAttr, i + 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);
if (entry) entry.push(t);
else edgeMap.set(ek, [t]);
}
}
// Convert edge map to adjacency list with per-edge dihedral angle
const adjacency = new Map();
for (let t = 0; t < triCount; t++) adjacency.set(t, []);
for (const [, tris] of edgeMap) {
if (tris.length !== 2) continue;
const [a, b] = tris;
const nAx = faceNormals[a * 3], nAy = faceNormals[a * 3 + 1], nAz = faceNormals[a * 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 angleDeg = Math.acos(dot) * (180 / Math.PI);
adjacency.get(a).push({ neighbor: b, angle: angleDeg });
adjacency.get(b).push({ neighbor: a, angle: angleDeg });
}
return { adjacency, centroids };
}
// ── Bucket fill ───────────────────────────────────────────────────────────────
/**
* BFS flood fill starting from seedTriIdx.
* Spreads across edges whose dihedral angle ≤ thresholdDeg.
*
* @param {number} seedTriIdx
* @param {Map<number, Array<{neighbor:number, angle:number}>>} adjacency
* @param {number} thresholdDeg
* @returns {Set<number>} set of triangle indices in the filled region
*/
export function bucketFill(seedTriIdx, adjacency, thresholdDeg) {
const visited = new Set([seedTriIdx]);
const queue = [seedTriIdx];
while (queue.length > 0) {
const cur = queue.shift();
const neighbors = adjacency.get(cur);
if (!neighbors) continue;
for (const { neighbor, angle } of neighbors) {
if (!visited.has(neighbor) && angle <= thresholdDeg) {
visited.add(neighbor);
queue.push(neighbor);
}
}
}
return visited;
}
// ── Overlay geometry ──────────────────────────────────────────────────────────
/**
* Build a compact non-indexed BufferGeometry containing only the excluded
* triangles' positions. Used to drive the orange overlay mesh in the viewer.
*
* @param {THREE.BufferGeometry} geometry non-indexed source geometry
* @param {Set<number>} excludedFaces
* @returns {THREE.BufferGeometry}
*/
export function buildExclusionOverlayGeo(geometry, excludedFaces) {
const srcPos = geometry.attributes.position.array;
const outPos = new Float32Array(excludedFaces.size * 9); // 3 verts × 3 floats
let dst = 0;
for (const t of excludedFaces) {
const src = t * 9;
for (let i = 0; i < 9; i++) outPos[dst++] = srcPos[src + i];
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(outPos, 3));
return geo;
}
// ── Face-weight array for subdivision ────────────────────────────────────────
/**
* Build a per-non-indexed-vertex exclusion weight array.
* Vertex i (in the non-indexed buffer) belongs to triangle floor(i/3).
* Excluded triangles get weight 1.0, all others 0.0.
* subdivision.js threads these through edge splits via linear interpolation,
* producing smooth 0→1 transitions at exclusion boundaries.
*
* @param {THREE.BufferGeometry} geometry
* @param {Set<number>} excludedFaces
* @returns {Float32Array} length = geometry.attributes.position.count
*/
export function buildFaceWeights(geometry, excludedFaces) {
const count = geometry.attributes.position.count;
const weights = new Float32Array(count); // default 0 (included)
for (const t of excludedFaces) {
weights[t * 3] = 1.0;
weights[t * 3 + 1] = 1.0;
weights[t * 3 + 2] = 1.0;
}
return weights;
}