mirror of
https://github.com/CNCKitchen/stlTexturizer.git
synced 2026-04-07 22:11:32 +00:00
Merge PR #27: Add mouse wheel fine-tuning for numeric settings inputs
This commit is contained in:
+7
-6
@@ -122,7 +122,7 @@
|
|||||||
<div class="form-row slider-row">
|
<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>
|
<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="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" />
|
<input type="number" class="val" id="texture-smoothing-val" value="0" min="0" max="20" step="any" data-wheel-decimals="1" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
<div class="form-row slider-row">
|
<div class="form-row slider-row">
|
||||||
<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>
|
<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="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" />
|
<input type="number" class="val" id="seam-band-width-val" value="0.5" min="0" max="1" step="0.01" data-wheel-decimals="2" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row slider-row" id="cap-angle-row" style="display:none">
|
<div class="form-row slider-row" id="cap-angle-row" style="display:none">
|
||||||
<label for="cap-angle" data-i18n="labels.capAngle" data-i18n-title="tooltips.capAngle" title="Angle (in degrees) from vertical at which the top/bottom cap projection kicks in. Smaller values limit cap projection to nearly flat faces.">Cap Angle ⓘ</label>
|
<label for="cap-angle" data-i18n="labels.capAngle" data-i18n-title="tooltips.capAngle" title="Angle (in degrees) from vertical at which the top/bottom cap projection kicks in. Smaller values limit cap projection to nearly flat faces.">Cap Angle ⓘ</label>
|
||||||
@@ -164,11 +164,12 @@
|
|||||||
<div class="form-row slider-row">
|
<div class="form-row slider-row">
|
||||||
<label for="amplitude" data-i18n="labels.amplitude">Amplitude</label>
|
<label for="amplitude" data-i18n="labels.amplitude">Amplitude</label>
|
||||||
<input type="range" id="amplitude" min="-2" max="2" step="0.01" value="0.5" />
|
<input type="range" id="amplitude" min="-2" max="2" step="0.01" value="0.5" />
|
||||||
<input type="number" class="val" id="amplitude-val" value="0.5" min="-100" max="100" step="0.01" />
|
<input type="number" class="val" id="amplitude-val" value="0.5" min="-100" max="100" step="0.01" data-wheel-decimals="2" />
|
||||||
</div>
|
</div>
|
||||||
<div id="amplitude-warning" class="amplitude-warning hidden" data-i18n="warnings.amplitudeOverlap">
|
<div id="amplitude-warning" class="amplitude-warning hidden" data-i18n="warnings.amplitudeOverlap">
|
||||||
⚠ Amplitude exceeds 10% of the smallest model dimension — geometry overlaps may occur in the exported STL.
|
⚠ Amplitude exceeds 10% of the smallest model dimension — geometry overlaps may occur in the exported STL.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label class="checkbox-label" for="symmetric-displacement"
|
<label class="checkbox-label" for="symmetric-displacement"
|
||||||
data-i18n-title="tooltips.symmetricDisplacement"
|
data-i18n-title="tooltips.symmetricDisplacement"
|
||||||
@@ -237,7 +238,7 @@
|
|||||||
<div class="form-row slider-row">
|
<div class="form-row slider-row">
|
||||||
<label for="boundary-falloff" data-i18n="labels.boundaryFalloff" data-i18n-title="tooltips.boundaryFalloff" title="Gradually reduces displacement to zero near masked boundaries, preventing triangle overlap where textured and non-textured regions meet.">Smooth Mask ⓘ</label>
|
<label for="boundary-falloff" data-i18n="labels.boundaryFalloff" data-i18n-title="tooltips.boundaryFalloff" title="Gradually reduces displacement to zero near masked boundaries, preventing triangle overlap where textured and non-textured regions meet.">Smooth Mask ⓘ</label>
|
||||||
<input type="range" id="boundary-falloff" min="0" max="10" step="0.1" value="0" />
|
<input type="range" id="boundary-falloff" min="0" max="10" step="0.1" value="0" />
|
||||||
<input type="number" class="val" id="boundary-falloff-val" value="0" min="0" max="10" step="0.1" />
|
<input type="number" class="val" id="boundary-falloff-val" value="0" min="0" max="10" step="0.1" data-wheel-decimals="2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- By Angle -->
|
<!-- By Angle -->
|
||||||
@@ -320,7 +321,7 @@
|
|||||||
<div id="excl-radius-row" class="form-row slider-row hidden">
|
<div id="excl-radius-row" class="form-row slider-row hidden">
|
||||||
<label for="excl-brush-radius-slider" data-i18n="labels.size">Size</label>
|
<label for="excl-brush-radius-slider" data-i18n="labels.size">Size</label>
|
||||||
<input type="range" id="excl-brush-radius-slider" min="0.2" max="100" step="0.2" value="10" />
|
<input type="range" id="excl-brush-radius-slider" min="0.2" max="100" step="0.2" value="10" />
|
||||||
<input type="number" class="val" id="excl-brush-radius-val" value="10" min="0.2" max="100" step="0.2" />
|
<input type="number" class="val" id="excl-brush-radius-val" value="10" min="0.2" max="100" step="0.2" data-wheel-decimals="1" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bucket threshold (shown when Fill is active) -->
|
<!-- Bucket threshold (shown when Fill is active) -->
|
||||||
@@ -344,7 +345,7 @@
|
|||||||
<div class="form-row slider-row">
|
<div class="form-row slider-row">
|
||||||
<label for="refine-length" data-i18n="labels.resolution" data-i18n-title="tooltips.resolution" title="Edges longer than this value will be split during export">Resolution</label>
|
<label for="refine-length" data-i18n="labels.resolution" data-i18n-title="tooltips.resolution" title="Edges longer than this value will be split during export">Resolution</label>
|
||||||
<input type="range" id="refine-length" min="0.01" max="5" step="0.01" value="1" />
|
<input type="range" id="refine-length" min="0.01" max="5" step="0.01" value="1" />
|
||||||
<input type="number" class="val" id="refine-length-val" value="1" min="0.01" max="100" step="0.01" />
|
<input type="number" class="val" id="refine-length-val" value="1" min="0.01" max="100" step="0.01" data-wheel-decimals="2" />
|
||||||
</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>
|
||||||
|
|||||||
+81
-16
@@ -479,6 +479,7 @@ function wireEvents() {
|
|||||||
scaleUSlider.addEventListener('input', () => applyScaleU(posToScale(parseFloat(scaleUSlider.value))));
|
scaleUSlider.addEventListener('input', () => applyScaleU(posToScale(parseFloat(scaleUSlider.value))));
|
||||||
scaleUSlider.addEventListener('dblclick', () => applyScaleU(posToScale(parseFloat(scaleUSlider.defaultValue))));
|
scaleUSlider.addEventListener('dblclick', () => applyScaleU(posToScale(parseFloat(scaleUSlider.defaultValue))));
|
||||||
scaleUVal.addEventListener('change', () => applyScaleU(parseFloat(scaleUVal.value)));
|
scaleUVal.addEventListener('change', () => applyScaleU(parseFloat(scaleUVal.value)));
|
||||||
|
addFineWheelSupport(scaleUVal, applyScaleU);
|
||||||
|
|
||||||
// Scale V — when lock is on, mirror to U
|
// Scale V — when lock is on, mirror to U
|
||||||
const applyScaleV = (v) => {
|
const applyScaleV = (v) => {
|
||||||
@@ -492,6 +493,7 @@ function wireEvents() {
|
|||||||
scaleVSlider.addEventListener('input', () => applyScaleV(posToScale(parseFloat(scaleVSlider.value))));
|
scaleVSlider.addEventListener('input', () => applyScaleV(posToScale(parseFloat(scaleVSlider.value))));
|
||||||
scaleVSlider.addEventListener('dblclick', () => applyScaleV(posToScale(parseFloat(scaleVSlider.defaultValue))));
|
scaleVSlider.addEventListener('dblclick', () => applyScaleV(posToScale(parseFloat(scaleVSlider.defaultValue))));
|
||||||
scaleVVal.addEventListener('change', () => applyScaleV(parseFloat(scaleVVal.value)));
|
scaleVVal.addEventListener('change', () => applyScaleV(parseFloat(scaleVVal.value)));
|
||||||
|
addFineWheelSupport(scaleVVal, applyScaleV);
|
||||||
|
|
||||||
// Lock toggle
|
// Lock toggle
|
||||||
lockScaleBtn.addEventListener('click', () => {
|
lockScaleBtn.addEventListener('click', () => {
|
||||||
@@ -635,6 +637,13 @@ function wireEvents() {
|
|||||||
exclBrushRadiusVal.value = diam;
|
exclBrushRadiusVal.value = diam;
|
||||||
checkPrecisionOutdated();
|
checkPrecisionOutdated();
|
||||||
});
|
});
|
||||||
|
addFineWheelSupport(exclBrushRadiusVal, (v) => {
|
||||||
|
const diam = Math.max(0.2, Math.min(100, v));
|
||||||
|
brushRadius = diam / 2;
|
||||||
|
exclBrushRadiusSlider.value = diam;
|
||||||
|
exclBrushRadiusVal.value = diam;
|
||||||
|
checkPrecisionOutdated();
|
||||||
|
});
|
||||||
|
|
||||||
exclThresholdSlider.addEventListener('input', () => {
|
exclThresholdSlider.addEventListener('input', () => {
|
||||||
bucketThreshold = parseFloat(exclThresholdSlider.value);
|
bucketThreshold = parseFloat(exclThresholdSlider.value);
|
||||||
@@ -653,6 +662,12 @@ function wireEvents() {
|
|||||||
exclThresholdVal.value = bucketThreshold;
|
exclThresholdVal.value = bucketThreshold;
|
||||||
_lastHoverTriIdx = -1;
|
_lastHoverTriIdx = -1;
|
||||||
});
|
});
|
||||||
|
addFineWheelSupport(exclThresholdVal, (v) => {
|
||||||
|
bucketThreshold = Math.max(0, Math.min(180, v));
|
||||||
|
exclThresholdSlider.value = bucketThreshold;
|
||||||
|
exclThresholdVal.value = bucketThreshold;
|
||||||
|
_lastHoverTriIdx = -1;
|
||||||
|
});
|
||||||
|
|
||||||
exclClearBtn.addEventListener('click', () => {
|
exclClearBtn.addEventListener('click', () => {
|
||||||
excludedFaces = new Set();
|
excludedFaces = new Set();
|
||||||
@@ -1426,8 +1441,69 @@ function updateBucketHover(e) {
|
|||||||
|
|
||||||
// ── Slider helper ─────────────────────────────────────────────────────────────
|
// ── Slider helper ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const INPUT_WHEEL_DECIMALS = 3;
|
||||||
|
|
||||||
|
function getInputPrecision(input) {
|
||||||
|
const configured = parseInt(input.dataset.wheelDecimals, 10);
|
||||||
|
if (!isNaN(configured) && configured >= 0) return configured;
|
||||||
|
const step = input.step;
|
||||||
|
if (step === 'any') return INPUT_WHEEL_DECIMALS;
|
||||||
|
const stepNum = parseFloat(step);
|
||||||
|
if (isNaN(stepNum)) return INPUT_WHEEL_DECIMALS;
|
||||||
|
if (Number.isInteger(stepNum)) return 0;
|
||||||
|
const frac = step.includes('.') ? step.split('.')[1].replace(/0+$/, '').length : 0;
|
||||||
|
return Math.max(INPUT_WHEEL_DECIMALS, frac);
|
||||||
|
}
|
||||||
|
|
||||||
|
function roundToPrecision(value, precision) {
|
||||||
|
if (precision <= 0) return Math.round(value);
|
||||||
|
const factor = 10 ** precision;
|
||||||
|
return Math.round(value * factor) / factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampToInputBounds(input, value) {
|
||||||
|
const min = parseFloat(input.min);
|
||||||
|
const max = parseFloat(input.max);
|
||||||
|
let clamped = value;
|
||||||
|
if (!isNaN(min)) clamped = Math.max(min, clamped);
|
||||||
|
if (!isNaN(max)) clamped = Math.min(max, clamped);
|
||||||
|
return clamped;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatInputValue(input, value) {
|
||||||
|
const precision = getInputPrecision(input);
|
||||||
|
if (precision <= 0) return String(Math.round(value));
|
||||||
|
return value.toFixed(precision).replace(/\.?0+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFineWheelSupport(input, applyFn) {
|
||||||
|
input.addEventListener('wheel', (e) => {
|
||||||
|
if (input.disabled || input.readOnly) return;
|
||||||
|
e.preventDefault();
|
||||||
|
input.focus({ preventScroll: true });
|
||||||
|
const precision = getInputPrecision(input);
|
||||||
|
const step = precision <= 0 ? 1 : 1 / (10 ** precision);
|
||||||
|
const current = parseFloat(input.value);
|
||||||
|
const fallback = parseFloat(input.defaultValue || input.min || '0');
|
||||||
|
const base = isNaN(current) ? (isNaN(fallback) ? 0 : fallback) : current;
|
||||||
|
const direction = e.deltaY < 0 ? 1 : -1;
|
||||||
|
const next = clampToInputBounds(input, roundToPrecision(base + direction * step, precision));
|
||||||
|
applyFn(next);
|
||||||
|
}, { passive: false });
|
||||||
|
}
|
||||||
|
|
||||||
function linkSlider(slider, valInput, onChangeFn, livePreview = true) {
|
function linkSlider(slider, valInput, onChangeFn, livePreview = true) {
|
||||||
const isSpan = valInput.tagName === 'SPAN';
|
const isSpan = valInput.tagName === 'SPAN';
|
||||||
|
const applyLinkedValue = (raw) => {
|
||||||
|
const clamped = clampToInputBounds(valInput, raw);
|
||||||
|
slider.value = Math.max(parseFloat(slider.min), Math.min(parseFloat(slider.max), clamped));
|
||||||
|
onChangeFn(clamped);
|
||||||
|
valInput.value = formatInputValue(valInput, clamped);
|
||||||
|
if (livePreview) {
|
||||||
|
clearTimeout(previewDebounce);
|
||||||
|
previewDebounce = setTimeout(updatePreview, 80);
|
||||||
|
}
|
||||||
|
};
|
||||||
slider.addEventListener('input', () => {
|
slider.addEventListener('input', () => {
|
||||||
const v = parseFloat(slider.value);
|
const v = parseFloat(slider.value);
|
||||||
const display = onChangeFn(v);
|
const display = onChangeFn(v);
|
||||||
@@ -1451,27 +1527,16 @@ function linkSlider(slider, valInput, onChangeFn, livePreview = true) {
|
|||||||
if (!isSpan) {
|
if (!isSpan) {
|
||||||
valInput.addEventListener('change', () => {
|
valInput.addEventListener('change', () => {
|
||||||
const raw = parseFloat(valInput.value);
|
const raw = parseFloat(valInput.value);
|
||||||
if (isNaN(raw)) { valInput.value = slider.value; return; }
|
if (isNaN(raw)) { valInput.value = formatInputValue(valInput, parseFloat(slider.value)); return; }
|
||||||
// Clamp to the input's own min/max (may be wider than the slider range)
|
applyLinkedValue(raw);
|
||||||
const inMin = parseFloat(valInput.min);
|
|
||||||
const inMax = parseFloat(valInput.max);
|
|
||||||
const clamped = (!isNaN(inMin) && !isNaN(inMax))
|
|
||||||
? Math.max(inMin, Math.min(inMax, raw))
|
|
||||||
: raw;
|
|
||||||
// Move slider thumb to nearest valid position (saturates at slider edges)
|
|
||||||
slider.value = Math.max(parseFloat(slider.min), Math.min(parseFloat(slider.max), clamped));
|
|
||||||
valInput.value = onChangeFn(clamped);
|
|
||||||
if (livePreview) {
|
|
||||||
clearTimeout(previewDebounce);
|
|
||||||
previewDebounce = setTimeout(updatePreview, 80);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
addFineWheelSupport(valInput, applyLinkedValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatM(n) {
|
function formatM(n) {
|
||||||
return n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)} M`
|
return n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1).replace(/\.0$/, '')} M`
|
||||||
: n >= 1_000 ? `${(n / 1_000).toFixed(0)} k`
|
: n >= 1_000 ? `${(n / 1_000).toFixed(1).replace(/\.0$/, '')} k`
|
||||||
: String(n);
|
: String(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user