Scale BIM empty-space wheel zoom

This commit is contained in:
CODEX 2026-06-23 21:05:54 +03:00
parent b90142a6c8
commit 9dc5dd4aa0
1 changed files with 67 additions and 5 deletions

View File

@ -303,6 +303,8 @@ let modelVersionHistoryLoading = false;
let modelVersionUploading = false; let modelVersionUploading = false;
let versionHistoryModal = null; let versionHistoryModal = null;
let emptyWheelNavigationRestoreTimer = null; let emptyWheelNavigationRestoreTimer = null;
let lastSurfaceDollyFactor = null;
let lastSurfaceDollyFactorAt = 0;
const GUEST_TOOLBAR_ACTIONS = new Set(["measure", "section", "projection"]); const GUEST_TOOLBAR_ACTIONS = new Set(["measure", "section", "projection"]);
const memoryStore = new Map(); // url -> ArrayBuffer const memoryStore = new Map(); // url -> ArrayBuffer
const blobUrls = []; const blobUrls = [];
@ -387,6 +389,7 @@ const ZOOM_SPEED_PERCENT_MAX = 100;
const ZOOM_DOLLY_RATE_MIN = 1; const ZOOM_DOLLY_RATE_MIN = 1;
const ZOOM_DOLLY_RATE_MAX = 100; const ZOOM_DOLLY_RATE_MAX = 100;
const ZOOM_DOLLY_RESPONSE_EXPONENT = 1.45; const ZOOM_DOLLY_RESPONSE_EXPONENT = 1.45;
const LAST_SURFACE_DOLLY_FACTOR_TTL = 1500;
const normalizeZoomSpeed = (value) => { const normalizeZoomSpeed = (value) => {
const numeric = Number(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)); 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 isHexColor = (value) => /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(String(value || "").trim());
const normalizeCustomDisplayColor = (value) => { const normalizeCustomDisplayColor = (value) => {
@ -466,7 +471,7 @@ const configureCameraControl = () => {
control.smartPivot = true; control.smartPivot = true;
control.zoomOnMouseWheel = true; control.zoomOnMouseWheel = true;
control.keyboardDollyRate = CAMERA_CONTROL_SETTINGS.keyboardDollyRate; control.keyboardDollyRate = CAMERA_CONTROL_SETTINGS.keyboardDollyRate;
control.mouseWheelDollyRate = getZoomDollyRate(activeZoomSpeed); control.mouseWheelDollyRate = getBaseMouseWheelDollyRate();
control.dollyInertia = CAMERA_CONTROL_SETTINGS.dollyInertia; control.dollyInertia = CAMERA_CONTROL_SETTINGS.dollyInertia;
control.dollyMinSpeed = CAMERA_CONTROL_SETTINGS.dollyMinSpeed; control.dollyMinSpeed = CAMERA_CONTROL_SETTINGS.dollyMinSpeed;
control.dollyProximityThreshold = CAMERA_CONTROL_SETTINGS.dollyProximityThreshold; 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 = () => { const restorePointerNavigation = () => {
if (emptyWheelNavigationRestoreTimer) { if (emptyWheelNavigationRestoreTimer) {
window.clearTimeout(emptyWheelNavigationRestoreTimer); window.clearTimeout(emptyWheelNavigationRestoreTimer);
@ -1350,6 +1409,7 @@ const restorePointerNavigation = () => {
if (!control) return; if (!control) return;
control.followPointer = true; control.followPointer = true;
control.smartPivot = true; control.smartPivot = true;
control.mouseWheelDollyRate = getBaseMouseWheelDollyRate();
}; };
const schedulePointerNavigationRestore = () => { const schedulePointerNavigationRestore = () => {
@ -1362,16 +1422,15 @@ const schedulePointerNavigationRestore = () => {
}, 80); }, 80);
}; };
const hasWheelSurfaceHit = (event) => { const getWheelSurfaceHit = (event) => {
if (!activeModels.length || !viewer?.scene?.pick) return true; if (!activeModels.length || !viewer?.scene?.pick) return true;
const point = getCanvasPointFromEvent(event); const point = getCanvasPointFromEvent(event);
if (!point) return true; if (!point) return true;
try { try {
const hit = viewer.scene.pick({ return viewer.scene.pick({
canvasPos: point.canvasPos, canvasPos: point.canvasPos,
pickSurface: true, pickSurface: true,
}); });
return Boolean(hit?.entity || hit?.worldPos);
} catch (_) { } catch (_) {
return true; return true;
} }
@ -1380,11 +1439,14 @@ const hasWheelSurfaceHit = (event) => {
const guardEmptyWheelNavigation = (event) => { const guardEmptyWheelNavigation = (event) => {
const control = viewer?.cameraControl; const control = viewer?.cameraControl;
if (!control) return; if (!control) return;
if (hasWheelSurfaceHit(event)) { const hit = getWheelSurfaceHit(event);
if (hit === true || hit?.entity || hit?.worldPos) {
rememberSurfaceDollyFactor(hit === true ? null : hit);
restorePointerNavigation(); restorePointerNavigation();
return; return;
} }
control.followPointer = false; control.followPointer = false;
control.mouseWheelDollyRate = getBaseMouseWheelDollyRate() * getEmptyWheelDollyFactor();
schedulePointerNavigationRestore(); schedulePointerNavigationRestore();
}; };