From fca353a142bc78094ff8eca17031224783c93eb0 Mon Sep 17 00:00:00 2001 From: CNCKitchen Date: Thu, 26 Mar 2026 10:42:29 +0100 Subject: [PATCH] feat: implement cursor-centric zoom functionality in viewer --- js/main.js | 24 ++++++++++++++++++++++++ js/viewer.js | 26 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/js/main.js b/js/main.js index 790e1f6..e27e56f 100644 --- a/js/main.js +++ b/js/main.js @@ -292,6 +292,7 @@ function wireEvents() { clearTimeout(previewDebounce); previewDebounce = setTimeout(updatePreview, 80); }; scaleUSlider.addEventListener('input', () => applyScaleU(posToScale(parseFloat(scaleUSlider.value)))); + scaleUSlider.addEventListener('dblclick', () => applyScaleU(posToScale(parseFloat(scaleUSlider.defaultValue)))); scaleUVal.addEventListener('change', () => applyScaleU(parseFloat(scaleUVal.value))); // Scale V — when lock is on, mirror to U @@ -304,6 +305,7 @@ function wireEvents() { clearTimeout(previewDebounce); previewDebounce = setTimeout(updatePreview, 80); }; scaleVSlider.addEventListener('input', () => applyScaleV(posToScale(parseFloat(scaleVSlider.value)))); + scaleVSlider.addEventListener('dblclick', () => applyScaleV(posToScale(parseFloat(scaleVSlider.defaultValue)))); scaleVVal.addEventListener('change', () => applyScaleV(parseFloat(scaleVVal.value))); // Lock toggle @@ -412,6 +414,11 @@ function wireEvents() { brushRadius = parseFloat(exclBrushRadiusSlider.value) / 2; exclBrushRadiusVal.value = parseFloat(exclBrushRadiusSlider.value); }); + exclBrushRadiusSlider.addEventListener('dblclick', () => { + exclBrushRadiusSlider.value = exclBrushRadiusSlider.defaultValue; + brushRadius = parseFloat(exclBrushRadiusSlider.value) / 2; + exclBrushRadiusVal.value = parseFloat(exclBrushRadiusSlider.value); + }); exclBrushRadiusVal.addEventListener('change', () => { let diam = Math.max(0.2, Math.min(100, parseFloat(exclBrushRadiusVal.value) || 10)); brushRadius = diam / 2; @@ -424,6 +431,12 @@ function wireEvents() { exclThresholdVal.value = bucketThreshold; _lastHoverTriIdx = -1; // invalidate hover so next mousemove re-computes }); + exclThresholdSlider.addEventListener('dblclick', () => { + exclThresholdSlider.value = exclThresholdSlider.defaultValue; + bucketThreshold = parseFloat(exclThresholdSlider.value); + exclThresholdVal.value = bucketThreshold; + _lastHoverTriIdx = -1; + }); exclThresholdVal.addEventListener('change', () => { bucketThreshold = Math.max(0, Math.min(180, parseFloat(exclThresholdVal.value) || 20)); exclThresholdSlider.value = bucketThreshold; @@ -907,6 +920,17 @@ function linkSlider(slider, valInput, onChangeFn, livePreview = true) { previewDebounce = setTimeout(updatePreview, 80); } }); + // Double-click resets to default value + slider.addEventListener('dblclick', () => { + slider.value = slider.defaultValue; + const v = parseFloat(slider.value); + const display = onChangeFn(v); + if (isSpan) valInput.textContent = display; else valInput.value = display; + if (livePreview) { + clearTimeout(previewDebounce); + previewDebounce = setTimeout(updatePreview, 80); + } + }); if (!isSpan) { valInput.addEventListener('change', () => { const raw = parseFloat(valInput.value); diff --git a/js/viewer.js b/js/viewer.js index a880796..abfc89d 100644 --- a/js/viewer.js +++ b/js/viewer.js @@ -183,6 +183,32 @@ export function initViewer(canvas) { controls.enableDamping = true; controls.dampingFactor = 0.08; controls.screenSpacePanning = true; + controls.enableZoom = false; // we handle zoom ourselves for cursor-centric behaviour + + // Cursor-centric zoom: zoom toward the mouse pointer instead of screen centre + renderer.domElement.addEventListener('wheel', (e) => { + e.preventDefault(); + const rect = renderer.domElement.getBoundingClientRect(); + const ndcX = ((e.clientX - rect.left) / rect.width) * 2 - 1; + const ndcY = -((e.clientY - rect.top) / rect.height) * 2 + 1; + + // World position under cursor before zoom + const before = new THREE.Vector3(ndcX, ndcY, 0).unproject(camera); + + // Apply zoom + const factor = e.deltaY > 0 ? 1 / 1.1 : 1.1; + camera.zoom = Math.max(0.05, Math.min(200, camera.zoom * factor)); + camera.updateProjectionMatrix(); + + // World position under cursor after zoom + const after = new THREE.Vector3(ndcX, ndcY, 0).unproject(camera); + + // Shift camera + target so the world point stays under the cursor + const delta = before.clone().sub(after); + camera.position.add(delta); + controls.target.add(delta); + controls.update(); + }, { passive: false }); // Resize observer const resizeObserver = new ResizeObserver(() => onResize());