diff --git a/js/main.js b/js/main.js index 9063339..ad08ea8 100644 --- a/js/main.js +++ b/js/main.js @@ -26,6 +26,8 @@ let previewDebounce = null; // Boundary edge data texture for per-fragment falloff in bump-only preview let _boundaryEdgeTex = null; let _boundaryEdgeCount = 0; +let _falloffDirty = true; // recompute falloff on next updateFaceMask +let _falloffGeometry = null; // geometry the falloff was last computed for // ── Exclusion state ─────────────────────────────────────────────────────────── let excludedFaces = new Set(); // triangle indices in currentGeometry @@ -331,11 +333,11 @@ function wireEvents() { linkSlider(rotationSlider, rotationVal, v => { settings.rotation = v; return Math.round(v); }); linkSlider(amplitudeSlider, amplitudeVal, v => { settings.amplitude = v; checkAmplitudeWarning(); return v.toFixed(2); }); amplitudeVal.addEventListener('change', checkAmplitudeWarning); - linkSlider(boundaryFalloffSlider, boundaryFalloffVal, v => { settings.boundaryFalloff = v; return v.toFixed(1); }); + linkSlider(boundaryFalloffSlider, boundaryFalloffVal, v => { settings.boundaryFalloff = v; _falloffDirty = true; return v.toFixed(1); }); linkSlider(refineLenSlider, refineLenVal, v => { settings.refineLength = v; return v.toFixed(2); }, false); linkSlider(maxTriSlider, maxTriVal, v => { settings.maxTriangles = v; return formatM(v); }, false); - linkSlider(bottomAngleLimitSlider, bottomAngleLimitVal, v => { settings.bottomAngleLimit = v; return v; }); - linkSlider(topAngleLimitSlider, topAngleLimitVal, v => { settings.topAngleLimit = v; return v; }); + linkSlider(bottomAngleLimitSlider, bottomAngleLimitVal, v => { settings.bottomAngleLimit = v; _falloffDirty = true; return v; }); + linkSlider(topAngleLimitSlider, topAngleLimitVal, v => { settings.topAngleLimit = v; _falloffDirty = true; return v; }); linkSlider(seamBlendSlider, seamBlendVal, v => { settings.mappingBlend = v; return v.toFixed(2); }); linkSlider(seamBandWidthSlider, seamBandWidthVal, v => { settings.seamBandWidth = v; return v.toFixed(2); }); symmetricDispToggle.addEventListener('change', () => { @@ -796,6 +798,7 @@ function handlePlaceOnFaceClick(e) { function refreshExclusionOverlay() { if (!currentGeometry) return; + _falloffDirty = true; if (selectionMode) { // Include Only mode: tint the complement (non-selected faces) with a pastel blue // so the model stays visible against the dark background before any faces are painted. @@ -1146,8 +1149,12 @@ function updateFaceMask(geometry) { addFaceNormals(geometry); } - computeBoundaryFalloffAttr(geometry, maskArr); - computeBoundaryEdges(geometry, maskArr); + if (_falloffDirty || geometry !== _falloffGeometry) { + computeBoundaryFalloffAttr(geometry, maskArr); + computeBoundaryEdges(geometry, maskArr); + _falloffDirty = false; + _falloffGeometry = geometry; + } syncBoundaryEdgeUniforms(); } @@ -1388,7 +1395,7 @@ function computeBoundaryEdges(geometry, userMaskArr) { } } - const MAX_EDGES = 512; + const MAX_EDGES = 64; const edges = []; for (const [key, faces] of edgeFaces) { if (edges.length >= MAX_EDGES) break; diff --git a/js/previewMaterial.js b/js/previewMaterial.js index 492de5e..da75466 100644 --- a/js/previewMaterial.js +++ b/js/previewMaterial.js @@ -248,7 +248,7 @@ const fragmentShader = /* glsl */` // compute the distance from each pixel to the nearest boundary edge. if (useDisplacement == 0 && boundaryFalloffDist > 0.001 && boundaryEdgeCount > 0) { float minDist = boundaryFalloffDist; - for (int i = 0; i < 512; i++) { + for (int i = 0; i < 64; i++) { if (i >= boundaryEdgeCount) break; float uA = (float(i * 2) + 0.5) / boundaryEdgeTexWidth; float uB = (float(i * 2 + 1) + 0.5) / boundaryEdgeTexWidth; @@ -258,7 +258,7 @@ const fragmentShader = /* glsl */` float abLen2 = dot(ab, ab); float t = clamp(dot(vModelPos - ea, ab) / max(abLen2, 1e-10), 0.0, 1.0); float d = length(vModelPos - (ea + t * ab)); - minDist = min(minDist, d); + if (d < minDist) { minDist = d; if (d < 1e-4) break; } } maskBlend *= clamp(minDist / boundaryFalloffDist, 0.0, 1.0); }