(file)
| 2888 | roundToPrecision(base + direction * step, precision + 2) |
| 2889 | ); |
| 2890 | |
| 2891 | applyFn(next); |
| 2892 | }, { passive: false }); |
| 2893 | } |
| 2894 | |
| 2895 | function linkSlider(slider, valInput, onChangeFn, livePreview = true) { |
| 2896 | const isSpan = valInput.tagName === 'SPAN'; |
| 2897 | const applyLinkedValue = (raw) => { |
| 2898 | const clamped = clampToInputBounds(valInput, raw); |
| 2899 | slider.value = Math.max(parseFloat(slider.min), Math.min(parseFloat(slider.max), clamped)); |
| 2900 | onChangeFn(clamped); |
| 2901 | valInput.value = formatInputValue(valInput, clamped); |
| 2902 | if (livePreview) { |
| 2903 | clearTimeout(previewDebounce); |
| 2904 | previewDebounce = setTimeout(updatePreview, 80); |
| 2905 | } |
| 2906 | }; |
| 2907 | slider.addEventListener('input', () => { |
| 2908 | const v = parseFloat(slider.value); |
| 2909 | const display = onChangeFn(v); |
| 2910 | if (isSpan) valInput.textContent = display; else valInput.value = display; |
| 2911 | if (livePreview) { |
| 2912 | clearTimeout(previewDebounce); |
| 2913 | previewDebounce = setTimeout(updatePreview, 80); |
| 2914 | } |
| 2915 | }); |
| 2916 | // Double-click resets to default value |
| 2917 | slider.addEventListener('dblclick', () => { |
| 2918 | slider.value = slider.defaultValue; |
| 2919 | const v = parseFloat(slider.value); |
| 2920 | const display = onChangeFn(v); |
| 2921 | if (isSpan) valInput.textContent = display; else valInput.value = display; |
| 2922 | if (livePreview) { |
| 2923 | clearTimeout(previewDebounce); |
| 2924 | previewDebounce = setTimeout(updatePreview, 80); |
| 2925 | } |
| 2926 | }); |
| 2927 | if (!isSpan) { |
| 2928 | valInput.addEventListener('change', () => { |
| 2929 | const raw = parseFloat(valInput.value); |
| 2930 | if (isNaN(raw)) { valInput.value = formatInputValue(valInput, parseFloat(slider.value)); return; } |
| 2931 | applyLinkedValue(raw); |
| 2932 | }); |
| 2933 | addFineWheelSupport(valInput, applyLinkedValue); |
| 2934 | } |
| 2935 | } |
| 2936 | |
| 2937 | function formatM(n) { |
| 2938 | return n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)} M` |
| 2939 | : n >= 1_000 ? `${(n / 1_000).toFixed(0)} k` |
| 2940 | : String(n); |
| 2941 | } |
| 2942 | |
| 2943 | // ── STL loading ─────────────────────────────────────────────────────────────── |
| 2944 | |
| 2945 | function loadDefaultCube() { |
| 2946 | // Create a 50×50×50 mm box; convert to non-indexed so it behaves like a |
| 2947 | // real STL (buildAdjacency and displacement expect non-indexed geometry). |
no test coverage detected