diff --git a/frontend/dcViewer.css b/frontend/dcViewer.css index 3b7b477..2d05245 100644 --- a/frontend/dcViewer.css +++ b/frontend/dcViewer.css @@ -214,7 +214,7 @@ header { #navCube { position: absolute; top: 64px; - right: 12px; + right: 64px; width: 100px; height: 100px; z-index: 5; @@ -1664,7 +1664,8 @@ header { #saveProjectBackdrop, #deleteProjectBackdrop, -#shareModalBackdrop { +#shareModalBackdrop, +.version-history-backdrop { z-index: var(--z-critical-backdrop); background: rgba(10, 12, 16, 0.38); backdrop-filter: blur(18px) saturate(1.18) brightness(0.82); @@ -2784,6 +2785,7 @@ header { gap: 10px; align-items: center; padding: 4px 6px 4px 6px; + touch-action: none; } .settings-panel .env-range-control.slider-row { @@ -3245,17 +3247,14 @@ html[data-share-startup="pending"] #loader.hidden { .version-history-backdrop { position: fixed; inset: 0; - z-index: var(--z-modal, 10000); - background: rgba(0, 0, 0, 0.58); - backdrop-filter: blur(18px); - -webkit-backdrop-filter: blur(18px); + z-index: var(--z-critical-backdrop); } .version-history-modal { position: fixed; left: 50%; top: 50%; - z-index: calc(var(--z-modal, 10000) + 1); + z-index: var(--z-critical-modal); display: flex; width: min(96vw, 1280px); max-height: min(76vh, 720px); diff --git a/frontend/dcViewer.js b/frontend/dcViewer.js index 8420eba..1d12226 100644 --- a/frontend/dcViewer.js +++ b/frontend/dcViewer.js @@ -381,10 +381,23 @@ const CAMERA_CONTROL_SETTINGS = { dollyProximityThreshold: 90, }; +const ZOOM_SPEED_PERCENT_MIN = 5; +const ZOOM_SPEED_PERCENT_MAX = 100; +const ZOOM_DOLLY_RATE_MIN = 1; +const ZOOM_DOLLY_RATE_MAX = 100; +const ZOOM_DOLLY_RESPONSE_EXPONENT = 1.45; + const normalizeZoomSpeed = (value) => { const numeric = Number(value); if (!Number.isFinite(numeric)) return 20; - return Math.min(100, Math.max(5, Math.round(numeric))); + return Math.min(ZOOM_SPEED_PERCENT_MAX, Math.max(ZOOM_SPEED_PERCENT_MIN, Math.round(numeric))); +}; + +const getZoomDollyRate = (value) => { + const normalized = normalizeZoomSpeed(value); + const range = ZOOM_SPEED_PERCENT_MAX - ZOOM_SPEED_PERCENT_MIN; + const t = range > 0 ? (normalized - ZOOM_SPEED_PERCENT_MIN) / range : 0; + return ZOOM_DOLLY_RATE_MIN + (Math.pow(t, ZOOM_DOLLY_RESPONSE_EXPONENT) * (ZOOM_DOLLY_RATE_MAX - ZOOM_DOLLY_RATE_MIN)); }; const isHexColor = (value) => /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(String(value || "").trim()); @@ -453,7 +466,7 @@ const configureCameraControl = () => { control.smartPivot = false; control.zoomOnMouseWheel = true; control.keyboardDollyRate = CAMERA_CONTROL_SETTINGS.keyboardDollyRate; - control.mouseWheelDollyRate = activeZoomSpeed; + control.mouseWheelDollyRate = getZoomDollyRate(activeZoomSpeed); control.dollyInertia = CAMERA_CONTROL_SETTINGS.dollyInertia; control.dollyMinSpeed = CAMERA_CONTROL_SETTINGS.dollyMinSpeed; control.dollyProximityThreshold = CAMERA_CONTROL_SETTINGS.dollyProximityThreshold; diff --git a/frontend/src/ui/controls/slider.js b/frontend/src/ui/controls/slider.js index 22af191..b79b772 100644 --- a/frontend/src/ui/controls/slider.js +++ b/frontend/src/ui/controls/slider.js @@ -60,6 +60,57 @@ export function createSliderControl({ onChange?.(v); }); + const clampValue = (next) => Math.min(Number(input.max), Math.max(Number(input.min), next)); + + const valueFromClientX = (clientX) => { + const rect = input.getBoundingClientRect(); + const minValue = Number(input.min); + const maxValue = Number(input.max); + const range = maxValue - minValue; + if (!rect.width || !range) return Number(input.value); + const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); + const raw = minValue + (ratio * range); + if (input.step === "any") return clampValue(raw); + const stepValue = Number(input.step) || 1; + return clampValue(minValue + (Math.round((raw - minValue) / stepValue) * stepValue)); + }; + + const commitValue = (nextValue) => { + input.value = nextValue; + updateFill(); + onChange?.(Number(input.value)); + }; + + let activeTouchPointerId = null; + + const isTouchLikePointer = (event) => event.pointerType === "touch" || event.pointerType === "pen"; + + row.addEventListener("pointerdown", (event) => { + if (!isTouchLikePointer(event) || (event.button !== undefined && event.button !== 0)) return; + activeTouchPointerId = event.pointerId; + event.preventDefault(); + row.setPointerCapture?.(event.pointerId); + input.focus?.({ preventScroll: true }); + commitValue(valueFromClientX(event.clientX)); + }); + + row.addEventListener("pointermove", (event) => { + if (activeTouchPointerId !== event.pointerId) return; + event.preventDefault(); + commitValue(valueFromClientX(event.clientX)); + }); + + const finishTouchPointer = (event) => { + if (activeTouchPointerId !== event.pointerId) return; + activeTouchPointerId = null; + if (row.hasPointerCapture?.(event.pointerId)) { + row.releasePointerCapture?.(event.pointerId); + } + }; + + row.addEventListener("pointerup", finishTouchPointer); + row.addEventListener("pointercancel", finishTouchPointer); + updateFill(); row.appendChild(lbl);