From 9dc5dd4aa06736f627073c4139fbba0c758790f2 Mon Sep 17 00:00:00 2001 From: CODEX Date: Tue, 23 Jun 2026 21:05:54 +0300 Subject: [PATCH] Scale BIM empty-space wheel zoom --- frontend/dcViewer.js | 72 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/frontend/dcViewer.js b/frontend/dcViewer.js index eff06da..9490d3b 100644 --- a/frontend/dcViewer.js +++ b/frontend/dcViewer.js @@ -303,6 +303,8 @@ let modelVersionHistoryLoading = false; let modelVersionUploading = false; let versionHistoryModal = null; let emptyWheelNavigationRestoreTimer = null; +let lastSurfaceDollyFactor = null; +let lastSurfaceDollyFactorAt = 0; const GUEST_TOOLBAR_ACTIONS = new Set(["measure", "section", "projection"]); const memoryStore = new Map(); // url -> ArrayBuffer const blobUrls = []; @@ -387,6 +389,7 @@ 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 LAST_SURFACE_DOLLY_FACTOR_TTL = 1500; const normalizeZoomSpeed = (value) => { const numeric = Number(value); @@ -401,6 +404,8 @@ const getZoomDollyRate = (value) => { return ZOOM_DOLLY_RATE_MIN + (Math.pow(t, ZOOM_DOLLY_RESPONSE_EXPONENT) * (ZOOM_DOLLY_RATE_MAX - ZOOM_DOLLY_RATE_MIN)); }; +const getBaseMouseWheelDollyRate = () => getZoomDollyRate(activeZoomSpeed); + const isHexColor = (value) => /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(String(value || "").trim()); const normalizeCustomDisplayColor = (value) => { @@ -466,7 +471,7 @@ const configureCameraControl = () => { control.smartPivot = true; control.zoomOnMouseWheel = true; control.keyboardDollyRate = CAMERA_CONTROL_SETTINGS.keyboardDollyRate; - control.mouseWheelDollyRate = getZoomDollyRate(activeZoomSpeed); + control.mouseWheelDollyRate = getBaseMouseWheelDollyRate(); control.dollyInertia = CAMERA_CONTROL_SETTINGS.dollyInertia; control.dollyMinSpeed = CAMERA_CONTROL_SETTINGS.dollyMinSpeed; control.dollyProximityThreshold = CAMERA_CONTROL_SETTINGS.dollyProximityThreshold; @@ -1341,6 +1346,60 @@ const getCanvasPointFromEvent = (event) => { }; }; +const getDollyFactorForDistance = (distance) => { + const numeric = Number(distance); + if (!Number.isFinite(numeric) || numeric <= 0) return 1; + return Math.max( + CAMERA_CONTROL_SETTINGS.dollyMinSpeed, + numeric / CAMERA_CONTROL_SETTINGS.dollyProximityThreshold, + ); +}; + +const getDollyFactorForWorldPos = (worldPos) => { + const eye = viewer?.camera?.eye || viewer?.scene?.camera?.eye; + if (!eye || !worldPos || worldPos.length < 3) return null; + const distance = Math.hypot( + Number(worldPos[0]) - Number(eye[0]), + Number(worldPos[1]) - Number(eye[1]), + Number(worldPos[2]) - Number(eye[2]), + ); + return Number.isFinite(distance) ? getDollyFactorForDistance(distance) : null; +}; + +const getCameraFocusDollyFactor = () => { + const camera = viewer?.camera || viewer?.scene?.camera; + const directDistance = Number(camera?.eyeLookDist); + if (Number.isFinite(directDistance) && directDistance > 0) { + return getDollyFactorForDistance(directDistance); + } + const eye = camera?.eye; + const look = camera?.look; + if (!eye || !look) return 1; + const distance = Math.hypot( + Number(look[0]) - Number(eye[0]), + Number(look[1]) - Number(eye[1]), + Number(look[2]) - Number(eye[2]), + ); + return Number.isFinite(distance) ? getDollyFactorForDistance(distance) : 1; +}; + +const rememberSurfaceDollyFactor = (hit) => { + const factor = getDollyFactorForWorldPos(hit?.worldPos); + if (factor === null) return; + lastSurfaceDollyFactor = factor; + lastSurfaceDollyFactorAt = window.performance?.now?.() || Date.now(); +}; + +const getEmptyWheelDollyFactor = () => { + const focusFactor = getCameraFocusDollyFactor(); + const now = window.performance?.now?.() || Date.now(); + const hasFreshSurfaceFactor = ( + lastSurfaceDollyFactor !== null && + now - lastSurfaceDollyFactorAt <= LAST_SURFACE_DOLLY_FACTOR_TTL + ); + return hasFreshSurfaceFactor ? Math.min(focusFactor, lastSurfaceDollyFactor) : focusFactor; +}; + const restorePointerNavigation = () => { if (emptyWheelNavigationRestoreTimer) { window.clearTimeout(emptyWheelNavigationRestoreTimer); @@ -1350,6 +1409,7 @@ const restorePointerNavigation = () => { if (!control) return; control.followPointer = true; control.smartPivot = true; + control.mouseWheelDollyRate = getBaseMouseWheelDollyRate(); }; const schedulePointerNavigationRestore = () => { @@ -1362,16 +1422,15 @@ const schedulePointerNavigationRestore = () => { }, 80); }; -const hasWheelSurfaceHit = (event) => { +const getWheelSurfaceHit = (event) => { if (!activeModels.length || !viewer?.scene?.pick) return true; const point = getCanvasPointFromEvent(event); if (!point) return true; try { - const hit = viewer.scene.pick({ + return viewer.scene.pick({ canvasPos: point.canvasPos, pickSurface: true, }); - return Boolean(hit?.entity || hit?.worldPos); } catch (_) { return true; } @@ -1380,11 +1439,14 @@ const hasWheelSurfaceHit = (event) => { const guardEmptyWheelNavigation = (event) => { const control = viewer?.cameraControl; if (!control) return; - if (hasWheelSurfaceHit(event)) { + const hit = getWheelSurfaceHit(event); + if (hit === true || hit?.entity || hit?.worldPos) { + rememberSurfaceDollyFactor(hit === true ? null : hit); restorePointerNavigation(); return; } control.followPointer = false; + control.mouseWheelDollyRate = getBaseMouseWheelDollyRate() * getEmptyWheelDollyFactor(); schedulePointerNavigationRestore(); };