mirror of
https://github.com/CNCKitchen/stlTexturizer.git
synced 2026-04-07 22:11:32 +00:00
feat: add texture smoothing feature and update translations for UI consistency
This commit is contained in:
+6
-1
@@ -121,6 +121,11 @@
|
||||
</label>
|
||||
<input type="file" id="texture-file-input" accept="image/*" hidden />
|
||||
<div id="active-map-name" class="active-map-name" data-i18n="ui.noMapSelected">No map selected</div>
|
||||
<div class="form-row slider-row">
|
||||
<label for="texture-smoothing" data-i18n="labels.textureSmoothing" data-i18n-title="tooltips.textureSmoothing" title="Applies a Gaussian blur to the displacement map. Higher values produce softer, more gradual surface detail. 0 = off.">Texture Smoothing ⓘ</label>
|
||||
<input type="range" id="texture-smoothing" min="0" max="20" step="any" value="0" />
|
||||
<input type="number" class="val" id="texture-smoothing-val" value="0" min="0" max="20" step="any" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Projection -->
|
||||
@@ -144,7 +149,7 @@
|
||||
<input type="number" class="val" id="seam-blend-val" value="1" min="0" max="1" step="0.01" />
|
||||
</div>
|
||||
<div class="form-row slider-row">
|
||||
<label for="seam-band-width" data-i18n="labels.smoothing" data-i18n-title="tooltips.smoothing" title="Width of the blending zone near seam edges. Lower values keep transitions tight to the seam; higher values blend a wider band.">Smoothing ⓘ</label>
|
||||
<label for="seam-band-width" data-i18n="labels.transitionSmoothing" data-i18n-title="tooltips.transitionSmoothing" title="Width of the blending zone near seam edges. Lower values keep transitions tight to the seam; higher values blend a wider band.">Transition Smoothing ⓘ</label>
|
||||
<input type="range" id="seam-band-width" min="0" max="1" step="0.01" value="0.5" />
|
||||
<input type="number" class="val" id="seam-band-width-val" value="0.5" min="0" max="1" step="0.01" />
|
||||
</div>
|
||||
|
||||
+8
-4
@@ -53,8 +53,10 @@ export const TRANSLATIONS = {
|
||||
// Seam blend
|
||||
'labels.seamBlend': 'Seam Blend \u24d8',
|
||||
'tooltips.seamBlend': 'Softens the hard seam where projection faces meet. Effective for Cubic and Cylindrical modes.',
|
||||
'labels.smoothing': 'Smoothing \u24d8',
|
||||
'tooltips.smoothing': 'Width of the blending zone near seam edges. Lower values keep transitions tight to the seam; higher values blend a wider band.',
|
||||
'labels.transitionSmoothing': 'Transition Smoothing \u24d8',
|
||||
'tooltips.transitionSmoothing': 'Width of the blending zone near seam edges. Lower values keep transitions tight to the seam; higher values blend a wider band.',
|
||||
'labels.textureSmoothing': 'Texture Smoothing \u24d8',
|
||||
'tooltips.textureSmoothing': 'Applies a Gaussian blur to the displacement map. Higher values produce softer, more gradual surface detail. 0 = off.',
|
||||
'labels.capAngle': 'Cap Angle \u24d8',
|
||||
'tooltips.capAngle': 'Angle (in degrees) from vertical at which the top/bottom cap projection kicks in. Smaller values limit cap projection to nearly flat faces.',
|
||||
|
||||
@@ -209,8 +211,10 @@ export const TRANSLATIONS = {
|
||||
// Seam blend
|
||||
'labels.seamBlend': 'Nahtglättung \u24d8',
|
||||
'tooltips.seamBlend': 'Glättet den scharfen Übergang zwischen Projektionsflächen. Wirksam für Kubische und Zylindrische Modi.',
|
||||
'labels.smoothing': 'Glättung \u24d8',
|
||||
'tooltips.smoothing': 'Breite der Übergangszone an Nahtkanten. Niedrige Werte halten den Übergang nah an der Naht; höhere Werte glätten einen breiteren Bereich.',
|
||||
'labels.transitionSmoothing': 'Übergangsglättung \u24d8',
|
||||
'tooltips.transitionSmoothing': 'Breite der Übergangszone an Nahtkanten. Niedrige Werte halten den Übergang nah an der Naht; höhere Werte glätten einen breiteren Bereich.',
|
||||
'labels.textureSmoothing': 'Texturglättung \u24d8',
|
||||
'tooltips.textureSmoothing': 'Wendet einen Gaußschen Weichzeichner auf die Verschiebungskarte an. Höhere Werte erzeugen weichere, fließendere Oberflächendetails. 0 = aus.',
|
||||
'labels.capAngle': 'Übergangswinkel \u24d8',
|
||||
'tooltips.capAngle': 'Winkel (in Grad) ab dem die Deckel-/Bodenprojektion einsetzt. Kleinere Werte beschränken die Deckelprojektion auf nahezu flache Flächen.',
|
||||
|
||||
|
||||
+54
-7
@@ -53,6 +53,7 @@ const settings = {
|
||||
topAngleLimit: 0,
|
||||
mappingBlend: 1,
|
||||
seamBandWidth: 0.5,
|
||||
textureSmoothing: 0,
|
||||
capAngle: 20,
|
||||
symmetricDisplacement: false,
|
||||
useDisplacement: false,
|
||||
@@ -112,6 +113,8 @@ 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 textureSmoothingSlider = document.getElementById('texture-smoothing');
|
||||
const textureSmoothingVal = document.getElementById('texture-smoothing-val');
|
||||
const capAngleSlider = document.getElementById('cap-angle');
|
||||
const capAngleVal = document.getElementById('cap-angle-val');
|
||||
const capAngleRow = document.getElementById('cap-angle-row');
|
||||
@@ -215,11 +218,18 @@ function buildPresetGrid() {
|
||||
});
|
||||
}
|
||||
|
||||
function resetTextureSmoothing() {
|
||||
settings.textureSmoothing = 0;
|
||||
textureSmoothingSlider.value = 0;
|
||||
textureSmoothingVal.value = 0;
|
||||
}
|
||||
|
||||
function selectPreset(idx, swatchEl) {
|
||||
document.querySelectorAll('.preset-swatch').forEach(s => s.classList.remove('active'));
|
||||
swatchEl.classList.add('active');
|
||||
activeMapEntry = PRESETS[idx];
|
||||
activeMapName.textContent = PRESETS[idx].name;
|
||||
resetTextureSmoothing();
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
@@ -274,6 +284,7 @@ function wireEvents() {
|
||||
activeMapEntry.isCustom = true;
|
||||
activeMapName.textContent = file.name;
|
||||
document.querySelectorAll('.preset-swatch').forEach(s => s.classList.remove('active'));
|
||||
resetTextureSmoothing();
|
||||
updatePreview();
|
||||
} catch (err) {
|
||||
console.error('Failed to load texture:', err);
|
||||
@@ -337,6 +348,7 @@ function wireEvents() {
|
||||
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); });
|
||||
linkSlider(textureSmoothingSlider, textureSmoothingVal, v => { settings.textureSmoothing = v; return v.toFixed(1); });
|
||||
linkSlider(capAngleSlider, capAngleVal, v => { settings.capAngle = v; return Math.round(v); });
|
||||
symmetricDispToggle.addEventListener('change', () => {
|
||||
settings.symmetricDisplacement = symmetricDispToggle.checked;
|
||||
@@ -1299,6 +1311,38 @@ function buildParentFaceMap(subdivGeo) {
|
||||
return parentMap;
|
||||
}
|
||||
|
||||
function getEffectiveMapEntry() {
|
||||
if (!activeMapEntry || settings.textureSmoothing === 0) return activeMapEntry;
|
||||
const { fullCanvas, width, height } = activeMapEntry;
|
||||
// Tile the source 3×3 before blurring so edge pixels have correct
|
||||
// neighbours and the blurred centre tile is seamlessly tileable.
|
||||
const tiled = document.createElement('canvas');
|
||||
tiled.width = width * 3;
|
||||
tiled.height = height * 3;
|
||||
const tc = tiled.getContext('2d');
|
||||
for (let row = 0; row < 3; row++) {
|
||||
for (let col = 0; col < 3; col++) {
|
||||
tc.drawImage(fullCanvas, col * width, row * height);
|
||||
}
|
||||
}
|
||||
// Blur the 3×3 canvas, then crop out only the centre tile.
|
||||
const blurred = document.createElement('canvas');
|
||||
blurred.width = width * 3;
|
||||
blurred.height = height * 3;
|
||||
const bc = blurred.getContext('2d');
|
||||
bc.filter = `blur(${settings.textureSmoothing}px)`;
|
||||
bc.drawImage(tiled, 0, 0);
|
||||
bc.filter = 'none';
|
||||
const offscreen = document.createElement('canvas');
|
||||
offscreen.width = width;
|
||||
offscreen.height = height;
|
||||
offscreen.getContext('2d').drawImage(blurred, width, height, width, height, 0, 0, width, height);
|
||||
const imageData = offscreen.getContext('2d').getImageData(0, 0, width, height);
|
||||
const texture = new THREE.CanvasTexture(offscreen);
|
||||
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
|
||||
return { ...activeMapEntry, imageData, texture };
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
if (!currentGeometry || !currentBounds) return;
|
||||
|
||||
@@ -1323,11 +1367,13 @@ function updatePreview() {
|
||||
// Ensure faceMask attribute is current before rendering
|
||||
updateFaceMask(activeGeo);
|
||||
|
||||
const effectiveEntry = getEffectiveMapEntry();
|
||||
|
||||
if (!previewMaterial) {
|
||||
previewMaterial = createPreviewMaterial(activeMapEntry.texture, fullSettings);
|
||||
previewMaterial = createPreviewMaterial(effectiveEntry.texture, fullSettings);
|
||||
loadGeometry(activeGeo, previewMaterial);
|
||||
} else {
|
||||
updateMaterial(previewMaterial, activeMapEntry.texture, fullSettings);
|
||||
updateMaterial(previewMaterial, effectiveEntry.texture, fullSettings);
|
||||
}
|
||||
|
||||
exportBtn.disabled = false;
|
||||
@@ -1447,7 +1493,7 @@ async function toggleDisplacementPreview(enable) {
|
||||
if (!enable) {
|
||||
// Revert to original geometry with bump-only shading.
|
||||
if (currentGeometry && previewMaterial) {
|
||||
updateMaterial(previewMaterial, activeMapEntry?.texture, { ...settings, bounds: currentBounds });
|
||||
updateMaterial(previewMaterial, getEffectiveMapEntry()?.texture, { ...settings, bounds: currentBounds });
|
||||
updateFaceMask(currentGeometry);
|
||||
setMeshGeometry(currentGeometry);
|
||||
}
|
||||
@@ -1499,7 +1545,7 @@ async function toggleDisplacementPreview(enable) {
|
||||
previewMaterial = null;
|
||||
}
|
||||
const fullSettings = { ...settings, bounds: currentBounds };
|
||||
previewMaterial = createPreviewMaterial(activeMapEntry.texture, fullSettings);
|
||||
previewMaterial = createPreviewMaterial(getEffectiveMapEntry().texture, fullSettings);
|
||||
setMeshGeometry(dispPreviewGeometry);
|
||||
setMeshMaterial(previewMaterial);
|
||||
|
||||
@@ -1585,12 +1631,13 @@ async function handleExport() {
|
||||
const subTriCount = subdivided.attributes.position.count / 3;
|
||||
setProgress(0.38, t('progress.applyingDisplacement', { n: subTriCount.toLocaleString() }));
|
||||
|
||||
const exportEntry = getEffectiveMapEntry();
|
||||
const displaced = await runAsync(() =>
|
||||
applyDisplacement(
|
||||
subdivided,
|
||||
activeMapEntry.imageData,
|
||||
activeMapEntry.width,
|
||||
activeMapEntry.height,
|
||||
exportEntry.imageData,
|
||||
exportEntry.width,
|
||||
exportEntry.height,
|
||||
settings,
|
||||
currentBounds,
|
||||
(p) => setProgress(0.38 + p * 0.32, t('progress.displacingVertices'))
|
||||
|
||||
Reference in New Issue
Block a user