From bfeb38fe3d8e9868ba2336ec21baa11c1c185393 Mon Sep 17 00:00:00 2001 From: Vincent JAILLOT Date: Thu, 22 Sep 2022 12:13:06 +0200 Subject: [PATCH] feat(cameraUtils): add helper to zoom to a 3D bounding box --- src/Utils/CameraUtils.js | 120 ++++++++++++++++++++++++++++----------- test/unit/CameraUtils.js | 31 +++++++++- 2 files changed, 118 insertions(+), 33 deletions(-) diff --git a/src/Utils/CameraUtils.js b/src/Utils/CameraUtils.js index fa41799095..8adb742535 100644 --- a/src/Utils/CameraUtils.js +++ b/src/Utils/CameraUtils.js @@ -341,6 +341,44 @@ export function getRig(camera) { return rigs[camera.uuid]; } +/** + * Computes the range for a camera to have a display that fits the rectangle with the given dimensions. In case of an + * ortographic camera, we compute and set the zoom value and return a default range of 1000. In case of a perspective + * camera, we compute and return the camera range. + * @param {View} view the view of the camera + * @param {THREE.Camera} camera the camera to compute the range for + * @param {Object} dimensions the dimensions of the rectangle to display + * @returns {Number} the range of the camera to fit the given dimensions + */ +function computeCameraRangeFromDimensions(view, camera, dimensions) { + let range = 0; + if (camera.isOrthographicCamera) { + // setup camera zoom + if (dimensions.x / dimensions.y > camera.aspect) { + camera.zoom = (camera.right - camera.left) / dimensions.x; + } else { + camera.zoom = (camera.top - camera.bottom) / dimensions.y; + } + camera.updateProjectionMatrix(); + + // setup camera placement + range = 1000; // TODO: why 1000? Make it configurable with a default value at least + } else if (camera.isPerspectiveCamera) { + // setup range for camera placement + const verticalFOV = THREE.Math.degToRad(camera.fov); + if (dimensions.x / dimensions.y > camera.aspect) { + const focal = (view.domElement.clientHeight * 0.5) / Math.tan(verticalFOV * 0.5); + const horizontalFOV = 2 * Math.atan(view.domElement.clientWidth * 0.5 / focal); + range = dimensions.x / (2 * Math.tan(horizontalFOV * 0.5)); + } else { + range = dimensions.y / (2 * Math.tan(verticalFOV * 0.5)); + } + } else { + console.error('CameraUtils only supports ortographic and perspective camera.'); + } + return range; +} + /** * @module CameraUtils */ @@ -377,9 +415,9 @@ export default { * Gets the current parameters transform camera looking at target. * * @param {View} view The camera view - * @param {Camera} camera The camera to get transform + * @param {THREE.Camera} camera The camera to get transform * @param {THREE.Vector3} [target] - The optional target - * @return {CameraUtils~CameraTransformOptions} The transform camera looking at target + * @returns {CameraUtils~CameraTransformOptions} The transform camera looking at target */ getTransformCameraLookingAtTarget(view, camera, target) { const rig = getRig(camera); @@ -390,9 +428,9 @@ export default { * Apply transform to camera * * @param {View} view The camera view - * @param {Camera} camera The camera to transform + * @param {THREE.Camera} camera The camera to transform * @param {CameraUtils~CameraTransformOptions|Extent} params The parameters - * @return {Promise} promise with resolve final CameraUtils~CameraTransformOptions + * @returns {Promise} promise with resolve final CameraUtils~CameraTransformOptions */ transformCameraToLookAtTarget(view, camera, params = {}) { if (params.isExtent) { @@ -417,10 +455,10 @@ export default { * Compute the CameraTransformOptions that allow a given camera to display a given extent in its entirety. * * @param {View} view The camera view - * @param {Camera} camera The camera to get the CameraTransformOptions from + * @param {THREE.Camera} camera The camera to get the CameraTransformOptions from * @param {Extent} extent The extent the camera must display * - * @return {CameraUtils~CameraTransformOptions} The CameraTransformOptions allowing camera to display the extent. + * @returns {CameraUtils~CameraTransformOptions} The CameraTransformOptions allowing camera to display the extent. */ getCameraTransformOptionsFromExtent(view, camera, extent) { const cameraTransformOptions = { @@ -444,28 +482,46 @@ export default { extent.center(cameraTransformOptions.coord); - if (camera.isOrthographicCamera) { - // setup camera zoom - if (dimensions.x / dimensions.y > camera.aspect) { - camera.zoom = (camera.right - camera.left) / dimensions.x; - } else { - camera.zoom = (camera.top - camera.bottom) / dimensions.y; - } - camera.updateProjectionMatrix(); - - // setup camera placement - cameraTransformOptions.range = 1000; - } else if (camera.isPerspectiveCamera) { - // setup range for camera placement - const verticalFOV = THREE.Math.degToRad(camera.fov); - if (dimensions.x / dimensions.y > camera.aspect) { - const focal = (view.domElement.clientHeight * 0.5) / Math.tan(verticalFOV * 0.5); - const horizontalFOV = 2 * Math.atan(view.domElement.clientWidth * 0.5 / focal); - cameraTransformOptions.range = dimensions.x / (2 * Math.tan(horizontalFOV * 0.5)); - } else { - cameraTransformOptions.range = dimensions.y / (2 * Math.tan(verticalFOV * 0.5)); - } + cameraTransformOptions.range = computeCameraRangeFromDimensions(view, camera, dimensions); + + return cameraTransformOptions; + }, + + /** + * Computes the {@link CameraUtils~CameraTransformOptions} to apply to the camera to center it on the given bounding + * box. This function computes the target coordinates and the range to apply to the camera. The other + * {@link CameraUtils~CameraTransformOptions} are set to their default values except for the tilt that is set to + * 45 and for the heading that is set to 0. All {@link CameraUtils~CameraTransformOptions} can be changed with + * the options argument. + * @param {View} view the camera view + * @param {THREE.Camera} camera the camera to get transform options for + * @param {Three.Box3} boundingBox the bounding box to center the camera on + * @param {CameraUtils~CameraTransformOptions} [options = {}] camera transform options to apply to the returned value + * (except coord and range). + * @returns {CameraUtils~CameraTransformOptions} The CameraTransformOptions to apply to the camera to display the + * bounding box. + */ + getCameraTransformOptionsFromBoundingBox(view, camera, boundingBox, options = {}) { + if (!view || !camera || !boundingBox || !boundingBox.isBox3) { + console.error('Invalid parameters. Cannot get camera transform options from bounding box'); } + const cameraTransformOptions = options; + + if (!cameraTransformOptions.tilt) { + cameraTransformOptions.tilt = 45; + } + if (!cameraTransformOptions.heading) { + cameraTransformOptions.heading = 0; + } + + const target = new THREE.Vector3(); + boundingBox.getCenter(target); + cameraTransformOptions.coord = new Coordinates(view.referenceCrs, target); + + var size = new THREE.Vector3(); + boundingBox.getSize(size); + var dimensions = { x: size.y, y: size.x }; + cameraTransformOptions.range = computeCameraRangeFromDimensions(view, camera, dimensions); return cameraTransformOptions; }, @@ -474,9 +530,9 @@ export default { * Apply transform to camera with animation * * @param {View} view The camera view - * @param {Camera} camera The camera to animate + * @param {THREE.Camera} camera The camera to animate * @param {CameraUtils~CameraTransformOptions} params The parameters - * @return {Promise} promise with resolve final CameraUtils~CameraTransformOptions + * @returns {Promise} promise with resolve final CameraUtils~CameraTransformOptions */ animateCameraToLookAtTarget(view, camera, params = {}) { params.proxy = params.proxy === undefined || params.proxy; @@ -501,9 +557,9 @@ export default { * chain animation transform to camera * * @param {View} view The camera view - * @param {Camera} camera The camera to animate + * @param {THREE.Camera} camera The camera to animate * @param {CameraUtils~CameraTransformOptions[]} params array parameters, each parameters transforms are apply to camera, in serial - * @return {Promise} promise with resolve final CameraUtils~CameraTransformOptions + * @returns {Promise} promise with resolve final CameraUtils~CameraTransformOptions */ sequenceAnimationsToLookAtTarget(view, camera, params = [{}]) { const promiseSerial = funcs => @@ -529,7 +585,7 @@ export default { * * @param {CameraUtils~CameraTransformOptions} first param to compare with the second * @param {CameraUtils~CameraTransformOptions} second param to compare with the first - * @return {object} The difference parameters + * @returns {object} The difference parameters */ getDiffParams(first, second) { if (!first || !second) { diff --git a/test/unit/CameraUtils.js b/test/unit/CameraUtils.js index 99adaeee3c..2a231dc38f 100644 --- a/test/unit/CameraUtils.js +++ b/test/unit/CameraUtils.js @@ -114,10 +114,11 @@ describe('Camera utils unit test', function () { }); }); - it('should transform camera from given extent', function () { + it('should transform ortographic camera from given extent in planar view (EPSG:2154)', function () { view.isPlanarView = true; view.isGlobeView = false; view.referenceCrs = 'EPSG:2154'; + const orthographicCamera = new Camera(view.referenceCrs, 60, 40, { type: CAMERA_TYPE.ORTHOGRAPHIC }); let camera3D = orthographicCamera.camera3D; @@ -158,4 +159,32 @@ describe('Camera utils unit test', function () { subExtent.planarDimensions().y / (2 * Math.tan(THREE.Math.degToRad(camera3D.fov) / 2)) < 10 ** -14, ); }); + + it('should transform perspective camera from given bounding box in globe view', function () { + view.isPlanarView = false; + view.isGlobeView = true; + view.referenceCrs = 'EPSG:4978'; + view.domElement = {}; + view.domElement.clientHeight = 350; + view.domElement.clientWidth = 556; + + const perspectiveCamera = new Camera(view.referenceCrs, 60, 40, { type: CAMERA_TYPE.PERSPECTIVE }); + const camera3D = perspectiveCamera.camera3D; + camera3D.aspect = 1.58; + camera3D.fov = 30; + + const boundingBox = new THREE.Box3( + new THREE.Vector3(4440583, 374007, 4547154), + new THREE.Vector3(4441385, 375904, 4547994)); + + const expectedCameraTransform = { + coord: new Coordinates(view.referenceCrs, 4440984, 374955.5, 4547574), + tilt: 45, + heading: 0, + range: 2228.3229619472627, + }; + const cameraTransform = CameraUtils.getCameraTransformOptionsFromBoundingBox(view, camera3D, boundingBox); + + assert.deepStrictEqual(cameraTransform, expectedCameraTransform); + }); });