diff --git a/src/three/controls/EnvironmentControls.js b/src/three/controls/EnvironmentControls.js index 46bc4d35..6942d9a5 100644 --- a/src/three/controls/EnvironmentControls.js +++ b/src/three/controls/EnvironmentControls.js @@ -93,6 +93,7 @@ export class EnvironmentControls extends EventDispatcher { this.needsUpdate = false; this.actionHeightOffset = 0; + // pivot point for drag and rotation this.pivotPoint = new Vector3(); this.zoomDirectionSet = false; @@ -182,7 +183,6 @@ export class EnvironmentControls extends EventDispatcher { e.preventDefault(); const { - camera, raycaster, domElement, up, @@ -218,8 +218,7 @@ export class EnvironmentControls extends EventDispatcher { mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); // find the hit point - raycaster.setFromCamera( _pointer, camera ); - const hit = this._raycast( raycaster ); + const hit = this._raycastAtMousePosition( _pointer, raycaster ); if ( hit ) { // if two fingers, right click, or shift click are being used then we trigger @@ -448,6 +447,15 @@ export class EnvironmentControls extends EventDispatcher { } + getLastInteractionPoint( target ) { + + // TODO: Ensure are handling the last zoom point if possible + // TODO: Return null (or get center point?) if it can't be provided + + return target.copy( this.pivotPoint ); + + } + detach() { this.domElement = null; @@ -633,6 +641,7 @@ export class EnvironmentControls extends EventDispatcher { } + // addjust the orthographic camera zoom if ( camera.isOrthographicCamera ) { // get the mouse position before zoom @@ -654,52 +663,55 @@ export class EnvironmentControls extends EventDispatcher { camera.position.sub( _mouseAfter ).add( _mouseBefore ); camera.updateMatrixWorld(); - } else { + } - // initialize the zoom direction - mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); - raycaster.setFromCamera( _pointer, camera ); - zoomDirection.copy( raycaster.ray.direction ).normalize(); - this.zoomDirectionSet = true; + // Adjust zoom position + // Needed for orthographic camera, as well, to avoid floating point error when + // shifting the camera position to keep the world under the mouse cursor + // ie due to the local "up" vector used for the camera orientation - // track the zoom direction we're going to use - const finalZoomDirection = _vec.copy( zoomDirection ); + // initialize the zoom direction + mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); + raycaster.setFromCamera( _pointer, camera ); + zoomDirection.copy( raycaster.ray.direction ).normalize(); + this.zoomDirectionSet = true; - // always update the zoom target point in case the tiles are changing - if ( this._updateZoomPoint() ) { + // track the zoom direction we're going to use + const finalZoomDirection = _vec.copy( zoomDirection ); - const dist = zoomPoint.distanceTo( camera.position ); + // always update the zoom target point in case the tiles are changing + if ( this._updateZoomPoint() ) { - // scale the distance based on how far there is to move - if ( scale < 0 ) { + const dist = zoomPoint.distanceTo( camera.position ); - const remainingDistance = Math.min( 0, dist - maxDistance ); - scale = scale * dist * zoomSpeed * 0.0025; - scale = Math.max( scale, remainingDistance ); + // scale the distance based on how far there is to move + if ( scale < 0 ) { - } else { + const remainingDistance = Math.min( 0, dist - maxDistance ); + scale = scale * dist * zoomSpeed * 0.0025; + scale = Math.max( scale, remainingDistance ); - const remainingDistance = Math.max( 0, dist - minDistance ); - scale = scale * ( dist - minDistance ) * zoomSpeed * 0.0025; - scale = Math.min( scale, remainingDistance ); + } else { - } + const remainingDistance = Math.max( 0, dist - minDistance ); + scale = scale * ( dist - minDistance ) * zoomSpeed * 0.0025; + scale = Math.min( scale, remainingDistance ); - camera.position.addScaledVector( zoomDirection, scale ); - camera.updateMatrixWorld(); + } - } else { + camera.position.addScaledVector( zoomDirection, scale ); + camera.updateMatrixWorld(); - // if we're zooming into nothing then use the distance from the ground to scale movement - const hit = this._getPointBelowCamera(); - if ( hit ) { + } else { - const dist = hit.distance; - finalZoomDirection.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); - camera.position.addScaledVector( finalZoomDirection, scale * dist * 0.01 ); - camera.updateMatrixWorld(); + // if we're zooming into nothing then use the distance from the ground to scale movement + const hit = this._getPointBelowCamera(); + if ( hit ) { - } + const dist = hit.distance; + finalZoomDirection.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); + camera.position.addScaledVector( finalZoomDirection, scale * dist * 0.01 ); + camera.updateMatrixWorld(); } @@ -835,7 +847,6 @@ export class EnvironmentControls extends EventDispatcher { pivotPoint, minAltitude, maxAltitude, - up, pointerTracker, rotationSpeed, } = this; @@ -857,10 +868,10 @@ export class EnvironmentControls extends EventDispatcher { this.getUpDirection( pivotPoint, _localUp ); // get the signed angle relative to the top down view - _vec.crossVectors( up, _forward ).normalize(); + _vec.crossVectors( _localUp, _forward ).normalize(); _right.set( 1, 0, 0 ).transformDirection( camera.matrixWorld ).normalize(); const sign = Math.sign( _vec.dot( _right ) ); - const angle = sign * up.angleTo( _forward ); + const angle = sign * _localUp.angleTo( _forward ); // clamp the rotation to be within the provided limits // clamp to 0 here, as well, so we don't "pop" to the the value range @@ -937,6 +948,20 @@ export class EnvironmentControls extends EventDispatcher { } + _raycastAtMousePosition( coords, raycaster ) { + + // position the ray for the raycaster at the near clip plane + const camera = this.camera; + const ray = raycaster.ray; + ray.origin.setFromMatrixPosition( camera.matrixWorld ); + ray.origin.set( coords.x, coords.y, - 1 ).unproject( camera ); + ray.direction.set( coords.x, coords.y, 0.5 ).unproject( camera ).sub( ray.origin ).normalize(); + raycaster.camera = camera; + + return this._raycast( raycaster ); + + } + _raycast( raycaster ) { const { scene, useFallbackPlane, fallbackPlane } = this; diff --git a/src/three/controls/GlobeControls.js b/src/three/controls/GlobeControls.js index e924b263..b4d421af 100644 --- a/src/three/controls/GlobeControls.js +++ b/src/three/controls/GlobeControls.js @@ -80,6 +80,28 @@ export class GlobeControls extends EnvironmentControls { } + getClosestPointOnSurface( target ) { + + const { ellipsoid, camera, tilesGroup } = this; + + _invMatrix + .copy( tilesGroup.matrixWorld ) + .invert(); + + // get camera position in local frame + target + .setFromMatrixPosition( camera.matrixWorld ) + .applyMatrix4( _invMatrix ); + + // get point on surface + ellipsoid + .getPositionToSurfacePoint( target, target ) + .applyMatrix4( tilesGroup.matrixWorld ); + + return target; + + } + // get the vector to the center of the provided globe getVectorToCenter( target ) { @@ -141,8 +163,9 @@ export class GlobeControls extends EnvironmentControls { } // if we're outside the transition threshold then we toggle some reorientation behavior - // when adjusting the up frame while moving hte camera - if ( distanceToCenter > GLOBE_TRANSITION_THRESHOLD ) { + // when adjusting the up frame + // whether controls are based on a far distance from the camera while moving hte camera + if ( ! this._isNearControls() ) { if ( this.state !== NONE && this._dragMode !== 1 && this._rotationMode !== 1 ) { @@ -179,6 +202,19 @@ export class GlobeControls extends EnvironmentControls { const horizonDistance = ellipsoid.calculateHorizonDistance( _latLon.lat, elevation ); camera.far = horizonDistance + 0.1; + + + if ( camera.isOrthographicCamera ) { + + _invMatrix.copy( camera.matrixWorld ).invert(); + + const v = new Vector3().setFromMatrixPosition( this.tilesGroup.matrixWorld ).applyMatrix4( _invMatrix ); + + camera.near = - Math.max( ...this.ellipsoid.radius ); + camera.far = - v.z; + + } + camera.updateProjectionMatrix(); } @@ -197,9 +233,9 @@ export class GlobeControls extends EnvironmentControls { super._setFrame( ...args ); - if ( this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { + if ( this._isNearControls() ) { - this._alignCameraUp( this.up ); + this._alignCameraUp( this.up, 1 ); } @@ -207,7 +243,8 @@ export class GlobeControls extends EnvironmentControls { _updatePosition( ...args ) { - if ( this._dragMode === 1 || this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { + // whether controls are based on a far distance from the camera + if ( this._dragMode === 1 || this._isNearControls() ) { this._dragMode = 1; @@ -264,7 +301,8 @@ export class GlobeControls extends EnvironmentControls { // disable rotation once we're outside the control transition _updateRotation( ...args ) { - if ( this._rotationMode === 1 || this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { + // whether controls are based on a far distance from the camera + if ( this._rotationMode === 1 || this._isNearControls() ) { this._rotationMode = 1; super._updateRotation( ...args ); @@ -280,21 +318,56 @@ export class GlobeControls extends EnvironmentControls { _updateZoom() { - const scale = this.zoomDelta; - if ( this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD || scale > 0 ) { + window.CONTROLS = this; + const { zoomDelta, camera, zoomSpeed } = this; + + // whether controls are based on a far distance from the camera + if ( this._isNearControls() || zoomDelta > 0 ) { super._updateZoom(); } else { - // orient the camera to focus on the earth during the zoom - const alpha = MathUtils.mapLinear( this.getDistanceToCenter(), GLOBE_TRANSITION_THRESHOLD, MAX_GLOBE_DISTANCE, 0, 1 ); - this._tiltTowardsCenter( MathUtils.lerp( 1, 0.8, alpha ) ); - this._alignCameraUpToNorth( MathUtils.lerp( 1, 0.9, alpha ) ); + if ( camera.isOrthographicCamera ) { + + // zoom the camera + const normalizedDelta = Math.pow( 0.95, Math.abs( zoomDelta * 0.05 ) ); + const scaleFactor = zoomDelta > 0 ? 1 / Math.abs( normalizedDelta ) : normalizedDelta; + const maxDiameter = 2.0 * Math.max( ...this.ellipsoid.radius ); + const minZoom = Math.min( camera.right - camera.left, camera.top - camera.bottom ) / maxDiameter; + + camera.zoom = Math.max( 0.75 * minZoom, camera.zoom * scaleFactor * zoomSpeed ); + camera.updateProjectionMatrix(); + + let alpha = MathUtils.mapLinear( camera.zoom, minZoom * 1.25, minZoom * 0.75, 0, 1 ); + if ( alpha > 0 ) { + + alpha = MathUtils.clamp( alpha, 0, 1 ); + this._tiltTowardsCenter( MathUtils.lerp( 1, 0.8, alpha ) ); + this._alignCameraUpToNorth( MathUtils.lerp( 1, 0.9, alpha ) ); + + } + + // this.zoomDelta = 0; + + } + + if ( camera.isPerspectiveCamera ) { + + // orient the camera to focus on the earth during the zoom + const alpha = MathUtils.mapLinear( this.getDistanceToCenter(), GLOBE_TRANSITION_THRESHOLD, MAX_GLOBE_DISTANCE, 0, 1 ); + this._tiltTowardsCenter( MathUtils.lerp( 1, 0.8, alpha ) ); + this._alignCameraUpToNorth( MathUtils.lerp( 1, 0.9, alpha ) ); + + } + + // get the distance to the surface of the sphere and compute teh zoom scale + const dist = this.getClosestPointOnSurface( _vec ).distanceTo( camera.position ); + const scale = zoomDelta * dist * zoomSpeed * 0.0025; // zoom out directly from the globe center - this.getVectorToCenter( _vec ); - this.camera.position.addScaledVector( _vec, scale * 0.0025 ); + this.getVectorToCenter( _vec ).normalize(); + this.camera.position.addScaledVector( _vec, scale ); this.camera.updateMatrixWorld(); this.zoomDelta = 0; @@ -352,4 +425,26 @@ export class GlobeControls extends EnvironmentControls { } + // whether controls are based on a far distance from the camera + _isNearControls() { + + const { camera, ellipsoid } = this; + const maxDiameter = 2.0 * Math.max( ...ellipsoid.radius ); + + let isFullyInView = false; + if ( camera.isOrthographicCamera ) { + + const maxView = Math.min( camera.right - camera.left, camera.top - camera.bottom ) / camera.zoom; + isFullyInView = 0.5 * maxDiameter > maxView; + + } else { + + isFullyInView = this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD; + + } + + return isFullyInView; + + } + }