mirror of
https://github.com/CNCKitchen/stlTexturizer.git
synced 2026-04-07 22:11:32 +00:00
feat: update max triangle limit to 750K and enhance triangle bounding radius calculations
This commit is contained in:
+2
-2
@@ -328,8 +328,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-row slider-row">
|
<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>
|
<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" />
|
<input type="range" id="max-triangles" min="10000" max="20000000" step="10000" value="750000" />
|
||||||
<span class="val" id="max-triangles-val">1.0 M</span>
|
<span class="val" id="max-triangles-val">0.75 M</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="tri-limit-warning" class="tri-limit-warning hidden" data-i18n="warnings.safetyCapHit">
|
<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.
|
⚠ 20M-triangle safety cap hit during subdivision — result may still be coarser than requested edge length.
|
||||||
|
|||||||
+13
-5
@@ -31,9 +31,10 @@ export function buildAdjacency(geometry) {
|
|||||||
const posAttr = geometry.attributes.position;
|
const posAttr = geometry.attributes.position;
|
||||||
const triCount = posAttr.count / 3;
|
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 faceNormals = new Float32Array(triCount * 3);
|
||||||
const centroids = 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 vA = new THREE.Vector3();
|
||||||
const vB = new THREE.Vector3();
|
const vB = new THREE.Vector3();
|
||||||
@@ -56,9 +57,16 @@ export function buildAdjacency(geometry) {
|
|||||||
faceNormals[i + 1] = fn.y;
|
faceNormals[i + 1] = fn.y;
|
||||||
faceNormals[i + 2] = fn.z;
|
faceNormals[i + 2] = fn.z;
|
||||||
|
|
||||||
centroids[i] = (vA.x + vB.x + vC.x) / 3;
|
const cx = (vA.x + vB.x + vC.x) / 3;
|
||||||
centroids[i + 1] = (vA.y + vB.y + vC.y) / 3;
|
const cy = (vA.y + vB.y + vC.y) / 3;
|
||||||
centroids[i + 2] = (vA.z + vB.z + vC.z) / 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
|
// 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 });
|
adjacency.get(b).push({ neighbor: a, angle: angleDeg });
|
||||||
}
|
}
|
||||||
|
|
||||||
return { adjacency, centroids };
|
return { adjacency, centroids, boundRadii };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Bucket fill ───────────────────────────────────────────────────────────────
|
// ── Bucket fill ───────────────────────────────────────────────────────────────
|
||||||
|
|||||||
+88
-25
@@ -27,6 +27,7 @@ let previewDebounce = null;
|
|||||||
let excludedFaces = new Set(); // triangle indices in currentGeometry
|
let excludedFaces = new Set(); // triangle indices in currentGeometry
|
||||||
let triangleAdjacency = null; // Map from buildAdjacency
|
let triangleAdjacency = null; // Map from buildAdjacency
|
||||||
let triangleCentroids = null; // Float32Array 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 exclusionTool = null; // 'brush' | 'bucket' | null
|
||||||
let eraseMode = false;
|
let eraseMode = false;
|
||||||
let brushIsRadius = false;
|
let brushIsRadius = false;
|
||||||
@@ -47,7 +48,7 @@ const settings = {
|
|||||||
offsetV: 0.0,
|
offsetV: 0.0,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
refineLength: 1.0,
|
refineLength: 1.0,
|
||||||
maxTriangles: 1_000_000,
|
maxTriangles: 750_000,
|
||||||
lockScale: true,
|
lockScale: true,
|
||||||
bottomAngleLimit: 5,
|
bottomAngleLimit: 5,
|
||||||
topAngleLimit: 0,
|
topAngleLimit: 0,
|
||||||
@@ -725,6 +726,81 @@ function pickTriangle(e) {
|
|||||||
return fi;
|
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) {
|
function paintAt(e) {
|
||||||
const mesh = getCurrentMesh();
|
const mesh = getCurrentMesh();
|
||||||
if (!mesh) return;
|
if (!mesh) return;
|
||||||
@@ -740,17 +816,10 @@ function paintAt(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (brushIsRadius) {
|
if (brushIsRadius) {
|
||||||
const hitPt = hit.point;
|
|
||||||
const triCount = triangleCentroids.length / 3;
|
|
||||||
const r2 = brushRadius * brushRadius;
|
const r2 = brushRadius * brushRadius;
|
||||||
for (let t = 0; t < triCount; t++) {
|
forEachTriInSphere(hit.point, r2, t => {
|
||||||
const dx = triangleCentroids[t * 3] - hitPt.x;
|
if (eraseMode) excludedFaces.delete(t); else excludedFaces.add(t);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (eraseMode) excludedFaces.delete(triIdx); else excludedFaces.add(triIdx);
|
if (eraseMode) excludedFaces.delete(triIdx); else excludedFaces.add(triIdx);
|
||||||
}
|
}
|
||||||
@@ -865,7 +934,7 @@ function handlePlaceOnFaceClick(e) {
|
|||||||
// Rebuild adjacency
|
// Rebuild adjacency
|
||||||
const adjData = buildAdjacency(currentGeometry);
|
const adjData = buildAdjacency(currentGeometry);
|
||||||
triangleAdjacency = adjData.adjacency;
|
triangleAdjacency = adjData.adjacency;
|
||||||
triangleCentroids = adjData.centroids;
|
triangleCentroids = adjData.centroids; triangleBoundRadii = adjData.boundRadii;
|
||||||
|
|
||||||
// Update edge length for new bounds
|
// Update edge length for new bounds
|
||||||
const maxDim = Math.max(currentBounds.size.x, currentBounds.size.y, currentBounds.size.z);
|
const maxDim = Math.max(currentBounds.size.x, currentBounds.size.y, currentBounds.size.z);
|
||||||
@@ -974,21 +1043,15 @@ function updateBrushHover(e) {
|
|||||||
if (triIdx === _lastHoverTriIdx) return;
|
if (triIdx === _lastHoverTriIdx) return;
|
||||||
_lastHoverTriIdx = triIdx;
|
_lastHoverTriIdx = triIdx;
|
||||||
|
|
||||||
|
const hoverColor = eraseMode ? 0x999999 : 0xffee00;
|
||||||
if (brushIsRadius) {
|
if (brushIsRadius) {
|
||||||
const hitPt = hit.point;
|
|
||||||
const triCount = triangleCentroids.length / 3;
|
|
||||||
const r2 = brushRadius * brushRadius;
|
const r2 = brushRadius * brushRadius;
|
||||||
const hovered = new Set();
|
const hovered = new Set();
|
||||||
for (let t = 0; t < triCount; t++) {
|
forEachTriInSphere(hit.point, r2, t => hovered.add(t));
|
||||||
const dx = triangleCentroids[t * 3] - hitPt.x;
|
setHoverPreview(buildExclusionOverlayGeo(currentGeometry, hovered), hoverColor);
|
||||||
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));
|
|
||||||
} else {
|
} else {
|
||||||
const hovered = new Set([triIdx]);
|
const hovered = new Set([triIdx]);
|
||||||
setHoverPreview(buildExclusionOverlayGeo(currentGeometry, hovered));
|
setHoverPreview(buildExclusionOverlayGeo(currentGeometry, hovered), hoverColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1001,7 +1064,7 @@ function updateBucketHover(e) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hovered = bucketFill(triIdx, triangleAdjacency, bucketThreshold);
|
const hovered = bucketFill(triIdx, triangleAdjacency, bucketThreshold);
|
||||||
setHoverPreview(buildExclusionOverlayGeo(currentGeometry, hovered));
|
setHoverPreview(buildExclusionOverlayGeo(currentGeometry, hovered), eraseMode ? 0x999999 : 0xffee00);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Slider helper ─────────────────────────────────────────────────────────────
|
// ── Slider helper ─────────────────────────────────────────────────────────────
|
||||||
@@ -1096,7 +1159,7 @@ function loadDefaultCube() {
|
|||||||
|
|
||||||
const adjData = buildAdjacency(geo);
|
const adjData = buildAdjacency(geo);
|
||||||
triangleAdjacency = adjData.adjacency;
|
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.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;
|
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)
|
// typical STL sizes processed by this tool)
|
||||||
const adjData = buildAdjacency(geometry);
|
const adjData = buildAdjacency(geometry);
|
||||||
triangleAdjacency = adjData.adjacency;
|
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
|
// Reset scale & offset sliders so scale=1 = one tile covers the full bounding box
|
||||||
const resetVal = (slider, valEl, value) => {
|
const resetVal = (slider, valEl, value) => {
|
||||||
|
|||||||
+2
-2
@@ -582,7 +582,7 @@ export function setExclusionOverlay(overlayGeo, color = 0xff6600, opacity = 1.0)
|
|||||||
*
|
*
|
||||||
* @param {THREE.BufferGeometry|null} overlayGeo
|
* @param {THREE.BufferGeometry|null} overlayGeo
|
||||||
*/
|
*/
|
||||||
export function setHoverPreview(overlayGeo) {
|
export function setHoverPreview(overlayGeo, color = 0xffee00) {
|
||||||
if (hoverMesh) {
|
if (hoverMesh) {
|
||||||
scene.remove(hoverMesh);
|
scene.remove(hoverMesh);
|
||||||
hoverMesh.geometry.dispose();
|
hoverMesh.geometry.dispose();
|
||||||
@@ -593,7 +593,7 @@ export function setHoverPreview(overlayGeo) {
|
|||||||
hoverMesh = new THREE.Mesh(
|
hoverMesh = new THREE.Mesh(
|
||||||
overlayGeo,
|
overlayGeo,
|
||||||
new THREE.MeshBasicMaterial({
|
new THREE.MeshBasicMaterial({
|
||||||
color: 0xffee00,
|
color,
|
||||||
side: THREE.DoubleSide,
|
side: THREE.DoubleSide,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
opacity: 0.45,
|
opacity: 0.45,
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.1 KiB |
Reference in New Issue
Block a user