feat: update max triangle limit to 750K and enhance triangle bounding radius calculations

This commit is contained in:
CNCKitchen
2026-04-03 09:18:44 +02:00
parent 7289c2cabc
commit 3c94df4504
5 changed files with 105 additions and 34 deletions
+2 -2
View File
@@ -328,8 +328,8 @@
</div>
<div class="form-row slider-row">
<label for="max-triangles" data-i18n="labels.outputTriangles" data-i18n-title="tooltips.outputTriangles" title="Mesh is fully subdivided first, then decimated down to this count">Output Triangles</label>
<input type="range" id="max-triangles" min="10000" max="20000000" step="10000" value="1000000" />
<span class="val" id="max-triangles-val">1.0 M</span>
<input type="range" id="max-triangles" min="10000" max="20000000" step="10000" value="750000" />
<span class="val" id="max-triangles-val">0.75 M</span>
</div>
<div id="tri-limit-warning" class="tri-limit-warning hidden" data-i18n="warnings.safetyCapHit">
⚠ 20M-triangle safety cap hit during subdivision — result may still be coarser than requested edge length.
+13 -5
View File
@@ -31,9 +31,10 @@ export function buildAdjacency(geometry) {
const posAttr = geometry.attributes.position;
const triCount = posAttr.count / 3;
// Pre-allocate face normals and centroids
// Pre-allocate face normals, centroids, and per-triangle bounding radii
const faceNormals = new Float32Array(triCount * 3);
const centroids = new Float32Array(triCount * 3);
const boundRadii = new Float32Array(triCount); // max vertex-to-centroid distance
const vA = new THREE.Vector3();
const vB = new THREE.Vector3();
@@ -56,9 +57,16 @@ export function buildAdjacency(geometry) {
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;
const cx = (vA.x + vB.x + vC.x) / 3;
const cy = (vA.y + vB.y + vC.y) / 3;
const cz = (vA.z + vB.z + vC.z) / 3;
centroids[i] = cx;
centroids[i + 1] = cy;
centroids[i + 2] = cz;
const dA = (vA.x-cx)**2 + (vA.y-cy)**2 + (vA.z-cz)**2;
const dB = (vB.x-cx)**2 + (vB.y-cy)**2 + (vB.z-cz)**2;
const dC = (vC.x-cx)**2 + (vC.y-cy)**2 + (vC.z-cz)**2;
boundRadii[t] = Math.sqrt(Math.max(dA, dB, dC));
}
// Build edge → triangle list (two triangles share an edge iff they share two
@@ -102,7 +110,7 @@ export function buildAdjacency(geometry) {
adjacency.get(b).push({ neighbor: a, angle: angleDeg });
}
return { adjacency, centroids };
return { adjacency, centroids, boundRadii };
}
// ── Bucket fill ───────────────────────────────────────────────────────────────
+88 -25
View File
@@ -27,6 +27,7 @@ let previewDebounce = null;
let excludedFaces = new Set(); // triangle indices in currentGeometry
let triangleAdjacency = null; // Map from buildAdjacency
let triangleCentroids = null; // Float32Array from buildAdjacency
let triangleBoundRadii = null; // Float32Array — max vertex-to-centroid dist per tri
let exclusionTool = null; // 'brush' | 'bucket' | null
let eraseMode = false;
let brushIsRadius = false;
@@ -47,7 +48,7 @@ const settings = {
offsetV: 0.0,
rotation: 0,
refineLength: 1.0,
maxTriangles: 1_000_000,
maxTriangles: 750_000,
lockScale: true,
bottomAngleLimit: 5,
topAngleLimit: 0,
@@ -725,6 +726,81 @@ function pickTriangle(e) {
return fi;
}
/**
* Squared distance from point P to the closest point on triangle ABC.
* Uses the Voronoi-region method (no allocations, pure arithmetic).
*/
function distSqPointToTri(px, py, pz, ax, ay, az, bx, by, bz, cx, cy, cz) {
const abx = bx-ax, aby = by-ay, abz = bz-az;
const acx = cx-ax, acy = cy-ay, acz = cz-az;
const apx = px-ax, apy = py-ay, apz = pz-az;
const d1 = abx*apx + aby*apy + abz*apz;
const d2 = acx*apx + acy*apy + acz*apz;
if (d1 <= 0 && d2 <= 0) return apx*apx + apy*apy + apz*apz; // vertex A
const bpx = px-bx, bpy = py-by, bpz = pz-bz;
const d3 = abx*bpx + aby*bpy + abz*bpz;
const d4 = acx*bpx + acy*bpy + acz*bpz;
if (d3 >= 0 && d4 <= d3) return bpx*bpx + bpy*bpy + bpz*bpz; // vertex B
const cpx = px-cx, cpy = py-cy, cpz = pz-cz;
const d5 = abx*cpx + aby*cpy + abz*cpz;
const d6 = acx*cpx + acy*cpy + acz*cpz;
if (d6 >= 0 && d5 <= d6) return cpx*cpx + cpy*cpy + cpz*cpz; // vertex C
const vc = d1*d4 - d3*d2;
if (vc <= 0 && d1 >= 0 && d3 <= 0) { // edge AB
const v = d1 / (d1 - d3);
const qx = ax+v*abx-px, qy = ay+v*aby-py, qz = az+v*abz-pz;
return qx*qx + qy*qy + qz*qz;
}
const vb = d5*d2 - d1*d6;
if (vb <= 0 && d2 >= 0 && d6 <= 0) { // edge AC
const w = d2 / (d2 - d6);
const qx = ax+w*acx-px, qy = ay+w*acy-py, qz = az+w*acz-pz;
return qx*qx + qy*qy + qz*qz;
}
const va = d3*d6 - d5*d4;
if (va <= 0 && (d4-d3) >= 0 && (d5-d6) >= 0) { // edge BC
const w = (d4-d3) / ((d4-d3) + (d5-d6));
const qx = bx+w*(cx-bx)-px, qy = by+w*(cy-by)-py, qz = bz+w*(cz-bz)-pz;
return qx*qx + qy*qy + qz*qz;
}
// Inside triangle
const den = 1 / (va + vb + vc);
const v = vb*den, w = vc*den;
const qx = ax+abx*v+acx*w-px, qy = ay+aby*v+acy*w-py, qz = az+abz*v+acz*w-pz;
return qx*qx + qy*qy + qz*qz;
}
/** Test all triangles against a sphere and invoke cb(triIdx) for each hit. */
function forEachTriInSphere(hitPt, r2, cb) {
const pos = currentGeometry.attributes.position;
const triCount = triangleCentroids.length / 3;
const r = Math.sqrt(r2);
for (let t = 0; t < triCount; t++) {
// Quick reject: centroid distance > brush radius + triangle bounding radius
const dx = triangleCentroids[t*3] - hitPt.x;
const dy = triangleCentroids[t*3+1] - hitPt.y;
const dz = triangleCentroids[t*3+2] - hitPt.z;
const bound = r + triangleBoundRadii[t];
if (dx*dx + dy*dy + dz*dz > bound*bound) continue;
// Precise sphere-triangle test
const i = t * 3;
const d2 = distSqPointToTri(
hitPt.x, hitPt.y, hitPt.z,
pos.getX(i), pos.getY(i), pos.getZ(i),
pos.getX(i+1), pos.getY(i+1), pos.getZ(i+1),
pos.getX(i+2), pos.getY(i+2), pos.getZ(i+2),
);
if (d2 <= r2) cb(t);
}
}
function paintAt(e) {
const mesh = getCurrentMesh();
if (!mesh) return;
@@ -740,17 +816,10 @@ function paintAt(e) {
}
if (brushIsRadius) {
const hitPt = hit.point;
const triCount = triangleCentroids.length / 3;
const r2 = brushRadius * brushRadius;
for (let t = 0; t < triCount; t++) {
const dx = triangleCentroids[t * 3] - hitPt.x;
const dy = triangleCentroids[t * 3 + 1] - hitPt.y;
const dz = triangleCentroids[t * 3 + 2] - hitPt.z;
if (dx * dx + dy * dy + dz * dz <= r2) {
if (eraseMode) excludedFaces.delete(t); else excludedFaces.add(t);
}
}
forEachTriInSphere(hit.point, r2, t => {
if (eraseMode) excludedFaces.delete(t); else excludedFaces.add(t);
});
} else {
if (eraseMode) excludedFaces.delete(triIdx); else excludedFaces.add(triIdx);
}
@@ -865,7 +934,7 @@ function handlePlaceOnFaceClick(e) {
// Rebuild adjacency
const adjData = buildAdjacency(currentGeometry);
triangleAdjacency = adjData.adjacency;
triangleCentroids = adjData.centroids;
triangleCentroids = adjData.centroids; triangleBoundRadii = adjData.boundRadii;
// Update edge length for new bounds
const maxDim = Math.max(currentBounds.size.x, currentBounds.size.y, currentBounds.size.z);
@@ -974,21 +1043,15 @@ function updateBrushHover(e) {
if (triIdx === _lastHoverTriIdx) return;
_lastHoverTriIdx = triIdx;
const hoverColor = eraseMode ? 0x999999 : 0xffee00;
if (brushIsRadius) {
const hitPt = hit.point;
const triCount = triangleCentroids.length / 3;
const r2 = brushRadius * brushRadius;
const hovered = new Set();
for (let t = 0; t < triCount; t++) {
const dx = triangleCentroids[t * 3] - hitPt.x;
const dy = triangleCentroids[t * 3 + 1] - hitPt.y;
const dz = triangleCentroids[t * 3 + 2] - hitPt.z;
if (dx * dx + dy * dy + dz * dz <= r2) hovered.add(t);
}
setHoverPreview(buildExclusionOverlayGeo(currentGeometry, hovered));
forEachTriInSphere(hit.point, r2, t => hovered.add(t));
setHoverPreview(buildExclusionOverlayGeo(currentGeometry, hovered), hoverColor);
} else {
const hovered = new Set([triIdx]);
setHoverPreview(buildExclusionOverlayGeo(currentGeometry, hovered));
setHoverPreview(buildExclusionOverlayGeo(currentGeometry, hovered), hoverColor);
}
}
@@ -1001,7 +1064,7 @@ function updateBucketHover(e) {
return;
}
const hovered = bucketFill(triIdx, triangleAdjacency, bucketThreshold);
setHoverPreview(buildExclusionOverlayGeo(currentGeometry, hovered));
setHoverPreview(buildExclusionOverlayGeo(currentGeometry, hovered), eraseMode ? 0x999999 : 0xffee00);
}
// ── Slider helper ─────────────────────────────────────────────────────────────
@@ -1096,7 +1159,7 @@ function loadDefaultCube() {
const adjData = buildAdjacency(geo);
triangleAdjacency = adjData.adjacency;
triangleCentroids = adjData.centroids;
triangleCentroids = adjData.centroids; triangleBoundRadii = adjData.boundRadii;
settings.scaleU = 0.5; scaleUSlider.value = scaleToPos(0.5); scaleUVal.value = 0.5;
settings.scaleV = 0.5; scaleVSlider.value = scaleToPos(0.5); scaleVVal.value = 0.5;
@@ -1174,7 +1237,7 @@ async function handleModelFile(file) {
// typical STL sizes processed by this tool)
const adjData = buildAdjacency(geometry);
triangleAdjacency = adjData.adjacency;
triangleCentroids = adjData.centroids;
triangleCentroids = adjData.centroids; triangleBoundRadii = adjData.boundRadii;
// Reset scale & offset sliders so scale=1 = one tile covers the full bounding box
const resetVal = (slider, valEl, value) => {
+2 -2
View File
@@ -582,7 +582,7 @@ export function setExclusionOverlay(overlayGeo, color = 0xff6600, opacity = 1.0)
*
* @param {THREE.BufferGeometry|null} overlayGeo
*/
export function setHoverPreview(overlayGeo) {
export function setHoverPreview(overlayGeo, color = 0xffee00) {
if (hoverMesh) {
scene.remove(hoverMesh);
hoverMesh.geometry.dispose();
@@ -593,7 +593,7 @@ export function setHoverPreview(overlayGeo) {
hoverMesh = new THREE.Mesh(
overlayGeo,
new THREE.MeshBasicMaterial({
color: 0xffee00,
color,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.45,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB