From becee8f44675e2f2ddf546c78a87577d85cdd9aa Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Tue, 26 Dec 2023 11:45:10 +0900 Subject: [PATCH 01/64] Add support for globe controls --- example/src/GlobeControls.js | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 example/src/GlobeControls.js diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js new file mode 100644 index 000000000..84008f63b --- /dev/null +++ b/example/src/GlobeControls.js @@ -0,0 +1,57 @@ +import { Matrix4, Vector3 } from 'three'; + +const NONE = 0; +const ZOOM = 1; +const DRAG = 2; +const ROTATE = 3; + +// TODO +// - zoom in to cursor +// - rotate around clicked point +// - click and drag enables cursor to remain under the mouse +// - when adjusting the camera based on terrain we rotate around the drag position +// - support both globe and flat terrain +// - support acceleration? +// - small world rotation pivots the camera frame. camera must retain direction and only pivot on the right axis +// - translation moves on a plane below the camera +export class GlobeControls { + + constructor( camera, domElement ) { + + this.raycastEnvironment = null; + this.spherecastEnvironment = null; + this.state = NONE; + this.pivot = new Vector3(); + this.frame = new Matrix4(); + + } + + attach( domElement ) { + + } + + detach( ) { + + } + + updateZoom( scale ) { + + // TODO: zoom to pivot + + } + + updatePosition() { + + } + + updateRotation() { + + } + + update() { + + + + } + +} From 8a4bf5374179c007b1f08b0c6b09ef3475ee8b25 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Wed, 27 Dec 2023 23:20:07 +0900 Subject: [PATCH 02/64] Globe controls progress --- example/src/GlobeControls.js | 226 +++++++++++++++++++++++++++++++---- 1 file changed, 206 insertions(+), 20 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 84008f63b..64c929e4d 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -1,57 +1,243 @@ -import { Matrix4, Vector3 } from 'three'; +import { Matrix4, Quaternion, Vector2, Vector3, Raycaster, Ray, Mesh, SphereGeometry } from 'three'; const NONE = 0; -const ZOOM = 1; -const DRAG = 2; -const ROTATE = 3; +const DRAG = 1; +const ROTATE = 2; + +const _delta = new Vector3(); +const _vec = new Vector3(); +const _cross = new Vector3(); +const _quaternion = new Quaternion(); +const _matrix = new Matrix4(); +const _ray = new Ray(); + +const _rotMatrix = new Matrix4(); + // TODO -// - zoom in to cursor -// - rotate around clicked point -// - click and drag enables cursor to remain under the mouse -// - when adjusting the camera based on terrain we rotate around the drag position -// - support both globe and flat terrain -// - support acceleration? -// - small world rotation pivots the camera frame. camera must retain direction and only pivot on the right axis -// - translation moves on a plane below the camera +// - Update the containing frame every frame (maintain height) +// - To start just use a group that the camera is added to export class GlobeControls { - constructor( camera, domElement ) { + constructor( scene, camera, domElement ) { + + this.camera = camera; + this.domElement = null; + this.scene = scene; - this.raycastEnvironment = null; - this.spherecastEnvironment = null; this.state = NONE; - this.pivot = new Vector3(); this.frame = new Matrix4(); + this.cameraRadius = 1; + + this.pivotPointSet = false; + this.pivotPoint = new Vector3(); + this.pivotDirectionSet = false; + this.pivotDirection = new Vector3(); + + this.rotationSpeed = 3; + this.raycaster = new Raycaster(); + this.raycaster.firstHitOnly = true; + + this.sphere = new Mesh( new SphereGeometry() ); + this.sphere.scale.setScalar( 0.25 ); + + this.attach( domElement ); } attach( domElement ) { - } + this.domElement = domElement; + + const _pointer = new Vector2(); + const _newPointer = new Vector2(); + const _delta = new Vector2(); + + domElement.addEventListener( 'pointerdown', e => { + + const { camera, raycaster, domElement, scene } = this; + + _pointer.x = ( e.clientX / domElement.clientWidth ) * 2 - 1; + _pointer.y = - ( e.clientY / domElement.clientHeight ) * 2 + 1; + + raycaster.setFromCamera( _pointer, camera ); + + const hit = raycaster.intersectObject( scene )[ 0 ] || null; + if ( hit ) { + + this.state = ROTATE; + this.pivotPoint.copy( hit.point ); + this.pivotPointSet = true; + + this.sphere.position.copy( hit.point ); + this.scene.add( this.sphere ); + + } + + } ); + + domElement.addEventListener( 'pointermove', e => { + + _newPointer.x = ( e.clientX / domElement.clientWidth ) * 2 - 1; + _newPointer.y = - ( e.clientY / domElement.clientHeight ) * 2 + 1; + + _delta.subVectors( _newPointer, _pointer ); + _pointer.copy( _newPointer ); + + if ( this.state === ROTATE ) { + + const { rotationSpeed } = this; + this.updateRotation( - _delta.x * rotationSpeed, - _delta.y * rotationSpeed ); + + } + + } ); + + domElement.addEventListener( 'pointerup', e => { + + this.state = NONE; + this.pivotPointSet = false; + this.scene.remove( this.sphere ); + + } ); + + domElement.addEventListener( 'wheel', e => { + + this.raycaster.setFromCamera( _pointer, this.camera ); + this.pivotDirection.copy( this.raycaster.ray.direction ).normalize(); + + this.updateZoom( - e.deltaY ); + + } ); + + domElement.addEventListener( 'pointerenter', e => { + + _pointer.x = ( e.clientX / domElement.clientWidth ) * 2 - 1; + _pointer.y = - ( e.clientY / domElement.clientHeight ) * 2 + 1; + + if ( ! ( e.buttons & 1 ) ) { + + this.state = NONE; + this.pivotPointSet = false; + this.scene.remove( this.sphere ); + + } - detach( ) { + } ); } + detach() {} + updateZoom( scale ) { - // TODO: zoom to pivot + const { pivotPointSet, pivotPoint, pivotDirection, camera, raycaster, scene } = this; + + let dist = Infinity; + if ( pivotPointSet ) { + + dist = pivotPoint.distanceTo( camera.position ); + + } else { + + raycaster.ray.origin.copy( camera.position ); + raycaster.ray.direction.copy( pivotDirection ); + + const hit = raycaster.intersectObject( scene )[ 0 ] || null; + if ( hit ) { + + dist = hit.distance; + + } + + } + + pivotDirection.normalize(); + scale = Math.min( scale * ( dist - 5 ) * 0.01, Math.max( 0, dist - 5 ) ); + + this.camera.position.addScaledVector( pivotDirection, scale ); } updatePosition() { + // TODO: when adjusting the frame we have to reproject the grab point + // so as the use drags it winds up in the same spot. + // Will this work? Or be good enough? + } - updateRotation() { + updateRotation( azimuth, altitude ) { + + const { camera, pivotPoint } = this; + + // zoom in frame around pivot point + _vec.set( 0, 1, 0 ); + _quaternion.setFromAxisAngle( _vec, azimuth ); + makeRotateAroundPoint( pivotPoint, _quaternion, _rotMatrix ); + camera.matrixWorld.premultiply( _rotMatrix ); + + _delta.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); + _cross.crossVectors( _vec, _delta ).normalize(); + + _quaternion.setFromAxisAngle( _cross, altitude ); + makeRotateAroundPoint( pivotPoint, _quaternion, _rotMatrix ); + camera.matrixWorld.premultiply( _rotMatrix ); + + camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); + camera.updateMatrixWorld(); } update() { + } + + dispose() { + this.detach(); } + _raycast( o, d ) { + + const { scene } = this; + const raycaster = new Raycaster(); + const { ray } = raycaster; + ray.direction.copy( d ); + ray.origin.copy( o ); + + return raycaster.intersectObject( scene )[ 0 ] || null; + + } + + _updateCameraPosition() { + + const { camera } = this; + _ray.direction.set( 0, - 1, 0 ); + _ray.origin.copy( camera.position ).addScaledVector( _ray.direction, - 100 ); + + const hit = this._raycast( _ray.origin, _ray.direction ); + if ( hit && hit.point.distanceTo( camera.position ) < this.cameraRadius ) { + + camera.position.hit.point.addScaledVector( _ray.direction, - this.cameraRadius ); + + } + + } + +} + +function makeRotateAroundPoint( point, quat, target ) { + + target.makeTranslation( - point.x, - point.y, - point.z ); + + _matrix.makeRotationFromQuaternion( quat ); + target.premultiply( _matrix ); + + _matrix.makeTranslation( point.x, point.y, point.z ); + target.premultiply( _matrix ); + + return target; + } From bb64692417c260e02a1004e02f8f715031ac9fdb Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Wed, 27 Dec 2023 23:34:19 +0900 Subject: [PATCH 03/64] Add comments --- example/src/GlobeControls.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 64c929e4d..24647f874 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -15,8 +15,16 @@ const _rotMatrix = new Matrix4(); // TODO -// - Update the containing frame every frame (maintain height) -// - To start just use a group that the camera is added to +// - Add drag +// - Add angle limits +// - Adjust the camera height +// - Test with globe (adjusting up vector) +// - Add drift animation +// - Add support for angled rotation plane +// - Fix zoom approach so wen can zoom far in and out more easily +// - Toggles for zoom to cursor, zoom forward, orbit around center, etc +// - Shift button use +// - Cleanup export class GlobeControls { constructor( scene, camera, domElement ) { From 9ea6cbc7b0da992d12e131e261edc4e8bfc68c3d Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 28 Dec 2023 17:13:03 +0900 Subject: [PATCH 04/64] Cleanup --- example/src/GlobeControls.js | 117 ++++++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 30 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 24647f874..9336b0887 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -1,4 +1,4 @@ -import { Matrix4, Quaternion, Vector2, Vector3, Raycaster, Ray, Mesh, SphereGeometry } from 'three'; +import { Matrix4, Quaternion, Vector2, Vector3, Raycaster, Ray, Mesh, SphereGeometry, Plane } from 'three'; const NONE = 0; const DRAG = 1; @@ -10,21 +10,21 @@ const _cross = new Vector3(); const _quaternion = new Quaternion(); const _matrix = new Matrix4(); const _ray = new Ray(); - +const _plane = new Plane(); const _rotMatrix = new Matrix4(); - // TODO -// - Add drag +// - Ensure rotation can not flp the opposite direction (clamp rotations) // - Add angle limits -// - Adjust the camera height +// - Adjust the camera height (possibly need to tilt or something based on which move mode is being used?) +// - Fix zoom approach so we can zoom far in and out more easily +// - Cleanup +// --- +// - Toggles for zoom to cursor, zoom forward, orbit around center, etc +// - Touch controls +// - Add support for angled rotation plane (based on where the pivot point is) // - Test with globe (adjusting up vector) // - Add drift animation -// - Add support for angled rotation plane -// - Fix zoom approach so wen can zoom far in and out more easily -// - Toggles for zoom to cursor, zoom forward, orbit around center, etc -// - Shift button use -// - Cleanup export class GlobeControls { constructor( scene, camera, domElement ) { @@ -34,7 +34,6 @@ export class GlobeControls { this.scene = scene; this.state = NONE; - this.frame = new Matrix4(); this.cameraRadius = 1; this.pivotPointSet = false; @@ -59,26 +58,64 @@ export class GlobeControls { const _pointer = new Vector2(); const _newPointer = new Vector2(); - const _delta = new Vector2(); + const _deltaPointer = new Vector2(); + let shiftClicked = false; + + domElement.addEventListener( 'contextmenu', e => { + + e.preventDefault(); + + } ); + + domElement.addEventListener( 'keydown', e => { + + if ( e.key === 'Shift' ) { + + shiftClicked = true; + + } + + } ); + + domElement.addEventListener( 'keyup', e => { + + if ( e.key === 'Shift' ) { + + shiftClicked = false; + + } + + } ); domElement.addEventListener( 'pointerdown', e => { const { camera, raycaster, domElement, scene } = this; - _pointer.x = ( e.clientX / domElement.clientWidth ) * 2 - 1; - _pointer.y = - ( e.clientY / domElement.clientHeight ) * 2 + 1; - + mouseToCoords( e, domElement, _pointer ); raycaster.setFromCamera( _pointer, camera ); const hit = raycaster.intersectObject( scene )[ 0 ] || null; if ( hit ) { - this.state = ROTATE; - this.pivotPoint.copy( hit.point ); - this.pivotPointSet = true; + if ( e.buttons & 2 || e.buttons & 1 && shiftClicked ) { + + this.state = ROTATE; + this.pivotPoint.copy( hit.point ); + this.pivotPointSet = true; + + this.sphere.position.copy( hit.point ); + this.scene.add( this.sphere ); + + } else if ( e.buttons & 1 ) { + + this.state = DRAG; + this.pivotPoint.copy( hit.point ); + this.pivotPointSet = true; + + this.sphere.position.copy( hit.point ); + this.scene.add( this.sphere ); - this.sphere.position.copy( hit.point ); - this.scene.add( this.sphere ); + } } @@ -86,16 +123,28 @@ export class GlobeControls { domElement.addEventListener( 'pointermove', e => { - _newPointer.x = ( e.clientX / domElement.clientWidth ) * 2 - 1; - _newPointer.y = - ( e.clientY / domElement.clientHeight ) * 2 + 1; - - _delta.subVectors( _newPointer, _pointer ); + mouseToCoords( e, domElement, _newPointer ); + _deltaPointer.subVectors( _newPointer, _pointer ); _pointer.copy( _newPointer ); - if ( this.state === ROTATE ) { + if ( this.state === DRAG ) { + + const { raycaster, camera, pivotPoint } = this; + _vec.set( 0, 1, 0 ); + _plane.setFromNormalAndCoplanarPoint( _vec, pivotPoint ); + raycaster.setFromCamera( _pointer, camera ); + + if ( raycaster.ray.intersectPlane( _plane, _vec ) ) { + + _delta.subVectors( pivotPoint, _vec ); + this.updatePosition( _delta ); + + } + + } else if ( this.state === ROTATE ) { const { rotationSpeed } = this; - this.updateRotation( - _delta.x * rotationSpeed, - _delta.y * rotationSpeed ); + this.updateRotation( - _deltaPointer.x * rotationSpeed, - _deltaPointer.y * rotationSpeed ); } @@ -120,10 +169,10 @@ export class GlobeControls { domElement.addEventListener( 'pointerenter', e => { - _pointer.x = ( e.clientX / domElement.clientWidth ) * 2 - 1; - _pointer.y = - ( e.clientY / domElement.clientHeight ) * 2 + 1; + mouseToCoords( e, domElement, _pointer ); + shiftClicked = false; - if ( ! ( e.buttons & 1 ) ) { + if ( e.buttons === 0 ) { this.state = NONE; this.pivotPointSet = false; @@ -167,11 +216,12 @@ export class GlobeControls { } - updatePosition() { + updatePosition( delta ) { // TODO: when adjusting the frame we have to reproject the grab point // so as the use drags it winds up in the same spot. // Will this work? Or be good enough? + this.camera.position.add( delta ); } @@ -249,3 +299,10 @@ function makeRotateAroundPoint( point, quat, target ) { return target; } + +function mouseToCoords( e, element, target ) { + + target.x = ( e.clientX / element.clientWidth ) * 2 - 1; + target.y = - ( e.clientY / element.clientHeight ) * 2 + 1; + +} From e44cad2de5def7fc0fc068cc40399a8b84620428 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 28 Dec 2023 17:45:42 +0900 Subject: [PATCH 05/64] Add detach --- example/src/GlobeControls.js | 156 +++++++++++++++++++++++++---------- 1 file changed, 113 insertions(+), 43 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 9336b0887..199abecf8 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -17,7 +17,6 @@ const _rotMatrix = new Matrix4(); // - Ensure rotation can not flp the opposite direction (clamp rotations) // - Add angle limits // - Adjust the camera height (possibly need to tilt or something based on which move mode is being used?) -// - Fix zoom approach so we can zoom far in and out more easily // - Cleanup // --- // - Toggles for zoom to cursor, zoom forward, orbit around center, etc @@ -36,10 +35,16 @@ export class GlobeControls { this.state = NONE; this.cameraRadius = 1; - this.pivotPointSet = false; - this.pivotPoint = new Vector3(); - this.pivotDirectionSet = false; - this.pivotDirection = new Vector3(); + this.dragPointSet = false; + this.dragPoint = new Vector3(); + + this.rotationPointSet = false; + this.rotationPoint = new Vector3(); + + this.zoomDirectionSet = false; + this.zoomPointSet = false; + this.zoomDirection = new Vector3(); + this.zoomPoint = new Vector3(); this.rotationSpeed = 3; this.raycaster = new Raycaster(); @@ -48,12 +53,19 @@ export class GlobeControls { this.sphere = new Mesh( new SphereGeometry() ); this.sphere.scale.setScalar( 0.25 ); + this._detachCallback = null; this.attach( domElement ); } attach( domElement ) { + if ( this.domElement ) { + + throw new Error( 'GlobeControls: Controls already attached to element' ); + + } + this.domElement = domElement; const _pointer = new Vector2(); @@ -61,13 +73,13 @@ export class GlobeControls { const _deltaPointer = new Vector2(); let shiftClicked = false; - domElement.addEventListener( 'contextmenu', e => { + const contextMenuCallback = e => { e.preventDefault(); - } ); + }; - domElement.addEventListener( 'keydown', e => { + const keydownCallback = e => { if ( e.key === 'Shift' ) { @@ -75,9 +87,9 @@ export class GlobeControls { } - } ); + }; - domElement.addEventListener( 'keyup', e => { + const keyupCallback = e => { if ( e.key === 'Shift' ) { @@ -85,9 +97,9 @@ export class GlobeControls { } - } ); + }; - domElement.addEventListener( 'pointerdown', e => { + const pointerdownCallback = e => { const { camera, raycaster, domElement, scene } = this; @@ -100,8 +112,8 @@ export class GlobeControls { if ( e.buttons & 2 || e.buttons & 1 && shiftClicked ) { this.state = ROTATE; - this.pivotPoint.copy( hit.point ); - this.pivotPointSet = true; + this.rotationPoint.copy( hit.point ); + this.rotationPointSet = true; this.sphere.position.copy( hit.point ); this.scene.add( this.sphere ); @@ -109,8 +121,8 @@ export class GlobeControls { } else if ( e.buttons & 1 ) { this.state = DRAG; - this.pivotPoint.copy( hit.point ); - this.pivotPointSet = true; + this.dragPoint.copy( hit.point ); + this.dragPointSet = true; this.sphere.position.copy( hit.point ); this.scene.add( this.sphere ); @@ -119,9 +131,12 @@ export class GlobeControls { } - } ); + }; + + const pointermoveCallback = e => { - domElement.addEventListener( 'pointermove', e => { + this.zoomDirectionSet = false; + this.zoomPointSet = false; mouseToCoords( e, domElement, _newPointer ); _deltaPointer.subVectors( _newPointer, _pointer ); @@ -129,14 +144,14 @@ export class GlobeControls { if ( this.state === DRAG ) { - const { raycaster, camera, pivotPoint } = this; - _vec.set( 0, 1, 0 ); - _plane.setFromNormalAndCoplanarPoint( _vec, pivotPoint ); + const { raycaster, camera, dragPoint } = this; + _vec.set( 0, 1, 0 ); // up vector + _plane.setFromNormalAndCoplanarPoint( _vec, dragPoint ); raycaster.setFromCamera( _pointer, camera ); if ( raycaster.ray.intersectPlane( _plane, _vec ) ) { - _delta.subVectors( pivotPoint, _vec ); + _delta.subVectors( dragPoint, _vec ); this.updatePosition( _delta ); } @@ -148,26 +163,42 @@ export class GlobeControls { } - } ); + }; - domElement.addEventListener( 'pointerup', e => { + const pointerupCallback = e => { this.state = NONE; - this.pivotPointSet = false; + this.rotationPointSet = false; + this.dragPointSet = false; this.scene.remove( this.sphere ); - } ); + }; - domElement.addEventListener( 'wheel', e => { + const wheelCallback = e => { + + if ( ! this.zoomDirectionSet ) { + + const { raycaster, scene } = this; + raycaster.setFromCamera( _pointer, this.camera ); + + const hit = raycaster.intersectObject( scene )[ 0 ] || null; + if ( hit ) { + + this.zoomPoint.copy( hit.point ); + this.zoomPointSet = true; + + } - this.raycaster.setFromCamera( _pointer, this.camera ); - this.pivotDirection.copy( this.raycaster.ray.direction ).normalize(); + this.zoomDirection.copy( this.raycaster.ray.direction ).normalize(); + this.zoomDirectionSet = true; + + } this.updateZoom( - e.deltaY ); - } ); + }; - domElement.addEventListener( 'pointerenter', e => { + const pointerenterCallback = e => { mouseToCoords( e, domElement, _pointer ); shiftClicked = false; @@ -175,30 +206,69 @@ export class GlobeControls { if ( e.buttons === 0 ) { this.state = NONE; - this.pivotPointSet = false; + this.dragPointSet = false; + this.rotationPointSet = false; this.scene.remove( this.sphere ); } - } ); + }; + + domElement.addEventListener( 'contextmenu', contextMenuCallback ); + domElement.addEventListener( 'keydown', keydownCallback ); + domElement.addEventListener( 'keyup', keyupCallback ); + domElement.addEventListener( 'pointerdown', pointerdownCallback ); + domElement.addEventListener( 'pointermove', pointermoveCallback ); + domElement.addEventListener( 'pointerup', pointerupCallback ); + domElement.addEventListener( 'wheel', wheelCallback ); + domElement.addEventListener( 'pointerenter', pointerenterCallback ); + + this._detachCallback = () => { + + domElement.removeEventListener( 'contextmenu', contextMenuCallback ); + domElement.removeEventListener( 'keydown', keydownCallback ); + domElement.removeEventListener( 'keyup', keyupCallback ); + domElement.removeEventListener( 'pointerdown', pointerdownCallback ); + domElement.removeEventListener( 'pointermove', pointermoveCallback ); + domElement.removeEventListener( 'pointerup', pointerupCallback ); + domElement.removeEventListener( 'wheel', wheelCallback ); + domElement.removeEventListener( 'pointerenter', pointerenterCallback ); + + }; } - detach() {} + detach() { + + if ( this._detachCallback ) { + + this._detachCallback(); + this._detachCallback = null; + + } + + } updateZoom( scale ) { - const { pivotPointSet, pivotPoint, pivotDirection, camera, raycaster, scene } = this; + const { + zoomPointSet, + zoomPoint, + zoomDirection, + camera, + raycaster, + scene, + } = this; let dist = Infinity; - if ( pivotPointSet ) { + if ( zoomPointSet ) { - dist = pivotPoint.distanceTo( camera.position ); + dist = zoomPoint.distanceTo( camera.position ); } else { raycaster.ray.origin.copy( camera.position ); - raycaster.ray.direction.copy( pivotDirection ); + raycaster.ray.direction.copy( zoomDirection ); const hit = raycaster.intersectObject( scene )[ 0 ] || null; if ( hit ) { @@ -209,10 +279,10 @@ export class GlobeControls { } - pivotDirection.normalize(); + zoomDirection.normalize(); scale = Math.min( scale * ( dist - 5 ) * 0.01, Math.max( 0, dist - 5 ) ); - this.camera.position.addScaledVector( pivotDirection, scale ); + this.camera.position.addScaledVector( zoomDirection, scale ); } @@ -227,19 +297,19 @@ export class GlobeControls { updateRotation( azimuth, altitude ) { - const { camera, pivotPoint } = this; + const { camera, rotationPoint } = this; // zoom in frame around pivot point _vec.set( 0, 1, 0 ); _quaternion.setFromAxisAngle( _vec, azimuth ); - makeRotateAroundPoint( pivotPoint, _quaternion, _rotMatrix ); + makeRotateAroundPoint( rotationPoint, _quaternion, _rotMatrix ); camera.matrixWorld.premultiply( _rotMatrix ); _delta.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); _cross.crossVectors( _vec, _delta ).normalize(); _quaternion.setFromAxisAngle( _cross, altitude ); - makeRotateAroundPoint( pivotPoint, _quaternion, _rotMatrix ); + makeRotateAroundPoint( rotationPoint, _quaternion, _rotMatrix ); camera.matrixWorld.premultiply( _rotMatrix ); camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); From ff344b703156e5e8bcbe503950ef0bc125eb5643 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 28 Dec 2023 17:54:20 +0900 Subject: [PATCH 06/64] Clean up --- example/src/GlobeControls.js | 45 ++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 199abecf8..f929af7f4 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -4,20 +4,19 @@ const NONE = 0; const DRAG = 1; const ROTATE = 2; +const _matrix = new Matrix4(); +const _rotMatrix = new Matrix4(); const _delta = new Vector3(); const _vec = new Vector3(); -const _cross = new Vector3(); +const _crossVec = new Vector3(); const _quaternion = new Quaternion(); -const _matrix = new Matrix4(); const _ray = new Ray(); const _plane = new Plane(); -const _rotMatrix = new Matrix4(); // TODO // - Ensure rotation can not flp the opposite direction (clamp rotations) // - Add angle limits // - Adjust the camera height (possibly need to tilt or something based on which move mode is being used?) -// - Cleanup // --- // - Toggles for zoom to cursor, zoom forward, orbit around center, etc // - Touch controls @@ -28,13 +27,21 @@ export class GlobeControls { constructor( scene, camera, domElement ) { - this.camera = camera; this.domElement = null; - this.scene = scene; + this.camera = null; + this.scene = null; + // settings this.state = NONE; this.cameraRadius = 1; + this.rotationSpeed = 3; + // group to display (TODO: make callback instead) + this.sphere = new Mesh( new SphereGeometry() ); + this.sphere.raycast = () => {}; + this.sphere.scale.setScalar( 0.25 ); + + // internal state this.dragPointSet = false; this.dragPoint = new Vector3(); @@ -46,15 +53,27 @@ export class GlobeControls { this.zoomDirection = new Vector3(); this.zoomPoint = new Vector3(); - this.rotationSpeed = 3; this.raycaster = new Raycaster(); this.raycaster.firstHitOnly = true; - this.sphere = new Mesh( new SphereGeometry() ); - this.sphere.scale.setScalar( 0.25 ); - this._detachCallback = null; + + // init this.attach( domElement ); + this.setCamera( camera ); + this.setScene( scene ); + + } + + setScene( scene ) { + + this.scene = scene; + + } + + setCamera( camera ) { + + this.camera = camera; } @@ -306,9 +325,9 @@ export class GlobeControls { camera.matrixWorld.premultiply( _rotMatrix ); _delta.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); - _cross.crossVectors( _vec, _delta ).normalize(); + _crossVec.crossVectors( _vec, _delta ).normalize(); - _quaternion.setFromAxisAngle( _cross, altitude ); + _quaternion.setFromAxisAngle( _crossVec, altitude ); makeRotateAroundPoint( rotationPoint, _quaternion, _rotMatrix ); camera.matrixWorld.premultiply( _rotMatrix ); @@ -356,6 +375,7 @@ export class GlobeControls { } +// helper function for constructing a matrix for rotating around a point function makeRotateAroundPoint( point, quat, target ) { target.makeTranslation( - point.x, - point.y, - point.z ); @@ -370,6 +390,7 @@ function makeRotateAroundPoint( point, quat, target ) { } +// get the three.js pointer coords from an event function mouseToCoords( e, element, target ) { target.x = ( e.clientX / element.clientWidth ) * 2 - 1; From a62919d5dbae0a90a95c6eb520af14b069e71c4c Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 28 Dec 2023 18:31:44 +0900 Subject: [PATCH 07/64] Add angle clamping --- example/src/GlobeControls.js | 40 +++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index f929af7f4..803bc7f31 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -14,15 +14,15 @@ const _ray = new Ray(); const _plane = new Plane(); // TODO -// - Ensure rotation can not flp the opposite direction (clamp rotations) -// - Add angle limits // - Adjust the camera height (possibly need to tilt or something based on which move mode is being used?) // --- -// - Toggles for zoom to cursor, zoom forward, orbit around center, etc // - Touch controls // - Add support for angled rotation plane (based on where the pivot point is) // - Test with globe (adjusting up vector) // - Add drift animation +// - provide fallback plane for cases where you're off the map? +// - Toggles for zoom to cursor, zoom forward, orbit around center, etc? + export class GlobeControls { constructor( scene, camera, domElement ) { @@ -35,6 +35,8 @@ export class GlobeControls { this.state = NONE; this.cameraRadius = 1; this.rotationSpeed = 3; + this.minAltitude = 0; + this.maxAltitude = Math.PI / 2; // group to display (TODO: make callback instead) this.sphere = new Mesh( new SphereGeometry() ); @@ -47,6 +49,7 @@ export class GlobeControls { this.rotationPointSet = false; this.rotationPoint = new Vector3(); + this.rotationClickDirection = new Vector3(); this.zoomDirectionSet = false; this.zoomPointSet = false; @@ -130,8 +133,11 @@ export class GlobeControls { if ( e.buttons & 2 || e.buttons & 1 && shiftClicked ) { + _matrix.copy( camera.matrixWorld ).invert(); + this.state = ROTATE; this.rotationPoint.copy( hit.point ); + this.rotationClickDirection.copy( raycaster.ray.direction ).transformDirection( _matrix ); this.rotationPointSet = true; this.sphere.position.copy( hit.point ); @@ -279,6 +285,7 @@ export class GlobeControls { scene, } = this; + const fallback = scale < 0 ? - 1 : 1; let dist = Infinity; if ( zoomPointSet ) { @@ -300,6 +307,13 @@ export class GlobeControls { zoomDirection.normalize(); scale = Math.min( scale * ( dist - 5 ) * 0.01, Math.max( 0, dist - 5 ) ); + if ( scale === Infinity || scale === - Infinity || Number.isNaN( scale ) ) { + + scale = fallback; + + } + + console.log( scale, dist ) this.camera.position.addScaledVector( zoomDirection, scale ); @@ -316,14 +330,30 @@ export class GlobeControls { updateRotation( azimuth, altitude ) { - const { camera, rotationPoint } = this; + const { camera, rotationPoint, minAltitude, maxAltitude } = this; + + // TODO: currently uses the camera forward for this work but it may be best to use a combination of camera + // forward and direction to pivot? Or just dir to pivot? + _delta.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ).multiplyScalar( - 1 ); + + const angle = _vec.set( 0, 1, 0 ).angleTo( _delta ); + if ( altitude > 0 ) { + + altitude = Math.min( angle - minAltitude - 1e-2, altitude ); + + } else { + + altitude = Math.max( angle - maxAltitude, altitude ); + + } // zoom in frame around pivot point - _vec.set( 0, 1, 0 ); + _vec.set( 0, 1, 0 ); // up vector _quaternion.setFromAxisAngle( _vec, azimuth ); makeRotateAroundPoint( rotationPoint, _quaternion, _rotMatrix ); camera.matrixWorld.premultiply( _rotMatrix ); + // TODO: why not just use camera-right here? _delta.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); _crossVec.crossVectors( _vec, _delta ).normalize(); From 8651e6b28fd4b1935d48dfb59b3c1a69e6091646 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 28 Dec 2023 19:05:48 +0900 Subject: [PATCH 08/64] Update controls --- example/src/GlobeControls.js | 57 ++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 803bc7f31..58034a4cf 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -1,4 +1,4 @@ -import { Matrix4, Quaternion, Vector2, Vector3, Raycaster, Ray, Mesh, SphereGeometry, Plane } from 'three'; +import { Matrix4, Quaternion, Vector2, Vector3, Raycaster, Mesh, SphereGeometry, Plane } from 'three'; const NONE = 0; const DRAG = 1; @@ -10,17 +10,18 @@ const _delta = new Vector3(); const _vec = new Vector3(); const _crossVec = new Vector3(); const _quaternion = new Quaternion(); -const _ray = new Ray(); +const _up = new Vector3( 0, 1, 0 ); const _plane = new Plane(); // TODO -// - Adjust the camera height (possibly need to tilt or something based on which move mode is being used?) +// - provide fallback plane for cases when you're off the map +// - add snap back for drag // --- // - Touch controls // - Add support for angled rotation plane (based on where the pivot point is) // - Test with globe (adjusting up vector) -// - Add drift animation -// - provide fallback plane for cases where you're off the map? +// --- +// - Consider using sphere intersect for positioning // - Toggles for zoom to cursor, zoom forward, orbit around center, etc? export class GlobeControls { @@ -170,8 +171,7 @@ export class GlobeControls { if ( this.state === DRAG ) { const { raycaster, camera, dragPoint } = this; - _vec.set( 0, 1, 0 ); // up vector - _plane.setFromNormalAndCoplanarPoint( _vec, dragPoint ); + _plane.setFromNormalAndCoplanarPoint( _up, dragPoint ); raycaster.setFromCamera( _pointer, camera ); if ( raycaster.ray.intersectPlane( _plane, _vec ) ) { @@ -313,8 +313,6 @@ export class GlobeControls { } - console.log( scale, dist ) - this.camera.position.addScaledVector( zoomDirection, scale ); } @@ -336,7 +334,7 @@ export class GlobeControls { // forward and direction to pivot? Or just dir to pivot? _delta.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ).multiplyScalar( - 1 ); - const angle = _vec.set( 0, 1, 0 ).angleTo( _delta ); + const angle = _up.angleTo( _delta ); if ( altitude > 0 ) { altitude = Math.min( angle - minAltitude - 1e-2, altitude ); @@ -348,14 +346,13 @@ export class GlobeControls { } // zoom in frame around pivot point - _vec.set( 0, 1, 0 ); // up vector - _quaternion.setFromAxisAngle( _vec, azimuth ); + _quaternion.setFromAxisAngle( _up, azimuth ); makeRotateAroundPoint( rotationPoint, _quaternion, _rotMatrix ); camera.matrixWorld.premultiply( _rotMatrix ); // TODO: why not just use camera-right here? _delta.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); - _crossVec.crossVectors( _vec, _delta ).normalize(); + _crossVec.crossVectors( _up, _delta ).normalize(); _quaternion.setFromAxisAngle( _crossVec, altitude ); makeRotateAroundPoint( rotationPoint, _quaternion, _rotMatrix ); @@ -368,6 +365,25 @@ export class GlobeControls { update() { + const { raycaster, camera, scene, cameraRadius, dragPoint } = this; + raycaster.ray.direction.copy( _up ).multiplyScalar( - 1 ); + raycaster.ray.origin.copy( camera.position ).addScaledVector( raycaster.ray.direction, - 100 ); + + const hit = raycaster.intersectObject( scene )[ 0 ]; + if ( hit ) { + + const dist = hit.distance - 100; + if ( dist < cameraRadius ) { + + // TODO: maybe this can snap back once the camera as gone over the hill so the drag point is back in the right spot + const delta = cameraRadius - dist; + camera.position.copy( hit.point ).addScaledVector( raycaster.ray.direction, - cameraRadius ); + dragPoint.addScaledVector( _up, delta ); + + } + + } + } dispose() { @@ -388,21 +404,6 @@ export class GlobeControls { } - _updateCameraPosition() { - - const { camera } = this; - _ray.direction.set( 0, - 1, 0 ); - _ray.origin.copy( camera.position ).addScaledVector( _ray.direction, - 100 ); - - const hit = this._raycast( _ray.origin, _ray.direction ); - if ( hit && hit.point.distanceTo( camera.position ) < this.cameraRadius ) { - - camera.position.hit.point.addScaledVector( _ray.direction, - this.cameraRadius ); - - } - - } - } // helper function for constructing a matrix for rotating around a point From efa428eb4babcffbd573ba9b8c202c5f294c2df9 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 28 Dec 2023 19:31:30 +0900 Subject: [PATCH 09/64] Add min and max distance --- example/src/GlobeControls.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 58034a4cf..9071fc8a6 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -38,6 +38,8 @@ export class GlobeControls { this.rotationSpeed = 3; this.minAltitude = 0; this.maxAltitude = Math.PI / 2; + this.minDistance = 2; + this.maxDistance = Infinity; // group to display (TODO: make callback instead) this.sphere = new Mesh( new SphereGeometry() ); @@ -283,6 +285,8 @@ export class GlobeControls { camera, raycaster, scene, + minDistance, + maxDistance, } = this; const fallback = scale < 0 ? - 1 : 1; @@ -306,13 +310,20 @@ export class GlobeControls { } zoomDirection.normalize(); - scale = Math.min( scale * ( dist - 5 ) * 0.01, Math.max( 0, dist - 5 ) ); + scale = Math.min( scale * ( dist - minDistance ) * 0.01, Math.max( 0, dist - minDistance ) ); if ( scale === Infinity || scale === - Infinity || Number.isNaN( scale ) ) { scale = fallback; } + if ( scale < 0 ) { + + const remainingDistance = Math.min( 0, dist - maxDistance ); + scale = Math.max( scale, remainingDistance ); + + } + this.camera.position.addScaledVector( zoomDirection, scale ); } From 4a993840b7b0fdc683f776f8d26591113c069545 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 28 Dec 2023 22:05:02 +0900 Subject: [PATCH 10/64] Add snap back --- example/src/GlobeControls.js | 44 +++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 9071fc8a6..a35b5ee47 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -1,4 +1,13 @@ -import { Matrix4, Quaternion, Vector2, Vector3, Raycaster, Mesh, SphereGeometry, Plane } from 'three'; +import { + Matrix4, + Quaternion, + Vector2, + Vector3, + Raycaster, + Mesh, + SphereGeometry, + Plane, +} from 'three'; const NONE = 0; const DRAG = 1; @@ -8,14 +17,13 @@ const _matrix = new Matrix4(); const _rotMatrix = new Matrix4(); const _delta = new Vector3(); const _vec = new Vector3(); -const _crossVec = new Vector3(); +const _rotationAxis = new Vector3(); const _quaternion = new Quaternion(); const _up = new Vector3( 0, 1, 0 ); const _plane = new Plane(); // TODO // - provide fallback plane for cases when you're off the map -// - add snap back for drag // --- // - Touch controls // - Add support for angled rotation plane (based on where the pivot point is) @@ -49,6 +57,7 @@ export class GlobeControls { // internal state this.dragPointSet = false; this.dragPoint = new Vector3(); + this.startDragPoint = new Vector3(); this.rotationPointSet = false; this.rotationPoint = new Vector3(); @@ -150,6 +159,7 @@ export class GlobeControls { this.state = DRAG; this.dragPoint.copy( hit.point ); + this.startDragPoint.copy( hit.point ); this.dragPointSet = true; this.sphere.position.copy( hit.point ); @@ -361,11 +371,9 @@ export class GlobeControls { makeRotateAroundPoint( rotationPoint, _quaternion, _rotMatrix ); camera.matrixWorld.premultiply( _rotMatrix ); - // TODO: why not just use camera-right here? - _delta.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); - _crossVec.crossVectors( _up, _delta ).normalize(); + _rotationAxis.set( - 1, 0, 0 ).transformDirection( camera.matrixWorld ); - _quaternion.setFromAxisAngle( _crossVec, altitude ); + _quaternion.setFromAxisAngle( _rotationAxis, altitude ); makeRotateAroundPoint( rotationPoint, _quaternion, _rotMatrix ); camera.matrixWorld.premultiply( _rotMatrix ); @@ -376,7 +384,27 @@ export class GlobeControls { update() { - const { raycaster, camera, scene, cameraRadius, dragPoint } = this; + const { + raycaster, + camera, + scene, + cameraRadius, + dragPoint, + startDragPoint, + } = this; + + // when dragging the camera and drag point may be moved + // to accommodate terrain so we try to move it back down + // to the original point. + if ( this.state === DRAG ) { + + _delta.subVectors( startDragPoint, dragPoint ); + camera.position.add( _delta ); + dragPoint.copy( startDragPoint ); + + } + + // cast down from the camera raycaster.ray.direction.copy( _up ).multiplyScalar( - 1 ); raycaster.ray.origin.copy( camera.position ).addScaledVector( raycaster.ray.direction, - 100 ); From 55d3c3a5245f6f175b88674339bf37e1d7f018ab Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 28 Dec 2023 22:52:33 +0900 Subject: [PATCH 11/64] Add better pivot point --- example/src/GlobeControls.js | 49 +++++++++-------- example/src/PivotPointMesh.js | 100 ++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 22 deletions(-) create mode 100644 example/src/PivotPointMesh.js diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index a35b5ee47..77341ddeb 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -8,6 +8,7 @@ import { SphereGeometry, Plane, } from 'three'; +import { PivotPointMesh } from './PivotPointMesh.js'; const NONE = 0; const DRAG = 1; @@ -32,6 +33,29 @@ const _plane = new Plane(); // - Consider using sphere intersect for positioning // - Toggles for zoom to cursor, zoom forward, orbit around center, etc? +// helper function for constructing a matrix for rotating around a point +function makeRotateAroundPoint( point, quat, target ) { + + target.makeTranslation( - point.x, - point.y, - point.z ); + + _matrix.makeRotationFromQuaternion( quat ); + target.premultiply( _matrix ); + + _matrix.makeTranslation( point.x, point.y, point.z ); + target.premultiply( _matrix ); + + return target; + +} + +// get the three.js pointer coords from an event +function mouseToCoords( e, element, target ) { + + target.x = ( e.clientX / element.clientWidth ) * 2 - 1; + target.y = - ( e.clientY / element.clientHeight ) * 2 + 1; + +} + export class GlobeControls { constructor( scene, camera, domElement ) { @@ -51,6 +75,9 @@ export class GlobeControls { // group to display (TODO: make callback instead) this.sphere = new Mesh( new SphereGeometry() ); + + this.sphere = new PivotPointMesh(); + this.sphere.raycast = () => {}; this.sphere.scale.setScalar( 0.25 ); @@ -445,25 +472,3 @@ export class GlobeControls { } -// helper function for constructing a matrix for rotating around a point -function makeRotateAroundPoint( point, quat, target ) { - - target.makeTranslation( - point.x, - point.y, - point.z ); - - _matrix.makeRotationFromQuaternion( quat ); - target.premultiply( _matrix ); - - _matrix.makeTranslation( point.x, point.y, point.z ); - target.premultiply( _matrix ); - - return target; - -} - -// get the three.js pointer coords from an event -function mouseToCoords( e, element, target ) { - - target.x = ( e.clientX / element.clientWidth ) * 2 - 1; - target.y = - ( e.clientY / element.clientHeight ) * 2 + 1; - -} diff --git a/example/src/PivotPointMesh.js b/example/src/PivotPointMesh.js new file mode 100644 index 000000000..4f1db8cee --- /dev/null +++ b/example/src/PivotPointMesh.js @@ -0,0 +1,100 @@ +import { Mesh, PlaneGeometry, ShaderMaterial, Vector2 } from 'three'; +export class PivotPointMesh extends Mesh { + + constructor() { + + super( new PlaneGeometry( 0, 0 ), new PivotMaterial() ); + this.renderOrder = Infinity; + + } + + onBeforeRender( renderer ) { + + const uniforms = this.material.uniforms; + renderer.getSize( uniforms.resolution.value ); + uniforms.pixelRatio.value = renderer.getPixelRatio(); + + } + + dispose() { + + this.geometry.dispose(); + this.material.dispose(); + + } + +} + +class PivotMaterial extends ShaderMaterial { + + constructor() { + + super( { + + depthWrite: false, + depthTest: false, + transparent: true, + + uniforms: { + + resolution: { value: new Vector2() }, + pixelRatio: { value: 1 }, + size: { value: 10 }, + thickness: { value: 1.5 }, + opacity: { value: 1 }, + + }, + + vertexShader: /* glsl */` + + uniform float pixelRatio; + uniform float size; + uniform float thickness; + uniform vec2 resolution; + varying vec2 vUv; + + void main() { + + vUv = uv; + + float aspect = resolution.x / resolution.y; + vec2 offset = uv * 2.0 - vec2( 1.0 ); + offset.y *= aspect; + + vec4 screenPoint = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); + screenPoint.xy += offset * pixelRatio * ( size + thickness ) * screenPoint.w / resolution.x; + + gl_Position = screenPoint; + + } + `, + + fragmentShader: /* glsl */` + + uniform float size; + uniform float thickness; + uniform float opacity; + + varying vec2 vUv; + void main() { + + float ht = 0.5 * thickness; + float planeDim = size + thickness; + float offset = ( planeDim - ht - 2.0 ) / planeDim; + float texelThickness = ht / planeDim; + + vec2 vec = vUv * 2.0 - vec2( 1.0 ); + float dist = abs( length( vec ) - offset ); + float fw = fwidth( dist ); + float a = smoothstep( texelThickness - fw, texelThickness + fw, dist ); + + gl_FragColor = vec4( 1, 1, 1, opacity * ( 1.0 - a ) ); + + } + `, + + } ); + + } + +} From a64f8cc7715c1bbcaaeddd25f9e3a95fd843eaaf Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 28 Dec 2023 22:55:58 +0900 Subject: [PATCH 12/64] Update pivot --- example/src/GlobeControls.js | 36 +++++++++--------------------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 77341ddeb..a0338ee7d 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -4,8 +4,6 @@ import { Vector2, Vector3, Raycaster, - Mesh, - SphereGeometry, Plane, } from 'three'; import { PivotPointMesh } from './PivotPointMesh.js'; @@ -73,13 +71,9 @@ export class GlobeControls { this.minDistance = 2; this.maxDistance = Infinity; - // group to display (TODO: make callback instead) - this.sphere = new Mesh( new SphereGeometry() ); - - this.sphere = new PivotPointMesh(); - - this.sphere.raycast = () => {}; - this.sphere.scale.setScalar( 0.25 ); + this.pivotMesh = new PivotPointMesh(); + this.pivotMesh.raycast = () => {}; + this.pivotMesh.scale.setScalar( 0.25 ); // internal state this.dragPointSet = false; @@ -179,8 +173,8 @@ export class GlobeControls { this.rotationClickDirection.copy( raycaster.ray.direction ).transformDirection( _matrix ); this.rotationPointSet = true; - this.sphere.position.copy( hit.point ); - this.scene.add( this.sphere ); + this.pivotMesh.position.copy( hit.point ); + this.scene.add( this.pivotMesh ); } else if ( e.buttons & 1 ) { @@ -189,8 +183,8 @@ export class GlobeControls { this.startDragPoint.copy( hit.point ); this.dragPointSet = true; - this.sphere.position.copy( hit.point ); - this.scene.add( this.sphere ); + this.pivotMesh.position.copy( hit.point ); + this.scene.add( this.pivotMesh ); } @@ -234,7 +228,7 @@ export class GlobeControls { this.state = NONE; this.rotationPointSet = false; this.dragPointSet = false; - this.scene.remove( this.sphere ); + this.scene.remove( this.pivotMesh ); }; @@ -272,7 +266,7 @@ export class GlobeControls { this.state = NONE; this.dragPointSet = false; this.rotationPointSet = false; - this.scene.remove( this.sphere ); + this.scene.remove( this.pivotMesh ); } @@ -458,17 +452,5 @@ export class GlobeControls { } - _raycast( o, d ) { - - const { scene } = this; - const raycaster = new Raycaster(); - const { ray } = raycaster; - ray.direction.copy( d ); - ray.origin.copy( o ); - - return raycaster.intersectObject( scene )[ 0 ] || null; - - } - } From 2c391b38847eed35ccd05472f0007e9c34b29a17 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 28 Dec 2023 22:59:09 +0900 Subject: [PATCH 13/64] Update params --- example/src/PivotPointMesh.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/src/PivotPointMesh.js b/example/src/PivotPointMesh.js index 4f1db8cee..2f43c1f10 100644 --- a/example/src/PivotPointMesh.js +++ b/example/src/PivotPointMesh.js @@ -39,8 +39,8 @@ class PivotMaterial extends ShaderMaterial { resolution: { value: new Vector2() }, pixelRatio: { value: 1 }, - size: { value: 10 }, - thickness: { value: 1.5 }, + size: { value: 7.5 }, + thickness: { value: 1 }, opacity: { value: 1 }, }, From 5ed9cadc63e63b309f792631f6774eee7fa6dd2f Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 28 Dec 2023 23:03:20 +0900 Subject: [PATCH 14/64] Improve pivot mesh transform --- example/src/PivotPointMesh.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/example/src/PivotPointMesh.js b/example/src/PivotPointMesh.js index 2f43c1f10..3b6531479 100644 --- a/example/src/PivotPointMesh.js +++ b/example/src/PivotPointMesh.js @@ -16,6 +16,12 @@ export class PivotPointMesh extends Mesh { } + updateMatrixWorld() { + + this.matrixWorld.makeTranslation( this.position ); + + } + dispose() { this.geometry.dispose(); From 81842dd2ddf721ca1ed7f7e5ed4b33d8aa400167 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 29 Dec 2023 14:45:40 +0900 Subject: [PATCH 15/64] Add initial support for pointers --- example/src/GlobeControls.js | 119 ++++++++++++++++++++++++++++------ example/src/PointerTracker.js | 95 +++++++++++++++++++++++++++ 2 files changed, 194 insertions(+), 20 deletions(-) create mode 100644 example/src/PointerTracker.js diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index a0338ee7d..d10e9a874 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -7,6 +7,7 @@ import { Plane, } from 'three'; import { PivotPointMesh } from './PivotPointMesh.js'; +import { PointerTracker } from './PointerTracker.js'; const NONE = 0; const DRAG = 1; @@ -22,14 +23,13 @@ const _up = new Vector3( 0, 1, 0 ); const _plane = new Plane(); // TODO -// - provide fallback plane for cases when you're off the map -// --- // - Touch controls // - Add support for angled rotation plane (based on where the pivot point is) // - Test with globe (adjusting up vector) // --- // - Consider using sphere intersect for positioning // - Toggles for zoom to cursor, zoom forward, orbit around center, etc? +// - provide fallback plane for cases when you're off the map // helper function for constructing a matrix for rotating around a point function makeRotateAroundPoint( point, quat, target ) { @@ -47,10 +47,10 @@ function makeRotateAroundPoint( point, quat, target ) { } // get the three.js pointer coords from an event -function mouseToCoords( e, element, target ) { +function mouseToCoords( clientX, clientY, element, target ) { - target.x = ( e.clientX / element.clientWidth ) * 2 - 1; - target.y = - ( e.clientY / element.clientHeight ) * 2 + 1; + target.x = ( clientX / element.clientWidth ) * 2 - 1; + target.y = - ( clientY / element.clientHeight ) * 2 + 1; } @@ -65,7 +65,7 @@ export class GlobeControls { // settings this.state = NONE; this.cameraRadius = 1; - this.rotationSpeed = 3; + this.rotationSpeed = 5; this.minAltitude = 0; this.maxAltitude = Math.PI / 2; this.minDistance = 2; @@ -126,6 +126,10 @@ export class GlobeControls { const _pointer = new Vector2(); const _newPointer = new Vector2(); const _deltaPointer = new Vector2(); + const _pointerArr = []; + + const _pointerTracker = new PointerTracker(); + let _pointerDist = 0; let shiftClicked = false; const contextMenuCallback = e => { @@ -158,13 +162,36 @@ export class GlobeControls { const { camera, raycaster, domElement, scene } = this; - mouseToCoords( e, domElement, _pointer ); + mouseToCoords( e.clientX, e.clientY, domElement, _pointer ); + + if ( e.pointerType === 'touch' ) { + + _pointerTracker.addPointer( e ); + _pointerDist = 0; + + if ( _pointerTracker.getPointerCount() === 2 ) { + + const center = new Vector2(); + _pointerTracker.getCenterPoint( center ); + _pointerDist = _pointerTracker.getPointerDistance(); + + mouseToCoords( center.x, center.y, domElement, _pointer ); + + } else if ( _pointerTracker.getPointerCount() > 2 ) { + + resetState(); + return; + + } + + } + raycaster.setFromCamera( _pointer, camera ); const hit = raycaster.intersectObject( scene )[ 0 ] || null; if ( hit ) { - if ( e.buttons & 2 || e.buttons & 1 && shiftClicked ) { + if ( _pointerArr.length === 2 || e.buttons & 2 || e.buttons & 1 && shiftClicked ) { _matrix.copy( camera.matrixWorld ).invert(); @@ -197,7 +224,51 @@ export class GlobeControls { this.zoomDirectionSet = false; this.zoomPointSet = false; - mouseToCoords( e, domElement, _newPointer ); + mouseToCoords( e.clientX, e.clientY, domElement, _newPointer ); + + if ( e.pointerType === 'touch' ) { + + if ( ! _pointerTracker.updatePointer( e ) ) { + + return; + + } + + if ( _pointerTracker.getPointerCount() === 2 ) { + + const center = new Vector2(); + _pointerTracker.getCenterPoint( center ); + mouseToCoords( center.x, center.y, domElement, _newPointer ); + + const previousDist = _pointerDist; + _pointerDist = _pointerTracker.getPointerDistance(); + if ( _pointerDist - previousDist > 20 ) { + + resetState(); + + } + + // perform zoom + const { raycaster, scene } = this; + raycaster.setFromCamera( _pointer, this.camera ); + + const hit = raycaster.intersectObject( scene )[ 0 ] || null; + if ( hit ) { + + this.zoomPoint.copy( hit.point ); + this.zoomPointSet = true; + + } + + this.zoomDirection.copy( this.raycaster.ray.direction ).normalize(); + this.zoomDirectionSet = true; + + this.updateZoom( _pointerDist - previousDist ); + + } + + } + _deltaPointer.subVectors( _newPointer, _pointer ); _pointer.copy( _newPointer ); @@ -225,10 +296,13 @@ export class GlobeControls { const pointerupCallback = e => { - this.state = NONE; - this.rotationPointSet = false; - this.dragPointSet = false; - this.scene.remove( this.pivotMesh ); + resetState(); + + if ( e.pointerType === 'touch' ) { + + _pointerTracker.deletePointer( e ); + + } }; @@ -258,20 +332,26 @@ export class GlobeControls { const pointerenterCallback = e => { - mouseToCoords( e, domElement, _pointer ); + mouseToCoords( e.clientX, e.clientY, domElement, _pointer ); shiftClicked = false; if ( e.buttons === 0 ) { - this.state = NONE; - this.dragPointSet = false; - this.rotationPointSet = false; - this.scene.remove( this.pivotMesh ); + resetState(); } }; + const resetState = () => { + + this.state = NONE; + this.dragPointSet = false; + this.rotationPointSet = false; + this.scene.remove( this.pivotMesh ); + + }; + domElement.addEventListener( 'contextmenu', contextMenuCallback ); domElement.addEventListener( 'keydown', keydownCallback ); domElement.addEventListener( 'keyup', keyupCallback ); @@ -372,7 +452,7 @@ export class GlobeControls { const { camera, rotationPoint, minAltitude, maxAltitude } = this; - // TODO: currently uses the camera forward for this work but it may be best to use a combination of camera + // currently uses the camera forward for this work but it may be best to use a combination of camera // forward and direction to pivot? Or just dir to pivot? _delta.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ).multiplyScalar( - 1 ); @@ -435,7 +515,6 @@ export class GlobeControls { const dist = hit.distance - 100; if ( dist < cameraRadius ) { - // TODO: maybe this can snap back once the camera as gone over the hill so the drag point is back in the right spot const delta = cameraRadius - dist; camera.position.copy( hit.point ).addScaledVector( raycaster.ray.direction, - cameraRadius ); dragPoint.addScaledVector( _up, delta ); diff --git a/example/src/PointerTracker.js b/example/src/PointerTracker.js new file mode 100644 index 000000000..675e17cc3 --- /dev/null +++ b/example/src/PointerTracker.js @@ -0,0 +1,95 @@ +import { Vector2 } from 'three'; + +export class PointerTracker { + + constructor() { + + this.pointerOrder = []; + this.pointerPositions = {}; + + } + + addPointer( e ) { + + const id = e.pointerId; + const position = new Vector2( e.clientX, e.clientY ); + this.pointerOrder.push( id ); + this.pointerPositions[ id ] = position; + + } + + updatePointer( e ) { + + const id = e.pointerId; + if ( ! ( id in this.pointerPositions ) ) { + + return false; + + } + + this.pointerPositions[ id ].set( e.clientX, e.clientY ); + return true; + + } + + deletePointer( e ) { + + const id = e.pointerId; + const pointerOrder = this.pointerOrder; + pointerOrder.splice( pointerOrder.indexOf( id ), 1 ); + delete this.pointerPositions; + + } + + getPointerCount() { + + return this.pointerOrder.length; + + } + + getCenterPoint( target ) { + + const pointerOrder = this.pointerOrder; + const pointerPositions = this.pointerPositions; + if ( this.getPointerCount() === 1 ) { + + const id = pointerOrder[ 0 ]; + target.copy( pointerPositions[ id ] ); + return target; + + } else if ( this.getPointerCount() === 2 ) { + + const id0 = this.pointerOrder[ 0 ]; + const id1 = this.pointerOrder[ 1 ]; + + const p0 = this.pointerPositions[ id0 ]; + const p1 = this.pointerPositions[ id1 ]; + + target.addVectors( p0, p1 ).multiplyScalar( 0.5 ); + return target; + + } + + return null; + + } + + getPointerDistance() { + + if ( this.getPointerCount() <= 1 ) { + + return 0; + + } + + const id0 = this.pointerOrder[ 0 ]; + const id1 = this.pointerOrder[ 1 ]; + + const p0 = this.pointerPositions[ id0 ]; + const p1 = this.pointerPositions[ id1 ]; + + return p0.distanceTo( p1 ); + + } + +} From 331591abbce148b6a6d41e8ef4683db5b131b7f9 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 29 Dec 2023 15:43:02 +0900 Subject: [PATCH 16/64] Get touch controls working --- example/src/GlobeControls.js | 100 +++++++++++++++++++++++----------- example/src/PointerTracker.js | 2 +- 2 files changed, 68 insertions(+), 34 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index d10e9a874..7917424e4 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -122,14 +122,16 @@ export class GlobeControls { } this.domElement = domElement; + domElement.style.touchAction = 'none'; const _pointer = new Vector2(); const _newPointer = new Vector2(); const _deltaPointer = new Vector2(); - const _pointerArr = []; + let _allowedZoom = false; const _pointerTracker = new PointerTracker(); let _pointerDist = 0; + let _originalDist = 0; let shiftClicked = false; const contextMenuCallback = e => { @@ -166,14 +168,22 @@ export class GlobeControls { if ( e.pointerType === 'touch' ) { + if ( _pointerTracker.getPointerCount() === 0 ) { + + domElement.setPointerCapture( e.pointerId ); + + } + _pointerTracker.addPointer( e ); _pointerDist = 0; + _originalDist = 0; if ( _pointerTracker.getPointerCount() === 2 ) { const center = new Vector2(); _pointerTracker.getCenterPoint( center ); _pointerDist = _pointerTracker.getPointerDistance(); + _originalDist = _pointerTracker.getPointerDistance(); mouseToCoords( center.x, center.y, domElement, _pointer ); @@ -191,7 +201,7 @@ export class GlobeControls { const hit = raycaster.intersectObject( scene )[ 0 ] || null; if ( hit ) { - if ( _pointerArr.length === 2 || e.buttons & 2 || e.buttons & 1 && shiftClicked ) { + if ( _pointerTracker.getPointerCount() === 2 || e.buttons & 2 || e.buttons & 1 && shiftClicked ) { _matrix.copy( camera.matrixWorld ).invert(); @@ -242,28 +252,39 @@ export class GlobeControls { const previousDist = _pointerDist; _pointerDist = _pointerTracker.getPointerDistance(); - if ( _pointerDist - previousDist > 20 ) { + + console.log( _allowedZoom, _pointerDist - _originalDist ); + if ( ! _allowedZoom && _pointerDist - _originalDist > 15 * window.devicePixelRatio ) { resetState(); + _allowedZoom = true; } - // perform zoom - const { raycaster, scene } = this; - raycaster.setFromCamera( _pointer, this.camera ); + if ( _allowedZoom ) { - const hit = raycaster.intersectObject( scene )[ 0 ] || null; - if ( hit ) { + // perform zoom + this.zoomDirectionSet = false; + performZoom( _pointerDist - previousDist ); - this.zoomPoint.copy( hit.point ); - this.zoomPointSet = true; + const { raycaster, scene } = this; + raycaster.setFromCamera( _pointer, this.camera ); - } + const hit = raycaster.intersectObject( scene )[ 0 ] || null; + if ( hit ) { + + this.zoomPoint.copy( hit.point ); + this.zoomPointSet = true; - this.zoomDirection.copy( this.raycaster.ray.direction ).normalize(); - this.zoomDirectionSet = true; + } - this.updateZoom( _pointerDist - previousDist ); + this.zoomDirection.copy( this.raycaster.ray.direction ).normalize(); + this.zoomDirectionSet = true; + + this.updateZoom( _pointerDist - previousDist ); + + + } } @@ -301,32 +322,21 @@ export class GlobeControls { if ( e.pointerType === 'touch' ) { _pointerTracker.deletePointer( e ); + _allowedZoom = false; - } - - }; - - const wheelCallback = e => { - - if ( ! this.zoomDirectionSet ) { - - const { raycaster, scene } = this; - raycaster.setFromCamera( _pointer, this.camera ); - - const hit = raycaster.intersectObject( scene )[ 0 ] || null; - if ( hit ) { + if ( _pointerTracker.getPointerCount() === 0 ) { - this.zoomPoint.copy( hit.point ); - this.zoomPointSet = true; + domElement.releasePointerCapture( e.pointerId ); } - this.zoomDirection.copy( this.raycaster.ray.direction ).normalize(); - this.zoomDirectionSet = true; - } - this.updateZoom( - e.deltaY ); + }; + + const wheelCallback = e => { + + performZoom( - e.deltaY ); }; @@ -352,6 +362,30 @@ export class GlobeControls { }; + const performZoom = delta => { + + if ( ! this.zoomDirectionSet ) { + + const { raycaster, scene } = this; + raycaster.setFromCamera( _pointer, this.camera ); + + const hit = raycaster.intersectObject( scene )[ 0 ] || null; + if ( hit ) { + + this.zoomPoint.copy( hit.point ); + this.zoomPointSet = true; + + } + + this.zoomDirection.copy( this.raycaster.ray.direction ).normalize(); + this.zoomDirectionSet = true; + + } + + this.updateZoom( delta ); + + }; + domElement.addEventListener( 'contextmenu', contextMenuCallback ); domElement.addEventListener( 'keydown', keydownCallback ); domElement.addEventListener( 'keyup', keyupCallback ); diff --git a/example/src/PointerTracker.js b/example/src/PointerTracker.js index 675e17cc3..3ccdb4ecc 100644 --- a/example/src/PointerTracker.js +++ b/example/src/PointerTracker.js @@ -37,7 +37,7 @@ export class PointerTracker { const id = e.pointerId; const pointerOrder = this.pointerOrder; pointerOrder.splice( pointerOrder.indexOf( id ), 1 ); - delete this.pointerPositions; + delete this.pointerPositions[ id ]; } From 556e7dc9cbeb05b3175c980ca7d504d7164b73dc Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 29 Dec 2023 16:13:16 +0900 Subject: [PATCH 17/64] Add better touch support --- example/src/GlobeControls.js | 136 +++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 60 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 7917424e4..9114f680d 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -12,6 +12,9 @@ import { PointerTracker } from './PointerTracker.js'; const NONE = 0; const DRAG = 1; const ROTATE = 2; +const ZOOM = 3; + +const ACTION_THRESHOLD = 2 * window.devicePixelRatio; const _matrix = new Matrix4(); const _rotMatrix = new Matrix4(); @@ -127,11 +130,13 @@ export class GlobeControls { const _pointer = new Vector2(); const _newPointer = new Vector2(); const _deltaPointer = new Vector2(); - let _allowedZoom = false; + const _centerPoint = new Vector2(); + const _originalCenterPoint = new Vector2(); + let _pinchAction = NONE; const _pointerTracker = new PointerTracker(); let _pointerDist = 0; - let _originalDist = 0; + let _originalPointerDist = 0; let shiftClicked = false; const contextMenuCallback = e => { @@ -176,16 +181,16 @@ export class GlobeControls { _pointerTracker.addPointer( e ); _pointerDist = 0; - _originalDist = 0; + _originalPointerDist = 0; if ( _pointerTracker.getPointerCount() === 2 ) { - const center = new Vector2(); - _pointerTracker.getCenterPoint( center ); + _pointerTracker.getCenterPoint( _originalCenterPoint ); + _pointerTracker.getCenterPoint( _centerPoint ); _pointerDist = _pointerTracker.getPointerDistance(); - _originalDist = _pointerTracker.getPointerDistance(); + _originalPointerDist = _pointerTracker.getPointerDistance(); - mouseToCoords( center.x, center.y, domElement, _pointer ); + mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _pointer ); } else if ( _pointerTracker.getPointerCount() > 2 ) { @@ -229,108 +234,118 @@ export class GlobeControls { }; + let _queued = false; const pointermoveCallback = e => { - this.zoomDirectionSet = false; - this.zoomPointSet = false; + if ( _queued ) return; - mouseToCoords( e.clientX, e.clientY, domElement, _newPointer ); + _queued = true; + queueMicrotask( () => { - if ( e.pointerType === 'touch' ) { + _queued = false; + this.zoomDirectionSet = false; + this.zoomPointSet = false; - if ( ! _pointerTracker.updatePointer( e ) ) { + mouseToCoords( e.clientX, e.clientY, domElement, _newPointer ); - return; + if ( e.pointerType === 'touch' ) { - } + if ( ! _pointerTracker.updatePointer( e ) ) { - if ( _pointerTracker.getPointerCount() === 2 ) { + return; - const center = new Vector2(); - _pointerTracker.getCenterPoint( center ); - mouseToCoords( center.x, center.y, domElement, _newPointer ); + } - const previousDist = _pointerDist; - _pointerDist = _pointerTracker.getPointerDistance(); + if ( _pointerTracker.getPointerCount() === 2 ) { - console.log( _allowedZoom, _pointerDist - _originalDist ); - if ( ! _allowedZoom && _pointerDist - _originalDist > 15 * window.devicePixelRatio ) { + // adjust the pointer position to be the center point + const _centerPoint = new Vector2(); + _pointerTracker.getCenterPoint( _centerPoint ); + mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _newPointer ); - resetState(); - _allowedZoom = true; + // detect zoom transition + const previousDist = _pointerDist; + _pointerDist = _pointerTracker.getPointerDistance(); + if ( _pinchAction === NONE ) { - } + if ( _pointerDist - _originalPointerDist > ACTION_THRESHOLD ) { - if ( _allowedZoom ) { + resetState(); + _pinchAction = ZOOM; + this.zoomDirectionSet = false; - // perform zoom - this.zoomDirectionSet = false; - performZoom( _pointerDist - previousDist ); + } else if ( _centerPoint.distanceTo( _originalCenterPoint ) > ACTION_THRESHOLD ) { - const { raycaster, scene } = this; - raycaster.setFromCamera( _pointer, this.camera ); + _pinchAction = ROTATE; - const hit = raycaster.intersectObject( scene )[ 0 ] || null; - if ( hit ) { - this.zoomPoint.copy( hit.point ); - this.zoomPointSet = true; + } } - this.zoomDirection.copy( this.raycaster.ray.direction ).normalize(); - this.zoomDirectionSet = true; + if ( _pinchAction === ZOOM ) { + + // perform zoom + performZoom( _pointerDist - previousDist ); - this.updateZoom( _pointerDist - previousDist ); + } else if ( _pinchAction === NONE ) { + _pointer.copy( _newPointer ); + return; + + } } } - } + _deltaPointer.subVectors( _newPointer, _pointer ); + _pointer.copy( _newPointer ); - _deltaPointer.subVectors( _newPointer, _pointer ); - _pointer.copy( _newPointer ); + if ( this.state === DRAG ) { - if ( this.state === DRAG ) { + const { raycaster, camera, dragPoint } = this; + _plane.setFromNormalAndCoplanarPoint( _up, dragPoint ); + raycaster.setFromCamera( _pointer, camera ); - const { raycaster, camera, dragPoint } = this; - _plane.setFromNormalAndCoplanarPoint( _up, dragPoint ); - raycaster.setFromCamera( _pointer, camera ); + if ( raycaster.ray.intersectPlane( _plane, _vec ) ) { - if ( raycaster.ray.intersectPlane( _plane, _vec ) ) { + _delta.subVectors( dragPoint, _vec ); + this.updatePosition( _delta ); - _delta.subVectors( dragPoint, _vec ); - this.updatePosition( _delta ); + } - } + } else if ( this.state === ROTATE ) { - } else if ( this.state === ROTATE ) { + const { rotationSpeed } = this; + this.updateRotation( - _deltaPointer.x * rotationSpeed, - _deltaPointer.y * rotationSpeed ); - const { rotationSpeed } = this; - this.updateRotation( - _deltaPointer.x * rotationSpeed, - _deltaPointer.y * rotationSpeed ); + } - } + } ); }; const pointerupCallback = e => { - resetState(); + queueMicrotask( () => { - if ( e.pointerType === 'touch' ) { + resetState(); - _pointerTracker.deletePointer( e ); - _allowedZoom = false; + if ( e.pointerType === 'touch' ) { - if ( _pointerTracker.getPointerCount() === 0 ) { + _pointerTracker.deletePointer( e ); + _pinchAction = NONE; + + if ( _pointerTracker.getPointerCount() === 0 ) { - domElement.releasePointerCapture( e.pointerId ); + domElement.releasePointerCapture( e.pointerId ); + + } } - } + } ); }; @@ -470,6 +485,7 @@ export class GlobeControls { } this.camera.position.addScaledVector( zoomDirection, scale ); + this.camera.updateMatrixWorld(); } From b80605479e443eb1b2ee2b01f821763a79e0fe5b Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 29 Dec 2023 17:00:57 +0900 Subject: [PATCH 18/64] Simplification --- example/src/GlobeControls.js | 119 ++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 50 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 9114f680d..9befaa390 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -169,6 +169,7 @@ export class GlobeControls { const { camera, raycaster, domElement, scene } = this; + // get the screen coordinates mouseToCoords( e.clientX, e.clientY, domElement, _pointer ); if ( e.pointerType === 'touch' ) { @@ -179,17 +180,21 @@ export class GlobeControls { } + // init fields _pointerTracker.addPointer( e ); _pointerDist = 0; _originalPointerDist = 0; + // if we find a second pointer init other values if ( _pointerTracker.getPointerCount() === 2 ) { _pointerTracker.getCenterPoint( _originalCenterPoint ); - _pointerTracker.getCenterPoint( _centerPoint ); + _centerPoint.copy( _originalCenterPoint ); + _pointerDist = _pointerTracker.getPointerDistance(); _originalPointerDist = _pointerTracker.getPointerDistance(); + // the "pointer" for zooming and rotating should be based on the center point mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _pointer ); } else if ( _pointerTracker.getPointerCount() > 2 ) { @@ -201,11 +206,13 @@ export class GlobeControls { } + // find the hit point raycaster.setFromCamera( _pointer, camera ); - const hit = raycaster.intersectObject( scene )[ 0 ] || null; if ( hit ) { + // if two fingers, right click, or shift click are being used then we trigger + // a rotation action to begin if ( _pointerTracker.getPointerCount() === 2 || e.buttons & 2 || e.buttons & 1 && shiftClicked ) { _matrix.copy( camera.matrixWorld ).invert(); @@ -234,71 +241,87 @@ export class GlobeControls { }; - let _queued = false; const pointermoveCallback = e => { - if ( _queued ) return; + this.zoomDirectionSet = false; + this.zoomPointSet = false; - _queued = true; - queueMicrotask( () => { + mouseToCoords( e.clientX, e.clientY, domElement, _newPointer ); - _queued = false; - this.zoomDirectionSet = false; - this.zoomPointSet = false; + if ( e.pointerType === 'touch' ) { - mouseToCoords( e.clientX, e.clientY, domElement, _newPointer ); + if ( ! _pointerTracker.updatePointer( e ) ) { - if ( e.pointerType === 'touch' ) { + return; - if ( ! _pointerTracker.updatePointer( e ) ) { + } - return; + if ( _pointerTracker.getPointerCount() === 2 ) { - } + // adjust the pointer position to be the center point + const _centerPoint = new Vector2(); + _pointerTracker.getCenterPoint( _centerPoint ); + mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _newPointer ); - if ( _pointerTracker.getPointerCount() === 2 ) { + // detect zoom transition + const previousDist = _pointerDist; + _pointerDist = _pointerTracker.getPointerDistance(); + if ( _pinchAction === NONE ) { - // adjust the pointer position to be the center point - const _centerPoint = new Vector2(); - _pointerTracker.getCenterPoint( _centerPoint ); - mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _newPointer ); + if ( _pointerDist - _originalPointerDist > ACTION_THRESHOLD ) { - // detect zoom transition - const previousDist = _pointerDist; - _pointerDist = _pointerTracker.getPointerDistance(); - if ( _pinchAction === NONE ) { + resetState(); + _pinchAction = ZOOM; + this.zoomDirectionSet = false; - if ( _pointerDist - _originalPointerDist > ACTION_THRESHOLD ) { + } else if ( _centerPoint.distanceTo( _originalCenterPoint ) > ACTION_THRESHOLD ) { - resetState(); - _pinchAction = ZOOM; - this.zoomDirectionSet = false; + _pinchAction = ROTATE; - } else if ( _centerPoint.distanceTo( _originalCenterPoint ) > ACTION_THRESHOLD ) { - _pinchAction = ROTATE; + } + } - } + if ( _pinchAction === ZOOM ) { - } + // perform zoom + performZoom( _pointerDist - previousDist ); - if ( _pinchAction === ZOOM ) { + } else if ( _pinchAction === NONE ) { - // perform zoom - performZoom( _pointerDist - previousDist ); + _pointer.copy( _newPointer ); + return; - } else if ( _pinchAction === NONE ) { + } - _pointer.copy( _newPointer ); - return; + } - } + _deltaPointer.subVectors( _newPointer, _pointer ); + _pointer.copy( _newPointer ); + + if ( this.state === DRAG ) { + + const { raycaster, camera, dragPoint } = this; + _plane.setFromNormalAndCoplanarPoint( _up, dragPoint ); + raycaster.setFromCamera( _pointer, camera ); + + if ( raycaster.ray.intersectPlane( _plane, _vec ) ) { + + _delta.subVectors( dragPoint, _vec ); + this.updatePosition( _delta ); } + } else if ( this.state === ROTATE ) { + + const { rotationSpeed } = this; + this.updateRotation( - _deltaPointer.x * rotationSpeed, - _deltaPointer.y * rotationSpeed ); + } + } else if ( e.pointerType === 'mouse' ) { + _deltaPointer.subVectors( _newPointer, _pointer ); _pointer.copy( _newPointer ); @@ -322,30 +345,26 @@ export class GlobeControls { } - } ); + } }; const pointerupCallback = e => { - queueMicrotask( () => { - - resetState(); + resetState(); - if ( e.pointerType === 'touch' ) { + if ( e.pointerType === 'touch' ) { - _pointerTracker.deletePointer( e ); - _pinchAction = NONE; + _pointerTracker.deletePointer( e ); + _pinchAction = NONE; - if ( _pointerTracker.getPointerCount() === 0 ) { + if ( _pointerTracker.getPointerCount() === 0 ) { - domElement.releasePointerCapture( e.pointerId ); - - } + domElement.releasePointerCapture( e.pointerId ); } - } ); + } }; From 630de00adb55fc1cc458ea1ee613927a6d2452a9 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 29 Dec 2023 17:08:27 +0900 Subject: [PATCH 19/64] Simplification --- example/src/GlobeControls.js | 76 ++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 9befaa390..41991dd7c 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -246,8 +246,6 @@ export class GlobeControls { this.zoomDirectionSet = false; this.zoomPointSet = false; - mouseToCoords( e.clientX, e.clientY, domElement, _newPointer ); - if ( e.pointerType === 'touch' ) { if ( ! _pointerTracker.updatePointer( e ) ) { @@ -256,12 +254,21 @@ export class GlobeControls { } - if ( _pointerTracker.getPointerCount() === 2 ) { + if ( _pointerTracker.getPointerCount() === 1 ) { + + // if there's only one pointer active then handle the drag event + mouseToCoords( e.clientX, e.clientY, domElement, _pointer ); + + if ( this.state === DRAG ) { + + performDrag(); + + } + + } else if ( _pointerTracker.getPointerCount() === 2 ) { // adjust the pointer position to be the center point - const _centerPoint = new Vector2(); _pointerTracker.getCenterPoint( _centerPoint ); - mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _newPointer ); // detect zoom transition const previousDist = _pointerDist; @@ -288,55 +295,33 @@ export class GlobeControls { // perform zoom performZoom( _pointerDist - previousDist ); - } else if ( _pinchAction === NONE ) { + } else if ( _pinchAction === ROTATE ) { + // perform rotation + const { rotationSpeed } = this; + mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _newPointer ); + _deltaPointer.subVectors( _newPointer, _pointer ); _pointer.copy( _newPointer ); - return; + this.updateRotation( - _deltaPointer.x * rotationSpeed, - _deltaPointer.y * rotationSpeed ); - } + } else { - } - - _deltaPointer.subVectors( _newPointer, _pointer ); - _pointer.copy( _newPointer ); - - if ( this.state === DRAG ) { - - const { raycaster, camera, dragPoint } = this; - _plane.setFromNormalAndCoplanarPoint( _up, dragPoint ); - raycaster.setFromCamera( _pointer, camera ); - - if ( raycaster.ray.intersectPlane( _plane, _vec ) ) { - - _delta.subVectors( dragPoint, _vec ); - this.updatePosition( _delta ); + // no action + _pointer.copy( _newPointer ); } - } else if ( this.state === ROTATE ) { - - const { rotationSpeed } = this; - this.updateRotation( - _deltaPointer.x * rotationSpeed, - _deltaPointer.y * rotationSpeed ); - } } else if ( e.pointerType === 'mouse' ) { + mouseToCoords( e.clientX, e.clientY, domElement, _newPointer ); _deltaPointer.subVectors( _newPointer, _pointer ); _pointer.copy( _newPointer ); if ( this.state === DRAG ) { - const { raycaster, camera, dragPoint } = this; - _plane.setFromNormalAndCoplanarPoint( _up, dragPoint ); - raycaster.setFromCamera( _pointer, camera ); - - if ( raycaster.ray.intersectPlane( _plane, _vec ) ) { - - _delta.subVectors( dragPoint, _vec ); - this.updatePosition( _delta ); - - } + performDrag(); } else if ( this.state === ROTATE ) { @@ -420,6 +405,21 @@ export class GlobeControls { }; + const performDrag = () => { + + const { raycaster, camera, dragPoint } = this; + _plane.setFromNormalAndCoplanarPoint( _up, dragPoint ); + raycaster.setFromCamera( _pointer, camera ); + + if ( raycaster.ray.intersectPlane( _plane, _vec ) ) { + + _delta.subVectors( dragPoint, _vec ); + this.updatePosition( _delta ); + + } + + }; + domElement.addEventListener( 'contextmenu', contextMenuCallback ); domElement.addEventListener( 'keydown', keydownCallback ); domElement.addEventListener( 'keyup', keyupCallback ); From a23dbd2ff29c1883ebe0633f11d602534dd5e9a4 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 29 Dec 2023 17:15:31 +0900 Subject: [PATCH 20/64] More updates --- example/src/GlobeControls.js | 67 +++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 41991dd7c..3d34e89d9 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -133,6 +133,7 @@ export class GlobeControls { const _centerPoint = new Vector2(); const _originalCenterPoint = new Vector2(); let _pinchAction = NONE; + let _pointerMoveQueued = false; const _pointerTracker = new PointerTracker(); let _pointerDist = 0; @@ -196,6 +197,7 @@ export class GlobeControls { // the "pointer" for zooming and rotating should be based on the center point mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _pointer ); + _newPointer.copy( _pointer ); } else if ( _pointerTracker.getPointerCount() > 2 ) { @@ -267,47 +269,58 @@ export class GlobeControls { } else if ( _pointerTracker.getPointerCount() === 2 ) { - // adjust the pointer position to be the center point - _pointerTracker.getCenterPoint( _centerPoint ); + if ( ! _pointerMoveQueued ) { - // detect zoom transition - const previousDist = _pointerDist; - _pointerDist = _pointerTracker.getPointerDistance(); - if ( _pinchAction === NONE ) { + _pointerMoveQueued = true; + queueMicrotask( () => { - if ( _pointerDist - _originalPointerDist > ACTION_THRESHOLD ) { + _pointerMoveQueued = false; - resetState(); - _pinchAction = ZOOM; - this.zoomDirectionSet = false; + // adjust the pointer position to be the center point + _pointerTracker.getCenterPoint( _centerPoint ); - } else if ( _centerPoint.distanceTo( _originalCenterPoint ) > ACTION_THRESHOLD ) { + // detect zoom transition + const previousDist = _pointerDist; + _pointerDist = _pointerTracker.getPointerDistance(); + if ( _pinchAction === NONE ) { - _pinchAction = ROTATE; + if ( _pointerDist - _originalPointerDist > ACTION_THRESHOLD ) { + resetState(); + _pinchAction = ZOOM; + this.zoomDirectionSet = false; - } + } else if ( _centerPoint.distanceTo( _originalCenterPoint ) > ACTION_THRESHOLD ) { - } + _pinchAction = ROTATE; + + + } + + } + + if ( _pinchAction === ZOOM ) { + + // perform zoom + performZoom( _pointerDist - previousDist ); - if ( _pinchAction === ZOOM ) { + } else if ( _pinchAction === ROTATE ) { - // perform zoom - performZoom( _pointerDist - previousDist ); + // perform rotation + const { rotationSpeed } = this; + mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _newPointer ); + _deltaPointer.subVectors( _newPointer, _pointer ); + _pointer.copy( _newPointer ); + this.updateRotation( - _deltaPointer.x * rotationSpeed, - _deltaPointer.y * rotationSpeed ); - } else if ( _pinchAction === ROTATE ) { + } else { - // perform rotation - const { rotationSpeed } = this; - mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _newPointer ); - _deltaPointer.subVectors( _newPointer, _pointer ); - _pointer.copy( _newPointer ); - this.updateRotation( - _deltaPointer.x * rotationSpeed, - _deltaPointer.y * rotationSpeed ); + // no action + _pointer.copy( _newPointer ); - } else { + } - // no action - _pointer.copy( _newPointer ); + } ); } From 8e14deef6365f568da34b91211a6d30dfde0c7a0 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 29 Dec 2023 17:16:07 +0900 Subject: [PATCH 21/64] Comment --- example/src/GlobeControls.js | 1 + 1 file changed, 1 insertion(+) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 3d34e89d9..e7e250e97 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -269,6 +269,7 @@ export class GlobeControls { } else if ( _pointerTracker.getPointerCount() === 2 ) { + // We queue this event to ensure that all pointers have been updated if ( ! _pointerMoveQueued ) { _pointerMoveQueued = true; From b290796b764161fe78135fa17109606cc515c7be Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Fri, 29 Dec 2023 17:20:15 +0900 Subject: [PATCH 22/64] Fix pivot icon display --- example/src/GlobeControls.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index e7e250e97..91564f15c 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -175,6 +175,8 @@ export class GlobeControls { if ( e.pointerType === 'touch' ) { + this.pivotMesh.visible = false; + if ( _pointerTracker.getPointerCount() === 0 ) { domElement.setPointerCapture( e.pointerId ); @@ -313,10 +315,12 @@ export class GlobeControls { _deltaPointer.subVectors( _newPointer, _pointer ); _pointer.copy( _newPointer ); this.updateRotation( - _deltaPointer.x * rotationSpeed, - _deltaPointer.y * rotationSpeed ); + this.pivotMesh.visible = true; } else { // no action + mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _newPointer ); _pointer.copy( _newPointer ); } @@ -392,6 +396,7 @@ export class GlobeControls { this.dragPointSet = false; this.rotationPointSet = false; this.scene.remove( this.pivotMesh ); + this.pivotMesh.visible = true; }; From 0ebf0c5a5e2b6ca502df1b341dc091589dc8aa39 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sat, 30 Dec 2023 00:28:44 +0900 Subject: [PATCH 23/64] more simplify --- example/src/GlobeControls.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 91564f15c..eaa27bd9b 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -25,8 +25,11 @@ const _quaternion = new Quaternion(); const _up = new Vector3( 0, 1, 0 ); const _plane = new Plane(); +const _deltaPointer = new Vector2(); +const _centerPoint = new Vector2(); +const _newPointer = new Vector2(); + // TODO -// - Touch controls // - Add support for angled rotation plane (based on where the pivot point is) // - Test with globe (adjusting up vector) // --- @@ -128,9 +131,6 @@ export class GlobeControls { domElement.style.touchAction = 'none'; const _pointer = new Vector2(); - const _newPointer = new Vector2(); - const _deltaPointer = new Vector2(); - const _centerPoint = new Vector2(); const _originalCenterPoint = new Vector2(); let _pinchAction = NONE; let _pointerMoveQueued = false; @@ -199,7 +199,6 @@ export class GlobeControls { // the "pointer" for zooming and rotating should be based on the center point mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _pointer ); - _newPointer.copy( _pointer ); } else if ( _pointerTracker.getPointerCount() > 2 ) { From 2c23a6994c41b9ca3ce406f5ff11fa3ea7d44a9d Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sat, 30 Dec 2023 00:35:27 +0900 Subject: [PATCH 24/64] Fix touch action init --- example/src/GlobeControls.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index eaa27bd9b..6a752712b 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -286,16 +286,23 @@ export class GlobeControls { _pointerDist = _pointerTracker.getPointerDistance(); if ( _pinchAction === NONE ) { - if ( _pointerDist - _originalPointerDist > ACTION_THRESHOLD ) { + // check which direction was moved in first + const separateDistance = _pointerDist - _originalPointerDist; + const rotateDistance = _centerPoint.distanceTo( _originalCenterPoint ); + if ( separateDistance > 0 && rotateDistance > 0 ) { - resetState(); - _pinchAction = ZOOM; - this.zoomDirectionSet = false; + if ( separateDistance > rotateDistance ) { - } else if ( _centerPoint.distanceTo( _originalCenterPoint ) > ACTION_THRESHOLD ) { + resetState(); + _pinchAction = ZOOM; + this.zoomDirectionSet = false; - _pinchAction = ROTATE; + } else { + _pinchAction = ROTATE; + + + } } From ee0f1fbf65f8c8dd9607ac568f748f63458e7527 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sat, 30 Dec 2023 00:47:00 +0900 Subject: [PATCH 25/64] Offset fixes --- example/src/GlobeControls.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 6a752712b..10e35f8d3 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -14,8 +14,6 @@ const DRAG = 1; const ROTATE = 2; const ZOOM = 3; -const ACTION_THRESHOLD = 2 * window.devicePixelRatio; - const _matrix = new Matrix4(); const _rotMatrix = new Matrix4(); const _delta = new Vector3(); @@ -36,6 +34,7 @@ const _newPointer = new Vector2(); // - Consider using sphere intersect for positioning // - Toggles for zoom to cursor, zoom forward, orbit around center, etc? // - provide fallback plane for cases when you're off the map +// - consider enabling drag with zoom // helper function for constructing a matrix for rotating around a point function makeRotateAroundPoint( point, quat, target ) { @@ -55,8 +54,10 @@ function makeRotateAroundPoint( point, quat, target ) { // get the three.js pointer coords from an event function mouseToCoords( clientX, clientY, element, target ) { - target.x = ( clientX / element.clientWidth ) * 2 - 1; - target.y = - ( clientY / element.clientHeight ) * 2 + 1; + console.log( element.offsetLeft ); + + target.x = ( ( clientX - element.offsetLeft ) / element.clientWidth ) * 2 - 1; + target.y = - ( ( clientY - element.offsetTop ) / element.clientHeight ) * 2 + 1; } From b094658f5eb7d9e72c1423356412fc4ebea5a8a6 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sat, 30 Dec 2023 12:28:53 +0900 Subject: [PATCH 26/64] Fix issue with reverse dragg --- example/src/GlobeControls.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 10e35f8d3..d4b9ea5ca 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -35,6 +35,8 @@ const _newPointer = new Vector2(); // - Toggles for zoom to cursor, zoom forward, orbit around center, etc? // - provide fallback plane for cases when you're off the map // - consider enabling drag with zoom +// - shift + scroll could adjust altitude +// - fade pivot icon in and out // helper function for constructing a matrix for rotating around a point function makeRotateAroundPoint( point, quat, target ) { @@ -54,8 +56,6 @@ function makeRotateAroundPoint( point, quat, target ) { // get the three.js pointer coords from an event function mouseToCoords( clientX, clientY, element, target ) { - console.log( element.offsetLeft ); - target.x = ( ( clientX - element.offsetLeft ) / element.clientWidth ) * 2 - 1; target.y = - ( ( clientY - element.offsetTop ) / element.clientHeight ) * 2 + 1; @@ -74,7 +74,7 @@ export class GlobeControls { this.cameraRadius = 1; this.rotationSpeed = 5; this.minAltitude = 0; - this.maxAltitude = Math.PI / 2; + this.maxAltitude = 0.45 * Math.PI; this.minDistance = 2; this.maxDistance = Infinity; @@ -231,13 +231,18 @@ export class GlobeControls { } else if ( e.buttons & 1 ) { - this.state = DRAG; - this.dragPoint.copy( hit.point ); - this.startDragPoint.copy( hit.point ); - this.dragPointSet = true; + // if the clicked point is coming from below the plane then don't perform the drag + if ( raycaster.ray.direction.dot( _up ) < 0 ) { - this.pivotMesh.position.copy( hit.point ); - this.scene.add( this.pivotMesh ); + this.state = DRAG; + this.dragPoint.copy( hit.point ); + this.startDragPoint.copy( hit.point ); + this.dragPointSet = true; + + this.pivotMesh.position.copy( hit.point ); + this.scene.add( this.pivotMesh ); + + } } From 3348bb8ce33cab5fc768f27633690b6e7f31d0ed Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sat, 30 Dec 2023 13:49:48 +0900 Subject: [PATCH 27/64] Update globe controls --- example/src/GlobeControls.js | 141 +++++++++++++++++++---------------- 1 file changed, 78 insertions(+), 63 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index d4b9ea5ca..7bd3df6ac 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -20,7 +20,6 @@ const _delta = new Vector3(); const _vec = new Vector3(); const _rotationAxis = new Vector3(); const _quaternion = new Quaternion(); -const _up = new Vector3( 0, 1, 0 ); const _plane = new Plane(); const _deltaPointer = new Vector2(); @@ -99,6 +98,8 @@ export class GlobeControls { this.raycaster = new Raycaster(); this.raycaster.firstHitOnly = true; + this.up = new Vector3( 0, 1, 0 ); + this._detachCallback = null; // init @@ -169,7 +170,13 @@ export class GlobeControls { const pointerdownCallback = e => { - const { camera, raycaster, domElement, scene } = this; + const { + camera, + raycaster, + domElement, + scene, + up, + } = this; // get the screen coordinates mouseToCoords( e.clientX, e.clientY, domElement, _pointer ); @@ -232,7 +239,7 @@ export class GlobeControls { } else if ( e.buttons & 1 ) { // if the clicked point is coming from below the plane then don't perform the drag - if ( raycaster.ray.direction.dot( _up ) < 0 ) { + if ( raycaster.ray.direction.dot( up ) < 0 ) { this.state = DRAG; this.dragPoint.copy( hit.point ); @@ -326,7 +333,7 @@ export class GlobeControls { mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _newPointer ); _deltaPointer.subVectors( _newPointer, _pointer ); _pointer.copy( _newPointer ); - this.updateRotation( - _deltaPointer.x * rotationSpeed, - _deltaPointer.y * rotationSpeed ); + this._updateRotation( - _deltaPointer.x * rotationSpeed, - _deltaPointer.y * rotationSpeed ); this.pivotMesh.visible = true; } else { @@ -356,7 +363,7 @@ export class GlobeControls { } else if ( this.state === ROTATE ) { const { rotationSpeed } = this; - this.updateRotation( - _deltaPointer.x * rotationSpeed, - _deltaPointer.y * rotationSpeed ); + this._updateRotation( - _deltaPointer.x * rotationSpeed, - _deltaPointer.y * rotationSpeed ); } @@ -432,20 +439,20 @@ export class GlobeControls { } - this.updateZoom( delta ); + this._updateZoom( delta ); }; const performDrag = () => { - const { raycaster, camera, dragPoint } = this; - _plane.setFromNormalAndCoplanarPoint( _up, dragPoint ); + const { raycaster, camera, dragPoint, up } = this; + _plane.setFromNormalAndCoplanarPoint( up, dragPoint ); raycaster.setFromCamera( _pointer, camera ); if ( raycaster.ray.intersectPlane( _plane, _vec ) ) { _delta.subVectors( dragPoint, _vec ); - this.updatePosition( _delta ); + this._updatePosition( _delta ); } @@ -486,7 +493,57 @@ export class GlobeControls { } - updateZoom( scale ) { + update() { + + const { + raycaster, + camera, + scene, + cameraRadius, + dragPoint, + startDragPoint, + up, + } = this; + + // when dragging the camera and drag point may be moved + // to accommodate terrain so we try to move it back down + // to the original point. + if ( this.state === DRAG ) { + + _delta.subVectors( startDragPoint, dragPoint ); + camera.position.add( _delta ); + dragPoint.copy( startDragPoint ); + + } + + // cast down from the camera + raycaster.ray.direction.copy( up ).multiplyScalar( - 1 ); + raycaster.ray.origin.copy( camera.position ).addScaledVector( raycaster.ray.direction, - 100 ); + + const hit = raycaster.intersectObject( scene )[ 0 ]; + if ( hit ) { + + const dist = hit.distance - 100; + if ( dist < cameraRadius ) { + + const delta = cameraRadius - dist; + camera.position.copy( hit.point ).addScaledVector( raycaster.ray.direction, - cameraRadius ); + dragPoint.addScaledVector( up, delta ); + + } + + } + + } + + dispose() { + + this.detach(); + + } + + // private + _updateZoom( scale ) { const { zoomPointSet, @@ -539,7 +596,7 @@ export class GlobeControls { } - updatePosition( delta ) { + _updatePosition( delta ) { // TODO: when adjusting the frame we have to reproject the grab point // so as the use drags it winds up in the same spot. @@ -548,15 +605,21 @@ export class GlobeControls { } - updateRotation( azimuth, altitude ) { + _updateRotation( azimuth, altitude ) { - const { camera, rotationPoint, minAltitude, maxAltitude } = this; + const { + camera, + rotationPoint, + minAltitude, + maxAltitude, + up, + } = this; // currently uses the camera forward for this work but it may be best to use a combination of camera // forward and direction to pivot? Or just dir to pivot? _delta.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ).multiplyScalar( - 1 ); - const angle = _up.angleTo( _delta ); + const angle = up.angleTo( _delta ); if ( altitude > 0 ) { altitude = Math.min( angle - minAltitude - 1e-2, altitude ); @@ -568,7 +631,7 @@ export class GlobeControls { } // zoom in frame around pivot point - _quaternion.setFromAxisAngle( _up, azimuth ); + _quaternion.setFromAxisAngle( up, azimuth ); makeRotateAroundPoint( rotationPoint, _quaternion, _rotMatrix ); camera.matrixWorld.premultiply( _rotMatrix ); @@ -583,53 +646,5 @@ export class GlobeControls { } - update() { - - const { - raycaster, - camera, - scene, - cameraRadius, - dragPoint, - startDragPoint, - } = this; - - // when dragging the camera and drag point may be moved - // to accommodate terrain so we try to move it back down - // to the original point. - if ( this.state === DRAG ) { - - _delta.subVectors( startDragPoint, dragPoint ); - camera.position.add( _delta ); - dragPoint.copy( startDragPoint ); - - } - - // cast down from the camera - raycaster.ray.direction.copy( _up ).multiplyScalar( - 1 ); - raycaster.ray.origin.copy( camera.position ).addScaledVector( raycaster.ray.direction, - 100 ); - - const hit = raycaster.intersectObject( scene )[ 0 ]; - if ( hit ) { - - const dist = hit.distance - 100; - if ( dist < cameraRadius ) { - - const delta = cameraRadius - dist; - camera.position.copy( hit.point ).addScaledVector( raycaster.ray.direction, - cameraRadius ); - dragPoint.addScaledVector( _up, delta ); - - } - - } - - } - - dispose() { - - this.detach(); - - } - } From 610f90b3d03a12c9d5c632e61abe0bd1c42079e0 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sat, 30 Dec 2023 15:49:15 +0900 Subject: [PATCH 28/64] Add set frame function --- example/src/GlobeControls.js | 68 ++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 7bd3df6ac..9b4163c0d 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -646,5 +646,73 @@ export class GlobeControls { } + setFrame( newUp ) { + + const right = new Vector3(); + const cross = new Vector3(); + const pivot = new Vector3(); + let dist = 0; + + // cast down from the camera to get the pivot to rotate around + const { up, raycaster, camera, scene, state } = this; + raycaster.ray.direction.copy( up ).multiplyScalar( - 1 ); + raycaster.ray.origin.copy( camera.position ).addScaledVector( raycaster.ray.direction, - 100 ); + + const hit = raycaster.intersectObject( scene )[ 0 ]; + if ( hit ) { + + _vec.setFromMatrixPosition( camera.matrixWorld ); + + pivot.copy( hit.point ); + dist = pivot.distanceTo( _vec ); + + } else { + + return; + + } + + // get the necessary rotation + right.set( 1, 0, 0 ).transformDirection( camera.matrixWorld ); + cross.crossVectors( up, newUp ); + + const angle = newUp.angleTo( up ) * Math.sign( cross.dot( right ) ); + + if ( + state === DRAG && this.dragPointSet || + this.zoomPointSet || + state === ROTATE && this.rotationPointSet + ) { + + // if we're performing an action currently then pivot around the current focus point + _quaternion.setFromAxisAngle( right, angle ); + + if ( state === DRAG ) { + + makeRotateAroundPoint( this.dragPoint, _quaternion, _rotMatrix ); + + } else if ( state === ROTATE ) { + + makeRotateAroundPoint( this.rotationPoint, _quaternion, _rotMatrix ); + + } else if ( this.zoomPointSet ) { + + makeRotateAroundPoint( this.zoomPoint, _quaternion, _rotMatrix ); + + } + + camera.matrixWorld.premultiply( _rotMatrix ); + camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); + + } else { + + camera.position.copy( pivot ).addScaledVector( newUp, dist ); + camera.rotateX( angle ); + camera.updateMatrixWorld(); + + } + + } + } From ab8a031f7b594fbde240153b10fc498ff44b33cb Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sat, 30 Dec 2023 19:31:01 +0900 Subject: [PATCH 29/64] Updates --- example/src/GlobeControls.js | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 9b4163c0d..aa4e8cddb 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -423,17 +423,8 @@ export class GlobeControls { if ( ! this.zoomDirectionSet ) { - const { raycaster, scene } = this; + const { raycaster } = this; raycaster.setFromCamera( _pointer, this.camera ); - - const hit = raycaster.intersectObject( scene )[ 0 ] || null; - if ( hit ) { - - this.zoomPoint.copy( hit.point ); - this.zoomPointSet = true; - - } - this.zoomDirection.copy( this.raycaster.ray.direction ).normalize(); this.zoomDirectionSet = true; @@ -546,7 +537,6 @@ export class GlobeControls { _updateZoom( scale ) { const { - zoomPointSet, zoomPoint, zoomDirection, camera, @@ -558,21 +548,14 @@ export class GlobeControls { const fallback = scale < 0 ? - 1 : 1; let dist = Infinity; - if ( zoomPointSet ) { - - dist = zoomPoint.distanceTo( camera.position ); - - } else { - - raycaster.ray.origin.copy( camera.position ); - raycaster.ray.direction.copy( zoomDirection ); + raycaster.ray.origin.copy( camera.position ); + raycaster.ray.direction.copy( zoomDirection ); - const hit = raycaster.intersectObject( scene )[ 0 ] || null; - if ( hit ) { - - dist = hit.distance; + const hit = raycaster.intersectObject( scene )[ 0 ] || null; + if ( hit ) { - } + dist = hit.distance; + zoomPoint.copy( hit.point ); } @@ -602,6 +585,7 @@ export class GlobeControls { // so as the use drags it winds up in the same spot. // Will this work? Or be good enough? this.camera.position.add( delta ); + this.camera.updateMatrixWorld(); } @@ -655,6 +639,8 @@ export class GlobeControls { // cast down from the camera to get the pivot to rotate around const { up, raycaster, camera, scene, state } = this; + camera.updateMatrixWorld(); + raycaster.ray.direction.copy( up ).multiplyScalar( - 1 ); raycaster.ray.origin.copy( camera.position ).addScaledVector( raycaster.ray.direction, - 100 ); @@ -712,6 +698,8 @@ export class GlobeControls { } + this.up.copy( newUp ); + } } From b0ff8ed9f741ded90eb50188d8fa3e091626817b Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sat, 30 Dec 2023 20:11:38 +0900 Subject: [PATCH 30/64] Fix rotation --- example/src/GlobeControls.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index aa4e8cddb..fe212a137 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -556,6 +556,7 @@ export class GlobeControls { dist = hit.distance; zoomPoint.copy( hit.point ); + this.zoomPointSet = true; } @@ -632,8 +633,6 @@ export class GlobeControls { setFrame( newUp ) { - const right = new Vector3(); - const cross = new Vector3(); const pivot = new Vector3(); let dist = 0; @@ -658,11 +657,7 @@ export class GlobeControls { } - // get the necessary rotation - right.set( 1, 0, 0 ).transformDirection( camera.matrixWorld ); - cross.crossVectors( up, newUp ); - - const angle = newUp.angleTo( up ) * Math.sign( cross.dot( right ) ); + _quaternion.setFromUnitVectors( up, newUp ); if ( state === DRAG && this.dragPointSet || @@ -671,8 +666,6 @@ export class GlobeControls { ) { // if we're performing an action currently then pivot around the current focus point - _quaternion.setFromAxisAngle( right, angle ); - if ( state === DRAG ) { makeRotateAroundPoint( this.dragPoint, _quaternion, _rotMatrix ); @@ -693,12 +686,12 @@ export class GlobeControls { } else { camera.position.copy( pivot ).addScaledVector( newUp, dist ); - camera.rotateX( angle ); + camera.quaternion.premultiply( _quaternion ); camera.updateMatrixWorld(); } - this.up.copy( newUp ); + up.copy( newUp ); } From dd00ca462e47447b9e1cf564a396858fe65a40bd Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sat, 30 Dec 2023 21:31:40 +0900 Subject: [PATCH 31/64] Reuse function --- example/src/GlobeControls.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index fe212a137..4d92dd384 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -234,6 +234,7 @@ export class GlobeControls { this.rotationPointSet = true; this.pivotMesh.position.copy( hit.point ); + this.pivotMesh.updateMatrixWorld(); this.scene.add( this.pivotMesh ); } else if ( e.buttons & 1 ) { @@ -247,6 +248,7 @@ export class GlobeControls { this.dragPointSet = true; this.pivotMesh.position.copy( hit.point ); + this.pivotMesh.updateMatrixWorld(); this.scene.add( this.pivotMesh ); } @@ -489,7 +491,6 @@ export class GlobeControls { const { raycaster, camera, - scene, cameraRadius, dragPoint, startDragPoint, @@ -508,10 +509,7 @@ export class GlobeControls { } // cast down from the camera - raycaster.ray.direction.copy( up ).multiplyScalar( - 1 ); - raycaster.ray.origin.copy( camera.position ).addScaledVector( raycaster.ray.direction, - 100 ); - - const hit = raycaster.intersectObject( scene )[ 0 ]; + const hit = this._getPointBelowCamera(); if ( hit ) { const dist = hit.distance - 100; @@ -580,6 +578,16 @@ export class GlobeControls { } + _getPointBelowCamera() { + + const { camera, raycaster, scene, up } = this; + raycaster.ray.direction.copy( up ).multiplyScalar( - 1 ); + raycaster.ray.origin.copy( camera.position ).addScaledVector( raycaster.ray.direction, - 100 ); + + return raycaster.intersectObject( scene )[ 0 ] || null; + + } + _updatePosition( delta ) { // TODO: when adjusting the frame we have to reproject the grab point @@ -637,13 +645,10 @@ export class GlobeControls { let dist = 0; // cast down from the camera to get the pivot to rotate around - const { up, raycaster, camera, scene, state } = this; + const { up, camera, state } = this; camera.updateMatrixWorld(); - raycaster.ray.direction.copy( up ).multiplyScalar( - 1 ); - raycaster.ray.origin.copy( camera.position ).addScaledVector( raycaster.ray.direction, - 100 ); - - const hit = raycaster.intersectObject( scene )[ 0 ]; + const hit = this._getPointBelowCamera(); if ( hit ) { _vec.setFromMatrixPosition( camera.matrixWorld ); @@ -691,6 +696,7 @@ export class GlobeControls { } + // TODO: do we need to potentially fix the camera twist here? up.copy( newUp ); } From ef832cf3983f1081a65f1044cae2eb6c798b5615 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sat, 30 Dec 2023 21:44:57 +0900 Subject: [PATCH 32/64] Fix drag issue --- example/src/GlobeControls.js | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 4d92dd384..0d8167c73 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -664,31 +664,13 @@ export class GlobeControls { _quaternion.setFromUnitVectors( up, newUp ); - if ( - state === DRAG && this.dragPointSet || - this.zoomPointSet || - state === ROTATE && this.rotationPointSet - ) { - - // if we're performing an action currently then pivot around the current focus point - if ( state === DRAG ) { - - makeRotateAroundPoint( this.dragPoint, _quaternion, _rotMatrix ); - - } else if ( state === ROTATE ) { - - makeRotateAroundPoint( this.rotationPoint, _quaternion, _rotMatrix ); - - } else if ( this.zoomPointSet ) { - - makeRotateAroundPoint( this.zoomPoint, _quaternion, _rotMatrix ); - - } + if ( this.zoomPointSet ) { + makeRotateAroundPoint( this.zoomPoint, _quaternion, _rotMatrix ); camera.matrixWorld.premultiply( _rotMatrix ); camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); - } else { + } else if ( state !== ROTATE ) { camera.position.copy( pivot ).addScaledVector( newUp, dist ); camera.quaternion.premultiply( _quaternion ); From 23914bc28f7c95931a71266d357f52397c239f4c Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sat, 30 Dec 2023 22:08:53 +0900 Subject: [PATCH 33/64] Add shared direction update function --- example/src/GlobeControls.js | 62 ++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 0d8167c73..5502a2686 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -538,23 +538,15 @@ export class GlobeControls { zoomPoint, zoomDirection, camera, - raycaster, - scene, minDistance, maxDistance, } = this; const fallback = scale < 0 ? - 1 : 1; let dist = Infinity; - raycaster.ray.origin.copy( camera.position ); - raycaster.ray.direction.copy( zoomDirection ); - - const hit = raycaster.intersectObject( scene )[ 0 ] || null; - if ( hit ) { + if ( this.zoomPointSet || this._updateZoomPoint() ) { - dist = hit.distance; - zoomPoint.copy( hit.point ); - this.zoomPointSet = true; + dist = zoomPoint.distanceTo( camera.position ); } @@ -578,6 +570,40 @@ export class GlobeControls { } + _updateZoomPoint() { + + const { + camera, + zoomDirectionSet, + zoomDirection, + raycaster, + scene, + zoomPoint, + } = this; + + if ( ! zoomDirectionSet ) { + + return false; + + } + + raycaster.ray.origin.copy( camera.position ); + raycaster.ray.direction.copy( zoomDirection ); + + console.log('UPDATING ZOOM POINT', performance.now(), new Error().stack) + const hit = raycaster.intersectObject( scene )[ 0 ] || null; + if ( hit ) { + + zoomPoint.copy( hit.point ); + this.zoomPointSet = true; + return true; + + } + + return false; + + } + _getPointBelowCamera() { const { camera, raycaster, scene, up } = this; @@ -664,11 +690,19 @@ export class GlobeControls { _quaternion.setFromUnitVectors( up, newUp ); - if ( this.zoomPointSet ) { + if ( this.zoomDirectionSet ) { + + if ( this.zoomPointSet || this._updateZoomPoint() ) { + + camera.updateMatrixWorld(); - makeRotateAroundPoint( this.zoomPoint, _quaternion, _rotMatrix ); - camera.matrixWorld.premultiply( _rotMatrix ); - camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); + // TODO: removing this fixes the zoom point but the orientation can be put in a position that's + // potentially below the valid altitude. Why does rotating around the zoom point not work? + makeRotateAroundPoint( this.zoomPoint, _quaternion, _rotMatrix ); + camera.matrixWorld.premultiply( _rotMatrix ); + camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); + + } } else if ( state !== ROTATE ) { From 4e9376a4ea8563f5088564765fd2a60ac9794205 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sat, 30 Dec 2023 22:23:25 +0900 Subject: [PATCH 34/64] fixes to zoom logic --- example/src/GlobeControls.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 5502a2686..1994450a6 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -590,7 +590,6 @@ export class GlobeControls { raycaster.ray.origin.copy( camera.position ); raycaster.ray.direction.copy( zoomDirection ); - console.log('UPDATING ZOOM POINT', performance.now(), new Error().stack) const hit = raycaster.intersectObject( scene )[ 0 ] || null; if ( hit ) { @@ -671,7 +670,7 @@ export class GlobeControls { let dist = 0; // cast down from the camera to get the pivot to rotate around - const { up, camera, state } = this; + const { up, camera, state, zoomPoint, zoomDirection } = this; camera.updateMatrixWorld(); const hit = this._getPointBelowCamera(); @@ -696,12 +695,12 @@ export class GlobeControls { camera.updateMatrixWorld(); - // TODO: removing this fixes the zoom point but the orientation can be put in a position that's - // potentially below the valid altitude. Why does rotating around the zoom point not work? - makeRotateAroundPoint( this.zoomPoint, _quaternion, _rotMatrix ); + makeRotateAroundPoint( zoomPoint, _quaternion, _rotMatrix ); camera.matrixWorld.premultiply( _rotMatrix ); camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); + zoomDirection.subVectors( zoomPoint, camera.position ).normalize(); + } } else if ( state !== ROTATE ) { From fbccc0f91c793149fa08cf873eb84ce45ec2cb87 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sun, 31 Dec 2023 17:22:30 +0900 Subject: [PATCH 35/64] Cleanup --- example/src/GlobeControls.js | 148 +++++++++++++++++++--------------- example/src/PointerTracker.js | 62 +++++++++++++- 2 files changed, 143 insertions(+), 67 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 1994450a6..df7353d8e 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -18,6 +18,9 @@ const _matrix = new Matrix4(); const _rotMatrix = new Matrix4(); const _delta = new Vector3(); const _vec = new Vector3(); +const _forward = new Vector3(); +const __pointer = new Vector2(); +const __prevPointer = new Vector2(); const _rotationAxis = new Vector3(); const _quaternion = new Quaternion(); const _plane = new Plane(); @@ -82,13 +85,14 @@ export class GlobeControls { this.pivotMesh.scale.setScalar( 0.25 ); // internal state + this.pointerTracker = new PointerTracker(); + this.dragPointSet = false; this.dragPoint = new Vector3(); this.startDragPoint = new Vector3(); this.rotationPointSet = false; this.rotationPoint = new Vector3(); - this.rotationClickDirection = new Vector3(); this.zoomDirectionSet = false; this.zoomPointSet = false; @@ -137,7 +141,7 @@ export class GlobeControls { let _pinchAction = NONE; let _pointerMoveQueued = false; - const _pointerTracker = new PointerTracker(); + const _pointerTracker = this.pointerTracker; let _pointerDist = 0; let _originalPointerDist = 0; let shiftClicked = false; @@ -176,37 +180,32 @@ export class GlobeControls { domElement, scene, up, + pivotMesh, } = this; - // get the screen coordinates - mouseToCoords( e.clientX, e.clientY, domElement, _pointer ); + + // init fields + _pointerTracker.addPointer( e ); + _pointerDist = 0; + _originalPointerDist = 0; if ( e.pointerType === 'touch' ) { - this.pivotMesh.visible = false; + pivotMesh.visible = false; if ( _pointerTracker.getPointerCount() === 0 ) { domElement.setPointerCapture( e.pointerId ); - } - - // init fields - _pointerTracker.addPointer( e ); - _pointerDist = 0; - _originalPointerDist = 0; - - // if we find a second pointer init other values - if ( _pointerTracker.getPointerCount() === 2 ) { + } else if ( _pointerTracker.getPointerCount() === 2 ) { + // if we find a second pointer init other values _pointerTracker.getCenterPoint( _originalCenterPoint ); _centerPoint.copy( _originalCenterPoint ); _pointerDist = _pointerTracker.getPointerDistance(); _originalPointerDist = _pointerTracker.getPointerDistance(); - // the "pointer" for zooming and rotating should be based on the center point - mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _pointer ); } else if ( _pointerTracker.getPointerCount() > 2 ) { @@ -217,6 +216,10 @@ export class GlobeControls { } + // the "pointer" for zooming and rotating should be based on the center point + _pointerTracker.getCenterPoint( __pointer ); + mouseToCoords( __pointer.x, __pointer.y, domElement, _pointer ); + // find the hit point raycaster.setFromCamera( _pointer, camera ); const hit = raycaster.intersectObject( scene )[ 0 ] || null; @@ -224,20 +227,23 @@ export class GlobeControls { // if two fingers, right click, or shift click are being used then we trigger // a rotation action to begin - if ( _pointerTracker.getPointerCount() === 2 || e.buttons & 2 || e.buttons & 1 && shiftClicked ) { + if ( + _pointerTracker.getPointerCount() === 2 || + _pointerTracker.isRightClicked() || + _pointerTracker.isLeftClicked() && shiftClicked + ) { _matrix.copy( camera.matrixWorld ).invert(); this.state = ROTATE; this.rotationPoint.copy( hit.point ); - this.rotationClickDirection.copy( raycaster.ray.direction ).transformDirection( _matrix ); this.rotationPointSet = true; this.pivotMesh.position.copy( hit.point ); this.pivotMesh.updateMatrixWorld(); this.scene.add( this.pivotMesh ); - } else if ( e.buttons & 1 ) { + } else if ( _pointerTracker.isLeftClicked() ) { // if the clicked point is coming from below the plane then don't perform the drag if ( raycaster.ray.direction.dot( up ) < 0 ) { @@ -264,22 +270,22 @@ export class GlobeControls { this.zoomDirectionSet = false; this.zoomPointSet = false; - if ( e.pointerType === 'touch' ) { + if ( ! _pointerTracker.updatePointer( e ) ) { - if ( ! _pointerTracker.updatePointer( e ) ) { + return; - return; + } - } + if ( _pointerTracker.getPointerType() === 'touch' ) { if ( _pointerTracker.getPointerCount() === 1 ) { - // if there's only one pointer active then handle the drag event + // TODO: remove mouseToCoords( e.clientX, e.clientY, domElement, _pointer ); if ( this.state === DRAG ) { - performDrag(); + this._updatePosition(); } @@ -330,12 +336,10 @@ export class GlobeControls { } else if ( _pinchAction === ROTATE ) { - // perform rotation - const { rotationSpeed } = this; - mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _newPointer ); - _deltaPointer.subVectors( _newPointer, _pointer ); - _pointer.copy( _newPointer ); - this._updateRotation( - _deltaPointer.x * rotationSpeed, - _deltaPointer.y * rotationSpeed ); + // TODO: remove + mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _pointer ); + + this._updateRotation(); this.pivotMesh.visible = true; } else { @@ -352,15 +356,14 @@ export class GlobeControls { } - } else if ( e.pointerType === 'mouse' ) { + } else if ( _pointerTracker.getPointerType() === 'mouse' ) { - mouseToCoords( e.clientX, e.clientY, domElement, _newPointer ); - _deltaPointer.subVectors( _newPointer, _pointer ); - _pointer.copy( _newPointer ); + // TODO: remove + mouseToCoords( e.clientX, e.clientY, domElement, _pointer ); if ( this.state === DRAG ) { - performDrag(); + this._updatePosition(); } else if ( this.state === ROTATE ) { @@ -377,9 +380,10 @@ export class GlobeControls { resetState(); + _pointerTracker.deletePointer( e ); + if ( e.pointerType === 'touch' ) { - _pointerTracker.deletePointer( e ); _pinchAction = NONE; if ( _pointerTracker.getPointerCount() === 0 ) { @@ -436,21 +440,6 @@ export class GlobeControls { }; - const performDrag = () => { - - const { raycaster, camera, dragPoint, up } = this; - _plane.setFromNormalAndCoplanarPoint( up, dragPoint ); - raycaster.setFromCamera( _pointer, camera ); - - if ( raycaster.ray.intersectPlane( _plane, _vec ) ) { - - _delta.subVectors( dragPoint, _vec ); - this._updatePosition( _delta ); - - } - - }; - domElement.addEventListener( 'contextmenu', contextMenuCallback ); domElement.addEventListener( 'keydown', keydownCallback ); domElement.addEventListener( 'keyup', keyupCallback ); @@ -481,6 +470,7 @@ export class GlobeControls { this._detachCallback(); this._detachCallback = null; + this.pointerTracker = new PointerTracker(); } @@ -512,7 +502,7 @@ export class GlobeControls { const hit = this._getPointBelowCamera(); if ( hit ) { - const dist = hit.distance - 100; + const dist = hit.distance - 1e5; if ( dist < cameraRadius ) { const delta = cameraRadius - dist; @@ -607,23 +597,40 @@ export class GlobeControls { const { camera, raycaster, scene, up } = this; raycaster.ray.direction.copy( up ).multiplyScalar( - 1 ); - raycaster.ray.origin.copy( camera.position ).addScaledVector( raycaster.ray.direction, - 100 ); + raycaster.ray.origin.copy( camera.position ).addScaledVector( raycaster.ray.direction, - 1e5 ); return raycaster.intersectObject( scene )[ 0 ] || null; } - _updatePosition( delta ) { + _updatePosition() { - // TODO: when adjusting the frame we have to reproject the grab point - // so as the use drags it winds up in the same spot. - // Will this work? Or be good enough? - this.camera.position.add( delta ); - this.camera.updateMatrixWorld(); + const { + raycaster, + camera, + dragPoint, + up, + pointerTracker, + domElement, + } = this; + + pointerTracker.getCenterPoint( __pointer ); + mouseToCoords( __pointer.x, __pointer.y, domElement, __pointer ); + + _plane.setFromNormalAndCoplanarPoint( up, dragPoint ); + raycaster.setFromCamera( __pointer, camera ); + + if ( raycaster.ray.intersectPlane( _plane, _vec ) ) { + + _delta.subVectors( dragPoint, _vec ); + this.camera.position.add( _delta ); + this.camera.updateMatrixWorld(); + + } } - _updateRotation( azimuth, altitude ) { + _updateRotation() { const { camera, @@ -631,13 +638,28 @@ export class GlobeControls { minAltitude, maxAltitude, up, + domElement, + pointerTracker, + rotationSpeed, } = this; + // get the rotation motion + pointerTracker.getCenterPoint( __pointer ); + mouseToCoords( __pointer.x, __pointer.y, domElement, __pointer ); + + pointerTracker.getPreviousCenterPoint( __prevPointer ); + mouseToCoords( __prevPointer.x, __prevPointer.y, domElement, __prevPointer ); + + _deltaPointer.subVectors( __pointer, __prevPointer ); + + const azimuth = - _deltaPointer.x * rotationSpeed; + let altitude = - _deltaPointer.y * rotationSpeed; + // currently uses the camera forward for this work but it may be best to use a combination of camera // forward and direction to pivot? Or just dir to pivot? - _delta.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ).multiplyScalar( - 1 ); + _forward.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ).multiplyScalar( - 1 ); - const angle = up.angleTo( _delta ); + const angle = up.angleTo( _forward ); if ( altitude > 0 ) { altitude = Math.min( angle - minAltitude - 1e-2, altitude ); diff --git a/example/src/PointerTracker.js b/example/src/PointerTracker.js index 3ccdb4ecc..ca42914db 100644 --- a/example/src/PointerTracker.js +++ b/example/src/PointerTracker.js @@ -1,11 +1,16 @@ import { Vector2 } from 'three'; +const _vec = new Vector2(); export class PointerTracker { constructor() { + this.buttons = 0; + this.pointerType = null; this.pointerOrder = []; + this.previousPositions = {}; this.pointerPositions = {}; + this.startPositions = {}; } @@ -15,6 +20,15 @@ export class PointerTracker { const position = new Vector2( e.clientX, e.clientY ); this.pointerOrder.push( id ); this.pointerPositions[ id ] = position; + this.previousPositions[ id ] = position.clone(); + this.startPositions[ id ] = position.clone(); + + if ( this.pointerType === null ) { + + this.pointerType = e.pointerType; + this.buttons = e.buttons; + + } } @@ -27,6 +41,8 @@ export class PointerTracker { } + const position = this.pointerPositions[ id ]; + this.previousPositions[ id ].copy( position ); this.pointerPositions[ id ].set( e.clientX, e.clientY ); return true; @@ -38,6 +54,15 @@ export class PointerTracker { const pointerOrder = this.pointerOrder; pointerOrder.splice( pointerOrder.indexOf( id ), 1 ); delete this.pointerPositions[ id ]; + delete this.previousPositions[ id ]; + delete this.startPositions[ id ]; + + if ( this.getPointerCount.length === 0 ) { + + this.buttons = 0; + this.pointerType = null; + + } } @@ -47,11 +72,10 @@ export class PointerTracker { } - getCenterPoint( target ) { + getCenterPoint( target, pointerPositions = this.pointerPositions ) { const pointerOrder = this.pointerOrder; - const pointerPositions = this.pointerPositions; - if ( this.getPointerCount() === 1 ) { + if ( this.getPointerCount() === 1 || this.getPointerType() === 'mouse' ) { const id = pointerOrder[ 0 ]; target.copy( pointerPositions[ id ] ); @@ -74,9 +98,15 @@ export class PointerTracker { } + getPreviousCenterPoint( target ) { + + return this.getCenterPoint( target, this.previousPositions ); + + } + getPointerDistance() { - if ( this.getPointerCount() <= 1 ) { + if ( this.getPointerCount() <= 1 || this.getPointerType() === 'mouse' ) { return 0; @@ -92,4 +122,28 @@ export class PointerTracker { } + getPointerType() { + + return this.pointerType; + + } + + getPointerButtons() { + + return this.buttons; + + } + + isLeftClicked() { + + return Boolean( this.buttons & 1 ); + + } + + isRightClicked() { + + return Boolean( this.buttons & 2 ); + + } + } From 52effdfcceb444a0bc291a27d38860c42b473f41 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sun, 31 Dec 2023 17:54:36 +0900 Subject: [PATCH 36/64] More cleanup --- example/src/GlobeControls.js | 131 +++++++++++++--------------------- example/src/PointerTracker.js | 19 +++-- 2 files changed, 64 insertions(+), 86 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index df7353d8e..c38f8d0b5 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -19,15 +19,15 @@ const _rotMatrix = new Matrix4(); const _delta = new Vector3(); const _vec = new Vector3(); const _forward = new Vector3(); -const __pointer = new Vector2(); -const __prevPointer = new Vector2(); const _rotationAxis = new Vector3(); const _quaternion = new Quaternion(); const _plane = new Plane(); +const _pointer = new Vector2(); +const _prevPointer = new Vector2(); const _deltaPointer = new Vector2(); const _centerPoint = new Vector2(); -const _newPointer = new Vector2(); +const _originalCenterPoint = new Vector2(); // TODO // - Add support for angled rotation plane (based on where the pivot point is) @@ -136,14 +136,10 @@ export class GlobeControls { this.domElement = domElement; domElement.style.touchAction = 'none'; - const _pointer = new Vector2(); - const _originalCenterPoint = new Vector2(); let _pinchAction = NONE; let _pointerMoveQueued = false; - const _pointerTracker = this.pointerTracker; - let _pointerDist = 0; - let _originalPointerDist = 0; + const pointerTracker = this.pointerTracker; let shiftClicked = false; const contextMenuCallback = e => { @@ -181,33 +177,21 @@ export class GlobeControls { scene, up, pivotMesh, + pointerTracker, } = this; - // init fields - _pointerTracker.addPointer( e ); - _pointerDist = 0; - _originalPointerDist = 0; + pointerTracker.addPointer( e ); if ( e.pointerType === 'touch' ) { pivotMesh.visible = false; - if ( _pointerTracker.getPointerCount() === 0 ) { + if ( pointerTracker.getPointerCount() === 0 ) { domElement.setPointerCapture( e.pointerId ); - } else if ( _pointerTracker.getPointerCount() === 2 ) { - - // if we find a second pointer init other values - _pointerTracker.getCenterPoint( _originalCenterPoint ); - _centerPoint.copy( _originalCenterPoint ); - - _pointerDist = _pointerTracker.getPointerDistance(); - _originalPointerDist = _pointerTracker.getPointerDistance(); - - - } else if ( _pointerTracker.getPointerCount() > 2 ) { + } else if ( pointerTracker.getPointerCount() > 2 ) { resetState(); return; @@ -217,8 +201,8 @@ export class GlobeControls { } // the "pointer" for zooming and rotating should be based on the center point - _pointerTracker.getCenterPoint( __pointer ); - mouseToCoords( __pointer.x, __pointer.y, domElement, _pointer ); + pointerTracker.getCenterPoint( _pointer ); + mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); // find the hit point raycaster.setFromCamera( _pointer, camera ); @@ -228,9 +212,9 @@ export class GlobeControls { // if two fingers, right click, or shift click are being used then we trigger // a rotation action to begin if ( - _pointerTracker.getPointerCount() === 2 || - _pointerTracker.isRightClicked() || - _pointerTracker.isLeftClicked() && shiftClicked + pointerTracker.getPointerCount() === 2 || + pointerTracker.isRightClicked() || + pointerTracker.isLeftClicked() && shiftClicked ) { _matrix.copy( camera.matrixWorld ).invert(); @@ -243,7 +227,7 @@ export class GlobeControls { this.pivotMesh.updateMatrixWorld(); this.scene.add( this.pivotMesh ); - } else if ( _pointerTracker.isLeftClicked() ) { + } else if ( pointerTracker.isLeftClicked() ) { // if the clicked point is coming from below the plane then don't perform the drag if ( raycaster.ray.direction.dot( up ) < 0 ) { @@ -270,18 +254,17 @@ export class GlobeControls { this.zoomDirectionSet = false; this.zoomPointSet = false; - if ( ! _pointerTracker.updatePointer( e ) ) { + const { pointerTracker } = this; + + if ( ! pointerTracker.updatePointer( e ) ) { return; } - if ( _pointerTracker.getPointerType() === 'touch' ) { - - if ( _pointerTracker.getPointerCount() === 1 ) { + if ( pointerTracker.getPointerType() === 'touch' ) { - // TODO: remove - mouseToCoords( e.clientX, e.clientY, domElement, _pointer ); + if ( pointerTracker.getPointerCount() === 1 ) { if ( this.state === DRAG ) { @@ -289,7 +272,7 @@ export class GlobeControls { } - } else if ( _pointerTracker.getPointerCount() === 2 ) { + } else if ( pointerTracker.getPointerCount() === 2 ) { // We queue this event to ensure that all pointers have been updated if ( ! _pointerMoveQueued ) { @@ -300,19 +283,22 @@ export class GlobeControls { _pointerMoveQueued = false; // adjust the pointer position to be the center point - _pointerTracker.getCenterPoint( _centerPoint ); + pointerTracker.getCenterPoint( _centerPoint ); // detect zoom transition - const previousDist = _pointerDist; - _pointerDist = _pointerTracker.getPointerDistance(); + const previousDist = pointerTracker.getPreviousPointerDistance(); + const pointerDist = pointerTracker.getPointerDistance(); + const separateDelta = pointerDist - previousDist; if ( _pinchAction === NONE ) { // check which direction was moved in first - const separateDistance = _pointerDist - _originalPointerDist; - const rotateDistance = _centerPoint.distanceTo( _originalCenterPoint ); - if ( separateDistance > 0 && rotateDistance > 0 ) { + pointerTracker.getCenterPoint( _centerPoint ); + pointerTracker.getPreviousCenterPoint( _originalCenterPoint ); - if ( separateDistance > rotateDistance ) { + const parallelDelta = _centerPoint.distanceTo( _originalCenterPoint ); + if ( separateDelta > 0 && parallelDelta > 0 ) { + + if ( separateDelta > parallelDelta ) { resetState(); _pinchAction = ZOOM; @@ -332,22 +318,13 @@ export class GlobeControls { if ( _pinchAction === ZOOM ) { // perform zoom - performZoom( _pointerDist - previousDist ); + performZoom( separateDelta ); } else if ( _pinchAction === ROTATE ) { - // TODO: remove - mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _pointer ); - this._updateRotation(); this.pivotMesh.visible = true; - } else { - - // no action - mouseToCoords( _centerPoint.x, _centerPoint.y, domElement, _newPointer ); - _pointer.copy( _newPointer ); - } } ); @@ -356,10 +333,7 @@ export class GlobeControls { } - } else if ( _pointerTracker.getPointerType() === 'mouse' ) { - - // TODO: remove - mouseToCoords( e.clientX, e.clientY, domElement, _pointer ); + } else if ( pointerTracker.getPointerType() === 'mouse' ) { if ( this.state === DRAG ) { @@ -380,17 +354,12 @@ export class GlobeControls { resetState(); - _pointerTracker.deletePointer( e ); - - if ( e.pointerType === 'touch' ) { + pointerTracker.deletePointer( e ); + _pinchAction = NONE; - _pinchAction = NONE; + if ( pointerTracker.getPointerType() === 'touch' && pointerTracker.getPointerCount() === 0 ) { - if ( _pointerTracker.getPointerCount() === 0 ) { - - domElement.releasePointerCapture( e.pointerId ); - - } + domElement.releasePointerCapture( e.pointerId ); } @@ -404,11 +373,13 @@ export class GlobeControls { const pointerenterCallback = e => { - mouseToCoords( e.clientX, e.clientY, domElement, _pointer ); + const { pointerTracker } = this; + shiftClicked = false; - if ( e.buttons === 0 ) { + if ( e.buttons !== pointerTracker.getPointerButtons() ) { + pointerTracker.deletePointer( e ); resetState(); } @@ -429,9 +400,9 @@ export class GlobeControls { if ( ! this.zoomDirectionSet ) { - const { raycaster } = this; - raycaster.setFromCamera( _pointer, this.camera ); - this.zoomDirection.copy( this.raycaster.ray.direction ).normalize(); + const { raycaster, camera } = this; + raycaster.setFromCamera( _pointer, camera ); + this.zoomDirection.copy( raycaster.ray.direction ).normalize(); this.zoomDirectionSet = true; } @@ -614,11 +585,11 @@ export class GlobeControls { domElement, } = this; - pointerTracker.getCenterPoint( __pointer ); - mouseToCoords( __pointer.x, __pointer.y, domElement, __pointer ); + pointerTracker.getCenterPoint( _pointer ); + mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); _plane.setFromNormalAndCoplanarPoint( up, dragPoint ); - raycaster.setFromCamera( __pointer, camera ); + raycaster.setFromCamera( _pointer, camera ); if ( raycaster.ray.intersectPlane( _plane, _vec ) ) { @@ -644,13 +615,13 @@ export class GlobeControls { } = this; // get the rotation motion - pointerTracker.getCenterPoint( __pointer ); - mouseToCoords( __pointer.x, __pointer.y, domElement, __pointer ); + pointerTracker.getCenterPoint( _pointer ); + mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); - pointerTracker.getPreviousCenterPoint( __prevPointer ); - mouseToCoords( __prevPointer.x, __prevPointer.y, domElement, __prevPointer ); + pointerTracker.getPreviousCenterPoint( _prevPointer ); + mouseToCoords( _prevPointer.x, _prevPointer.y, domElement, _prevPointer ); - _deltaPointer.subVectors( __pointer, __prevPointer ); + _deltaPointer.subVectors( _pointer, _prevPointer ); const azimuth = - _deltaPointer.x * rotationSpeed; let altitude = - _deltaPointer.y * rotationSpeed; diff --git a/example/src/PointerTracker.js b/example/src/PointerTracker.js index ca42914db..be4a6757b 100644 --- a/example/src/PointerTracker.js +++ b/example/src/PointerTracker.js @@ -23,7 +23,7 @@ export class PointerTracker { this.previousPositions[ id ] = position.clone(); this.startPositions[ id ] = position.clone(); - if ( this.pointerType === null ) { + if ( this.getPointerCount() === 1 ) { this.pointerType = e.pointerType; this.buttons = e.buttons; @@ -104,7 +104,7 @@ export class PointerTracker { } - getPointerDistance() { + getPointerDistance( pointerPositions = this.pointerPositions ) { if ( this.getPointerCount() <= 1 || this.getPointerType() === 'mouse' ) { @@ -112,16 +112,23 @@ export class PointerTracker { } - const id0 = this.pointerOrder[ 0 ]; - const id1 = this.pointerOrder[ 1 ]; + const { pointerOrder } = this; + const id0 = pointerOrder[ 0 ]; + const id1 = pointerOrder[ 1 ]; - const p0 = this.pointerPositions[ id0 ]; - const p1 = this.pointerPositions[ id1 ]; + const p0 = pointerPositions[ id0 ]; + const p1 = pointerPositions[ id1 ]; return p0.distanceTo( p1 ); } + getPreviousPointerDistance() { + + return this.getPointerDistance( this.previousPositions ); + + } + getPointerType() { return this.pointerType; From 9e345df1997145a88f362bc03833b56667455da3 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sun, 31 Dec 2023 18:28:58 +0900 Subject: [PATCH 37/64] Fix zoom --- example/src/GlobeControls.js | 55 +++++++++++++++++------------------ example/src/PointerTracker.js | 34 +++++++++++++++++++++- 2 files changed, 60 insertions(+), 29 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index c38f8d0b5..11588e7cb 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -137,7 +137,6 @@ export class GlobeControls { domElement.style.touchAction = 'none'; let _pinchAction = NONE; - let _pointerMoveQueued = false; const pointerTracker = this.pointerTracker; let shiftClicked = false; @@ -249,6 +248,7 @@ export class GlobeControls { }; + let _pointerMoveQueued = false; const pointermoveCallback = e => { this.zoomDirectionSet = false; @@ -256,6 +256,7 @@ export class GlobeControls { const { pointerTracker } = this; + pointerTracker.setHoverEvent( e ); if ( ! pointerTracker.updatePointer( e ) ) { return; @@ -318,7 +319,7 @@ export class GlobeControls { if ( _pinchAction === ZOOM ) { // perform zoom - performZoom( separateDelta ); + this._updateZoom( separateDelta ); } else if ( _pinchAction === ROTATE ) { @@ -367,7 +368,7 @@ export class GlobeControls { const wheelCallback = e => { - performZoom( - e.deltaY ); + this._updateZoom( - e.deltaY ); }; @@ -396,21 +397,6 @@ export class GlobeControls { }; - const performZoom = delta => { - - if ( ! this.zoomDirectionSet ) { - - const { raycaster, camera } = this; - raycaster.setFromCamera( _pointer, camera ); - this.zoomDirection.copy( raycaster.ray.direction ).normalize(); - this.zoomDirectionSet = true; - - } - - this._updateZoom( delta ); - - }; - domElement.addEventListener( 'contextmenu', contextMenuCallback ); domElement.addEventListener( 'keydown', keydownCallback ); domElement.addEventListener( 'keyup', keyupCallback ); @@ -501,33 +487,46 @@ export class GlobeControls { camera, minDistance, maxDistance, + raycaster, + pointerTracker, + domElement, } = this; - const fallback = scale < 0 ? - 1 : 1; + if ( ! pointerTracker.getLatestPoint( _pointer ) ) { + + return; + + } + + mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); + raycaster.setFromCamera( _pointer, camera ); + zoomDirection.copy( raycaster.ray.direction ).normalize(); + this.zoomDirectionSet = true; + + // get the target zoom point let dist = Infinity; if ( this.zoomPointSet || this._updateZoomPoint() ) { dist = zoomPoint.distanceTo( camera.position ); - } - - zoomDirection.normalize(); - scale = Math.min( scale * ( dist - minDistance ) * 0.01, Math.max( 0, dist - minDistance ) ); - if ( scale === Infinity || scale === - Infinity || Number.isNaN( scale ) ) { + } else { - scale = fallback; + // if we're zooming into nothing then skip zooming + return; } + scale = Math.min( scale * ( dist - minDistance ) * 0.01, Math.max( 0, dist - minDistance ) ); + if ( scale < 0 ) { - const remainingDistance = Math.min( 0, dist - maxDistance ); + const remainingDistance = Math.min( 0, dist - maxDistance ) || 0; scale = Math.max( scale, remainingDistance ); } - this.camera.position.addScaledVector( zoomDirection, scale ); - this.camera.updateMatrixWorld(); + camera.position.addScaledVector( zoomDirection, scale ); + camera.updateMatrixWorld(); } diff --git a/example/src/PointerTracker.js b/example/src/PointerTracker.js index be4a6757b..d0cf52b60 100644 --- a/example/src/PointerTracker.js +++ b/example/src/PointerTracker.js @@ -1,6 +1,5 @@ import { Vector2 } from 'three'; -const _vec = new Vector2(); export class PointerTracker { constructor() { @@ -11,6 +10,39 @@ export class PointerTracker { this.previousPositions = {}; this.pointerPositions = {}; this.startPositions = {}; + this.hoverPosition = new Vector2(); + this.hoverSet = false; + + } + + setHoverEvent( e ) { + + if ( e.pointerType === 'mouse' ) { + + this.hoverPosition.set( e.clientX, e.clientY ); + this.hoverSet = true; + + } + + } + + getLatestPoint( target ) { + + if ( ! this.hoverSet ) { + + return null; + + } else if ( this.pointerType !== null ) { + + this.getCenterPoint( target ); + + } else { + + target.copy( this.hoverPosition ); + + } + + return target; } From c100be5da1b69cbc1dd5dceda3256814de3e621c Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sun, 31 Dec 2023 19:16:40 +0900 Subject: [PATCH 38/64] Fix zoom scaling --- example/src/GlobeControls.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 11588e7cb..3fb556763 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -516,13 +516,24 @@ export class GlobeControls { } - scale = Math.min( scale * ( dist - minDistance ) * 0.01, Math.max( 0, dist - minDistance ) ); - + // scale the distance based on how far there is to move if ( scale < 0 ) { - const remainingDistance = Math.min( 0, dist - maxDistance ) || 0; + const remainingDistance = Math.min( 0, dist - maxDistance ); + scale = scale * ( dist - 0 ) * 0.01; scale = Math.max( scale, remainingDistance ); + } else { + + const remainingDistance = Math.max( 0, dist - minDistance ); + scale = scale * ( dist - minDistance ) * 0.01; + scale = Math.min( scale, remainingDistance ); + + } + + if ( scale < 0 ) { + + } camera.position.addScaledVector( zoomDirection, scale ); From 73123d98cebfb9983f981694aa5226b94d710802 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sun, 31 Dec 2023 19:32:39 +0900 Subject: [PATCH 39/64] Add function for getting the up direction --- example/src/GlobeControls.js | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 3fb556763..a67bb02ab 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -22,6 +22,7 @@ const _forward = new Vector3(); const _rotationAxis = new Vector3(); const _quaternion = new Quaternion(); const _plane = new Plane(); +const _up = new Vector3(); const _pointer = new Vector2(); const _prevPointer = new Vector2(); @@ -79,10 +80,7 @@ export class GlobeControls { this.maxAltitude = 0.45 * Math.PI; this.minDistance = 2; this.maxDistance = Infinity; - - this.pivotMesh = new PivotPointMesh(); - this.pivotMesh.raycast = () => {}; - this.pivotMesh.scale.setScalar( 0.25 ); + this.getUpDirection = null; // internal state this.pointerTracker = new PointerTracker(); @@ -99,12 +97,17 @@ export class GlobeControls { this.zoomDirection = new Vector3(); this.zoomPoint = new Vector3(); + this.pivotMesh = new PivotPointMesh(); + this.pivotMesh.raycast = () => {}; + this.pivotMesh.scale.setScalar( 0.25 ); + this.raycaster = new Raycaster(); this.raycaster.firstHitOnly = true; this.up = new Vector3( 0, 1, 0 ); this._detachCallback = null; + this._upInitialized = false; // init this.attach( domElement ); @@ -444,6 +447,24 @@ export class GlobeControls { up, } = this; + if ( this.getUpDirection ) { + + this.getUpDirection( camera.position, _up ); + if ( ! this._upInitialized ) { + + // TODO: do we need to do more here? Possibly add a helper for initializing + // the camera orientation? + this._upInitialized = true; + this.up.copy( _up ); + + } else { + + this.setFrame( _up ); + + } + + } + // when dragging the camera and drag point may be moved // to accommodate terrain so we try to move it back down // to the original point. @@ -503,9 +524,9 @@ export class GlobeControls { zoomDirection.copy( raycaster.ray.direction ).normalize(); this.zoomDirectionSet = true; - // get the target zoom point + // always update the zoom target point in case the tiles are changing let dist = Infinity; - if ( this.zoomPointSet || this._updateZoomPoint() ) { + if ( this._updateZoomPoint() ) { dist = zoomPoint.distanceTo( camera.position ); From 1e5c5461da0b6f41d907d8103d6372eb9376d2d6 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Sun, 31 Dec 2023 20:27:00 +0900 Subject: [PATCH 40/64] Zoom based on distance to tileset --- example/src/GlobeControls.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index a67bb02ab..f146395d1 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -532,8 +532,17 @@ export class GlobeControls { } else { - // if we're zooming into nothing then skip zooming - return; + // if we're zooming into nothing then use the distance from the ground to scale movement + const hit = this._getPointBelowCamera(); + if ( hit ) { + + dist = hit.distance; + + } else { + + return; + + } } @@ -550,11 +559,6 @@ export class GlobeControls { scale = scale * ( dist - minDistance ) * 0.01; scale = Math.min( scale, remainingDistance ); - } - - if ( scale < 0 ) { - - } camera.position.addScaledVector( zoomDirection, scale ); From 0db2e5fa635c74c13e2af7cced3358efb13494ef Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Mon, 1 Jan 2024 17:33:51 +0900 Subject: [PATCH 41/64] Fix dragging --- example/src/GlobeControls.js | 44 ++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index f146395d1..8161e73fb 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -14,6 +14,9 @@ const DRAG = 1; const ROTATE = 2; const ZOOM = 3; +const DRAG_PLANE_THRESHOLD = 0.05; +const DRAG_UP_THRESHOLD = 0.025; + const _matrix = new Matrix4(); const _rotMatrix = new Matrix4(); const _delta = new Vector3(); @@ -626,6 +629,45 @@ export class GlobeControls { _plane.setFromNormalAndCoplanarPoint( up, dragPoint ); raycaster.setFromCamera( _pointer, camera ); + // prevent the drag distance from getting too severe + if ( - raycaster.ray.direction.dot( up ) < DRAG_PLANE_THRESHOLD ) { + + const angle = Math.acos( DRAG_PLANE_THRESHOLD ); + + _rotationAxis + .crossVectors( raycaster.ray.direction, up ) + .normalize(); + + raycaster.ray.direction + .copy( up ) + .applyAxisAngle( _rotationAxis, angle ) + .multiplyScalar( - 1 ); + + } + + // TODO: dragging causes the camera to rise because we're getting "pushed" up by lower resolution tiles and + // don't lower back down. We should maintain a target height above tiles where possible + // prevent the drag from inverting + if ( this.getUpDirection ) { + + this.getUpDirection( dragPoint, _up ); + if ( - _up.dot( raycaster.ray.direction ) < DRAG_UP_THRESHOLD ) { + + const angle = Math.acos( DRAG_UP_THRESHOLD ); + + _rotationAxis + .crossVectors( raycaster.ray.direction, _up ) + .normalize(); + + raycaster.ray.direction + .copy( _up ) + .applyAxisAngle( _rotationAxis, angle ) + .multiplyScalar( - 1 ); + + } + + } + if ( raycaster.ray.intersectPlane( _plane, _vec ) ) { _delta.subVectors( dragPoint, _vec ); @@ -721,8 +763,6 @@ export class GlobeControls { if ( this.zoomPointSet || this._updateZoomPoint() ) { - camera.updateMatrixWorld(); - makeRotateAroundPoint( zoomPoint, _quaternion, _rotMatrix ); camera.matrixWorld.premultiply( _rotMatrix ); camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); From 5fb73e5e5540f38cd9e5b35b98d4898492da8f0c Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Mon, 1 Jan 2024 18:10:27 +0900 Subject: [PATCH 42/64] Add Maps, GlobeControls --- example/src/GlobeControls.js | 161 ++++++++++++++++++++++++++++++++++- 1 file changed, 160 insertions(+), 1 deletion(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 8161e73fb..c296426d2 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -67,7 +67,7 @@ function mouseToCoords( clientX, clientY, element, target ) { } -export class GlobeControls { +export class MapControls { constructor( scene, camera, domElement ) { @@ -786,3 +786,162 @@ export class GlobeControls { } +const MAX_GLOBE_DISTANCE = 3 * 1e7; +const GLOBE_TRANSITION_THRESHOLD = 1e7; +export class GlobeControls extends MapControls { + + getVectorToCenter( target ) { + + const { scene, camera } = this; + return target + .setFromMatrixPosition( scene.matrixWorld ) + .sub( camera.position ); + + } + + getDistanceToCenter() { + + return this + .getVectorToCenter( _vec ) + .length(); + + } + + update() { + + super.update(); + + const { + camera, + scene, + } = this; + + // clamp the camera distance + const distanceToCenter = this.getDistanceToCenter(); + if ( distanceToCenter > MAX_GLOBE_DISTANCE ) { + + _vec.setFromMatrixPosition( scene.matrixWorld ).sub( camera.position ).normalize().multiplyScalar( - 1 ); + camera.position.setFromMatrixPosition( scene.matrixWorld ).addScaledVector( _vec, MAX_GLOBE_DISTANCE ); + camera.updateMatrixWorld(); + + } + + } + + // setFrame( ...args ) { + + // if ( this.getDistanceToCenter() > GLOBE_TRANSITION_THRESHOLD ) { + + // this.zoomDirectionSet = false; + + // } + + // super.setFrame( ...args ); + + // } + + // _updateRotation() { + + // if ( this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { + + // super._updateRotation(); + + // } + + // } + + // _updatePosition() { + + // if ( this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { + + // super._updatePosition(); + + // } else { + + // const { + // pointerTracker, + // domElement, + // camera, + // scene, + // } = this; + + // // get delta pointer + // pointerTracker.getCenterPoint( _pointer ); + // // mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); + + // pointerTracker.getPreviousCenterPoint( _prevPointer ); + // // mouseToCoords( _prevPointer.x, _prevPointer.y, domElement, _prevPointer ); + + // _deltaPointer.subVectors( _pointer, _prevPointer ); + // _deltaPointer.multiplyScalar( 0.01 / window.devicePixelRatio ); + + // // get the rotation axes + // const _right = new Vector3(); + // const _towardCenter = new Vector3(); + // const _up = new Vector3(); + // const _center = new Vector3(); + + // _right.set( 1, 0, 0 ).transformDirection( camera.matrixWorld ).normalize(); + // this.getVectorToCenter( _towardCenter ).normalize().multiplyScalar( - 1 ); + // _up.crossVectors( _right, _towardCenter ); + // _center.setFromMatrixPosition( scene.matrixWorld ); + + // _quaternion.setFromAxisAngle( _right, - _deltaPointer.y ); + // makeRotateAroundPoint( _center, _quaternion, _rotMatrix ); + // camera.matrixWorld.premultiply( _rotMatrix ); + // camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); + + // _quaternion.setFromAxisAngle( _up, _deltaPointer.x ); + // makeRotateAroundPoint( _center, _quaternion, _rotMatrix ); + // camera.matrixWorld.premultiply( _rotMatrix ); + // camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); + + // console.log('TEST') + // this._tiltTowardsCenter(); + + // } + + // } + + // _updateZoom( delta ) { + + // const { camera } = this; + + // if ( camera.position.length() < MAX_GLOBE_DISTANCE - 1 || delta > 0 ) { + + // super._updateZoom( delta ); + + // } + + // // TODO: twist the camera + // if ( delta < 0 ) { + + // if ( this.getDistanceToCenter() > GLOBE_TRANSITION_THRESHOLD ) { + + // this._tiltTowardsCenter(); + + // } + + + // } + + // } + + // _tiltTowardsCenter() { + + // const { + // camera, + // scene, + // } = this; + + // _forward.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ).normalize(); + // _vec.setFromMatrixPosition( scene.matrixWorld ).sub( camera.position ).normalize(); + // _vec.lerp( _forward, 0.97 ).normalize(); + + // _quaternion.setFromUnitVectors( _forward, _vec ); + // camera.quaternion.premultiply( _quaternion ); + // camera.updateMatrixWorld(); + + // } + +} From b127af89a6925dbebd251fe4d4a08615aa55536c Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Mon, 1 Jan 2024 19:23:34 +0900 Subject: [PATCH 43/64] Fix zoom --- example/src/GlobeControls.js | 148 +++++++++++++++-------------------- 1 file changed, 63 insertions(+), 85 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index c296426d2..0451e8d3a 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -5,9 +5,11 @@ import { Vector3, Raycaster, Plane, + MathUtils, } from 'three'; import { PivotPointMesh } from './PivotPointMesh.js'; import { PointerTracker } from './PointerTracker.js'; +import { WGS84_ELLIPSOID } from '../../src/index.js'; const NONE = 0; const DRAG = 1; @@ -761,6 +763,7 @@ export class MapControls { if ( this.zoomDirectionSet ) { + // TODO: just zoom backwards if we're at a steep angle if ( this.zoomPointSet || this._updateZoomPoint() ) { makeRotateAroundPoint( zoomPoint, _quaternion, _rotMatrix ); @@ -779,14 +782,13 @@ export class MapControls { } - // TODO: do we need to potentially fix the camera twist here? up.copy( newUp ); } } -const MAX_GLOBE_DISTANCE = 3 * 1e7; +const MAX_GLOBE_DISTANCE = 2 * 1e7; const GLOBE_TRANSITION_THRESHOLD = 1e7; export class GlobeControls extends MapControls { @@ -817,131 +819,107 @@ export class GlobeControls extends MapControls { } = this; // clamp the camera distance - const distanceToCenter = this.getDistanceToCenter(); + let distanceToCenter = this.getDistanceToCenter(); if ( distanceToCenter > MAX_GLOBE_DISTANCE ) { _vec.setFromMatrixPosition( scene.matrixWorld ).sub( camera.position ).normalize().multiplyScalar( - 1 ); camera.position.setFromMatrixPosition( scene.matrixWorld ).addScaledVector( _vec, MAX_GLOBE_DISTANCE ); camera.updateMatrixWorld(); - } - - } - - // setFrame( ...args ) { - - // if ( this.getDistanceToCenter() > GLOBE_TRANSITION_THRESHOLD ) { - - // this.zoomDirectionSet = false; - - // } - - // super.setFrame( ...args ); + distanceToCenter = MAX_GLOBE_DISTANCE; - // } - - // _updateRotation() { + } - // if ( this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { + // update the projection matrix + const largestDistance = Math.max( ...WGS84_ELLIPSOID.radius ); + camera.near = Math.max( 1, distanceToCenter - largestDistance * 1.25 ); + camera.far = distanceToCenter + largestDistance + 0.1; + camera.updateProjectionMatrix(); - // super._updateRotation(); + } - // } + _updatePosition( ...args ) { - // } + if ( this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { - // _updatePosition() { + super._updatePosition( ...args ); - // if ( this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { + } else { - // super._updatePosition(); - // } else { + } - // const { - // pointerTracker, - // domElement, - // camera, - // scene, - // } = this; + } - // // get delta pointer - // pointerTracker.getCenterPoint( _pointer ); - // // mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); + _updateRotation( ...args ) { - // pointerTracker.getPreviousCenterPoint( _prevPointer ); - // // mouseToCoords( _prevPointer.x, _prevPointer.y, domElement, _prevPointer ); + if ( this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { - // _deltaPointer.subVectors( _pointer, _prevPointer ); - // _deltaPointer.multiplyScalar( 0.01 / window.devicePixelRatio ); + super._updateRotation( ...args ); - // // get the rotation axes - // const _right = new Vector3(); - // const _towardCenter = new Vector3(); - // const _up = new Vector3(); - // const _center = new Vector3(); + } else { - // _right.set( 1, 0, 0 ).transformDirection( camera.matrixWorld ).normalize(); - // this.getVectorToCenter( _towardCenter ).normalize().multiplyScalar( - 1 ); - // _up.crossVectors( _right, _towardCenter ); - // _center.setFromMatrixPosition( scene.matrixWorld ); - // _quaternion.setFromAxisAngle( _right, - _deltaPointer.y ); - // makeRotateAroundPoint( _center, _quaternion, _rotMatrix ); - // camera.matrixWorld.premultiply( _rotMatrix ); - // camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); + } - // _quaternion.setFromAxisAngle( _up, _deltaPointer.x ); - // makeRotateAroundPoint( _center, _quaternion, _rotMatrix ); - // camera.matrixWorld.premultiply( _rotMatrix ); - // camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); + } - // console.log('TEST') - // this._tiltTowardsCenter(); + _updateZoom( delta ) { - // } + if ( this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD || delta > 0 ) { - // } + super._updateZoom( delta ); - // _updateZoom( delta ) { + } else { - // const { camera } = this; + // TODO: zoom forward if the user is zooming into the sky - // if ( camera.position.length() < MAX_GLOBE_DISTANCE - 1 || delta > 0 ) { + // orient the camera during the zoom + const alpha = MathUtils.mapLinear( this.getDistanceToCenter(), GLOBE_TRANSITION_THRESHOLD, MAX_GLOBE_DISTANCE, 0, 1 ); + this._tiltTowardsCenter( MathUtils.lerp( 1, 0.6, alpha ) ); + this._alignUpward( MathUtils.lerp( 1, 0.8, alpha ) ); - // super._updateZoom( delta ); + this.getVectorToCenter( _vec ); + const dist = _vec.length(); - // } + this.camera.position.addScaledVector( _vec, delta * 0.0025 * dist / dist ); + this.camera.updateMatrixWorld(); - // // TODO: twist the camera - // if ( delta < 0 ) { + } - // if ( this.getDistanceToCenter() > GLOBE_TRANSITION_THRESHOLD ) { + } - // this._tiltTowardsCenter(); + _alignUpward( alpha ) { - // } + const { scene, camera } = this; + const _globalUp = new Vector3( 0, 0, 1 ).transformDirection( scene.matrixWorld ); + const _forward = new Vector3( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); + const _right = new Vector3( - 1, 0, 0 ).transformDirection( camera.matrixWorld ); + const _targetRight = new Vector3().crossVectors( _globalUp, _forward ); + _targetRight.lerp( _right, alpha ).normalize(); - // } + _quaternion.setFromUnitVectors( _right, _targetRight ); + camera.quaternion.premultiply( _quaternion ); + camera.updateMatrixWorld(); - // } + } - // _tiltTowardsCenter() { + _tiltTowardsCenter( alpha ) { - // const { - // camera, - // scene, - // } = this; + const { + camera, + scene, + } = this; - // _forward.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ).normalize(); - // _vec.setFromMatrixPosition( scene.matrixWorld ).sub( camera.position ).normalize(); - // _vec.lerp( _forward, 0.97 ).normalize(); + _forward.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ).normalize(); + _vec.setFromMatrixPosition( scene.matrixWorld ).sub( camera.position ).normalize(); + _vec.lerp( _forward, alpha ).normalize(); - // _quaternion.setFromUnitVectors( _forward, _vec ); - // camera.quaternion.premultiply( _quaternion ); - // camera.updateMatrixWorld(); + _quaternion.setFromUnitVectors( _forward, _vec ); + camera.quaternion.premultiply( _quaternion ); + camera.updateMatrixWorld(); - // } + } } From 968fd0ce8eefabfd8a1451c2581e988f74d59ea3 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Mon, 1 Jan 2024 20:05:47 +0900 Subject: [PATCH 44/64] Improvements --- example/src/GlobeControls.js | 62 ++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 0451e8d3a..f5668ec9d 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -529,6 +529,8 @@ export class MapControls { zoomDirection.copy( raycaster.ray.direction ).normalize(); this.zoomDirectionSet = true; + const finalZoomDirection = _vec.copy( zoomDirection ); + // always update the zoom target point in case the tiles are changing let dist = Infinity; if ( this._updateZoomPoint() ) { @@ -549,6 +551,8 @@ export class MapControls { } + finalZoomDirection.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); + } // scale the distance based on how far there is to move @@ -566,7 +570,7 @@ export class MapControls { } - camera.position.addScaledVector( zoomDirection, scale ); + camera.position.addScaledVector( finalZoomDirection, scale ); camera.updateMatrixWorld(); } @@ -772,10 +776,17 @@ export class MapControls { zoomDirection.subVectors( zoomPoint, camera.position ).normalize(); + } else { + + camera.position.copy( pivot ).addScaledVector( newUp, dist ); + camera.quaternion.premultiply( _quaternion ); + camera.updateMatrixWorld(); + } } else if ( state !== ROTATE ) { + // TODO: fix this for dragging from afar camera.position.copy( pivot ).addScaledVector( newUp, dist ); camera.quaternion.premultiply( _quaternion ); camera.updateMatrixWorld(); @@ -816,6 +827,7 @@ export class GlobeControls extends MapControls { const { camera, scene, + pivotMesh, } = this; // clamp the camera distance @@ -830,6 +842,16 @@ export class GlobeControls extends MapControls { } + if ( distanceToCenter > GLOBE_TRANSITION_THRESHOLD ) { + + pivotMesh.visible = false; + + } else { + + pivotMesh.visible = true; + + } + // update the projection matrix const largestDistance = Math.max( ...WGS84_ELLIPSOID.radius ); camera.near = Math.max( 1, distanceToCenter - largestDistance * 1.25 ); @@ -846,6 +868,40 @@ export class GlobeControls extends MapControls { } else { + const { + pointerTracker, + rotationSpeed, + camera, + pivotMesh, + } = this; + pointerTracker.getCenterPoint( _pointer ); + pointerTracker.getPreviousCenterPoint( _prevPointer ); + + _deltaPointer + .subVectors( _pointer, _prevPointer ) + .multiplyScalar( camera.position.distanceTo( this.dragPoint ) * 1e-8 * 5 * 1e-3); + + const azimuth = - _deltaPointer.x * rotationSpeed; + const altitude = - _deltaPointer.y * rotationSpeed; + + const _center = new Vector3().setFromMatrixPosition( this.scene.matrixWorld ); + const _right = new Vector3( 1, 0, 0 ).transformDirection( camera.matrixWorld ); + const _up = new Vector3( 0, 1, 0 ).transformDirection( camera.matrixWorld ); + + _quaternion.setFromAxisAngle( _right, altitude ); + camera.quaternion.premultiply( _quaternion ); + makeRotateAroundPoint( _center, _quaternion, _rotMatrix ); + camera.matrixWorld.premultiply( _rotMatrix ); + + _quaternion.setFromAxisAngle( _up, azimuth ); + camera.quaternion.premultiply( _quaternion ); + makeRotateAroundPoint( _center, _quaternion, _rotMatrix ); + camera.matrixWorld.premultiply( _rotMatrix ); + + camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); + + pivotMesh.visible = false; + } @@ -876,8 +932,8 @@ export class GlobeControls extends MapControls { // orient the camera during the zoom const alpha = MathUtils.mapLinear( this.getDistanceToCenter(), GLOBE_TRANSITION_THRESHOLD, MAX_GLOBE_DISTANCE, 0, 1 ); - this._tiltTowardsCenter( MathUtils.lerp( 1, 0.6, alpha ) ); - this._alignUpward( MathUtils.lerp( 1, 0.8, alpha ) ); + this._tiltTowardsCenter( MathUtils.lerp( 1, 0.8, alpha ) ); + this._alignUpward( MathUtils.lerp( 1, 0.9, alpha ) ); this.getVectorToCenter( _vec ); const dist = _vec.length(); From 38ad5db64ca8b6679708f3763e467a3b607583a1 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Mon, 1 Jan 2024 20:26:10 +0900 Subject: [PATCH 45/64] Fix drag reorientation issue --- example/src/GlobeControls.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index f5668ec9d..050fd676d 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -86,6 +86,7 @@ export class MapControls { this.minDistance = 2; this.maxDistance = Infinity; this.getUpDirection = null; + this.reorientOnDrag = true; // internal state this.pointerTracker = new PointerTracker(); @@ -784,7 +785,7 @@ export class MapControls { } - } else if ( state !== ROTATE ) { + } else if ( state !== ROTATE && this.reorientOnDrag ) { // TODO: fix this for dragging from afar camera.position.copy( pivot ).addScaledVector( newUp, dist ); @@ -845,10 +846,12 @@ export class GlobeControls extends MapControls { if ( distanceToCenter > GLOBE_TRANSITION_THRESHOLD ) { pivotMesh.visible = false; + this.reorientOnDrag = false; } else { pivotMesh.visible = true; + this.reorientOnDrag = true; } From 9cadcc7688aa95b731855d11d66c4fcd619e3f4f Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Mon, 1 Jan 2024 20:51:33 +0900 Subject: [PATCH 46/64] Fix zoom issue --- example/src/GlobeControls.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 050fd676d..ce0af26ef 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -80,13 +80,14 @@ export class MapControls { // settings this.state = NONE; this.cameraRadius = 1; - this.rotationSpeed = 5; + this.rotationSpeed = 3; this.minAltitude = 0; this.maxAltitude = 0.45 * Math.PI; this.minDistance = 2; this.maxDistance = Infinity; this.getUpDirection = null; this.reorientOnDrag = true; + this.reorientOnZoom = false; // internal state this.pointerTracker = new PointerTracker(); @@ -771,11 +772,15 @@ export class MapControls { // TODO: just zoom backwards if we're at a steep angle if ( this.zoomPointSet || this._updateZoomPoint() ) { - makeRotateAroundPoint( zoomPoint, _quaternion, _rotMatrix ); - camera.matrixWorld.premultiply( _rotMatrix ); - camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); + if ( this.reorientOnZoom ) { - zoomDirection.subVectors( zoomPoint, camera.position ).normalize(); + makeRotateAroundPoint( zoomPoint, _quaternion, _rotMatrix ); + camera.matrixWorld.premultiply( _rotMatrix ); + camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); + + zoomDirection.subVectors( zoomPoint, camera.position ).normalize(); + + } } else { @@ -801,7 +806,7 @@ export class MapControls { } const MAX_GLOBE_DISTANCE = 2 * 1e7; -const GLOBE_TRANSITION_THRESHOLD = 1e7; +const GLOBE_TRANSITION_THRESHOLD = 0.75 * 1e7; export class GlobeControls extends MapControls { getVectorToCenter( target ) { @@ -847,11 +852,13 @@ export class GlobeControls extends MapControls { pivotMesh.visible = false; this.reorientOnDrag = false; + this.reorientOnZoom = true; } else { pivotMesh.visible = true; this.reorientOnDrag = true; + this.reorientOnZoom = false; } @@ -931,12 +938,10 @@ export class GlobeControls extends MapControls { } else { - // TODO: zoom forward if the user is zooming into the sky - // orient the camera 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._alignUpward( MathUtils.lerp( 1, 0.9, alpha ) ); + this._alignCameraUpToNorth( MathUtils.lerp( 1, 0.9, alpha ) ); this.getVectorToCenter( _vec ); const dist = _vec.length(); @@ -948,7 +953,7 @@ export class GlobeControls extends MapControls { } - _alignUpward( alpha ) { + _alignCameraUpToNorth( alpha ) { const { scene, camera } = this; const _globalUp = new Vector3( 0, 0, 1 ).transformDirection( scene.matrixWorld ); From 991487e42a5db27f195f1472572452bf6affd10b Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Mon, 1 Jan 2024 21:12:45 +0900 Subject: [PATCH 47/64] Separate tiles --- example/src/GlobeControls.js | 817 ++--------------------------------- example/src/TileControls.js | 804 ++++++++++++++++++++++++++++++++++ 2 files changed, 830 insertions(+), 791 deletions(-) create mode 100644 example/src/TileControls.js diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index ce0af26ef..d86b9bffb 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -3,812 +3,33 @@ import { Quaternion, Vector2, Vector3, - Raycaster, - Plane, MathUtils, } from 'three'; -import { PivotPointMesh } from './PivotPointMesh.js'; -import { PointerTracker } from './PointerTracker.js'; +import { TileControls, makeRotateAroundPoint } from './TileControls.js'; import { WGS84_ELLIPSOID } from '../../src/index.js'; -const NONE = 0; -const DRAG = 1; -const ROTATE = 2; -const ZOOM = 3; - -const DRAG_PLANE_THRESHOLD = 0.05; -const DRAG_UP_THRESHOLD = 0.025; - -const _matrix = new Matrix4(); const _rotMatrix = new Matrix4(); -const _delta = new Vector3(); const _vec = new Vector3(); const _forward = new Vector3(); -const _rotationAxis = new Vector3(); const _quaternion = new Quaternion(); -const _plane = new Plane(); -const _up = new Vector3(); const _pointer = new Vector2(); const _prevPointer = new Vector2(); const _deltaPointer = new Vector2(); -const _centerPoint = new Vector2(); -const _originalCenterPoint = new Vector2(); - -// TODO -// - Add support for angled rotation plane (based on where the pivot point is) -// - Test with globe (adjusting up vector) -// --- -// - Consider using sphere intersect for positioning -// - Toggles for zoom to cursor, zoom forward, orbit around center, etc? -// - provide fallback plane for cases when you're off the map -// - consider enabling drag with zoom -// - shift + scroll could adjust altitude -// - fade pivot icon in and out - -// helper function for constructing a matrix for rotating around a point -function makeRotateAroundPoint( point, quat, target ) { - - target.makeTranslation( - point.x, - point.y, - point.z ); - - _matrix.makeRotationFromQuaternion( quat ); - target.premultiply( _matrix ); - - _matrix.makeTranslation( point.x, point.y, point.z ); - target.premultiply( _matrix ); - - return target; - -} - -// get the three.js pointer coords from an event -function mouseToCoords( clientX, clientY, element, target ) { - - target.x = ( ( clientX - element.offsetLeft ) / element.clientWidth ) * 2 - 1; - target.y = - ( ( clientY - element.offsetTop ) / element.clientHeight ) * 2 + 1; - -} - -export class MapControls { - - constructor( scene, camera, domElement ) { - - this.domElement = null; - this.camera = null; - this.scene = null; - - // settings - this.state = NONE; - this.cameraRadius = 1; - this.rotationSpeed = 3; - this.minAltitude = 0; - this.maxAltitude = 0.45 * Math.PI; - this.minDistance = 2; - this.maxDistance = Infinity; - this.getUpDirection = null; - this.reorientOnDrag = true; - this.reorientOnZoom = false; - - // internal state - this.pointerTracker = new PointerTracker(); - - this.dragPointSet = false; - this.dragPoint = new Vector3(); - this.startDragPoint = new Vector3(); - - this.rotationPointSet = false; - this.rotationPoint = new Vector3(); - - this.zoomDirectionSet = false; - this.zoomPointSet = false; - this.zoomDirection = new Vector3(); - this.zoomPoint = new Vector3(); - - this.pivotMesh = new PivotPointMesh(); - this.pivotMesh.raycast = () => {}; - this.pivotMesh.scale.setScalar( 0.25 ); - - this.raycaster = new Raycaster(); - this.raycaster.firstHitOnly = true; - - this.up = new Vector3( 0, 1, 0 ); - - this._detachCallback = null; - this._upInitialized = false; - - // init - this.attach( domElement ); - this.setCamera( camera ); - this.setScene( scene ); - - } - - setScene( scene ) { - - this.scene = scene; - - } - - setCamera( camera ) { - - this.camera = camera; - - } - - attach( domElement ) { - - if ( this.domElement ) { - - throw new Error( 'GlobeControls: Controls already attached to element' ); - - } - - this.domElement = domElement; - domElement.style.touchAction = 'none'; - - let _pinchAction = NONE; - - const pointerTracker = this.pointerTracker; - let shiftClicked = false; - - const contextMenuCallback = e => { - - e.preventDefault(); - - }; - - const keydownCallback = e => { - - if ( e.key === 'Shift' ) { - - shiftClicked = true; - - } - - }; - - const keyupCallback = e => { - - if ( e.key === 'Shift' ) { - - shiftClicked = false; - - } - - }; - - const pointerdownCallback = e => { - - const { - camera, - raycaster, - domElement, - scene, - up, - pivotMesh, - pointerTracker, - } = this; - - // init fields - pointerTracker.addPointer( e ); - - if ( e.pointerType === 'touch' ) { - - pivotMesh.visible = false; - - if ( pointerTracker.getPointerCount() === 0 ) { - - domElement.setPointerCapture( e.pointerId ); - - } else if ( pointerTracker.getPointerCount() > 2 ) { - - resetState(); - return; - - } - - } - - // the "pointer" for zooming and rotating should be based on the center point - pointerTracker.getCenterPoint( _pointer ); - mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); - - // find the hit point - raycaster.setFromCamera( _pointer, camera ); - const hit = raycaster.intersectObject( scene )[ 0 ] || null; - if ( hit ) { - - // if two fingers, right click, or shift click are being used then we trigger - // a rotation action to begin - if ( - pointerTracker.getPointerCount() === 2 || - pointerTracker.isRightClicked() || - pointerTracker.isLeftClicked() && shiftClicked - ) { - - _matrix.copy( camera.matrixWorld ).invert(); - - this.state = ROTATE; - this.rotationPoint.copy( hit.point ); - this.rotationPointSet = true; - - this.pivotMesh.position.copy( hit.point ); - this.pivotMesh.updateMatrixWorld(); - this.scene.add( this.pivotMesh ); - - } else if ( pointerTracker.isLeftClicked() ) { - - // if the clicked point is coming from below the plane then don't perform the drag - if ( raycaster.ray.direction.dot( up ) < 0 ) { - - this.state = DRAG; - this.dragPoint.copy( hit.point ); - this.startDragPoint.copy( hit.point ); - this.dragPointSet = true; - - this.pivotMesh.position.copy( hit.point ); - this.pivotMesh.updateMatrixWorld(); - this.scene.add( this.pivotMesh ); - - } - - } - - } - - }; - - let _pointerMoveQueued = false; - const pointermoveCallback = e => { - - this.zoomDirectionSet = false; - this.zoomPointSet = false; - - const { pointerTracker } = this; - - pointerTracker.setHoverEvent( e ); - if ( ! pointerTracker.updatePointer( e ) ) { - - return; - - } - - if ( pointerTracker.getPointerType() === 'touch' ) { - - if ( pointerTracker.getPointerCount() === 1 ) { - - if ( this.state === DRAG ) { - - this._updatePosition(); - - } - - } else if ( pointerTracker.getPointerCount() === 2 ) { - - // We queue this event to ensure that all pointers have been updated - if ( ! _pointerMoveQueued ) { - - _pointerMoveQueued = true; - queueMicrotask( () => { - - _pointerMoveQueued = false; - - // adjust the pointer position to be the center point - pointerTracker.getCenterPoint( _centerPoint ); - - // detect zoom transition - const previousDist = pointerTracker.getPreviousPointerDistance(); - const pointerDist = pointerTracker.getPointerDistance(); - const separateDelta = pointerDist - previousDist; - if ( _pinchAction === NONE ) { - - // check which direction was moved in first - pointerTracker.getCenterPoint( _centerPoint ); - pointerTracker.getPreviousCenterPoint( _originalCenterPoint ); - - const parallelDelta = _centerPoint.distanceTo( _originalCenterPoint ); - if ( separateDelta > 0 && parallelDelta > 0 ) { - - if ( separateDelta > parallelDelta ) { - - resetState(); - _pinchAction = ZOOM; - this.zoomDirectionSet = false; - - } else { - - _pinchAction = ROTATE; - - - } - - } - - } - - if ( _pinchAction === ZOOM ) { - - // perform zoom - this._updateZoom( separateDelta ); - - } else if ( _pinchAction === ROTATE ) { - - this._updateRotation(); - this.pivotMesh.visible = true; - - } - - } ); - - } - - } - - } else if ( pointerTracker.getPointerType() === 'mouse' ) { - - if ( this.state === DRAG ) { - - this._updatePosition(); - - } else if ( this.state === ROTATE ) { - - const { rotationSpeed } = this; - this._updateRotation( - _deltaPointer.x * rotationSpeed, - _deltaPointer.y * rotationSpeed ); - - } - - } - - }; - - const pointerupCallback = e => { - - resetState(); - - pointerTracker.deletePointer( e ); - _pinchAction = NONE; - - if ( pointerTracker.getPointerType() === 'touch' && pointerTracker.getPointerCount() === 0 ) { - - domElement.releasePointerCapture( e.pointerId ); - - } - - }; - - const wheelCallback = e => { - - this._updateZoom( - e.deltaY ); - - }; - - const pointerenterCallback = e => { - - const { pointerTracker } = this; - - shiftClicked = false; - - if ( e.buttons !== pointerTracker.getPointerButtons() ) { - - pointerTracker.deletePointer( e ); - resetState(); - - } - - }; - - const resetState = () => { - - this.state = NONE; - this.dragPointSet = false; - this.rotationPointSet = false; - this.scene.remove( this.pivotMesh ); - this.pivotMesh.visible = true; - - }; - - domElement.addEventListener( 'contextmenu', contextMenuCallback ); - domElement.addEventListener( 'keydown', keydownCallback ); - domElement.addEventListener( 'keyup', keyupCallback ); - domElement.addEventListener( 'pointerdown', pointerdownCallback ); - domElement.addEventListener( 'pointermove', pointermoveCallback ); - domElement.addEventListener( 'pointerup', pointerupCallback ); - domElement.addEventListener( 'wheel', wheelCallback ); - domElement.addEventListener( 'pointerenter', pointerenterCallback ); - - this._detachCallback = () => { - - domElement.removeEventListener( 'contextmenu', contextMenuCallback ); - domElement.removeEventListener( 'keydown', keydownCallback ); - domElement.removeEventListener( 'keyup', keyupCallback ); - domElement.removeEventListener( 'pointerdown', pointerdownCallback ); - domElement.removeEventListener( 'pointermove', pointermoveCallback ); - domElement.removeEventListener( 'pointerup', pointerupCallback ); - domElement.removeEventListener( 'wheel', wheelCallback ); - domElement.removeEventListener( 'pointerenter', pointerenterCallback ); - - }; - - } - - detach() { - - if ( this._detachCallback ) { - - this._detachCallback(); - this._detachCallback = null; - this.pointerTracker = new PointerTracker(); - - } - - } - - update() { - - const { - raycaster, - camera, - cameraRadius, - dragPoint, - startDragPoint, - up, - } = this; - - if ( this.getUpDirection ) { - this.getUpDirection( camera.position, _up ); - if ( ! this._upInitialized ) { - - // TODO: do we need to do more here? Possibly add a helper for initializing - // the camera orientation? - this._upInitialized = true; - this.up.copy( _up ); - - } else { - - this.setFrame( _up ); - - } - - } - - // when dragging the camera and drag point may be moved - // to accommodate terrain so we try to move it back down - // to the original point. - if ( this.state === DRAG ) { - - _delta.subVectors( startDragPoint, dragPoint ); - camera.position.add( _delta ); - dragPoint.copy( startDragPoint ); - - } - - // cast down from the camera - const hit = this._getPointBelowCamera(); - if ( hit ) { - - const dist = hit.distance - 1e5; - if ( dist < cameraRadius ) { - - const delta = cameraRadius - dist; - camera.position.copy( hit.point ).addScaledVector( raycaster.ray.direction, - cameraRadius ); - dragPoint.addScaledVector( up, delta ); - - } - - } - - } - - dispose() { - - this.detach(); - - } - - // private - _updateZoom( scale ) { - - const { - zoomPoint, - zoomDirection, - camera, - minDistance, - maxDistance, - raycaster, - pointerTracker, - domElement, - } = this; - - if ( ! pointerTracker.getLatestPoint( _pointer ) ) { - - return; - - } - - mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); - raycaster.setFromCamera( _pointer, camera ); - zoomDirection.copy( raycaster.ray.direction ).normalize(); - this.zoomDirectionSet = true; - - const finalZoomDirection = _vec.copy( zoomDirection ); - - // always update the zoom target point in case the tiles are changing - let dist = Infinity; - if ( this._updateZoomPoint() ) { - - dist = zoomPoint.distanceTo( camera.position ); - - } else { - - // if we're zooming into nothing then use the distance from the ground to scale movement - const hit = this._getPointBelowCamera(); - if ( hit ) { - - dist = hit.distance; - - } else { - - return; - - } - - finalZoomDirection.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); - - } - - // scale the distance based on how far there is to move - if ( scale < 0 ) { - - const remainingDistance = Math.min( 0, dist - maxDistance ); - scale = scale * ( dist - 0 ) * 0.01; - scale = Math.max( scale, remainingDistance ); - - } else { - - const remainingDistance = Math.max( 0, dist - minDistance ); - scale = scale * ( dist - minDistance ) * 0.01; - scale = Math.min( scale, remainingDistance ); - - } - - camera.position.addScaledVector( finalZoomDirection, scale ); - camera.updateMatrixWorld(); - - } - - _updateZoomPoint() { - - const { - camera, - zoomDirectionSet, - zoomDirection, - raycaster, - scene, - zoomPoint, - } = this; - - if ( ! zoomDirectionSet ) { - - return false; - - } - - raycaster.ray.origin.copy( camera.position ); - raycaster.ray.direction.copy( zoomDirection ); - - const hit = raycaster.intersectObject( scene )[ 0 ] || null; - if ( hit ) { - - zoomPoint.copy( hit.point ); - this.zoomPointSet = true; - return true; - - } - - return false; - - } - - _getPointBelowCamera() { - - const { camera, raycaster, scene, up } = this; - raycaster.ray.direction.copy( up ).multiplyScalar( - 1 ); - raycaster.ray.origin.copy( camera.position ).addScaledVector( raycaster.ray.direction, - 1e5 ); - - return raycaster.intersectObject( scene )[ 0 ] || null; - - } - - _updatePosition() { - - const { - raycaster, - camera, - dragPoint, - up, - pointerTracker, - domElement, - } = this; - - pointerTracker.getCenterPoint( _pointer ); - mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); - - _plane.setFromNormalAndCoplanarPoint( up, dragPoint ); - raycaster.setFromCamera( _pointer, camera ); - - // prevent the drag distance from getting too severe - if ( - raycaster.ray.direction.dot( up ) < DRAG_PLANE_THRESHOLD ) { - - const angle = Math.acos( DRAG_PLANE_THRESHOLD ); - - _rotationAxis - .crossVectors( raycaster.ray.direction, up ) - .normalize(); - - raycaster.ray.direction - .copy( up ) - .applyAxisAngle( _rotationAxis, angle ) - .multiplyScalar( - 1 ); - - } - - // TODO: dragging causes the camera to rise because we're getting "pushed" up by lower resolution tiles and - // don't lower back down. We should maintain a target height above tiles where possible - // prevent the drag from inverting - if ( this.getUpDirection ) { - - this.getUpDirection( dragPoint, _up ); - if ( - _up.dot( raycaster.ray.direction ) < DRAG_UP_THRESHOLD ) { - - const angle = Math.acos( DRAG_UP_THRESHOLD ); - - _rotationAxis - .crossVectors( raycaster.ray.direction, _up ) - .normalize(); - - raycaster.ray.direction - .copy( _up ) - .applyAxisAngle( _rotationAxis, angle ) - .multiplyScalar( - 1 ); - - } - - } - - if ( raycaster.ray.intersectPlane( _plane, _vec ) ) { - - _delta.subVectors( dragPoint, _vec ); - this.camera.position.add( _delta ); - this.camera.updateMatrixWorld(); - - } - - } - - _updateRotation() { - - const { - camera, - rotationPoint, - minAltitude, - maxAltitude, - up, - domElement, - pointerTracker, - rotationSpeed, - } = this; - - // get the rotation motion - pointerTracker.getCenterPoint( _pointer ); - mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); - - pointerTracker.getPreviousCenterPoint( _prevPointer ); - mouseToCoords( _prevPointer.x, _prevPointer.y, domElement, _prevPointer ); - - _deltaPointer.subVectors( _pointer, _prevPointer ); - - const azimuth = - _deltaPointer.x * rotationSpeed; - let altitude = - _deltaPointer.y * rotationSpeed; - - // currently uses the camera forward for this work but it may be best to use a combination of camera - // forward and direction to pivot? Or just dir to pivot? - _forward.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ).multiplyScalar( - 1 ); - - const angle = up.angleTo( _forward ); - if ( altitude > 0 ) { - - altitude = Math.min( angle - minAltitude - 1e-2, altitude ); - - } else { - - altitude = Math.max( angle - maxAltitude, altitude ); - - } - - // zoom in frame around pivot point - _quaternion.setFromAxisAngle( up, azimuth ); - makeRotateAroundPoint( rotationPoint, _quaternion, _rotMatrix ); - camera.matrixWorld.premultiply( _rotMatrix ); - - _rotationAxis.set( - 1, 0, 0 ).transformDirection( camera.matrixWorld ); - - _quaternion.setFromAxisAngle( _rotationAxis, altitude ); - makeRotateAroundPoint( rotationPoint, _quaternion, _rotMatrix ); - camera.matrixWorld.premultiply( _rotMatrix ); - - camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); - camera.updateMatrixWorld(); - - } - - setFrame( newUp ) { - - const pivot = new Vector3(); - let dist = 0; - - // cast down from the camera to get the pivot to rotate around - const { up, camera, state, zoomPoint, zoomDirection } = this; - camera.updateMatrixWorld(); - - const hit = this._getPointBelowCamera(); - if ( hit ) { - - _vec.setFromMatrixPosition( camera.matrixWorld ); - - pivot.copy( hit.point ); - dist = pivot.distanceTo( _vec ); - - } else { - - return; - - } - - _quaternion.setFromUnitVectors( up, newUp ); - - if ( this.zoomDirectionSet ) { - - // TODO: just zoom backwards if we're at a steep angle - if ( this.zoomPointSet || this._updateZoomPoint() ) { - - if ( this.reorientOnZoom ) { - - makeRotateAroundPoint( zoomPoint, _quaternion, _rotMatrix ); - camera.matrixWorld.premultiply( _rotMatrix ); - camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); - - zoomDirection.subVectors( zoomPoint, camera.position ).normalize(); - - } - - } else { - - camera.position.copy( pivot ).addScaledVector( newUp, dist ); - camera.quaternion.premultiply( _quaternion ); - camera.updateMatrixWorld(); - - } - - } else if ( state !== ROTATE && this.reorientOnDrag ) { - - // TODO: fix this for dragging from afar - camera.position.copy( pivot ).addScaledVector( newUp, dist ); - camera.quaternion.premultiply( _quaternion ); - camera.updateMatrixWorld(); +const MAX_GLOBE_DISTANCE = 2 * 1e7; +const GLOBE_TRANSITION_THRESHOLD = 0.75 * 1e7; +export class GlobeControls extends TileControls { - } + constructor( ...args ) { - up.copy( newUp ); + // store which mode the drag stats are in + super( ...args ); + this._dragMode = 0; + this._rotationMode = 0; } -} - -const MAX_GLOBE_DISTANCE = 2 * 1e7; -const GLOBE_TRANSITION_THRESHOLD = 0.75 * 1e7; -export class GlobeControls extends MapControls { - getVectorToCenter( target ) { const { scene, camera } = this; @@ -870,14 +91,26 @@ export class GlobeControls extends MapControls { } + resetState() { + + super.resetState(); + this._dragMode = 0; + this._rotationMode = 0; + + } + _updatePosition( ...args ) { - if ( this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { + if ( this._dragMode === 1 || this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { + + this._dragMode = 1; super._updatePosition( ...args ); } else { + this._dragMode = - 1; + const { pointerTracker, rotationSpeed, @@ -889,7 +122,7 @@ export class GlobeControls extends MapControls { _deltaPointer .subVectors( _pointer, _prevPointer ) - .multiplyScalar( camera.position.distanceTo( this.dragPoint ) * 1e-8 * 5 * 1e-3); + .multiplyScalar( camera.position.distanceTo( this.dragPoint ) * 1e-8 * 5 * 1e-3 ); const azimuth = - _deltaPointer.x * rotationSpeed; const altitude = - _deltaPointer.y * rotationSpeed; @@ -919,12 +152,14 @@ export class GlobeControls extends MapControls { _updateRotation( ...args ) { - if ( this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { + if ( this._rotationMode === 1 || this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { + this._rotationMode = 1; super._updateRotation( ...args ); } else { + this._rotationMode = - 1; } diff --git a/example/src/TileControls.js b/example/src/TileControls.js new file mode 100644 index 000000000..72137535b --- /dev/null +++ b/example/src/TileControls.js @@ -0,0 +1,804 @@ +import { + Matrix4, + Quaternion, + Vector2, + Vector3, + Raycaster, + Plane, +} from 'three'; +import { PivotPointMesh } from './PivotPointMesh.js'; +import { PointerTracker } from './PointerTracker.js'; + +const NONE = 0; +const DRAG = 1; +const ROTATE = 2; +const ZOOM = 3; + +const DRAG_PLANE_THRESHOLD = 0.05; +const DRAG_UP_THRESHOLD = 0.025; + +const _matrix = new Matrix4(); +const _rotMatrix = new Matrix4(); +const _delta = new Vector3(); +const _vec = new Vector3(); +const _forward = new Vector3(); +const _rotationAxis = new Vector3(); +const _quaternion = new Quaternion(); +const _plane = new Plane(); +const _up = new Vector3(); + +const _pointer = new Vector2(); +const _prevPointer = new Vector2(); +const _deltaPointer = new Vector2(); +const _centerPoint = new Vector2(); +const _originalCenterPoint = new Vector2(); + +// TODO +// - Add support for angled rotation plane (based on where the pivot point is) +// - Test with globe (adjusting up vector) +// --- +// - Consider using sphere intersect for positioning +// - Toggles for zoom to cursor, zoom forward, orbit around center, etc? +// - provide fallback plane for cases when you're off the map +// - consider enabling drag with zoom +// - shift + scroll could adjust altitude +// - fade pivot icon in and out + +// helper function for constructing a matrix for rotating around a point +export function makeRotateAroundPoint( point, quat, target ) { + + target.makeTranslation( - point.x, - point.y, - point.z ); + + _matrix.makeRotationFromQuaternion( quat ); + target.premultiply( _matrix ); + + _matrix.makeTranslation( point.x, point.y, point.z ); + target.premultiply( _matrix ); + + return target; + +} + +// get the three.js pointer coords from an event +function mouseToCoords( clientX, clientY, element, target ) { + + target.x = ( ( clientX - element.offsetLeft ) / element.clientWidth ) * 2 - 1; + target.y = - ( ( clientY - element.offsetTop ) / element.clientHeight ) * 2 + 1; + +} + +export class TileControls { + + constructor( scene, camera, domElement ) { + + this.domElement = null; + this.camera = null; + this.scene = null; + + // settings + this.state = NONE; + this.cameraRadius = 1; + this.rotationSpeed = 3; + this.minAltitude = 0; + this.maxAltitude = 0.45 * Math.PI; + this.minDistance = 2; + this.maxDistance = Infinity; + this.getUpDirection = null; + this.reorientOnDrag = true; + this.reorientOnZoom = false; + + // internal state + this.pointerTracker = new PointerTracker(); + + this.dragPointSet = false; + this.dragPoint = new Vector3(); + this.startDragPoint = new Vector3(); + + this.rotationPointSet = false; + this.rotationPoint = new Vector3(); + + this.zoomDirectionSet = false; + this.zoomPointSet = false; + this.zoomDirection = new Vector3(); + this.zoomPoint = new Vector3(); + + this.pivotMesh = new PivotPointMesh(); + this.pivotMesh.raycast = () => {}; + this.pivotMesh.scale.setScalar( 0.25 ); + + this.raycaster = new Raycaster(); + this.raycaster.firstHitOnly = true; + + this.up = new Vector3( 0, 1, 0 ); + + this._detachCallback = null; + this._upInitialized = false; + + // init + this.attach( domElement ); + this.setCamera( camera ); + this.setScene( scene ); + + } + + setScene( scene ) { + + this.scene = scene; + + } + + setCamera( camera ) { + + this.camera = camera; + + } + + attach( domElement ) { + + if ( this.domElement ) { + + throw new Error( 'GlobeControls: Controls already attached to element' ); + + } + + this.domElement = domElement; + domElement.style.touchAction = 'none'; + + let _pinchAction = NONE; + + const pointerTracker = this.pointerTracker; + let shiftClicked = false; + + const contextMenuCallback = e => { + + e.preventDefault(); + + }; + + const keydownCallback = e => { + + if ( e.key === 'Shift' ) { + + shiftClicked = true; + + } + + }; + + const keyupCallback = e => { + + if ( e.key === 'Shift' ) { + + shiftClicked = false; + + } + + }; + + const pointerdownCallback = e => { + + const { + camera, + raycaster, + domElement, + scene, + up, + pivotMesh, + pointerTracker, + } = this; + + // init fields + pointerTracker.addPointer( e ); + + if ( e.pointerType === 'touch' ) { + + pivotMesh.visible = false; + + if ( pointerTracker.getPointerCount() === 0 ) { + + domElement.setPointerCapture( e.pointerId ); + + } else if ( pointerTracker.getPointerCount() > 2 ) { + + this.resetState(); + return; + + } + + } + + // the "pointer" for zooming and rotating should be based on the center point + pointerTracker.getCenterPoint( _pointer ); + mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); + + // find the hit point + raycaster.setFromCamera( _pointer, camera ); + const hit = raycaster.intersectObject( scene )[ 0 ] || null; + if ( hit ) { + + // if two fingers, right click, or shift click are being used then we trigger + // a rotation action to begin + if ( + pointerTracker.getPointerCount() === 2 || + pointerTracker.isRightClicked() || + pointerTracker.isLeftClicked() && shiftClicked + ) { + + _matrix.copy( camera.matrixWorld ).invert(); + + this.state = ROTATE; + this.rotationPoint.copy( hit.point ); + this.rotationPointSet = true; + + this.pivotMesh.position.copy( hit.point ); + this.pivotMesh.updateMatrixWorld(); + this.scene.add( this.pivotMesh ); + + } else if ( pointerTracker.isLeftClicked() ) { + + // if the clicked point is coming from below the plane then don't perform the drag + if ( raycaster.ray.direction.dot( up ) < 0 ) { + + this.state = DRAG; + this.dragPoint.copy( hit.point ); + this.startDragPoint.copy( hit.point ); + this.dragPointSet = true; + + this.pivotMesh.position.copy( hit.point ); + this.pivotMesh.updateMatrixWorld(); + this.scene.add( this.pivotMesh ); + + } + + } + + } + + }; + + let _pointerMoveQueued = false; + const pointermoveCallback = e => { + + this.zoomDirectionSet = false; + this.zoomPointSet = false; + + const { pointerTracker } = this; + + pointerTracker.setHoverEvent( e ); + if ( ! pointerTracker.updatePointer( e ) ) { + + return; + + } + + if ( pointerTracker.getPointerType() === 'touch' ) { + + if ( pointerTracker.getPointerCount() === 1 ) { + + if ( this.state === DRAG ) { + + this._updatePosition(); + + } + + } else if ( pointerTracker.getPointerCount() === 2 ) { + + // We queue this event to ensure that all pointers have been updated + if ( ! _pointerMoveQueued ) { + + _pointerMoveQueued = true; + queueMicrotask( () => { + + _pointerMoveQueued = false; + + // adjust the pointer position to be the center point + pointerTracker.getCenterPoint( _centerPoint ); + + // detect zoom transition + const previousDist = pointerTracker.getPreviousPointerDistance(); + const pointerDist = pointerTracker.getPointerDistance(); + const separateDelta = pointerDist - previousDist; + if ( _pinchAction === NONE ) { + + // check which direction was moved in first + pointerTracker.getCenterPoint( _centerPoint ); + pointerTracker.getPreviousCenterPoint( _originalCenterPoint ); + + const parallelDelta = _centerPoint.distanceTo( _originalCenterPoint ); + if ( separateDelta > 0 && parallelDelta > 0 ) { + + if ( separateDelta > parallelDelta ) { + + this.resetState(); + _pinchAction = ZOOM; + this.zoomDirectionSet = false; + + } else { + + _pinchAction = ROTATE; + + + } + + } + + } + + if ( _pinchAction === ZOOM ) { + + // perform zoom + this._updateZoom( separateDelta ); + + } else if ( _pinchAction === ROTATE ) { + + this._updateRotation(); + this.pivotMesh.visible = true; + + } + + } ); + + } + + } + + } else if ( pointerTracker.getPointerType() === 'mouse' ) { + + if ( this.state === DRAG ) { + + this._updatePosition(); + + } else if ( this.state === ROTATE ) { + + const { rotationSpeed } = this; + this._updateRotation( - _deltaPointer.x * rotationSpeed, - _deltaPointer.y * rotationSpeed ); + + } + + } + + }; + + const pointerupCallback = e => { + + this.resetState(); + + pointerTracker.deletePointer( e ); + _pinchAction = NONE; + + if ( pointerTracker.getPointerType() === 'touch' && pointerTracker.getPointerCount() === 0 ) { + + domElement.releasePointerCapture( e.pointerId ); + + } + + }; + + const wheelCallback = e => { + + this._updateZoom( - e.deltaY ); + + }; + + const pointerenterCallback = e => { + + const { pointerTracker } = this; + + shiftClicked = false; + + if ( e.buttons !== pointerTracker.getPointerButtons() ) { + + pointerTracker.deletePointer( e ); + this.resetState(); + + } + + }; + + domElement.addEventListener( 'contextmenu', contextMenuCallback ); + domElement.addEventListener( 'keydown', keydownCallback ); + domElement.addEventListener( 'keyup', keyupCallback ); + domElement.addEventListener( 'pointerdown', pointerdownCallback ); + domElement.addEventListener( 'pointermove', pointermoveCallback ); + domElement.addEventListener( 'pointerup', pointerupCallback ); + domElement.addEventListener( 'wheel', wheelCallback ); + domElement.addEventListener( 'pointerenter', pointerenterCallback ); + + this._detachCallback = () => { + + domElement.removeEventListener( 'contextmenu', contextMenuCallback ); + domElement.removeEventListener( 'keydown', keydownCallback ); + domElement.removeEventListener( 'keyup', keyupCallback ); + domElement.removeEventListener( 'pointerdown', pointerdownCallback ); + domElement.removeEventListener( 'pointermove', pointermoveCallback ); + domElement.removeEventListener( 'pointerup', pointerupCallback ); + domElement.removeEventListener( 'wheel', wheelCallback ); + domElement.removeEventListener( 'pointerenter', pointerenterCallback ); + + }; + + } + + detach() { + + if ( this._detachCallback ) { + + this._detachCallback(); + this._detachCallback = null; + this.pointerTracker = new PointerTracker(); + + } + + } + + resetState() { + + this.state = NONE; + this.dragPointSet = false; + this.rotationPointSet = false; + this.scene.remove( this.pivotMesh ); + this.pivotMesh.visible = true; + + } + + update() { + + const { + raycaster, + camera, + cameraRadius, + dragPoint, + startDragPoint, + up, + } = this; + + if ( this.getUpDirection ) { + + this.getUpDirection( camera.position, _up ); + if ( ! this._upInitialized ) { + + // TODO: do we need to do more here? Possibly add a helper for initializing + // the camera orientation? + this._upInitialized = true; + this.up.copy( _up ); + + } else { + + this.setFrame( _up ); + + } + + } + + // when dragging the camera and drag point may be moved + // to accommodate terrain so we try to move it back down + // to the original point. + if ( this.state === DRAG ) { + + _delta.subVectors( startDragPoint, dragPoint ); + camera.position.add( _delta ); + dragPoint.copy( startDragPoint ); + + } + + // cast down from the camera + const hit = this._getPointBelowCamera(); + if ( hit ) { + + const dist = hit.distance - 1e5; + if ( dist < cameraRadius ) { + + const delta = cameraRadius - dist; + camera.position.copy( hit.point ).addScaledVector( raycaster.ray.direction, - cameraRadius ); + dragPoint.addScaledVector( up, delta ); + + } + + } + + } + + dispose() { + + this.detach(); + + } + + // private + _updateZoom( scale ) { + + const { + zoomPoint, + zoomDirection, + camera, + minDistance, + maxDistance, + raycaster, + pointerTracker, + domElement, + } = this; + + if ( ! pointerTracker.getLatestPoint( _pointer ) ) { + + return; + + } + + mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); + raycaster.setFromCamera( _pointer, camera ); + zoomDirection.copy( raycaster.ray.direction ).normalize(); + this.zoomDirectionSet = true; + + const finalZoomDirection = _vec.copy( zoomDirection ); + + // always update the zoom target point in case the tiles are changing + let dist = Infinity; + if ( this._updateZoomPoint() ) { + + dist = zoomPoint.distanceTo( camera.position ); + + } else { + + // if we're zooming into nothing then use the distance from the ground to scale movement + const hit = this._getPointBelowCamera(); + if ( hit ) { + + dist = hit.distance; + + } else { + + return; + + } + + finalZoomDirection.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); + + } + + // scale the distance based on how far there is to move + if ( scale < 0 ) { + + const remainingDistance = Math.min( 0, dist - maxDistance ); + scale = scale * ( dist - 0 ) * 0.01; + scale = Math.max( scale, remainingDistance ); + + } else { + + const remainingDistance = Math.max( 0, dist - minDistance ); + scale = scale * ( dist - minDistance ) * 0.01; + scale = Math.min( scale, remainingDistance ); + + } + + camera.position.addScaledVector( finalZoomDirection, scale ); + camera.updateMatrixWorld(); + + } + + _updateZoomPoint() { + + const { + camera, + zoomDirectionSet, + zoomDirection, + raycaster, + scene, + zoomPoint, + } = this; + + if ( ! zoomDirectionSet ) { + + return false; + + } + + raycaster.ray.origin.copy( camera.position ); + raycaster.ray.direction.copy( zoomDirection ); + + const hit = raycaster.intersectObject( scene )[ 0 ] || null; + if ( hit ) { + + zoomPoint.copy( hit.point ); + this.zoomPointSet = true; + return true; + + } + + return false; + + } + + _getPointBelowCamera() { + + const { camera, raycaster, scene, up } = this; + raycaster.ray.direction.copy( up ).multiplyScalar( - 1 ); + raycaster.ray.origin.copy( camera.position ).addScaledVector( raycaster.ray.direction, - 1e5 ); + + return raycaster.intersectObject( scene )[ 0 ] || null; + + } + + _updatePosition() { + + const { + raycaster, + camera, + dragPoint, + up, + pointerTracker, + domElement, + } = this; + + pointerTracker.getCenterPoint( _pointer ); + mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); + + _plane.setFromNormalAndCoplanarPoint( up, dragPoint ); + raycaster.setFromCamera( _pointer, camera ); + + // prevent the drag distance from getting too severe + if ( - raycaster.ray.direction.dot( up ) < DRAG_PLANE_THRESHOLD ) { + + const angle = Math.acos( DRAG_PLANE_THRESHOLD ); + + _rotationAxis + .crossVectors( raycaster.ray.direction, up ) + .normalize(); + + raycaster.ray.direction + .copy( up ) + .applyAxisAngle( _rotationAxis, angle ) + .multiplyScalar( - 1 ); + + } + + // TODO: dragging causes the camera to rise because we're getting "pushed" up by lower resolution tiles and + // don't lower back down. We should maintain a target height above tiles where possible + // prevent the drag from inverting + if ( this.getUpDirection ) { + + this.getUpDirection( dragPoint, _up ); + if ( - _up.dot( raycaster.ray.direction ) < DRAG_UP_THRESHOLD ) { + + const angle = Math.acos( DRAG_UP_THRESHOLD ); + + _rotationAxis + .crossVectors( raycaster.ray.direction, _up ) + .normalize(); + + raycaster.ray.direction + .copy( _up ) + .applyAxisAngle( _rotationAxis, angle ) + .multiplyScalar( - 1 ); + + } + + } + + if ( raycaster.ray.intersectPlane( _plane, _vec ) ) { + + _delta.subVectors( dragPoint, _vec ); + this.camera.position.add( _delta ); + this.camera.updateMatrixWorld(); + + } + + } + + _updateRotation() { + + const { + camera, + rotationPoint, + minAltitude, + maxAltitude, + up, + domElement, + pointerTracker, + rotationSpeed, + } = this; + + // get the rotation motion + pointerTracker.getCenterPoint( _pointer ); + mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); + + pointerTracker.getPreviousCenterPoint( _prevPointer ); + mouseToCoords( _prevPointer.x, _prevPointer.y, domElement, _prevPointer ); + + _deltaPointer.subVectors( _pointer, _prevPointer ); + + const azimuth = - _deltaPointer.x * rotationSpeed; + let altitude = - _deltaPointer.y * rotationSpeed; + + // currently uses the camera forward for this work but it may be best to use a combination of camera + // forward and direction to pivot? Or just dir to pivot? + _forward.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ).multiplyScalar( - 1 ); + + const angle = up.angleTo( _forward ); + if ( altitude > 0 ) { + + altitude = Math.min( angle - minAltitude - 1e-2, altitude ); + + } else { + + altitude = Math.max( angle - maxAltitude, altitude ); + + } + + // zoom in frame around pivot point + _quaternion.setFromAxisAngle( up, azimuth ); + makeRotateAroundPoint( rotationPoint, _quaternion, _rotMatrix ); + camera.matrixWorld.premultiply( _rotMatrix ); + + _rotationAxis.set( - 1, 0, 0 ).transformDirection( camera.matrixWorld ); + + _quaternion.setFromAxisAngle( _rotationAxis, altitude ); + makeRotateAroundPoint( rotationPoint, _quaternion, _rotMatrix ); + camera.matrixWorld.premultiply( _rotMatrix ); + + camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); + camera.updateMatrixWorld(); + + } + + setFrame( newUp ) { + + const pivot = new Vector3(); + let dist = 0; + + // cast down from the camera to get the pivot to rotate around + const { up, camera, state, zoomPoint, zoomDirection } = this; + camera.updateMatrixWorld(); + + const hit = this._getPointBelowCamera(); + if ( hit ) { + + _vec.setFromMatrixPosition( camera.matrixWorld ); + + pivot.copy( hit.point ); + dist = pivot.distanceTo( _vec ); + + } else { + + return; + + } + + _quaternion.setFromUnitVectors( up, newUp ); + + if ( this.zoomDirectionSet ) { + + // TODO: just zoom backwards if we're at a steep angle + if ( this.zoomPointSet || this._updateZoomPoint() ) { + + if ( this.reorientOnZoom ) { + + makeRotateAroundPoint( zoomPoint, _quaternion, _rotMatrix ); + camera.matrixWorld.premultiply( _rotMatrix ); + camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); + + zoomDirection.subVectors( zoomPoint, camera.position ).normalize(); + + } + + } else { + + camera.position.copy( pivot ).addScaledVector( newUp, dist ); + camera.quaternion.premultiply( _quaternion ); + camera.updateMatrixWorld(); + + } + + } else if ( state !== ROTATE && this.reorientOnDrag ) { + + // TODO: fix this for dragging from afar + camera.position.copy( pivot ).addScaledVector( newUp, dist ); + camera.quaternion.premultiply( _quaternion ); + camera.updateMatrixWorld(); + + } + + up.copy( newUp ); + + } + +} From 7ba20eb68e3ddf7b36774b6a9a72f4cac0f33951 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Mon, 1 Jan 2024 21:52:08 +0900 Subject: [PATCH 48/64] Fix globe orientation --- example/src/TileControls.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/example/src/TileControls.js b/example/src/TileControls.js index 72137535b..95df4b117 100644 --- a/example/src/TileControls.js +++ b/example/src/TileControls.js @@ -78,7 +78,7 @@ export class TileControls { // settings this.state = NONE; this.cameraRadius = 1; - this.rotationSpeed = 3; + this.rotationSpeed = 5; this.minAltitude = 0; this.maxAltitude = 0.45 * Math.PI; this.minDistance = 2; @@ -713,7 +713,17 @@ export class TileControls { // forward and direction to pivot? Or just dir to pivot? _forward.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ).multiplyScalar( - 1 ); - const angle = up.angleTo( _forward ); + if ( this.getUpDirection ) { + + this.getUpDirection( rotationPoint, _up ); + + } else { + + _up.copy( up ); + + } + + const angle = _up.angleTo( _forward ); if ( altitude > 0 ) { altitude = Math.min( angle - minAltitude - 1e-2, altitude ); @@ -725,7 +735,7 @@ export class TileControls { } // zoom in frame around pivot point - _quaternion.setFromAxisAngle( up, azimuth ); + _quaternion.setFromAxisAngle( _up, azimuth ); makeRotateAroundPoint( rotationPoint, _quaternion, _rotMatrix ); camera.matrixWorld.premultiply( _rotMatrix ); From e9a8bc4ea69cdef1266b0e3f48606cc7df10e754 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Mon, 1 Jan 2024 21:56:11 +0900 Subject: [PATCH 49/64] Fix up orientation issue, embed "getUpDirection --- example/src/GlobeControls.js | 41 ++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index d86b9bffb..623dbc590 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -8,6 +8,7 @@ import { import { TileControls, makeRotateAroundPoint } from './TileControls.js'; import { WGS84_ELLIPSOID } from '../../src/index.js'; +const _invMatrix = new Matrix4(); const _rotMatrix = new Matrix4(); const _vec = new Vector3(); const _forward = new Vector3(); @@ -28,6 +29,17 @@ export class GlobeControls extends TileControls { this._dragMode = 0; this._rotationMode = 0; + this.getUpDirection = ( point, target ) => { + + const { scene } = this; + const invMatrix = _invMatrix.copy( scene.matrixWorld ).invert(); + const pos = point.clone().applyMatrix4( invMatrix ); + + WGS84_ELLIPSOID.getPositionToNormal( pos, target ); + target.transformDirection( scene.matrixWorld ); + + }; + } getVectorToCenter( target ) { @@ -190,11 +202,36 @@ export class GlobeControls extends TileControls { _alignCameraUpToNorth( alpha ) { - const { scene, camera } = this; + const { scene } = this; const _globalUp = new Vector3( 0, 0, 1 ).transformDirection( scene.matrixWorld ); + this._alignCameraUp( _globalUp, alpha ); + + } + + setFrame( ...args ) { + + super.setFrame( ...args ); + + if ( this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { + + this._alignCameraUp( this.up ); + + } + + } + + _alignCameraUp( up, alpha = null ) { + + const { camera } = this; const _forward = new Vector3( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); const _right = new Vector3( - 1, 0, 0 ).transformDirection( camera.matrixWorld ); - const _targetRight = new Vector3().crossVectors( _globalUp, _forward ); + const _targetRight = new Vector3().crossVectors( up, _forward ); + + if ( alpha === null ) { + + alpha = Math.abs( _forward.dot( up ) ); + + } _targetRight.lerp( _right, alpha ).normalize(); From 50360a9e92ae4aebf070d8ada2112df63d600bdb Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Mon, 1 Jan 2024 22:20:03 +0900 Subject: [PATCH 50/64] Cleanup --- example/src/GlobeControls.js | 79 ++++++++++++++++++++++-------------- example/src/TileControls.js | 35 ++++++---------- 2 files changed, 60 insertions(+), 54 deletions(-) diff --git a/example/src/GlobeControls.js b/example/src/GlobeControls.js index 623dbc590..bf5083648 100644 --- a/example/src/GlobeControls.js +++ b/example/src/GlobeControls.js @@ -11,7 +11,12 @@ import { WGS84_ELLIPSOID } from '../../src/index.js'; const _invMatrix = new Matrix4(); const _rotMatrix = new Matrix4(); const _vec = new Vector3(); +const _center = new Vector3(); +const _up = new Vector3(); const _forward = new Vector3(); +const _right = new Vector3(); +const _targetRight = new Vector3(); +const _globalUp = new Vector3(); const _quaternion = new Quaternion(); const _pointer = new Vector2(); @@ -26,22 +31,24 @@ export class GlobeControls extends TileControls { // store which mode the drag stats are in super( ...args ); + this.ellipsoid = WGS84_ELLIPSOID; this._dragMode = 0; this._rotationMode = 0; this.getUpDirection = ( point, target ) => { - const { scene } = this; + const { scene, ellipsoid } = this; const invMatrix = _invMatrix.copy( scene.matrixWorld ).invert(); const pos = point.clone().applyMatrix4( invMatrix ); - WGS84_ELLIPSOID.getPositionToNormal( pos, target ); + ellipsoid.getPositionToNormal( pos, target ); target.transformDirection( scene.matrixWorld ); }; } + // get the vector to the center of the provided globe getVectorToCenter( target ) { const { scene, camera } = this; @@ -51,6 +58,7 @@ export class GlobeControls extends TileControls { } + // get the distance to the center of the globe getDistanceToCenter() { return this @@ -66,7 +74,6 @@ export class GlobeControls extends TileControls { const { camera, scene, - pivotMesh, } = this; // clamp the camera distance @@ -81,15 +88,15 @@ export class GlobeControls extends TileControls { } + // 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 ) { - pivotMesh.visible = false; this.reorientOnDrag = false; this.reorientOnZoom = true; } else { - pivotMesh.visible = true; this.reorientOnDrag = true; this.reorientOnZoom = false; @@ -103,6 +110,7 @@ export class GlobeControls extends TileControls { } + // resets the "stuck" drag modes resetState() { super.resetState(); @@ -111,6 +119,19 @@ export class GlobeControls extends TileControls { } + // animate the frame to align to an up direction + setFrame( ...args ) { + + super.setFrame( ...args ); + + if ( this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { + + this._alignCameraUp( this.up ); + + } + + } + _updatePosition( ...args ) { if ( this._dragMode === 1 || this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { @@ -128,21 +149,26 @@ export class GlobeControls extends TileControls { rotationSpeed, camera, pivotMesh, + dragPoint, } = this; + + // get the delta movement with magic numbers scaled by the distance to the + // grabbed point so it feels okay + // TODO: it would be better to properly calculate angle based on drag distance pointerTracker.getCenterPoint( _pointer ); pointerTracker.getPreviousCenterPoint( _prevPointer ); - _deltaPointer .subVectors( _pointer, _prevPointer ) - .multiplyScalar( camera.position.distanceTo( this.dragPoint ) * 1e-8 * 5 * 1e-3 ); + .multiplyScalar( camera.position.distanceTo( dragPoint ) * 1e-11 * 5 ); const azimuth = - _deltaPointer.x * rotationSpeed; const altitude = - _deltaPointer.y * rotationSpeed; - const _center = new Vector3().setFromMatrixPosition( this.scene.matrixWorld ); - const _right = new Vector3( 1, 0, 0 ).transformDirection( camera.matrixWorld ); - const _up = new Vector3( 0, 1, 0 ).transformDirection( camera.matrixWorld ); + _center.setFromMatrixPosition( this.scene.matrixWorld ); + _right.set( 1, 0, 0 ).transformDirection( camera.matrixWorld ); + _up.set( 0, 1, 0 ).transformDirection( camera.matrixWorld ); + // apply the altitude and azimuth adjustment _quaternion.setFromAxisAngle( _right, altitude ); camera.quaternion.premultiply( _quaternion ); makeRotateAroundPoint( _center, _quaternion, _rotMatrix ); @@ -157,11 +183,11 @@ export class GlobeControls extends TileControls { pivotMesh.visible = false; - } } + // disable rotation once we're outside the control transition _updateRotation( ...args ) { if ( this._rotationMode === 1 || this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { @@ -171,6 +197,7 @@ export class GlobeControls extends TileControls { } else { + this.pivotMesh.visible = false; this._rotationMode = - 1; } @@ -185,47 +212,36 @@ export class GlobeControls extends TileControls { } else { - // orient the camera during the zoom + // 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 ) ); + // zoom out directly from the globe center this.getVectorToCenter( _vec ); - const dist = _vec.length(); - - this.camera.position.addScaledVector( _vec, delta * 0.0025 * dist / dist ); + this.camera.position.addScaledVector( _vec, delta * 0.0025 ); this.camera.updateMatrixWorld(); } } + // tilt the camera to align with north _alignCameraUpToNorth( alpha ) { const { scene } = this; - const _globalUp = new Vector3( 0, 0, 1 ).transformDirection( scene.matrixWorld ); + _globalUp.set( 0, 0, 1 ).transformDirection( scene.matrixWorld ); this._alignCameraUp( _globalUp, alpha ); } - setFrame( ...args ) { - - super.setFrame( ...args ); - - if ( this.getDistanceToCenter() < GLOBE_TRANSITION_THRESHOLD ) { - - this._alignCameraUp( this.up ); - - } - - } - + // tilt the camera to align with the provided "up" value _alignCameraUp( up, alpha = null ) { const { camera } = this; - const _forward = new Vector3( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); - const _right = new Vector3( - 1, 0, 0 ).transformDirection( camera.matrixWorld ); - const _targetRight = new Vector3().crossVectors( up, _forward ); + _forward.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); + _right.set( - 1, 0, 0 ).transformDirection( camera.matrixWorld ); + _targetRight.crossVectors( up, _forward ); if ( alpha === null ) { @@ -241,6 +257,7 @@ export class GlobeControls extends TileControls { } + // tilt the camera to look at the center of the globe _tiltTowardsCenter( alpha ) { const { diff --git a/example/src/TileControls.js b/example/src/TileControls.js index 95df4b117..c66d5376a 100644 --- a/example/src/TileControls.js +++ b/example/src/TileControls.js @@ -25,7 +25,7 @@ const _forward = new Vector3(); const _rotationAxis = new Vector3(); const _quaternion = new Quaternion(); const _plane = new Plane(); -const _up = new Vector3(); +const _localUp = new Vector3(); const _pointer = new Vector2(); const _prevPointer = new Vector2(); @@ -33,17 +33,6 @@ const _deltaPointer = new Vector2(); const _centerPoint = new Vector2(); const _originalCenterPoint = new Vector2(); -// TODO -// - Add support for angled rotation plane (based on where the pivot point is) -// - Test with globe (adjusting up vector) -// --- -// - Consider using sphere intersect for positioning -// - Toggles for zoom to cursor, zoom forward, orbit around center, etc? -// - provide fallback plane for cases when you're off the map -// - consider enabling drag with zoom -// - shift + scroll could adjust altitude -// - fade pivot icon in and out - // helper function for constructing a matrix for rotating around a point export function makeRotateAroundPoint( point, quat, target ) { @@ -454,17 +443,17 @@ export class TileControls { if ( this.getUpDirection ) { - this.getUpDirection( camera.position, _up ); + this.getUpDirection( camera.position, _localUp ); if ( ! this._upInitialized ) { // TODO: do we need to do more here? Possibly add a helper for initializing // the camera orientation? this._upInitialized = true; - this.up.copy( _up ); + this.up.copy( _localUp ); } else { - this.setFrame( _up ); + this.setFrame( _localUp ); } @@ -656,17 +645,17 @@ export class TileControls { // prevent the drag from inverting if ( this.getUpDirection ) { - this.getUpDirection( dragPoint, _up ); - if ( - _up.dot( raycaster.ray.direction ) < DRAG_UP_THRESHOLD ) { + this.getUpDirection( dragPoint, _localUp ); + if ( - _localUp.dot( raycaster.ray.direction ) < DRAG_UP_THRESHOLD ) { const angle = Math.acos( DRAG_UP_THRESHOLD ); _rotationAxis - .crossVectors( raycaster.ray.direction, _up ) + .crossVectors( raycaster.ray.direction, _localUp ) .normalize(); raycaster.ray.direction - .copy( _up ) + .copy( _localUp ) .applyAxisAngle( _rotationAxis, angle ) .multiplyScalar( - 1 ); @@ -715,15 +704,15 @@ export class TileControls { if ( this.getUpDirection ) { - this.getUpDirection( rotationPoint, _up ); + this.getUpDirection( rotationPoint, _localUp ); } else { - _up.copy( up ); + _localUp.copy( up ); } - const angle = _up.angleTo( _forward ); + const angle = _localUp.angleTo( _forward ); if ( altitude > 0 ) { altitude = Math.min( angle - minAltitude - 1e-2, altitude ); @@ -735,7 +724,7 @@ export class TileControls { } // zoom in frame around pivot point - _quaternion.setFromAxisAngle( _up, azimuth ); + _quaternion.setFromAxisAngle( _localUp, azimuth ); makeRotateAroundPoint( rotationPoint, _quaternion, _rotMatrix ); camera.matrixWorld.premultiply( _rotMatrix ); From b498c06266d964246692154e7d305647f9114fbe Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Mon, 1 Jan 2024 22:25:00 +0900 Subject: [PATCH 51/64] Move to controls folder --- example/src/{ => controls}/GlobeControls.js | 5 ++-- example/src/{ => controls}/PivotPointMesh.js | 0 example/src/{ => controls}/PointerTracker.js | 0 example/src/{ => controls}/TileControls.js | 24 +----------------- example/src/controls/utils.js | 26 ++++++++++++++++++++ 5 files changed, 30 insertions(+), 25 deletions(-) rename example/src/{ => controls}/GlobeControls.js (97%) rename example/src/{ => controls}/PivotPointMesh.js (100%) rename example/src/{ => controls}/PointerTracker.js (100%) rename example/src/{ => controls}/TileControls.js (96%) create mode 100644 example/src/controls/utils.js diff --git a/example/src/GlobeControls.js b/example/src/controls/GlobeControls.js similarity index 97% rename from example/src/GlobeControls.js rename to example/src/controls/GlobeControls.js index bf5083648..a89d8af59 100644 --- a/example/src/GlobeControls.js +++ b/example/src/controls/GlobeControls.js @@ -5,8 +5,9 @@ import { Vector3, MathUtils, } from 'three'; -import { TileControls, makeRotateAroundPoint } from './TileControls.js'; -import { WGS84_ELLIPSOID } from '../../src/index.js'; +import { TileControls } from './TileControls.js'; +import { makeRotateAroundPoint } from './utils.js'; +import { WGS84_ELLIPSOID } from '../../../src/index.js'; const _invMatrix = new Matrix4(); const _rotMatrix = new Matrix4(); diff --git a/example/src/PivotPointMesh.js b/example/src/controls/PivotPointMesh.js similarity index 100% rename from example/src/PivotPointMesh.js rename to example/src/controls/PivotPointMesh.js diff --git a/example/src/PointerTracker.js b/example/src/controls/PointerTracker.js similarity index 100% rename from example/src/PointerTracker.js rename to example/src/controls/PointerTracker.js diff --git a/example/src/TileControls.js b/example/src/controls/TileControls.js similarity index 96% rename from example/src/TileControls.js rename to example/src/controls/TileControls.js index c66d5376a..0c8c0f5e7 100644 --- a/example/src/TileControls.js +++ b/example/src/controls/TileControls.js @@ -8,6 +8,7 @@ import { } from 'three'; import { PivotPointMesh } from './PivotPointMesh.js'; import { PointerTracker } from './PointerTracker.js'; +import { mouseToCoords, makeRotateAroundPoint } from './utils.js'; const NONE = 0; const DRAG = 1; @@ -33,29 +34,6 @@ const _deltaPointer = new Vector2(); const _centerPoint = new Vector2(); const _originalCenterPoint = new Vector2(); -// helper function for constructing a matrix for rotating around a point -export function makeRotateAroundPoint( point, quat, target ) { - - target.makeTranslation( - point.x, - point.y, - point.z ); - - _matrix.makeRotationFromQuaternion( quat ); - target.premultiply( _matrix ); - - _matrix.makeTranslation( point.x, point.y, point.z ); - target.premultiply( _matrix ); - - return target; - -} - -// get the three.js pointer coords from an event -function mouseToCoords( clientX, clientY, element, target ) { - - target.x = ( ( clientX - element.offsetLeft ) / element.clientWidth ) * 2 - 1; - target.y = - ( ( clientY - element.offsetTop ) / element.clientHeight ) * 2 + 1; - -} - export class TileControls { constructor( scene, camera, domElement ) { diff --git a/example/src/controls/utils.js b/example/src/controls/utils.js new file mode 100644 index 000000000..9fb6e0255 --- /dev/null +++ b/example/src/controls/utils.js @@ -0,0 +1,26 @@ +import { Matrix4 } from 'three'; + +const _matrix = new Matrix4(); + +// helper function for constructing a matrix for rotating around a point +export function makeRotateAroundPoint( point, quat, target ) { + + target.makeTranslation( - point.x, - point.y, - point.z ); + + _matrix.makeRotationFromQuaternion( quat ); + target.premultiply( _matrix ); + + _matrix.makeTranslation( point.x, point.y, point.z ); + target.premultiply( _matrix ); + + return target; + +} + +// get the three.js pointer coords from an event +export function mouseToCoords( clientX, clientY, element, target ) { + + target.x = ( ( clientX - element.offsetLeft ) / element.clientWidth ) * 2 - 1; + target.y = - ( ( clientY - element.offsetTop ) / element.clientHeight ) * 2 + 1; + +} From a47bd0d43c1bcd2c1b1ef94f1cf014dd149aae10 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Mon, 1 Jan 2024 22:57:12 +0900 Subject: [PATCH 52/64] Cleanup --- example/src/controls/TileControls.js | 111 +++++++++++++++------------ 1 file changed, 64 insertions(+), 47 deletions(-) diff --git a/example/src/controls/TileControls.js b/example/src/controls/TileControls.js index 0c8c0f5e7..a07df799c 100644 --- a/example/src/controls/TileControls.js +++ b/example/src/controls/TileControls.js @@ -18,7 +18,7 @@ const ZOOM = 3; const DRAG_PLANE_THRESHOLD = 0.05; const DRAG_UP_THRESHOLD = 0.025; -const _matrix = new Matrix4(); +const _pivot = new Vector3(); const _rotMatrix = new Matrix4(); const _delta = new Vector3(); const _vec = new Vector3(); @@ -108,12 +108,12 @@ export class TileControls { } + // set the touch action to none so the browser does not + // drag the page to refresh or scroll this.domElement = domElement; domElement.style.touchAction = 'none'; - let _pinchAction = NONE; - - const pointerTracker = this.pointerTracker; + let pinchAction = NONE; let shiftClicked = false; const contextMenuCallback = e => { @@ -154,10 +154,12 @@ export class TileControls { pointerTracker, } = this; - // init fields + // init the pointer pointerTracker.addPointer( e ); - if ( e.pointerType === 'touch' ) { + // handle cases where we need to capture the pointer or + // reset state when we have too many pointers + if ( pointerTracker.getPointerType() === 'touch' ) { pivotMesh.visible = false; @@ -168,6 +170,7 @@ export class TileControls { } else if ( pointerTracker.getPointerCount() > 2 ) { this.resetState(); + pinchAction = NONE; return; } @@ -191,8 +194,6 @@ export class TileControls { pointerTracker.isLeftClicked() && shiftClicked ) { - _matrix.copy( camera.matrixWorld ).invert(); - this.state = ROTATE; this.rotationPoint.copy( hit.point ); this.rotationPointSet = true; @@ -226,11 +227,11 @@ export class TileControls { let _pointerMoveQueued = false; const pointermoveCallback = e => { + // whenever the pointer moves we need to re-derive the zoom direction and point this.zoomDirectionSet = false; this.zoomPointSet = false; const { pointerTracker } = this; - pointerTracker.setHoverEvent( e ); if ( ! pointerTracker.updatePointer( e ) ) { @@ -265,9 +266,10 @@ export class TileControls { const previousDist = pointerTracker.getPreviousPointerDistance(); const pointerDist = pointerTracker.getPointerDistance(); const separateDelta = pointerDist - previousDist; - if ( _pinchAction === NONE ) { + if ( pinchAction === NONE ) { - // check which direction was moved in first + // check which direction was moved in first - if the pointers are pinching then + // it's a zoom. But if they move in parallel it's a rotation pointerTracker.getCenterPoint( _centerPoint ); pointerTracker.getPreviousCenterPoint( _originalCenterPoint ); @@ -277,13 +279,12 @@ export class TileControls { if ( separateDelta > parallelDelta ) { this.resetState(); - _pinchAction = ZOOM; + pinchAction = ZOOM; this.zoomDirectionSet = false; } else { - _pinchAction = ROTATE; - + pinchAction = ROTATE; } @@ -291,12 +292,11 @@ export class TileControls { } - if ( _pinchAction === ZOOM ) { + if ( pinchAction === ZOOM ) { - // perform zoom this._updateZoom( separateDelta ); - } else if ( _pinchAction === ROTATE ) { + } else if ( pinchAction === ROTATE ) { this._updateRotation(); this.pivotMesh.visible = true; @@ -317,8 +317,7 @@ export class TileControls { } else if ( this.state === ROTATE ) { - const { rotationSpeed } = this; - this._updateRotation( - _deltaPointer.x * rotationSpeed, - _deltaPointer.y * rotationSpeed ); + this._updateRotation(); } @@ -328,17 +327,22 @@ export class TileControls { const pointerupCallback = e => { - this.resetState(); + const { pointerTracker } = this; pointerTracker.deletePointer( e ); - _pinchAction = NONE; + pinchAction = NONE; - if ( pointerTracker.getPointerType() === 'touch' && pointerTracker.getPointerCount() === 0 ) { + if ( + pointerTracker.getPointerType() === 'touch' && + pointerTracker.getPointerCount() === 0 + ) { domElement.releasePointerCapture( e.pointerId ); } + this.resetState(); + }; const wheelCallback = e => { @@ -424,8 +428,6 @@ export class TileControls { this.getUpDirection( camera.position, _localUp ); if ( ! this._upInitialized ) { - // TODO: do we need to do more here? Possibly add a helper for initializing - // the camera orientation? this._upInitialized = true; this.up.copy( _localUp ); @@ -485,17 +487,20 @@ export class TileControls { domElement, } = this; + // get the latest hover / touch point if ( ! pointerTracker.getLatestPoint( _pointer ) ) { return; } + // initialize the zoom direction mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); raycaster.setFromCamera( _pointer, camera ); zoomDirection.copy( raycaster.ray.direction ).normalize(); this.zoomDirectionSet = true; + // track the zoom direction we're going to use const finalZoomDirection = _vec.copy( zoomDirection ); // always update the zoom target point in case the tiles are changing @@ -511,6 +516,7 @@ export class TileControls { if ( hit ) { dist = hit.distance; + finalZoomDirection.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); } else { @@ -518,8 +524,6 @@ export class TileControls { } - finalZoomDirection.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); - } // scale the distance based on how far there is to move @@ -542,6 +546,7 @@ export class TileControls { } + // update the point being zoomed in to based on the zoom direction _updateZoomPoint() { const { @@ -575,16 +580,18 @@ export class TileControls { } + // returns the point below the camera _getPointBelowCamera() { const { camera, raycaster, scene, up } = this; raycaster.ray.direction.copy( up ).multiplyScalar( - 1 ); - raycaster.ray.origin.copy( camera.position ).addScaledVector( raycaster.ray.direction, - 1e5 ); + raycaster.ray.origin.copy( camera.position ).addScaledVector( up, 1e5 ); return raycaster.intersectObject( scene )[ 0 ] || null; } + // update the drag action _updatePosition() { const { @@ -596,15 +603,18 @@ export class TileControls { domElement, } = this; + // get the pointer and plane pointerTracker.getCenterPoint( _pointer ); mouseToCoords( _pointer.x, _pointer.y, domElement, _pointer ); _plane.setFromNormalAndCoplanarPoint( up, dragPoint ); raycaster.setFromCamera( _pointer, camera ); - // prevent the drag distance from getting too severe + // prevent the drag distance from getting too severe by limiting the drag point + // to a reasonable angle with the drag plane if ( - raycaster.ray.direction.dot( up ) < DRAG_PLANE_THRESHOLD ) { + // rotate the pointer direction down to the correct angle for horizontal draggin const angle = Math.acos( DRAG_PLANE_THRESHOLD ); _rotationAxis @@ -623,8 +633,10 @@ export class TileControls { // prevent the drag from inverting if ( this.getUpDirection ) { + // if we drag to a point that's near the edge of the earth then we want to prevent it + // from wrapping around and causing unexpected rotations this.getUpDirection( dragPoint, _localUp ); - if ( - _localUp.dot( raycaster.ray.direction ) < DRAG_UP_THRESHOLD ) { + if ( - raycaster.ray.direction.dot( _localUp ) < DRAG_UP_THRESHOLD ) { const angle = Math.acos( DRAG_UP_THRESHOLD ); @@ -641,6 +653,7 @@ export class TileControls { } + // find the point on the plane that we should drag to if ( raycaster.ray.intersectPlane( _plane, _vec ) ) { _delta.subVectors( dragPoint, _vec ); @@ -676,9 +689,11 @@ export class TileControls { const azimuth = - _deltaPointer.x * rotationSpeed; let altitude = - _deltaPointer.y * rotationSpeed; - // currently uses the camera forward for this work but it may be best to use a combination of camera - // forward and direction to pivot? Or just dir to pivot? - _forward.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ).multiplyScalar( - 1 ); + // calculate current angles and clamp + _forward + .set( 0, 0, - 1 ) + .transformDirection( camera.matrixWorld ) + .multiplyScalar( - 1 ); if ( this.getUpDirection ) { @@ -701,38 +716,37 @@ export class TileControls { } - // zoom in frame around pivot point + // rotate around the up axis _quaternion.setFromAxisAngle( _localUp, azimuth ); makeRotateAroundPoint( rotationPoint, _quaternion, _rotMatrix ); camera.matrixWorld.premultiply( _rotMatrix ); + // get a rotation axis for altitude and rotate _rotationAxis.set( - 1, 0, 0 ).transformDirection( camera.matrixWorld ); _quaternion.setFromAxisAngle( _rotationAxis, altitude ); makeRotateAroundPoint( rotationPoint, _quaternion, _rotMatrix ); camera.matrixWorld.premultiply( _rotMatrix ); + // update the transform members camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); - camera.updateMatrixWorld(); } + // sets the "up" axis for the current surface of the tile set setFrame( newUp ) { - const pivot = new Vector3(); - let dist = 0; - - // cast down from the camera to get the pivot to rotate around const { up, camera, state, zoomPoint, zoomDirection } = this; camera.updateMatrixWorld(); + // TODO: this raycast isn't necessary a lot of the time + // get the pivot to rotate the frame if needed + let dist = 0; const hit = this._getPointBelowCamera(); if ( hit ) { - _vec.setFromMatrixPosition( camera.matrixWorld ); - - pivot.copy( hit.point ); - dist = pivot.distanceTo( _vec ); + _pivot.copy( hit.point ); + dist = _pivot.distanceTo( camera.position ); } else { @@ -740,15 +754,16 @@ export class TileControls { } + // get the amount needed to rotate _quaternion.setFromUnitVectors( up, newUp ); if ( this.zoomDirectionSet ) { - // TODO: just zoom backwards if we're at a steep angle if ( this.zoomPointSet || this._updateZoomPoint() ) { if ( this.reorientOnZoom ) { + // rotates the camera position around the point being zoomed in to makeRotateAroundPoint( zoomPoint, _quaternion, _rotMatrix ); camera.matrixWorld.premultiply( _rotMatrix ); camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); @@ -759,16 +774,18 @@ export class TileControls { } else { - camera.position.copy( pivot ).addScaledVector( newUp, dist ); + // TODO: if zooming into the sky we should reorient and zoom parallel to the ground + // if zooming in or out from the sky we just adjust to the ground orientation + camera.position.copy( _pivot ).addScaledVector( newUp, dist ); camera.quaternion.premultiply( _quaternion ); camera.updateMatrixWorld(); } - } else if ( state !== ROTATE && this.reorientOnDrag ) { + } else if ( state === NONE || state === DRAG && this.reorientOnDrag ) { - // TODO: fix this for dragging from afar - camera.position.copy( pivot ).addScaledVector( newUp, dist ); + // perform a simple realignment + camera.position.copy( _pivot ).addScaledVector( newUp, dist ); camera.quaternion.premultiply( _quaternion ); camera.updateMatrixWorld(); From 0040ab5b7cd17e364c577ef9bc36d5ef924103a6 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Mon, 1 Jan 2024 23:25:46 +0900 Subject: [PATCH 53/64] clean up, comments --- example/src/controls/TileControls.js | 108 ++++++++++++--------------- 1 file changed, 48 insertions(+), 60 deletions(-) diff --git a/example/src/controls/TileControls.js b/example/src/controls/TileControls.js index a07df799c..f87a73346 100644 --- a/example/src/controls/TileControls.js +++ b/example/src/controls/TileControls.js @@ -44,11 +44,11 @@ export class TileControls { // settings this.state = NONE; - this.cameraRadius = 1; + this.cameraRadius = 5; this.rotationSpeed = 5; this.minAltitude = 0; this.maxAltitude = 0.45 * Math.PI; - this.minDistance = 2; + this.minDistance = 10; this.maxDistance = Infinity; this.getUpDirection = null; this.reorientOnDrag = true; @@ -454,7 +454,7 @@ export class TileControls { const hit = this._getPointBelowCamera(); if ( hit ) { - const dist = hit.distance - 1e5; + const dist = hit.distance; if ( dist < cameraRadius ) { const delta = cameraRadius - dist; @@ -509,41 +509,39 @@ export class TileControls { dist = zoomPoint.distanceTo( camera.position ); - } else { - - // if we're zooming into nothing then use the distance from the ground to scale movement - const hit = this._getPointBelowCamera(); - if ( hit ) { + // scale the distance based on how far there is to move + if ( scale < 0 ) { - dist = hit.distance; - finalZoomDirection.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); + const remainingDistance = Math.min( 0, dist - maxDistance ); + scale = scale * ( dist - 0 ) * 0.01; + scale = Math.max( scale, remainingDistance ); } else { - return; + const remainingDistance = Math.max( 0, dist - minDistance ); + scale = scale * ( dist - minDistance ) * 0.01; + scale = Math.min( scale, remainingDistance ); } - } + camera.position.addScaledVector( zoomDirection, scale ); + camera.updateMatrixWorld(); - // 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 - 0 ) * 0.01; - scale = Math.max( scale, remainingDistance ); + // if we're zooming into nothing then use the distance from the ground to scale movement + const hit = this._getPointBelowCamera(); + if ( hit ) { - } else { + dist = hit.distance; + finalZoomDirection.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld ); + camera.position.addScaledVector( finalZoomDirection, scale * dist * 0.01 ); + camera.updateMatrixWorld(); - const remainingDistance = Math.max( 0, dist - minDistance ); - scale = scale * ( dist - minDistance ) * 0.01; - scale = Math.min( scale, remainingDistance ); + } } - camera.position.addScaledVector( finalZoomDirection, scale ); - camera.updateMatrixWorld(); - } // update the point being zoomed in to based on the zoom direction @@ -587,7 +585,14 @@ export class TileControls { raycaster.ray.direction.copy( up ).multiplyScalar( - 1 ); raycaster.ray.origin.copy( camera.position ).addScaledVector( up, 1e5 ); - return raycaster.intersectObject( scene )[ 0 ] || null; + const hit = raycaster.intersectObject( scene )[ 0 ] || null; + if ( hit ) { + + hit.distance -= 1e5; + + } + + return hit; } @@ -614,7 +619,7 @@ export class TileControls { // to a reasonable angle with the drag plane if ( - raycaster.ray.direction.dot( up ) < DRAG_PLANE_THRESHOLD ) { - // rotate the pointer direction down to the correct angle for horizontal draggin + // rotate the pointer direction down to the correct angle for horizontal dragging const angle = Math.acos( DRAG_PLANE_THRESHOLD ); _rotationAxis @@ -739,56 +744,39 @@ export class TileControls { const { up, camera, state, zoomPoint, zoomDirection } = this; camera.updateMatrixWorld(); - // TODO: this raycast isn't necessary a lot of the time - // get the pivot to rotate the frame if needed - let dist = 0; - const hit = this._getPointBelowCamera(); - if ( hit ) { - - _pivot.copy( hit.point ); - dist = _pivot.distanceTo( camera.position ); - - } else { - - return; - - } - // get the amount needed to rotate _quaternion.setFromUnitVectors( up, newUp ); - if ( this.zoomDirectionSet ) { + if ( this.zoomDirectionSet && ( this.zoomPointSet || this._updateZoomPoint() ) ) { - if ( this.zoomPointSet || this._updateZoomPoint() ) { + if ( this.reorientOnZoom ) { - if ( this.reorientOnZoom ) { + // rotates the camera position around the point being zoomed in to + makeRotateAroundPoint( zoomPoint, _quaternion, _rotMatrix ); + camera.matrixWorld.premultiply( _rotMatrix ); + camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); - // rotates the camera position around the point being zoomed in to - makeRotateAroundPoint( zoomPoint, _quaternion, _rotMatrix ); - camera.matrixWorld.premultiply( _rotMatrix ); - camera.matrixWorld.decompose( camera.position, camera.quaternion, _vec ); + zoomDirection.subVectors( zoomPoint, camera.position ).normalize(); - zoomDirection.subVectors( zoomPoint, camera.position ).normalize(); + } - } + } else if ( state === NONE || state === DRAG && this.reorientOnDrag ) { - } else { + // get the pivot to rotate the frame if needed + let dist = 0; + const hit = this._getPointBelowCamera(); + if ( hit ) { + + _pivot.copy( hit.point ); + dist = _pivot.distanceTo( camera.position ); - // TODO: if zooming into the sky we should reorient and zoom parallel to the ground - // if zooming in or out from the sky we just adjust to the ground orientation + // perform a simple realignment camera.position.copy( _pivot ).addScaledVector( newUp, dist ); camera.quaternion.premultiply( _quaternion ); camera.updateMatrixWorld(); } - } else if ( state === NONE || state === DRAG && this.reorientOnDrag ) { - - // perform a simple realignment - camera.position.copy( _pivot ).addScaledVector( newUp, dist ); - camera.quaternion.premultiply( _quaternion ); - camera.updateMatrixWorld(); - } up.copy( newUp ); From 638c4d378d144674cfe47e0ca35b8600a7e0ab61 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Mon, 1 Jan 2024 23:29:19 +0900 Subject: [PATCH 54/64] Move getUpDirection function --- example/src/controls/GlobeControls.js | 23 ++++++++++++----------- example/src/controls/TileControls.js | 7 ++++++- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/example/src/controls/GlobeControls.js b/example/src/controls/GlobeControls.js index a89d8af59..0197ac23c 100644 --- a/example/src/controls/GlobeControls.js +++ b/example/src/controls/GlobeControls.js @@ -36,17 +36,6 @@ export class GlobeControls extends TileControls { this._dragMode = 0; this._rotationMode = 0; - this.getUpDirection = ( point, target ) => { - - const { scene, ellipsoid } = this; - const invMatrix = _invMatrix.copy( scene.matrixWorld ).invert(); - const pos = point.clone().applyMatrix4( invMatrix ); - - ellipsoid.getPositionToNormal( pos, target ); - target.transformDirection( scene.matrixWorld ); - - }; - } // get the vector to the center of the provided globe @@ -68,6 +57,18 @@ export class GlobeControls extends TileControls { } + getUpDirection( point, target ) { + + // get the "up" direction based on the wgs84 ellipsoid + const { scene, ellipsoid } = this; + const invMatrix = _invMatrix.copy( scene.matrixWorld ).invert(); + const pos = _vec.copy( point ).applyMatrix4( invMatrix ); + + ellipsoid.getPositionToNormal( pos, target ); + target.transformDirection( scene.matrixWorld ); + + } + update() { super.update(); diff --git a/example/src/controls/TileControls.js b/example/src/controls/TileControls.js index f87a73346..d5c161478 100644 --- a/example/src/controls/TileControls.js +++ b/example/src/controls/TileControls.js @@ -50,7 +50,6 @@ export class TileControls { this.maxAltitude = 0.45 * Math.PI; this.minDistance = 10; this.maxDistance = Infinity; - this.getUpDirection = null; this.reorientOnDrag = true; this.reorientOnZoom = false; @@ -390,6 +389,12 @@ export class TileControls { } + getUpDirection( point, target ) { + + target.copy( this.up ); + + } + detach() { if ( this._detachCallback ) { From 5ff26b1a328b9513e82dbe950a57a7923c364b6c Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Mon, 1 Jan 2024 23:35:23 +0900 Subject: [PATCH 55/64] Fix rotation limit --- example/src/controls/TileControls.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/example/src/controls/TileControls.js b/example/src/controls/TileControls.js index d5c161478..7560ef2f5 100644 --- a/example/src/controls/TileControls.js +++ b/example/src/controls/TileControls.js @@ -715,14 +715,18 @@ export class TileControls { } + // 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 const angle = _localUp.angleTo( _forward ); if ( altitude > 0 ) { altitude = Math.min( angle - minAltitude - 1e-2, altitude ); + altitude = Math.max( 0, altitude ); } else { altitude = Math.max( angle - maxAltitude, altitude ); + altitude = Math.min( 0, altitude ); } From e8bb741e95daab4beaf9c029c3ca74b58b590b98 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Mon, 1 Jan 2024 23:41:13 +0900 Subject: [PATCH 56/64] Update google maps example --- example/googleMapsExample.js | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/example/googleMapsExample.js b/example/googleMapsExample.js index f8a3c7c7b..f83eb030c 100644 --- a/example/googleMapsExample.js +++ b/example/googleMapsExample.js @@ -31,7 +31,6 @@ const params = { 'apiKey': apiKey, 'displayBoxBounds': false, - 'displaySphereBounds': false, 'displayRegionBounds': false, 'reload': reinstantiateTiles, @@ -107,7 +106,6 @@ function init() { const debug = gui.addFolder( 'Debug Options' ); debug.add( params, 'displayBoxBounds' ); - debug.add( params, 'displaySphereBounds' ); debug.add( params, 'displayRegionBounds' ); const exampleOptions = gui.addFolder( 'Example Options' ); @@ -234,6 +232,8 @@ function animate() { // update options tiles.setResolutionFromRenderer( camera, renderer ); tiles.setCamera( camera ); + tiles.displayBoxBounds = params.displayBoxBounds; + tiles.displayRegionBounds = params.displayRegionBounds; // update tiles if ( params.enableUpdate ) { @@ -243,15 +243,14 @@ function animate() { } - render(); + renderer.render( scene, camera ); stats.update(); -} + updateHtml(); -function render() { +} - // render primary view - renderer.render( scene, camera ); +function updateHtml() { // render html text updates const cacheFullness = tiles.lruCache.itemList.length / tiles.lruCache.maxSize; @@ -303,15 +302,11 @@ function render() { } - if ( tiles ) { - - const mat = tiles.group.matrixWorld.clone().invert(); - const vec = camera.position.clone().applyMatrix4( mat ); - - const res = {}; - WGS84_ELLIPSOID.getPositionToCartographic( vec, res ); - document.getElementById( 'credits' ).innerText = GeoUtils.toLatLonString( res.lat, res.lon ) + '\n' + tiles.getCreditsString(); + const mat = tiles.group.matrixWorld.clone().invert(); + const vec = camera.position.clone().applyMatrix4( mat ); - } + const res = {}; + WGS84_ELLIPSOID.getPositionToCartographic( vec, res ); + document.getElementById( 'credits' ).innerText = GeoUtils.toLatLonString( res.lat, res.lon ) + '\n' + tiles.getCreditsString(); } From ee1910b01772f9bb44d4c71dd78fb19e8277d77a Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Mon, 1 Jan 2024 23:50:36 +0900 Subject: [PATCH 57/64] Start cleanup of google tiles example --- example/googleMapsExample.js | 74 ++++++++++++------------------------ 1 file changed, 25 insertions(+), 49 deletions(-) diff --git a/example/googleMapsExample.js b/example/googleMapsExample.js index f83eb030c..aac4e7957 100644 --- a/example/googleMapsExample.js +++ b/example/googleMapsExample.js @@ -10,8 +10,9 @@ import { import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'; import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'; -import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'; +import { estimateBytesUsed } from 'three/examples/jsm/utils/BufferGeometryUtils.js'; import Stats from 'three/examples/jsm/libs/stats.module.js'; +import { GlobeControls } from './src/controls/GlobeControls.js'; import { MapControls } from './src/lib/MapControls.js'; @@ -52,7 +53,7 @@ function reinstantiateTiles() { } tiles = new GoogleTilesRenderer( params.apiKey ); - tiles.group.rotation.x = - Math.PI / 2; + // tiles.group.rotation.x = - Math.PI / 2; // Note the DRACO compression files need to be supplied via an explicit source. // We use unpkg here but in practice should be provided by the application. @@ -68,6 +69,8 @@ function reinstantiateTiles() { tiles.setResolutionFromRenderer( camera, renderer ); tiles.setCamera( camera ); + controls.setScene( tiles.group ); + } function init() { @@ -82,13 +85,14 @@ function init() { camera = new PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 160000000 ); camera.position.set( 7326000, 10279000, - 823000 ); + camera.lookAt( 0, 0, 0 ); // controls - controls = new MapControls( camera, renderer.domElement ); - controls.minDistance = 1; - controls.maxDistance = Infinity; - controls.minPolarAngle = Math.PI / 4; - controls.target.set( 0, 0, 1 ); + // controls = new MapControls( camera, renderer.domElement ); + controls = new GlobeControls( scene, camera, renderer.domElement ); + + // console.log( GlobeControls ) + reinstantiateTiles(); @@ -152,34 +156,6 @@ function onWindowResize() { function updateControls() { - const raycaster = new Raycaster(); - raycaster.ray.origin.copy( controls.target ).normalize().multiplyScalar( WGS84_RADIUS * 1.5 ); - raycaster.ray.direction.copy( raycaster.ray.origin ).normalize().multiplyScalar( - 1 ); - raycaster.firstHitOnly = true; - - const hit = raycaster.intersectObject( scene, true )[ 0 ]; - if ( hit ) { - - controls.target.copy( hit.point ); - - } else { - - controls.target.normalize().multiplyScalar( WGS84_RADIUS ); - - } - controls.panPlane.copy( controls.target ).normalize(); - - const dist = camera.position.length(); - camera.position.copy( controls.target ).normalize().multiplyScalar( dist ); - camera.lookAt( controls.target ); - controls.update(); - - const box = new Box3(); - tiles.getBounds( box ); - - camera.far = dist; - camera.near = Math.max( 1, dist - Math.max( ...box.min, ...box.max ) ); - camera.updateProjectionMatrix(); } @@ -191,14 +167,14 @@ function updateHash() { } - const res = {}; - const mat = tiles.group.matrixWorld.clone().invert(); - const vec = controls.target.clone().applyMatrix4( mat ); - WGS84_ELLIPSOID.getPositionToCartographic( vec, res ); + // const res = {}; + // const mat = tiles.group.matrixWorld.clone().invert(); + // const vec = controls.target.clone().applyMatrix4( mat ); + // WGS84_ELLIPSOID.getPositionToCartographic( vec, res ); - res.lat *= MathUtils.RAD2DEG; - res.lon *= MathUtils.RAD2DEG; - window.history.replaceState( undefined, undefined, `#${ res.lat.toFixed( 4 ) },${ res.lon.toFixed( 4 ) }` ); + // res.lat *= MathUtils.RAD2DEG; + // res.lon *= MathUtils.RAD2DEG; + // window.history.replaceState( undefined, undefined, `#${ res.lat.toFixed( 4 ) },${ res.lon.toFixed( 4 ) }` ); } @@ -212,12 +188,12 @@ function initFromHash() { } - const [ lat, lon ] = tokens; - WGS84_ELLIPSOID.getCartographicToPosition( lat * MathUtils.DEG2RAD, lon * MathUtils.DEG2RAD, 0, controls.target ); + // const [ lat, lon ] = tokens; + // WGS84_ELLIPSOID.getCartographicToPosition( lat * MathUtils.DEG2RAD, lon * MathUtils.DEG2RAD, 0, controls.target ); - tiles.group.updateMatrixWorld(); - controls.target.applyMatrix4( tiles.group.matrixWorld ); - updateControls(); + // tiles.group.updateMatrixWorld(); + // controls.target.applyMatrix4( tiles.group.matrixWorld ); + // updateControls(); } @@ -227,7 +203,7 @@ function animate() { if ( ! tiles ) return; - updateControls(); + controls.update(); // update options tiles.setResolutionFromRenderer( camera, renderer ); @@ -281,7 +257,7 @@ function updateHtml() { let count = 0; geomSet.forEach( g => { - count += BufferGeometryUtils.estimateBytesUsed( g ); + count += estimateBytesUsed( g ); } ); str += `
Cache: ${ ( 100 * cacheFullness ).toFixed( 2 ) }% ~${ ( count / 1000 / 1000 ).toFixed( 2 ) }mb`; From 9c24b8a19f28968698ef11c494d8297f934c91de Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Tue, 2 Jan 2024 00:07:50 +0900 Subject: [PATCH 58/64] More example update --- example/googleMapsExample.js | 84 +++++++--------------- src/three/renderers/GoogleTilesRenderer.js | 2 +- 2 files changed, 25 insertions(+), 61 deletions(-) diff --git a/example/googleMapsExample.js b/example/googleMapsExample.js index aac4e7957..8a56b264e 100644 --- a/example/googleMapsExample.js +++ b/example/googleMapsExample.js @@ -1,10 +1,8 @@ -import { GeoUtils, WGS84_ELLIPSOID, WGS84_RADIUS, DebugGoogleTilesRenderer as GoogleTilesRenderer } from '../src/index.js'; +import { GeoUtils, WGS84_ELLIPSOID, DebugGoogleTilesRenderer as GoogleTilesRenderer } from '../src/index.js'; import { Scene, WebGLRenderer, PerspectiveCamera, - Raycaster, - Box3, MathUtils, } from 'three'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; @@ -14,26 +12,20 @@ import { estimateBytesUsed } from 'three/examples/jsm/utils/BufferGeometryUtils. import Stats from 'three/examples/jsm/libs/stats.module.js'; import { GlobeControls } from './src/controls/GlobeControls.js'; -import { MapControls } from './src/lib/MapControls.js'; - let camera, controls, scene, renderer, tiles; let statsContainer, stats; -const raycaster = new Raycaster(); -raycaster.firstHitOnly = true; - const apiKey = localStorage.getItem( 'googleApiKey' ) ?? 'put-your-api-key-here'; const params = { - 'enableUpdate': true, - 'enableCacheDisplay': false, - 'enableRendererStats': false, - 'apiKey': apiKey, + enableCacheDisplay: false, + enableRendererStats: false, + apiKey: apiKey, - 'displayBoxBounds': false, - 'displayRegionBounds': false, 'reload': reinstantiateTiles, + displayBoxBounds: false, + displayRegionBounds: false, }; @@ -42,8 +34,6 @@ animate(); function reinstantiateTiles() { - localStorage.setItem( 'googleApiKey', params.apiKey ); - if ( tiles ) { scene.remove( tiles.group ); @@ -52,8 +42,10 @@ function reinstantiateTiles() { } + localStorage.setItem( 'googleApiKey', params.apiKey ); + tiles = new GoogleTilesRenderer( params.apiKey ); - // tiles.group.rotation.x = - Math.PI / 2; + tiles.group.rotation.x = - Math.PI / 2; // Note the DRACO compression files need to be supplied via an explicit source. // We use unpkg here but in practice should be provided by the application. @@ -84,16 +76,12 @@ function init() { document.body.appendChild( renderer.domElement ); camera = new PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 160000000 ); - camera.position.set( 7326000, 10279000, - 823000 ); + camera.position.set( 4800000, 2570000, 14720000 ); camera.lookAt( 0, 0, 0 ); // controls - // controls = new MapControls( camera, renderer.domElement ); controls = new GlobeControls( scene, camera, renderer.domElement ); - // console.log( GlobeControls ) - - reinstantiateTiles(); onWindowResize(); @@ -106,29 +94,14 @@ function init() { const mapsOptions = gui.addFolder( 'Google Tiles' ); mapsOptions.add( params, 'apiKey' ); mapsOptions.add( params, 'reload' ); - mapsOptions.open(); const debug = gui.addFolder( 'Debug Options' ); debug.add( params, 'displayBoxBounds' ); debug.add( params, 'displayRegionBounds' ); const exampleOptions = gui.addFolder( 'Example Options' ); - exampleOptions.add( params, 'enableUpdate' ).onChange( v => { - - tiles.parseQueue.autoUpdate = v; - tiles.downloadQueue.autoUpdate = v; - - if ( v ) { - - tiles.parseQueue.scheduleJobRun(); - tiles.downloadQueue.scheduleJobRun(); - - } - - } ); exampleOptions.add( params, 'enableCacheDisplay' ); exampleOptions.add( params, 'enableRendererStats' ); - gui.open(); statsContainer = document.createElement( 'div' ); document.getElementById( 'info' ).appendChild( statsContainer ); @@ -152,11 +125,6 @@ function onWindowResize() { camera.updateProjectionMatrix(); renderer.setPixelRatio( window.devicePixelRatio ); -} - -function updateControls() { - - } function updateHash() { @@ -167,14 +135,14 @@ function updateHash() { } - // const res = {}; - // const mat = tiles.group.matrixWorld.clone().invert(); - // const vec = controls.target.clone().applyMatrix4( mat ); - // WGS84_ELLIPSOID.getPositionToCartographic( vec, res ); + const res = {}; + const mat = tiles.group.matrixWorld.clone().invert(); + const vec = camera.position.clone().applyMatrix4( mat ); + WGS84_ELLIPSOID.getPositionToCartographic( vec, res ); - // res.lat *= MathUtils.RAD2DEG; - // res.lon *= MathUtils.RAD2DEG; - // window.history.replaceState( undefined, undefined, `#${ res.lat.toFixed( 4 ) },${ res.lon.toFixed( 4 ) }` ); + res.lat *= MathUtils.RAD2DEG; + res.lon *= MathUtils.RAD2DEG; + window.history.replaceState( undefined, undefined, `#${ res.lat.toFixed( 4 ) },${ res.lon.toFixed( 4 ) }` ); } @@ -188,12 +156,12 @@ function initFromHash() { } - // const [ lat, lon ] = tokens; - // WGS84_ELLIPSOID.getCartographicToPosition( lat * MathUtils.DEG2RAD, lon * MathUtils.DEG2RAD, 0, controls.target ); + const [ lat, lon ] = tokens; + WGS84_ELLIPSOID.getCartographicToPosition( lat * MathUtils.DEG2RAD, lon * MathUtils.DEG2RAD, 0, camera.position ); - // tiles.group.updateMatrixWorld(); - // controls.target.applyMatrix4( tiles.group.matrixWorld ); - // updateControls(); + tiles.group.updateMatrixWorld(); + camera.position.applyMatrix4( tiles.group.matrixWorld ).multiplyScalar( 2 ); + camera.lookAt( 0, 0, 0 ); } @@ -212,12 +180,8 @@ function animate() { tiles.displayRegionBounds = params.displayRegionBounds; // update tiles - if ( params.enableUpdate ) { - - camera.updateMatrixWorld(); - tiles.update(); - - } + camera.updateMatrixWorld(); + tiles.update(); renderer.render( scene, camera ); stats.update(); diff --git a/src/three/renderers/GoogleTilesRenderer.js b/src/three/renderers/GoogleTilesRenderer.js index 50a538404..e9efa45b0 100644 --- a/src/three/renderers/GoogleTilesRenderer.js +++ b/src/three/renderers/GoogleTilesRenderer.js @@ -27,7 +27,7 @@ const GoogleTilesRendererMixin = base => class extends base { this.downloadQueue.maxJobs = 30; this.lruCache.minSize = 3000; this.lruCache.maxSize = 5000; - this.errorTarget = 20; + this.errorTarget = 40; this.onLoadTileSet = tileset => { From 91df6ec83e3e613391ed91fc59f64db436583ed4 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Tue, 2 Jan 2024 02:00:32 +0900 Subject: [PATCH 59/64] Small fixes, demo update, fix to FadeRenderer --- example/googleMapsExample.js | 8 +++++--- example/src/FadeTilesRenderer.js | 2 +- example/src/controls/GlobeControls.js | 6 ++++++ example/src/controls/TileControls.js | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/example/googleMapsExample.js b/example/googleMapsExample.js index 8a56b264e..5d7408847 100644 --- a/example/googleMapsExample.js +++ b/example/googleMapsExample.js @@ -194,10 +194,12 @@ function updateHtml() { // render html text updates const cacheFullness = tiles.lruCache.itemList.length / tiles.lruCache.maxSize; - let str = `Downloading: ${ tiles.stats.downloading } Parsing: ${ tiles.stats.parsing } Visible: ${ tiles.visibleTiles.size }`; + let str = ''; if ( params.enableCacheDisplay ) { + str += `Downloading: ${ tiles.stats.downloading } Parsing: ${ tiles.stats.parsing } Visible: ${ tiles.visibleTiles.size }
`; + const geomSet = new Set(); tiles.traverse( tile => { @@ -224,7 +226,7 @@ function updateHtml() { count += estimateBytesUsed( g ); } ); - str += `
Cache: ${ ( 100 * cacheFullness ).toFixed( 2 ) }% ~${ ( count / 1000 / 1000 ).toFixed( 2 ) }mb`; + str += `Cache: ${ ( 100 * cacheFullness ).toFixed( 2 ) }% ~${ ( count / 1000 / 1000 ).toFixed( 2 ) }mb
`; } @@ -232,7 +234,7 @@ function updateHtml() { const memory = renderer.info.memory; const programCount = renderer.info.programs.length; - str += `
Geometries: ${ memory.geometries } Textures: ${ memory.textures } Programs: ${ programCount }`; + str += `Geometries: ${ memory.geometries } Textures: ${ memory.textures } Programs: ${ programCount }`; } diff --git a/example/src/FadeTilesRenderer.js b/example/src/FadeTilesRenderer.js index 0cd8ace9f..0ba4961d4 100644 --- a/example/src/FadeTilesRenderer.js +++ b/example/src/FadeTilesRenderer.js @@ -30,7 +30,7 @@ function onTileVisibilityChange( scene, tile, visible ) { } - if ( ! isRootTile || ! this.fadeRootTiles || this.initialLayerRendered ) { + if ( ! isRootTile || this.fadeRootTiles || this.initialLayerRendered ) { this._fadeManager.fadeIn( scene ); diff --git a/example/src/controls/GlobeControls.js b/example/src/controls/GlobeControls.js index 0197ac23c..4adf4d617 100644 --- a/example/src/controls/GlobeControls.js +++ b/example/src/controls/GlobeControls.js @@ -76,6 +76,7 @@ export class GlobeControls extends TileControls { const { camera, scene, + pivotMesh, } = this; // clamp the camera distance @@ -94,6 +95,11 @@ export class GlobeControls extends TileControls { // when adjusting the up frame while moving hte camera if ( distanceToCenter > GLOBE_TRANSITION_THRESHOLD ) { + if ( this._dragMode !== 1 && this._rotationMode !== 1 ) { + + pivotMesh.visible = false; + + } this.reorientOnDrag = false; this.reorientOnZoom = true; diff --git a/example/src/controls/TileControls.js b/example/src/controls/TileControls.js index 7560ef2f5..28b7cbc5e 100644 --- a/example/src/controls/TileControls.js +++ b/example/src/controls/TileControls.js @@ -717,7 +717,7 @@ export class TileControls { // 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 - const angle = _localUp.angleTo( _forward ); + const angle = up.angleTo( _forward ); if ( altitude > 0 ) { altitude = Math.min( angle - minAltitude - 1e-2, altitude ); From e164c2de207d7b1a254145f5df1ddc6bc5d4afba Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Tue, 2 Jan 2024 02:18:24 +0900 Subject: [PATCH 60/64] Fix pivot point --- example/src/controls/GlobeControls.js | 4 ++-- example/src/controls/TileControls.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/example/src/controls/GlobeControls.js b/example/src/controls/GlobeControls.js index 4adf4d617..5fb2bd4d9 100644 --- a/example/src/controls/GlobeControls.js +++ b/example/src/controls/GlobeControls.js @@ -5,7 +5,7 @@ import { Vector3, MathUtils, } from 'three'; -import { TileControls } from './TileControls.js'; +import { TileControls, NONE } from './TileControls.js'; import { makeRotateAroundPoint } from './utils.js'; import { WGS84_ELLIPSOID } from '../../../src/index.js'; @@ -95,7 +95,7 @@ export class GlobeControls extends TileControls { // when adjusting the up frame while moving hte camera if ( distanceToCenter > GLOBE_TRANSITION_THRESHOLD ) { - if ( this._dragMode !== 1 && this._rotationMode !== 1 ) { + if ( this.state !== NONE && this._dragMode !== 1 && this._rotationMode !== 1 ) { pivotMesh.visible = false; diff --git a/example/src/controls/TileControls.js b/example/src/controls/TileControls.js index 28b7cbc5e..9e3c179bf 100644 --- a/example/src/controls/TileControls.js +++ b/example/src/controls/TileControls.js @@ -10,10 +10,10 @@ import { PivotPointMesh } from './PivotPointMesh.js'; import { PointerTracker } from './PointerTracker.js'; import { mouseToCoords, makeRotateAroundPoint } from './utils.js'; -const NONE = 0; -const DRAG = 1; -const ROTATE = 2; -const ZOOM = 3; +export const NONE = 0; +export const DRAG = 1; +export const ROTATE = 2; +export const ZOOM = 3; const DRAG_PLANE_THRESHOLD = 0.05; const DRAG_UP_THRESHOLD = 0.025; From 7c802f71fe7e0ba1a42945e19ef98ab6ae8ddfaf Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Tue, 2 Jan 2024 10:02:48 +0900 Subject: [PATCH 61/64] setFrame to _setFrame, reuse pivot cast --- example/src/controls/TileControls.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/example/src/controls/TileControls.js b/example/src/controls/TileControls.js index 9e3c179bf..8c759d82c 100644 --- a/example/src/controls/TileControls.js +++ b/example/src/controls/TileControls.js @@ -18,7 +18,6 @@ export const ZOOM = 3; const DRAG_PLANE_THRESHOLD = 0.05; const DRAG_UP_THRESHOLD = 0.025; -const _pivot = new Vector3(); const _rotMatrix = new Matrix4(); const _delta = new Vector3(); const _vec = new Vector3(); @@ -428,6 +427,8 @@ export class TileControls { up, } = this; + // reuse the "hit" information since it can be slow to perform multiple hits + const hit = this._getPointBelowCamera(); if ( this.getUpDirection ) { this.getUpDirection( camera.position, _localUp ); @@ -438,7 +439,7 @@ export class TileControls { } else { - this.setFrame( _localUp ); + this._setFrame( _localUp, hit && hit.point || null ); } @@ -456,7 +457,6 @@ export class TileControls { } // cast down from the camera - const hit = this._getPointBelowCamera(); if ( hit ) { const dist = hit.distance; @@ -748,7 +748,7 @@ export class TileControls { } // sets the "up" axis for the current surface of the tile set - setFrame( newUp ) { + _setFrame( newUp, pivot ) { const { up, camera, state, zoomPoint, zoomDirection } = this; camera.updateMatrixWorld(); @@ -771,16 +771,14 @@ export class TileControls { } else if ( state === NONE || state === DRAG && this.reorientOnDrag ) { - // get the pivot to rotate the frame if needed - let dist = 0; - const hit = this._getPointBelowCamera(); - if ( hit ) { + // NOTE: We used to derive the pivot point here by getting the point below the camera + // but decided to pass it in via "update" to avoid multiple ray casts - _pivot.copy( hit.point ); - dist = _pivot.distanceTo( camera.position ); + if ( pivot ) { - // perform a simple realignment - camera.position.copy( _pivot ).addScaledVector( newUp, dist ); + // perform a simple realignment by rotating the camera and adjusting the height + const dist = pivot.distanceTo( camera.position ); + camera.position.copy( pivot ).addScaledVector( newUp, dist ); camera.quaternion.premultiply( _quaternion ); camera.updateMatrixWorld(); From 1ec6ccb7214ec4ce39719c33ab73e6124571bc7e Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Tue, 2 Jan 2024 10:27:38 +0900 Subject: [PATCH 62/64] Fix touch zoom --- example/src/controls/PointerTracker.js | 14 +++++++------- example/src/controls/TileControls.js | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/example/src/controls/PointerTracker.js b/example/src/controls/PointerTracker.js index d0cf52b60..e33028282 100644 --- a/example/src/controls/PointerTracker.js +++ b/example/src/controls/PointerTracker.js @@ -28,22 +28,22 @@ export class PointerTracker { getLatestPoint( target ) { - if ( ! this.hoverSet ) { + if ( this.pointerType !== null ) { - return null; + this.getCenterPoint( target ); + return target; - } else if ( this.pointerType !== null ) { + } else if ( this.hoverSet ) { - this.getCenterPoint( target ); + target.copy( this.hoverPosition ); + return target; } else { - target.copy( this.hoverPosition ); + return null; } - return target; - } addPointer( e ) { diff --git a/example/src/controls/TileControls.js b/example/src/controls/TileControls.js index 8c759d82c..d6713abf1 100644 --- a/example/src/controls/TileControls.js +++ b/example/src/controls/TileControls.js @@ -272,7 +272,7 @@ export class TileControls { pointerTracker.getPreviousCenterPoint( _originalCenterPoint ); const parallelDelta = _centerPoint.distanceTo( _originalCenterPoint ); - if ( separateDelta > 0 && parallelDelta > 0 ) { + if ( Math.abs( separateDelta ) > 0 || parallelDelta > 0 ) { if ( separateDelta > parallelDelta ) { From 952fdec296fdc6411a8be2a439ee5f79af1d27eb Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Tue, 2 Jan 2024 10:35:13 +0900 Subject: [PATCH 63/64] Fix touch rotation --- example/src/controls/PointerTracker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/src/controls/PointerTracker.js b/example/src/controls/PointerTracker.js index e33028282..2dcaf53a2 100644 --- a/example/src/controls/PointerTracker.js +++ b/example/src/controls/PointerTracker.js @@ -118,8 +118,8 @@ export class PointerTracker { const id0 = this.pointerOrder[ 0 ]; const id1 = this.pointerOrder[ 1 ]; - const p0 = this.pointerPositions[ id0 ]; - const p1 = this.pointerPositions[ id1 ]; + const p0 = pointerPositions[ id0 ]; + const p1 = pointerPositions[ id1 ]; target.addVectors( p0, p1 ).multiplyScalar( 0.5 ); return target; From 71f4ef782525a97b8c46fed3ddf132fb24cc0967 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Tue, 2 Jan 2024 10:40:58 +0900 Subject: [PATCH 64/64] Fixes --- example/src/controls/TileControls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/controls/TileControls.js b/example/src/controls/TileControls.js index d6713abf1..23128ae25 100644 --- a/example/src/controls/TileControls.js +++ b/example/src/controls/TileControls.js @@ -274,7 +274,7 @@ export class TileControls { const parallelDelta = _centerPoint.distanceTo( _originalCenterPoint ); if ( Math.abs( separateDelta ) > 0 || parallelDelta > 0 ) { - if ( separateDelta > parallelDelta ) { + if ( Math.abs( separateDelta ) > parallelDelta ) { this.resetState(); pinchAction = ZOOM;