From 43a09e8b14c7d66eed4008456aee90204a676986 Mon Sep 17 00:00:00 2001 From: CNCKitchen Date: Thu, 19 Mar 2026 20:12:54 +0100 Subject: [PATCH] feat: add seam band width control and integrate with displacement logic for improved blending --- index.html | 7 ++++- js/displacement.js | 3 ++- js/main.js | 61 +++++++++++++++++++++++++++++++++++++++++++ js/mapping.js | 6 ++--- js/previewMaterial.js | 5 +++- js/viewer.js | 3 ++- 6 files changed, 78 insertions(+), 7 deletions(-) diff --git a/index.html b/index.html index 18795da..19d61fd 100644 --- a/index.html +++ b/index.html @@ -130,11 +130,16 @@ -
+ +
+ + + +
diff --git a/js/displacement.js b/js/displacement.js index 94d217e..7c1f23b 100644 --- a/js/displacement.js +++ b/js/displacement.js @@ -124,8 +124,9 @@ export function applyDisplacement(geometry, imageData, imgWidth, imgHeight, sett let czX = 0, czY = 0, czZ = 0; if (settings.mappingMode === 6 && faceArea > 1e-12) { const cubicBlend = settings.mappingBlend ?? 0; + const cubicBandWidth = settings.seamBandWidth ?? 0.35; const unitFaceNrm = { x: faceNrm.x / faceArea, y: faceNrm.y / faceArea, z: faceNrm.z / faceArea }; - const w = getCubicBlendWeights(unitFaceNrm, cubicBlend); + const w = getCubicBlendWeights(unitFaceNrm, cubicBlend, cubicBandWidth); czX = w.x * faceArea; czY = w.y * faceArea; czZ = w.z * faceArea; diff --git a/js/main.js b/js/main.js index ce67aa0..31c22fc 100644 --- a/js/main.js +++ b/js/main.js @@ -51,6 +51,7 @@ const settings = { bottomAngleLimit: 5, topAngleLimit: 0, mappingBlend: 1, + seamBandWidth: 0.5, symmetricDisplacement: false, }; @@ -100,6 +101,8 @@ const bottomAngleLimitVal = document.getElementById('bottom-angle-limit-val') const topAngleLimitVal = document.getElementById('top-angle-limit-val'); const seamBlendSlider = document.getElementById('seam-blend'); const seamBlendVal = document.getElementById('seam-blend-val'); +const seamBandWidthSlider = document.getElementById('seam-band-width'); +const seamBandWidthVal = document.getElementById('seam-band-width-val'); const symmetricDispToggle = document.getElementById('symmetric-displacement'); // ── Exclusion panel DOM refs ────────────────────────────────────────────────── @@ -166,6 +169,7 @@ scaleVVal.value = posToScale(parseFloat(scaleVSlider.value)); loadPresets().then(presets => { PRESETS = presets; buildPresetGrid(); + loadDefaultCube(); // Select Crystal as the default preset const noiseIdx = PRESETS.findIndex(p => p.name === 'Crystal'); const defaultIdx = noiseIdx !== -1 ? noiseIdx : 0; @@ -312,6 +316,7 @@ function wireEvents() { linkSlider(bottomAngleLimitSlider, bottomAngleLimitVal, v => { settings.bottomAngleLimit = v; return v; }); linkSlider(topAngleLimitSlider, topAngleLimitVal, v => { settings.topAngleLimit = v; 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', () => { settings.symmetricDisplacement = symmetricDispToggle.checked; updatePreview(); @@ -675,6 +680,62 @@ function formatM(n) { // ── STL loading ─────────────────────────────────────────────────────────────── +function loadDefaultCube() { + // Create a 50×50×50 mm box; convert to non-indexed so it behaves like a + // real STL (buildAdjacency and displacement expect non-indexed geometry). + const geo = new THREE.BoxGeometry(50, 50, 50).toNonIndexed(); + geo.computeBoundingBox(); + geo.computeVertexNormals(); + + currentGeometry = geo; + currentBounds = computeBounds(geo); + currentStlName = 'cube_50x50x50'; + checkAmplitudeWarning(); + + loadGeometry(geo); + dropHint.classList.add('hidden'); + + // Reset exclusion state + excludedFaces = new Set(); + exclusionTool = null; + eraseMode = false; + isPainting = false; + exclBrushBtn.classList.remove('active'); + exclBucketBtn.classList.remove('active'); + exclEraseToggle.classList.remove('active'); + exclBrushTypeRow.classList.add('hidden'); + exclRadiusRow.classList.add('hidden'); + exclThresholdRow.classList.add('hidden'); + canvas.style.cursor = ''; + setExclusionOverlay(null); + setHoverPreview(null); + _lastHoverTriIdx = -1; + exclCount.textContent = t('excl.initExcluded'); + + const adjData = buildAdjacency(geo); + triangleAdjacency = adjData.adjacency; + triangleCentroids = adjData.centroids; + + 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.offsetU = 0; offsetUSlider.value = 0; offsetUVal.value = 0; + settings.offsetV = 0; offsetVSlider.value = 0; offsetVVal.value = 0; + triLimitWarning.classList.add('hidden'); + + const maxDim = Math.max(currentBounds.size.x, currentBounds.size.y, currentBounds.size.z); + const defaultEdge = Math.max(0.05, Math.min(5.0, +(maxDim / 200).toFixed(2))); + settings.refineLength = defaultEdge; + refineLenSlider.value = defaultEdge; + refineLenVal.value = defaultEdge; + + const triCount = getTriangleCount(geo); + const mb = ((geo.attributes.position.array.byteLength) / 1024 / 1024).toFixed(2); + meshInfo.textContent = t('ui.meshInfo', { n: triCount.toLocaleString(), mb }); + + exportBtn.disabled = (activeMapEntry === null); + updatePreview(); +} + async function handleSTL(file) { try { const { geometry, bounds } = await loadSTLFile(file); diff --git a/js/mapping.js b/js/mapping.js index 92c31a1..c5d9b9a 100644 --- a/js/mapping.js +++ b/js/mapping.js @@ -37,7 +37,7 @@ export function isAmbiguousCubicNormal(normal) { return primary - secondary <= CUBIC_AXIS_EPSILON; } -export function getCubicBlendWeights(normal, blend) { +export function getCubicBlendWeights(normal, blend, seamBandWidth = 0.35) { const axis = getDominantCubicAxis(normal); const ax = Math.abs(normal.x); const ay = Math.abs(normal.y); @@ -61,7 +61,7 @@ export function getCubicBlendWeights(normal, blend) { // Only blend inside a seam band around the cube-face boundary. This keeps // strongly dominant faces fully textured even when the slider is barely on. - const seamWidth = Math.max(blend * 0.35, CUBIC_AXIS_EPSILON * 2); + const seamWidth = Math.max(seamBandWidth, CUBIC_AXIS_EPSILON * 2); const seamMixRaw = 1 - Math.min(1, Math.max(0, (primary - secondary) / seamWidth)); const seamMix = blend * seamMixRaw * seamMixRaw * (3 - 2 * seamMixRaw); if (seamMix <= 0.001) return oneHot; @@ -183,7 +183,7 @@ export function computeUV(pos, normal, mode, settings, bounds) { } case MODE_CUBIC: { - const weights = getCubicBlendWeights(normal, settings.mappingBlend ?? 0.0); + const weights = getCubicBlendWeights(normal, settings.mappingBlend ?? 0.0, settings.seamBandWidth ?? 0.35); const tYZ = applyTransform((pos.y - min.y) / md, (pos.z - min.z) / md, scaleU, scaleV, offsetU, offsetV, rotRad); const tXZ = applyTransform((pos.x - min.x) / md, (pos.z - min.z) / md, scaleU, scaleV, offsetU, offsetV, rotRad); const tXY = applyTransform((pos.x - min.x) / md, (pos.y - min.y) / md, scaleU, scaleV, offsetU, offsetV, rotRad); diff --git a/js/previewMaterial.js b/js/previewMaterial.js index 66a84af..14bab60 100644 --- a/js/previewMaterial.js +++ b/js/previewMaterial.js @@ -55,6 +55,7 @@ const fragmentShader = /* glsl */` uniform float bottomAngleLimit; // degrees from horizontal; 0 = disabled uniform float topAngleLimit; // degrees from horizontal; 0 = disabled uniform float mappingBlend; // 0 = sharp seams, 1 = fully blended + uniform float seamBandWidth; // width of the blend zone near cube-face seams uniform int symmetricDisplacement; // 1 = remap [0,1]→[-1,1] so 50% grey = no disp varying vec3 vModelPos; @@ -91,7 +92,7 @@ const fragmentShader = /* glsl */` : axis == 1 ? vec3(0.0, 1.0, 0.0) : vec3(0.0, 0.0, 1.0); - float seamWidth = max(mappingBlend * 0.35, CUBIC_AXIS_EPSILON * 2.0); + float seamWidth = max(seamBandWidth, CUBIC_AXIS_EPSILON * 2.0); float seamMixRaw = 1.0 - clamp((primary - secondary) / seamWidth, 0.0, 1.0); float seamMix = mappingBlend * seamMixRaw * seamMixRaw * (3.0 - 2.0 * seamMixRaw); if (seamMix <= 0.001) return oneHot; @@ -290,6 +291,7 @@ export function updateMaterial(material, displacementTexture, settings) { u.bottomAngleLimit.value = settings.bottomAngleLimit ?? 5.0; u.topAngleLimit.value = settings.topAngleLimit ?? 0.0; u.mappingBlend.value = settings.mappingBlend ?? 0.0; + u.seamBandWidth.value = settings.seamBandWidth ?? 0.35; u.symmetricDisplacement.value = settings.symmetricDisplacement ? 1 : 0; } @@ -314,6 +316,7 @@ function buildUniforms(tex, settings) { bottomAngleLimit: { value: settings.bottomAngleLimit ?? 5.0 }, topAngleLimit: { value: settings.topAngleLimit ?? 0.0 }, mappingBlend: { value: settings.mappingBlend ?? 0.0 }, + seamBandWidth: { value: settings.seamBandWidth ?? 0.35 }, symmetricDisplacement: { value: settings.symmetricDisplacement ? 1 : 0 }, }; } diff --git a/js/viewer.js b/js/viewer.js index f018d14..48eb4cf 100644 --- a/js/viewer.js +++ b/js/viewer.js @@ -341,6 +341,7 @@ export function setSceneBackground(hexColor) { export function setViewerTheme(isLight) { if (!scene) return; scene.background = new THREE.Color(isLight ? 0xf0f0f5 : 0x111114); + const savedZ = grid ? grid.position.z : 0; if (grid) { scene.remove(grid); grid.geometry.dispose(); @@ -352,7 +353,7 @@ export function setViewerTheme(isLight) { isLight ? 0xd0d0e0 : 0x1e1e24 ); grid.rotation.x = Math.PI / 2; - grid.position.z = 0; + grid.position.z = savedZ; scene.add(grid); }