diff --git a/frontend/dcViewer.js b/frontend/dcViewer.js index 1d12226..eff06da 100644 --- a/frontend/dcViewer.js +++ b/frontend/dcViewer.js @@ -302,6 +302,7 @@ let modelVersionHistory = null; let modelVersionHistoryLoading = false; let modelVersionUploading = false; let versionHistoryModal = null; +let emptyWheelNavigationRestoreTimer = null; const GUEST_TOOLBAR_ACTIONS = new Set(["measure", "section", "projection"]); const memoryStore = new Map(); // url -> ArrayBuffer const blobUrls = []; @@ -377,8 +378,8 @@ const treeModelIds = new Set(); const CAMERA_CONTROL_SETTINGS = { keyboardDollyRate: 45, dollyInertia: 0, - dollyMinSpeed: 0.008, - dollyProximityThreshold: 90, + dollyMinSpeed: 0.04, + dollyProximityThreshold: 30, }; const ZOOM_SPEED_PERCENT_MIN = 5; @@ -460,10 +461,9 @@ const configureCameraControl = () => { if (!control) return; activeZoomSpeed = normalizeZoomSpeed(activeZoomSpeed); configureCameraPanMap(control); - // Keep wheel dolly speed independent from whether the pointer is over model geometry. - control.followPointer = false; + control.followPointer = true; control.doublePickFlyTo = false; - control.smartPivot = false; + control.smartPivot = true; control.zoomOnMouseWheel = true; control.keyboardDollyRate = CAMERA_CONTROL_SETTINGS.keyboardDollyRate; control.mouseWheelDollyRate = getZoomDollyRate(activeZoomSpeed); @@ -1341,6 +1341,53 @@ const getCanvasPointFromEvent = (event) => { }; }; +const restorePointerNavigation = () => { + if (emptyWheelNavigationRestoreTimer) { + window.clearTimeout(emptyWheelNavigationRestoreTimer); + emptyWheelNavigationRestoreTimer = null; + } + const control = viewer?.cameraControl; + if (!control) return; + control.followPointer = true; + control.smartPivot = true; +}; + +const schedulePointerNavigationRestore = () => { + if (emptyWheelNavigationRestoreTimer) { + window.clearTimeout(emptyWheelNavigationRestoreTimer); + } + emptyWheelNavigationRestoreTimer = window.setTimeout(() => { + emptyWheelNavigationRestoreTimer = null; + restorePointerNavigation(); + }, 80); +}; + +const hasWheelSurfaceHit = (event) => { + if (!activeModels.length || !viewer?.scene?.pick) return true; + const point = getCanvasPointFromEvent(event); + if (!point) return true; + try { + const hit = viewer.scene.pick({ + canvasPos: point.canvasPos, + pickSurface: true, + }); + return Boolean(hit?.entity || hit?.worldPos); + } catch (_) { + return true; + } +}; + +const guardEmptyWheelNavigation = (event) => { + const control = viewer?.cameraControl; + if (!control) return; + if (hasWheelSurfaceHit(event)) { + restorePointerNavigation(); + return; + } + control.followPointer = false; + schedulePointerNavigationRestore(); +}; + const getWorldPosFromPick = (hit) => { const direct = hit?.worldPos || hit?.worldPosition || hit?.position || hit?.origin; if (direct && direct.length >= 3) { @@ -5705,6 +5752,8 @@ const initViewer = async () => { const opened = openCommentContextMenuFromEvent(event); if (!opened) event.preventDefault(); }); + canvasEl.addEventListener("pointerdown", restorePointerNavigation, { capture: true }); + canvasEl.addEventListener("wheel", guardEmptyWheelNavigation, { capture: true, passive: true }); canvasEl.addEventListener("pointerdown", (event) => { if (measureActive) { downPos = null;