From f172918a44189bced8113dca253255b41813671d Mon Sep 17 00:00:00 2001 From: allala0 Date: Thu, 17 Nov 2022 22:17:07 +0100 Subject: [PATCH] Added zooming to cursor in OrbitControls --- docs/examples/en/controls/OrbitControls.html | 5 + examples/js/controls/OrbitControls.js | 1902 ++++++++---------- examples/jsm/controls/OrbitControls.js | 113 ++ 3 files changed, 957 insertions(+), 1063 deletions(-) diff --git a/docs/examples/en/controls/OrbitControls.html b/docs/examples/en/controls/OrbitControls.html index c7429566a84a04..a5f11933570108 100644 --- a/docs/examples/en/controls/OrbitControls.html +++ b/docs/examples/en/controls/OrbitControls.html @@ -133,6 +133,11 @@

[property:Boolean enableZoom]

Enable or disable zooming (dollying) of the camera.

+

[property:Boolean enableZoomToCursor]

+

+ Set to `true`, camera is zoomed to mouse cursor (or touch pinch on mobile) position. Set to `false`, zoom as dolly. Default is `false`. +

+

[property:Float keyPanSpeed]

How fast to pan the camera when the keyboard is used. Default is 7.0 pixels per keypress. diff --git a/examples/js/controls/OrbitControls.js b/examples/js/controls/OrbitControls.js index 38eb76e3a70662..fbac3829adc9ce 100644 --- a/examples/js/controls/OrbitControls.js +++ b/examples/js/controls/OrbitControls.js @@ -1,1065 +1,841 @@ ( function () { - - // This set of controls performs orbiting, dollying (zooming), and panning. - // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). - // - // Orbit - left mouse / touch: one-finger move - // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish - // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move - - const _changeEvent = { - type: 'change' - }; - const _startEvent = { - type: 'start' - }; - const _endEvent = { - type: 'end' - }; - class OrbitControls extends THREE.EventDispatcher { - - constructor( object, domElement ) { - - super(); - this.object = object; - this.domElement = domElement; - this.domElement.style.touchAction = 'none'; // disable touch scroll - - // Set to false to disable this control - this.enabled = true; - - // "target" sets the location of focus, where the object orbits around - this.target = new THREE.Vector3(); - - // How far you can dolly in and out ( PerspectiveCamera only ) - this.minDistance = 0; - this.maxDistance = Infinity; - - // How far you can zoom in and out ( OrthographicCamera only ) - this.minZoom = 0; - this.maxZoom = Infinity; - - // How far you can orbit vertically, upper and lower limits. - // Range is 0 to Math.PI radians. - this.minPolarAngle = 0; // radians - this.maxPolarAngle = Math.PI; // radians - - // How far you can orbit horizontally, upper and lower limits. - // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) - this.minAzimuthAngle = - Infinity; // radians - this.maxAzimuthAngle = Infinity; // radians - - // Set to true to enable damping (inertia) - // If damping is enabled, you must call controls.update() in your animation loop - this.enableDamping = false; - this.dampingFactor = 0.05; - - // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. - // Set to false to disable zooming - this.enableZoom = true; - this.zoomSpeed = 1.0; - - // Set to false to disable rotating - this.enableRotate = true; - this.rotateSpeed = 1.0; - - // Set to false to disable panning - this.enablePan = true; - this.panSpeed = 1.0; - this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up - this.keyPanSpeed = 7.0; // pixels moved per arrow key push - - // Set to true to automatically rotate around the target - // If auto-rotate is enabled, you must call controls.update() in your animation loop - this.autoRotate = false; - this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 - - // The four arrow keys - this.keys = { - LEFT: 'ArrowLeft', - UP: 'ArrowUp', - RIGHT: 'ArrowRight', - BOTTOM: 'ArrowDown' - }; - - // Mouse buttons - this.mouseButtons = { - LEFT: THREE.MOUSE.ROTATE, - MIDDLE: THREE.MOUSE.DOLLY, - RIGHT: THREE.MOUSE.PAN - }; - - // Touch fingers - this.touches = { - ONE: THREE.TOUCH.ROTATE, - TWO: THREE.TOUCH.DOLLY_PAN - }; - - // for reset - this.target0 = this.target.clone(); - this.position0 = this.object.position.clone(); - this.zoom0 = this.object.zoom; - - // the target DOM element for key events - this._domElementKeyEvents = null; - - // - // public methods - // - - this.getPolarAngle = function () { - - return spherical.phi; - - }; - - this.getAzimuthalAngle = function () { - - return spherical.theta; - - }; - - this.getDistance = function () { - - return this.object.position.distanceTo( this.target ); - - }; - - this.listenToKeyEvents = function ( domElement ) { - - domElement.addEventListener( 'keydown', onKeyDown ); - this._domElementKeyEvents = domElement; - - }; - - this.saveState = function () { - - scope.target0.copy( scope.target ); - scope.position0.copy( scope.object.position ); - scope.zoom0 = scope.object.zoom; - - }; - - this.reset = function () { - - scope.target.copy( scope.target0 ); - scope.object.position.copy( scope.position0 ); - scope.object.zoom = scope.zoom0; - scope.object.updateProjectionMatrix(); - scope.dispatchEvent( _changeEvent ); - scope.update(); - state = STATE.NONE; - - }; - - // this method is exposed, but perhaps it would be better if we can make it private... - this.update = function () { - - const offset = new THREE.Vector3(); - - // so camera.up is the orbit axis - const quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); - const quatInverse = quat.clone().invert(); - const lastPosition = new THREE.Vector3(); - const lastQuaternion = new THREE.Quaternion(); - const twoPI = 2 * Math.PI; - return function update() { - - const position = scope.object.position; - offset.copy( position ).sub( scope.target ); - - // rotate offset to "y-axis-is-up" space - offset.applyQuaternion( quat ); - - // angle from z-axis around y-axis - spherical.setFromVector3( offset ); - if ( scope.autoRotate && state === STATE.NONE ) { - - rotateLeft( getAutoRotationAngle() ); - - } - - if ( scope.enableDamping ) { - - spherical.theta += sphericalDelta.theta * scope.dampingFactor; - spherical.phi += sphericalDelta.phi * scope.dampingFactor; - - } else { - - spherical.theta += sphericalDelta.theta; - spherical.phi += sphericalDelta.phi; - - } - - // restrict theta to be between desired limits - - let min = scope.minAzimuthAngle; - let max = scope.maxAzimuthAngle; - if ( isFinite( min ) && isFinite( max ) ) { - - if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI; - if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI; - if ( min <= max ) { - - spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); - - } else { - - spherical.theta = spherical.theta > ( min + max ) / 2 ? Math.max( min, spherical.theta ) : Math.min( max, spherical.theta ); - - } - - } - - // restrict phi to be between desired limits - spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); - spherical.makeSafe(); - spherical.radius *= scale; - - // restrict radius to be between desired limits - spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); - - // move target to panned location - - if ( scope.enableDamping === true ) { - - scope.target.addScaledVector( panOffset, scope.dampingFactor ); - - } else { - - scope.target.add( panOffset ); - - } - - offset.setFromSpherical( spherical ); - - // rotate offset back to "camera-up-vector-is-up" space - offset.applyQuaternion( quatInverse ); - position.copy( scope.target ).add( offset ); - scope.object.lookAt( scope.target ); - if ( scope.enableDamping === true ) { - - sphericalDelta.theta *= 1 - scope.dampingFactor; - sphericalDelta.phi *= 1 - scope.dampingFactor; - panOffset.multiplyScalar( 1 - scope.dampingFactor ); - - } else { - - sphericalDelta.set( 0, 0, 0 ); - panOffset.set( 0, 0, 0 ); - - } - - scale = 1; - - // update condition is: - // min(camera displacement, camera rotation in radians)^2 > EPS - // using small-angle approximation cos(x/2) = 1 - x^2 / 8 - - if ( zoomChanged || lastPosition.distanceToSquared( scope.object.position ) > EPS || 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { - - scope.dispatchEvent( _changeEvent ); - lastPosition.copy( scope.object.position ); - lastQuaternion.copy( scope.object.quaternion ); - zoomChanged = false; - return true; - - } - - return false; - - }; - - }(); - this.dispose = function () { - - scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); - scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); - scope.domElement.removeEventListener( 'pointercancel', onPointerCancel ); - scope.domElement.removeEventListener( 'wheel', onMouseWheel ); - scope.domElement.removeEventListener( 'pointermove', onPointerMove ); - scope.domElement.removeEventListener( 'pointerup', onPointerUp ); - if ( scope._domElementKeyEvents !== null ) { - - scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); - - } - - //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? - - }; - - // - // internals - // - - const scope = this; - const STATE = { - NONE: - 1, - ROTATE: 0, - DOLLY: 1, - PAN: 2, - TOUCH_ROTATE: 3, - TOUCH_PAN: 4, - TOUCH_DOLLY_PAN: 5, - TOUCH_DOLLY_ROTATE: 6 - }; - let state = STATE.NONE; - const EPS = 0.000001; - - // current position in spherical coordinates - const spherical = new THREE.Spherical(); - const sphericalDelta = new THREE.Spherical(); - let scale = 1; - const panOffset = new THREE.Vector3(); - let zoomChanged = false; - const rotateStart = new THREE.Vector2(); - const rotateEnd = new THREE.Vector2(); - const rotateDelta = new THREE.Vector2(); - const panStart = new THREE.Vector2(); - const panEnd = new THREE.Vector2(); - const panDelta = new THREE.Vector2(); - const dollyStart = new THREE.Vector2(); - const dollyEnd = new THREE.Vector2(); - const dollyDelta = new THREE.Vector2(); - const pointers = []; - const pointerPositions = {}; - function getAutoRotationAngle() { - - return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; - - } - - function getZoomScale() { - - return Math.pow( 0.95, scope.zoomSpeed ); - - } - - function rotateLeft( angle ) { - - sphericalDelta.theta -= angle; - - } - - function rotateUp( angle ) { - - sphericalDelta.phi -= angle; - - } - - const panLeft = function () { - - const v = new THREE.Vector3(); - return function panLeft( distance, objectMatrix ) { - - v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix - v.multiplyScalar( - distance ); - panOffset.add( v ); - - }; - - }(); - const panUp = function () { - - const v = new THREE.Vector3(); - return function panUp( distance, objectMatrix ) { - - if ( scope.screenSpacePanning === true ) { - - v.setFromMatrixColumn( objectMatrix, 1 ); - - } else { - - v.setFromMatrixColumn( objectMatrix, 0 ); - v.crossVectors( scope.object.up, v ); - - } - - v.multiplyScalar( distance ); - panOffset.add( v ); - - }; - - }(); - - // deltaX and deltaY are in pixels; right and down are positive - const pan = function () { - - const offset = new THREE.Vector3(); - return function pan( deltaX, deltaY ) { - - const element = scope.domElement; - if ( scope.object.isPerspectiveCamera ) { - - // perspective - const position = scope.object.position; - offset.copy( position ).sub( scope.target ); - let targetDistance = offset.length(); - - // half of the fov is center to top of screen - targetDistance *= Math.tan( scope.object.fov / 2 * Math.PI / 180.0 ); - - // we use only clientHeight here so aspect ratio does not distort speed - panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); - panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); - - } else if ( scope.object.isOrthographicCamera ) { - - // orthographic - panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); - panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); - - } else { - - // camera neither orthographic nor perspective - console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); - scope.enablePan = false; - - } - - }; - - }(); - function dollyOut( dollyScale ) { - - if ( scope.object.isPerspectiveCamera ) { - - scale /= dollyScale; - - } else if ( scope.object.isOrthographicCamera ) { - - scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); - scope.object.updateProjectionMatrix(); - zoomChanged = true; - - } else { - - console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); - scope.enableZoom = false; - - } - - } - - function dollyIn( dollyScale ) { - - if ( scope.object.isPerspectiveCamera ) { - - scale *= dollyScale; - - } else if ( scope.object.isOrthographicCamera ) { - - scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); - scope.object.updateProjectionMatrix(); - zoomChanged = true; - - } else { - - console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); - scope.enableZoom = false; - - } - - } - - // - // event callbacks - update the object state - // - - function handleMouseDownRotate( event ) { - - rotateStart.set( event.clientX, event.clientY ); - - } - - function handleMouseDownDolly( event ) { - - dollyStart.set( event.clientX, event.clientY ); - - } - - function handleMouseDownPan( event ) { - - panStart.set( event.clientX, event.clientY ); - - } - - function handleMouseMoveRotate( event ) { - - rotateEnd.set( event.clientX, event.clientY ); - rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); - const element = scope.domElement; - rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height - - rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); - rotateStart.copy( rotateEnd ); - scope.update(); - - } - - function handleMouseMoveDolly( event ) { - - dollyEnd.set( event.clientX, event.clientY ); - dollyDelta.subVectors( dollyEnd, dollyStart ); - if ( dollyDelta.y > 0 ) { - - dollyOut( getZoomScale() ); - - } else if ( dollyDelta.y < 0 ) { - - dollyIn( getZoomScale() ); - - } - - dollyStart.copy( dollyEnd ); - scope.update(); - - } - - function handleMouseMovePan( event ) { - - panEnd.set( event.clientX, event.clientY ); - panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); - pan( panDelta.x, panDelta.y ); - panStart.copy( panEnd ); - scope.update(); - - } - - function handleMouseWheel( event ) { - - if ( event.deltaY < 0 ) { - - dollyIn( getZoomScale() ); - - } else if ( event.deltaY > 0 ) { - - dollyOut( getZoomScale() ); - - } - - scope.update(); - - } - - function handleKeyDown( event ) { - - let needsUpdate = false; - switch ( event.code ) { - - case scope.keys.UP: - pan( 0, scope.keyPanSpeed ); - needsUpdate = true; - break; - case scope.keys.BOTTOM: - pan( 0, - scope.keyPanSpeed ); - needsUpdate = true; - break; - case scope.keys.LEFT: - pan( scope.keyPanSpeed, 0 ); - needsUpdate = true; - break; - case scope.keys.RIGHT: - pan( - scope.keyPanSpeed, 0 ); - needsUpdate = true; - break; - - } - - if ( needsUpdate ) { - - // prevent the browser from scrolling on cursor keys - event.preventDefault(); - scope.update(); - - } - - } - - function handleTouchStartRotate() { - - if ( pointers.length === 1 ) { - - rotateStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); - - } else { - - const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); - const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); - rotateStart.set( x, y ); - - } - - } - - function handleTouchStartPan() { - - if ( pointers.length === 1 ) { - - panStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); - - } else { - - const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); - const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); - panStart.set( x, y ); - - } - - } - - function handleTouchStartDolly() { - - const dx = pointers[ 0 ].pageX - pointers[ 1 ].pageX; - const dy = pointers[ 0 ].pageY - pointers[ 1 ].pageY; - const distance = Math.sqrt( dx * dx + dy * dy ); - dollyStart.set( 0, distance ); - - } - - function handleTouchStartDollyPan() { - - if ( scope.enableZoom ) handleTouchStartDolly(); - if ( scope.enablePan ) handleTouchStartPan(); - - } - - function handleTouchStartDollyRotate() { - - if ( scope.enableZoom ) handleTouchStartDolly(); - if ( scope.enableRotate ) handleTouchStartRotate(); - - } - - function handleTouchMoveRotate( event ) { - - if ( pointers.length == 1 ) { - - rotateEnd.set( event.pageX, event.pageY ); - - } else { - - const position = getSecondPointerPosition( event ); - const x = 0.5 * ( event.pageX + position.x ); - const y = 0.5 * ( event.pageY + position.y ); - rotateEnd.set( x, y ); - - } - - rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); - const element = scope.domElement; - rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height - - rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); - rotateStart.copy( rotateEnd ); - - } - - function handleTouchMovePan( event ) { - - if ( pointers.length === 1 ) { - - panEnd.set( event.pageX, event.pageY ); - - } else { - - const position = getSecondPointerPosition( event ); - const x = 0.5 * ( event.pageX + position.x ); - const y = 0.5 * ( event.pageY + position.y ); - panEnd.set( x, y ); - - } - - panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); - pan( panDelta.x, panDelta.y ); - panStart.copy( panEnd ); - - } - - function handleTouchMoveDolly( event ) { - - const position = getSecondPointerPosition( event ); - const dx = event.pageX - position.x; - const dy = event.pageY - position.y; - const distance = Math.sqrt( dx * dx + dy * dy ); - dollyEnd.set( 0, distance ); - dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); - dollyOut( dollyDelta.y ); - dollyStart.copy( dollyEnd ); - - } - - function handleTouchMoveDollyPan( event ) { - - if ( scope.enableZoom ) handleTouchMoveDolly( event ); - if ( scope.enablePan ) handleTouchMovePan( event ); - - } - - function handleTouchMoveDollyRotate( event ) { - - if ( scope.enableZoom ) handleTouchMoveDolly( event ); - if ( scope.enableRotate ) handleTouchMoveRotate( event ); - - } - - // - // event handlers - FSM: listen for events and reset state - // - - function onPointerDown( event ) { - - if ( scope.enabled === false ) return; - if ( pointers.length === 0 ) { - - scope.domElement.setPointerCapture( event.pointerId ); - scope.domElement.addEventListener( 'pointermove', onPointerMove ); - scope.domElement.addEventListener( 'pointerup', onPointerUp ); - - } - - // - - addPointer( event ); - if ( event.pointerType === 'touch' ) { - - onTouchStart( event ); - - } else { - - onMouseDown( event ); - - } - - } - - function onPointerMove( event ) { - - if ( scope.enabled === false ) return; - if ( event.pointerType === 'touch' ) { - - onTouchMove( event ); - - } else { - - onMouseMove( event ); - - } - - } - - function onPointerUp( event ) { - - removePointer( event ); - if ( pointers.length === 0 ) { - - scope.domElement.releasePointerCapture( event.pointerId ); - scope.domElement.removeEventListener( 'pointermove', onPointerMove ); - scope.domElement.removeEventListener( 'pointerup', onPointerUp ); - - } - - scope.dispatchEvent( _endEvent ); - state = STATE.NONE; - - } - - function onPointerCancel( event ) { - - removePointer( event ); - - } - - function onMouseDown( event ) { - - let mouseAction; - switch ( event.button ) { - - case 0: - mouseAction = scope.mouseButtons.LEFT; - break; - case 1: - mouseAction = scope.mouseButtons.MIDDLE; - break; - case 2: - mouseAction = scope.mouseButtons.RIGHT; - break; - default: - mouseAction = - 1; - - } - - switch ( mouseAction ) { - - case THREE.MOUSE.DOLLY: - if ( scope.enableZoom === false ) return; - handleMouseDownDolly( event ); - state = STATE.DOLLY; - break; - case THREE.MOUSE.ROTATE: - if ( event.ctrlKey || event.metaKey || event.shiftKey ) { - - if ( scope.enablePan === false ) return; - handleMouseDownPan( event ); - state = STATE.PAN; - - } else { - - if ( scope.enableRotate === false ) return; - handleMouseDownRotate( event ); - state = STATE.ROTATE; - - } - - break; - case THREE.MOUSE.PAN: - if ( event.ctrlKey || event.metaKey || event.shiftKey ) { - - if ( scope.enableRotate === false ) return; - handleMouseDownRotate( event ); - state = STATE.ROTATE; - - } else { - - if ( scope.enablePan === false ) return; - handleMouseDownPan( event ); - state = STATE.PAN; - - } - - break; - default: - state = STATE.NONE; - - } - - if ( state !== STATE.NONE ) { - - scope.dispatchEvent( _startEvent ); - - } - - } - - function onMouseMove( event ) { - - switch ( state ) { - - case STATE.ROTATE: - if ( scope.enableRotate === false ) return; - handleMouseMoveRotate( event ); - break; - case STATE.DOLLY: - if ( scope.enableZoom === false ) return; - handleMouseMoveDolly( event ); - break; - case STATE.PAN: - if ( scope.enablePan === false ) return; - handleMouseMovePan( event ); - break; - - } - - } - - function onMouseWheel( event ) { - - if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return; - event.preventDefault(); - scope.dispatchEvent( _startEvent ); - handleMouseWheel( event ); - scope.dispatchEvent( _endEvent ); - - } - - function onKeyDown( event ) { - - if ( scope.enabled === false || scope.enablePan === false ) return; - handleKeyDown( event ); - - } - - function onTouchStart( event ) { - - trackPointer( event ); - switch ( pointers.length ) { - - case 1: - switch ( scope.touches.ONE ) { - - case THREE.TOUCH.ROTATE: - if ( scope.enableRotate === false ) return; - handleTouchStartRotate(); - state = STATE.TOUCH_ROTATE; - break; - case THREE.TOUCH.PAN: - if ( scope.enablePan === false ) return; - handleTouchStartPan(); - state = STATE.TOUCH_PAN; - break; - default: - state = STATE.NONE; - - } - - break; - case 2: - switch ( scope.touches.TWO ) { - - case THREE.TOUCH.DOLLY_PAN: - if ( scope.enableZoom === false && scope.enablePan === false ) return; - handleTouchStartDollyPan(); - state = STATE.TOUCH_DOLLY_PAN; - break; - case THREE.TOUCH.DOLLY_ROTATE: - if ( scope.enableZoom === false && scope.enableRotate === false ) return; - handleTouchStartDollyRotate(); - state = STATE.TOUCH_DOLLY_ROTATE; - break; - default: - state = STATE.NONE; - - } - - break; - default: - state = STATE.NONE; - - } - - if ( state !== STATE.NONE ) { - - scope.dispatchEvent( _startEvent ); - - } - - } - - function onTouchMove( event ) { - - trackPointer( event ); - switch ( state ) { - - case STATE.TOUCH_ROTATE: - if ( scope.enableRotate === false ) return; - handleTouchMoveRotate( event ); - scope.update(); - break; - case STATE.TOUCH_PAN: - if ( scope.enablePan === false ) return; - handleTouchMovePan( event ); - scope.update(); - break; - case STATE.TOUCH_DOLLY_PAN: - if ( scope.enableZoom === false && scope.enablePan === false ) return; - handleTouchMoveDollyPan( event ); - scope.update(); - break; - case STATE.TOUCH_DOLLY_ROTATE: - if ( scope.enableZoom === false && scope.enableRotate === false ) return; - handleTouchMoveDollyRotate( event ); - scope.update(); - break; - default: - state = STATE.NONE; - - } - - } - - function onContextMenu( event ) { - - if ( scope.enabled === false ) return; - event.preventDefault(); - - } - - function addPointer( event ) { - - pointers.push( event ); - - } - - function removePointer( event ) { - - delete pointerPositions[ event.pointerId ]; - for ( let i = 0; i < pointers.length; i ++ ) { - - if ( pointers[ i ].pointerId == event.pointerId ) { - - pointers.splice( i, 1 ); - return; - - } - - } - - } - - function trackPointer( event ) { - - let position = pointerPositions[ event.pointerId ]; - if ( position === undefined ) { - - position = new THREE.Vector2(); - pointerPositions[ event.pointerId ] = position; - - } - - position.set( event.pageX, event.pageY ); - - } - - function getSecondPointerPosition( event ) { - - const pointer = event.pointerId === pointers[ 0 ].pointerId ? pointers[ 1 ] : pointers[ 0 ]; - return pointerPositions[ pointer.pointerId ]; - - } - - // - - scope.domElement.addEventListener( 'contextmenu', onContextMenu ); - scope.domElement.addEventListener( 'pointerdown', onPointerDown ); - scope.domElement.addEventListener( 'pointercancel', onPointerCancel ); - scope.domElement.addEventListener( 'wheel', onMouseWheel, { - passive: false - } ); - - // force an update at start - - this.update(); - - } - - } - - // This set of controls performs orbiting, dollying (zooming), and panning. - // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). - // This is very similar to OrbitControls, another set of touch behavior - // - // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate - // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish - // Pan - left mouse, or arrow keys / touch: one-finger move - - class MapControls extends OrbitControls { - - constructor( object, domElement ) { - - super( object, domElement ); - this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up - - this.mouseButtons.LEFT = THREE.MOUSE.PAN; - this.mouseButtons.RIGHT = THREE.MOUSE.ROTATE; - this.touches.ONE = THREE.TOUCH.PAN; - this.touches.TWO = THREE.TOUCH.DOLLY_ROTATE; - - } - - } - - THREE.MapControls = MapControls; - THREE.OrbitControls = OrbitControls; - +// This set of controls performs orbiting, dollying (zooming), and panning. +// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). +// +// Orbit - left mouse / touch: one-finger move +// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish +// Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move + +const _changeEvent = { + type: 'change' +}; +const _startEvent = { + type: 'start' +}; +const _endEvent = { + type: 'end' +}; +class OrbitControls extends THREE.EventDispatcher { + constructor(object, domElement) { + super(); + if (domElement === undefined) console.warn('THREE.OrbitControls: The second parameter "domElement" is now mandatory.'); + if (domElement === document) console.error('THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.'); + this.object = object; + this.domElement = domElement; + this.domElement.style.touchAction = 'none'; // disable touch scroll + + // Set to false to disable this control + this.enabled = true; + + // "target" sets the location of focus, where the object orbits around + this.target = new THREE.Vector3(); + + // How far you can dolly in and out ( PerspectiveCamera only ) + this.minDistance = 0; + this.maxDistance = Infinity; + + // How far you can zoom in and out ( OrthographicCamera only ) + this.minZoom = 0; + this.maxZoom = Infinity; + + // How far you can orbit vertically, upper and lower limits. + // Range is 0 to Math.PI radians. + this.minPolarAngle = 0; // radians + this.maxPolarAngle = Math.PI; // radians + + // How far you can orbit horizontally, upper and lower limits. + // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) + this.minAzimuthAngle = -Infinity; // radians + this.maxAzimuthAngle = Infinity; // radians + + // Set to true to enable damping (inertia) + // If damping is enabled, you must call controls.update() in your animation loop + this.enableDamping = false; + this.dampingFactor = 0.05; + + // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. + // Set to false to disable zooming + this.enableZoom = true; + this.zoomSpeed = 1.0; + + // Set to false to disable rotating + this.enableRotate = true; + this.rotateSpeed = 1.0; + + // Set to false to disable panning + this.enablePan = true; + this.panSpeed = 1.0; + this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up + this.keyPanSpeed = 7.0; // pixels moved per arrow key push + + // Set to true to automatically rotate around the target + // If auto-rotate is enabled, you must call controls.update() in your animation loop + this.autoRotate = false; + this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 + + // The four arrow keys + this.keys = { + LEFT: 'ArrowLeft', + UP: 'ArrowUp', + RIGHT: 'ArrowRight', + BOTTOM: 'ArrowDown' + }; + + // Mouse buttons + this.mouseButtons = { + LEFT: THREE.MOUSE.ROTATE, + MIDDLE: THREE.MOUSE.DOLLY, + RIGHT: THREE.MOUSE.PAN + }; + + // Touch fingers + this.touches = { + ONE: THREE.TOUCH.ROTATE, + TWO: THREE.TOUCH.DOLLY_PAN + }; + + // for reset + this.target0 = this.target.clone(); + this.position0 = this.object.position.clone(); + this.zoom0 = this.object.zoom; + + // the target DOM element for key events + this._domElementKeyEvents = null; + + // ZOOM-TO-CURSOR + this.cursorScreen = new THREE.Vector3(); + this.cursorWorld = new THREE.Vector3(); + this.zoomToCursor = false; + this.adjustmentAfterZoomNeeded = false; + // + + // + // public methods + // + + // ZOOM-TO-CURSOR + this.adjustAfterZoom = function () { + const newCursorWorld = new THREE.Vector3(scope.cursorScreen.x, scope.cursorScreen.y, scope.target.clone().project(scope.object).z).clone().unproject(scope.object); + const delta = new THREE.Vector3().subVectors(scope.cursorWorld, newCursorWorld); + scope.object.position.add(delta); + scope.target.add(delta); + }; + this.setCursorWorld = function () { + scope.cursorWorld.copy(new THREE.Vector3(scope.cursorScreen.x, scope.cursorScreen.y, scope.target.clone().project(scope.object).z).unproject(scope.object)); + }; + // + + this.getPolarAngle = function () { + return spherical.phi; + }; + this.getAzimuthalAngle = function () { + return spherical.theta; + }; + this.getDistance = function () { + return this.object.position.distanceTo(this.target); + }; + this.listenToKeyEvents = function (domElement) { + domElement.addEventListener('keydown', onKeyDown); + this._domElementKeyEvents = domElement; + }; + this.saveState = function () { + scope.target0.copy(scope.target); + scope.position0.copy(scope.object.position); + scope.zoom0 = scope.object.zoom; + }; + this.reset = function () { + scope.target.copy(scope.target0); + scope.object.position.copy(scope.position0); + scope.object.zoom = scope.zoom0; + scope.object.updateProjectionMatrix(); + scope.dispatchEvent(_changeEvent); + scope.update(); + state = STATE.NONE; + }; + + // this method is exposed, but perhaps it would be better if we can make it private... + this.update = function () { + const offset = new THREE.Vector3(); + + // so camera.up is the orbit axis + const quat = new THREE.Quaternion().setFromUnitVectors(object.up, new THREE.Vector3(0, 1, 0)); + const quatInverse = quat.clone().invert(); + const lastPosition = new THREE.Vector3(); + const lastQuaternion = new THREE.Quaternion(); + const twoPI = 2 * Math.PI; + return function update() { + const position = scope.object.position; + offset.copy(position).sub(scope.target); + + // rotate offset to "y-axis-is-up" space + offset.applyQuaternion(quat); + + // angle from z-axis around y-axis + spherical.setFromVector3(offset); + if (scope.autoRotate && state === STATE.NONE) { + rotateLeft(getAutoRotationAngle()); + } + if (scope.enableDamping) { + spherical.theta += sphericalDelta.theta * scope.dampingFactor; + spherical.phi += sphericalDelta.phi * scope.dampingFactor; + } else { + spherical.theta += sphericalDelta.theta; + spherical.phi += sphericalDelta.phi; + } + + // restrict theta to be between desired limits + + let min = scope.minAzimuthAngle; + let max = scope.maxAzimuthAngle; + if (isFinite(min) && isFinite(max)) { + if (min < -Math.PI) min += twoPI;else if (min > Math.PI) min -= twoPI; + if (max < -Math.PI) max += twoPI;else if (max > Math.PI) max -= twoPI; + if (min <= max) { + spherical.theta = Math.max(min, Math.min(max, spherical.theta)); + } else { + spherical.theta = spherical.theta > (min + max) / 2 ? Math.max(min, spherical.theta) : Math.min(max, spherical.theta); + } + } + + // restrict phi to be between desired limits + spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi)); + spherical.makeSafe(); + spherical.radius *= scale; + + // restrict radius to be between desired limits + spherical.radius = Math.max(scope.minDistance, Math.min(scope.maxDistance, spherical.radius)); + + // move target to panned location + + if (scope.enableDamping === true) { + scope.target.addScaledVector(panOffset, scope.dampingFactor); + } else { + scope.target.add(panOffset); + } + offset.setFromSpherical(spherical); + + // rotate offset back to "camera-up-vector-is-up" space + offset.applyQuaternion(quatInverse); + position.copy(scope.target).add(offset); + scope.object.lookAt(scope.target); + if (scope.enableDamping === true) { + sphericalDelta.theta *= 1 - scope.dampingFactor; + sphericalDelta.phi *= 1 - scope.dampingFactor; + panOffset.multiplyScalar(1 - scope.dampingFactor); + } else { + sphericalDelta.set(0, 0, 0); + panOffset.set(0, 0, 0); + } + scale = 1; + + // update condition is: + // min(camera displacement, camera rotation in radians)^2 > EPS + // using small-angle approximation cos(x/2) = 1 - x^2 / 8 + + if (zoomChanged || lastPosition.distanceToSquared(scope.object.position) > EPS || 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS) { + scope.dispatchEvent(_changeEvent); + + // ZOOM-TO-CURSOR + if (scope.zoomToCursor && scope.adjustmentAfterZoomNeeded) { + scope.adjustmentAfterZoomNeeded = false; + this.adjustAfterZoom(); + } + // + + lastPosition.copy(scope.object.position); + lastQuaternion.copy(scope.object.quaternion); + zoomChanged = false; + return true; + } + return false; + }; + }(); + this.dispose = function () { + scope.domElement.removeEventListener('contextmenu', onContextMenu); + scope.domElement.removeEventListener('pointerdown', onPointerDown); + scope.domElement.removeEventListener('pointercancel', onPointerCancel); + scope.domElement.removeEventListener('wheel', onMouseWheel); + scope.domElement.removeEventListener('pointermove', onPointerMove); + scope.domElement.removeEventListener('pointerup', onPointerUp); + if (scope._domElementKeyEvents !== null) { + scope._domElementKeyEvents.removeEventListener('keydown', onKeyDown); + } + + //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? + }; + + // + // internals + // + + const scope = this; + const STATE = { + NONE: -1, + ROTATE: 0, + DOLLY: 1, + PAN: 2, + TOUCH_ROTATE: 3, + TOUCH_PAN: 4, + TOUCH_DOLLY_PAN: 5, + TOUCH_DOLLY_ROTATE: 6 + }; + let state = STATE.NONE; + const EPS = 0.000001; + + // current position in spherical coordinates + const spherical = new THREE.Spherical(); + const sphericalDelta = new THREE.Spherical(); + let scale = 1; + const panOffset = new THREE.Vector3(); + let zoomChanged = false; + const rotateStart = new THREE.Vector2(); + const rotateEnd = new THREE.Vector2(); + const rotateDelta = new THREE.Vector2(); + const panStart = new THREE.Vector2(); + const panEnd = new THREE.Vector2(); + const panDelta = new THREE.Vector2(); + const dollyStart = new THREE.Vector2(); + const dollyEnd = new THREE.Vector2(); + const dollyDelta = new THREE.Vector2(); + const pointers = []; + const pointerPositions = {}; + function getAutoRotationAngle() { + return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; + } + function getZoomScale() { + return Math.pow(0.95, scope.zoomSpeed); + } + function rotateLeft(angle) { + sphericalDelta.theta -= angle; + } + function rotateUp(angle) { + sphericalDelta.phi -= angle; + } + const panLeft = function () { + const v = new THREE.Vector3(); + return function panLeft(distance, objectMatrix) { + v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix + v.multiplyScalar(-distance); + panOffset.add(v); + }; + }(); + const panUp = function () { + const v = new THREE.Vector3(); + return function panUp(distance, objectMatrix) { + if (scope.screenSpacePanning === true) { + v.setFromMatrixColumn(objectMatrix, 1); + } else { + v.setFromMatrixColumn(objectMatrix, 0); + v.crossVectors(scope.object.up, v); + } + v.multiplyScalar(distance); + panOffset.add(v); + }; + }(); + + // deltaX and deltaY are in pixels; right and down are positive + const pan = function () { + const offset = new THREE.Vector3(); + return function pan(deltaX, deltaY) { + const element = scope.domElement; + if (scope.object.isPerspectiveCamera) { + // perspective + const position = scope.object.position; + offset.copy(position).sub(scope.target); + let targetDistance = offset.length(); + + // half of the fov is center to top of screen + targetDistance *= Math.tan(scope.object.fov / 2 * Math.PI / 180.0); + + // we use only clientHeight here so aspect ratio does not distort speed + panLeft(2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix); + panUp(2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix); + } else if (scope.object.isOrthographicCamera) { + // orthographic + panLeft(deltaX * (scope.object.right - scope.object.left) / scope.object.zoom / element.clientWidth, scope.object.matrix); + panUp(deltaY * (scope.object.top - scope.object.bottom) / scope.object.zoom / element.clientHeight, scope.object.matrix); + } else { + // camera neither orthographic nor perspective + console.warn('WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.'); + scope.enablePan = false; + } + }; + }(); + function dollyOut(dollyScale) { + // ZOOM-TO-CURSOR + if (scope.zoomToCursor) scope.setCursorWorld(); + // + + if (scope.object.isPerspectiveCamera) { + scale /= dollyScale; + } else if (scope.object.isOrthographicCamera) { + scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom * dollyScale)); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + } else { + console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.'); + scope.enableZoom = false; + } + + // ZOOM-TO-CURSOR + if (scope.zoomToCursor) { + if (scope.object.isOrthographicCamera) scope.adjustAfterZoom();else if (scope.object.isPerspectiveCamera) scope.adjustmentAfterZoomNeeded = true; + } + // + } + + function dollyIn(dollyScale) { + // ZOOM-TO-CURSOR + if (scope.zoomToCursor) scope.setCursorWorld(); + // + + if (scope.object.isPerspectiveCamera) { + scale *= dollyScale; + } else if (scope.object.isOrthographicCamera) { + scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / dollyScale)); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + } else { + console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.'); + scope.enableZoom = false; + } + + // ZOOM-TO-CURSOR + if (scope.zoomToCursor) { + if (scope.object.isOrthographicCamera) scope.adjustAfterZoom();else if (scope.object.isPerspectiveCamera) scope.adjustmentAfterZoomNeeded = true; + } + // + } + + // + // event callbacks - update the object state + // + + function handleMouseDownRotate(event) { + rotateStart.set(event.clientX, event.clientY); + } + function handleMouseDownDolly(event) { + dollyStart.set(event.clientX, event.clientY); + } + function handleMouseDownPan(event) { + panStart.set(event.clientX, event.clientY); + } + function handleMouseMoveRotate(event) { + rotateEnd.set(event.clientX, event.clientY); + rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed); + const element = scope.domElement; + rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); // yes, height + + rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight); + rotateStart.copy(rotateEnd); + scope.update(); + } + function handleMouseMoveDolly(event) { + dollyEnd.set(event.clientX, event.clientY); + dollyDelta.subVectors(dollyEnd, dollyStart); + if (dollyDelta.y > 0) { + dollyOut(getZoomScale()); + } else if (dollyDelta.y < 0) { + dollyIn(getZoomScale()); + } + dollyStart.copy(dollyEnd); + scope.update(); + } + function handleMouseMovePan(event) { + panEnd.set(event.clientX, event.clientY); + panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed); + pan(panDelta.x, panDelta.y); + panStart.copy(panEnd); + scope.update(); + } + function handleMouseWheel(event) { + if (event.deltaY < 0) { + dollyIn(getZoomScale()); + } else if (event.deltaY > 0) { + dollyOut(getZoomScale()); + } + scope.update(); + } + function handleKeyDown(event) { + let needsUpdate = false; + switch (event.code) { + case scope.keys.UP: + pan(0, scope.keyPanSpeed); + needsUpdate = true; + break; + case scope.keys.BOTTOM: + pan(0, -scope.keyPanSpeed); + needsUpdate = true; + break; + case scope.keys.LEFT: + pan(scope.keyPanSpeed, 0); + needsUpdate = true; + break; + case scope.keys.RIGHT: + pan(-scope.keyPanSpeed, 0); + needsUpdate = true; + break; + } + if (needsUpdate) { + // prevent the browser from scrolling on cursor keys + event.preventDefault(); + scope.update(); + } + } + function handleTouchStartRotate() { + if (pointers.length === 1) { + rotateStart.set(pointers[0].pageX, pointers[0].pageY); + } else { + const x = 0.5 * (pointers[0].pageX + pointers[1].pageX); + const y = 0.5 * (pointers[0].pageY + pointers[1].pageY); + rotateStart.set(x, y); + } + } + function handleTouchStartPan() { + if (pointers.length === 1) { + panStart.set(pointers[0].pageX, pointers[0].pageY); + } else { + const x = 0.5 * (pointers[0].pageX + pointers[1].pageX); + const y = 0.5 * (pointers[0].pageY + pointers[1].pageY); + panStart.set(x, y); + } + } + function handleTouchStartDolly() { + const dx = pointers[0].pageX - pointers[1].pageX; + const dy = pointers[0].pageY - pointers[1].pageY; + const distance = Math.sqrt(dx * dx + dy * dy); + dollyStart.set(0, distance); + } + function handleTouchStartDollyPan() { + if (scope.enableZoom) handleTouchStartDolly(); + if (scope.enablePan) handleTouchStartPan(); + } + function handleTouchStartDollyRotate() { + if (scope.enableZoom) handleTouchStartDolly(); + if (scope.enableRotate) handleTouchStartRotate(); + } + function handleTouchMoveRotate(event) { + if (pointers.length == 1) { + rotateEnd.set(event.pageX, event.pageY); + } else { + const position = getSecondPointerPosition(event); + const x = 0.5 * (event.pageX + position.x); + const y = 0.5 * (event.pageY + position.y); + rotateEnd.set(x, y); + } + rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed); + const element = scope.domElement; + rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); // yes, height + + rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight); + rotateStart.copy(rotateEnd); + } + function handleTouchMovePan(event) { + if (pointers.length === 1) { + panEnd.set(event.pageX, event.pageY); + } else { + const position = getSecondPointerPosition(event); + const x = 0.5 * (event.pageX + position.x); + const y = 0.5 * (event.pageY + position.y); + panEnd.set(x, y); + } + panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed); + pan(panDelta.x, panDelta.y); + panStart.copy(panEnd); + } + function handleTouchMoveDolly(event) { + const position = getSecondPointerPosition(event); + const dx = event.pageX - position.x; + const dy = event.pageY - position.y; + const distance = Math.sqrt(dx * dx + dy * dy); + dollyEnd.set(0, distance); + dollyDelta.set(0, Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed)); + dollyOut(dollyDelta.y); + dollyStart.copy(dollyEnd); + } + function handleTouchMoveDollyPan(event) { + if (scope.enableZoom) handleTouchMoveDolly(event); + if (scope.enablePan) handleTouchMovePan(event); + } + function handleTouchMoveDollyRotate(event) { + if (scope.enableZoom) handleTouchMoveDolly(event); + if (scope.enableRotate) handleTouchMoveRotate(event); + } + + // + // event handlers - FSM: listen for events and reset state + // + + function onPointerDown(event) { + if (scope.enabled === false) return; + if (pointers.length === 0) { + scope.domElement.setPointerCapture(event.pointerId); + scope.domElement.addEventListener('pointermove', onPointerMove); + scope.domElement.addEventListener('pointerup', onPointerUp); + } + + // + + addPointer(event); + if (event.pointerType === 'touch') { + onTouchStart(event); + } else { + onMouseDown(event); + } + } + function onPointerMove(event) { + if (scope.enabled === false) return; + if (event.pointerType === 'touch') { + onTouchMove(event); + } else { + onMouseMove(event); + } + } + function onPointerUp(event) { + removePointer(event); + if (pointers.length === 0) { + scope.domElement.releasePointerCapture(event.pointerId); + scope.domElement.removeEventListener('pointermove', onPointerMove); + scope.domElement.removeEventListener('pointerup', onPointerUp); + } + scope.dispatchEvent(_endEvent); + state = STATE.NONE; + } + function onPointerCancel(event) { + removePointer(event); + } + function onMouseDown(event) { + let mouseAction; + switch (event.button) { + case 0: + mouseAction = scope.mouseButtons.LEFT; + break; + case 1: + mouseAction = scope.mouseButtons.MIDDLE; + break; + case 2: + mouseAction = scope.mouseButtons.RIGHT; + break; + default: + mouseAction = -1; + } + switch (mouseAction) { + case THREE.MOUSE.DOLLY: + if (scope.enableZoom === false) return; + handleMouseDownDolly(event); + state = STATE.DOLLY; + break; + case THREE.MOUSE.ROTATE: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + if (scope.enablePan === false) return; + handleMouseDownPan(event); + state = STATE.PAN; + } else { + if (scope.enableRotate === false) return; + handleMouseDownRotate(event); + state = STATE.ROTATE; + } + break; + case THREE.MOUSE.PAN: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + if (scope.enableRotate === false) return; + handleMouseDownRotate(event); + state = STATE.ROTATE; + } else { + if (scope.enablePan === false) return; + handleMouseDownPan(event); + state = STATE.PAN; + } + break; + default: + state = STATE.NONE; + } + if (state !== STATE.NONE) { + scope.dispatchEvent(_startEvent); + } + } + function onMouseMove(event) { + if (scope.enabled === false) return; + switch (state) { + case STATE.ROTATE: + if (scope.enableRotate === false) return; + handleMouseMoveRotate(event); + break; + case STATE.DOLLY: + if (scope.enableZoom === false) return; + handleMouseMoveDolly(event); + break; + case STATE.PAN: + if (scope.enablePan === false) return; + handleMouseMovePan(event); + break; + } + } + function onMouseWheel(event) { + if (scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE) return; + event.preventDefault(); + scope.dispatchEvent(_startEvent); + handleMouseWheel(event); + scope.dispatchEvent(_endEvent); + } + function onKeyDown(event) { + if (scope.enabled === false || scope.enablePan === false) return; + handleKeyDown(event); + } + function onTouchStart(event) { + trackPointer(event); + switch (pointers.length) { + case 1: + switch (scope.touches.ONE) { + case THREE.TOUCH.ROTATE: + if (scope.enableRotate === false) return; + handleTouchStartRotate(); + state = STATE.TOUCH_ROTATE; + break; + case THREE.TOUCH.PAN: + if (scope.enablePan === false) return; + handleTouchStartPan(); + state = STATE.TOUCH_PAN; + break; + default: + state = STATE.NONE; + } + break; + case 2: + switch (scope.touches.TWO) { + case THREE.TOUCH.DOLLY_PAN: + if (scope.enableZoom === false && scope.enablePan === false) return; + handleTouchStartDollyPan(); + state = STATE.TOUCH_DOLLY_PAN; + break; + case THREE.TOUCH.DOLLY_ROTATE: + if (scope.enableZoom === false && scope.enableRotate === false) return; + handleTouchStartDollyRotate(); + state = STATE.TOUCH_DOLLY_ROTATE; + break; + default: + state = STATE.NONE; + } + break; + default: + state = STATE.NONE; + } + if (state !== STATE.NONE) { + scope.dispatchEvent(_startEvent); + } + } + function onTouchMove(event) { + trackPointer(event); + switch (state) { + case STATE.TOUCH_ROTATE: + if (scope.enableRotate === false) return; + handleTouchMoveRotate(event); + scope.update(); + break; + case STATE.TOUCH_PAN: + if (scope.enablePan === false) return; + handleTouchMovePan(event); + scope.update(); + break; + case STATE.TOUCH_DOLLY_PAN: + if (scope.enableZoom === false && scope.enablePan === false) return; + handleTouchMoveDollyPan(event); + scope.update(); + break; + case STATE.TOUCH_DOLLY_ROTATE: + if (scope.enableZoom === false && scope.enableRotate === false) return; + handleTouchMoveDollyRotate(event); + scope.update(); + break; + default: + state = STATE.NONE; + } + } + function onContextMenu(event) { + if (scope.enabled === false) return; + event.preventDefault(); + } + function addPointer(event) { + pointers.push(event); + } + function removePointer(event) { + delete pointerPositions[event.pointerId]; + for (let i = 0; i < pointers.length; i++) { + if (pointers[i].pointerId == event.pointerId) { + pointers.splice(i, 1); + return; + } + } + } + function trackPointer(event) { + let position = pointerPositions[event.pointerId]; + if (position === undefined) { + position = new THREE.Vector2(); + pointerPositions[event.pointerId] = position; + } + position.set(event.pageX, event.pageY); + } + function getSecondPointerPosition(event) { + const pointer = event.pointerId === pointers[0].pointerId ? pointers[1] : pointers[0]; + return pointerPositions[pointer.pointerId]; + } + + // + + scope.domElement.addEventListener('contextmenu', onContextMenu); + scope.domElement.addEventListener('pointerdown', onPointerDown); + scope.domElement.addEventListener('pointercancel', onPointerCancel); + scope.domElement.addEventListener('wheel', onMouseWheel, { + passive: false + }); + + // ZOOM-TO-CURSOR + scope.domElement.addEventListener('mousemove', event => { + if (!scope.zoomToCursor) return; + scope.cursorScreen.copy(new THREE.Vector3(event.clientX / scope.domElement.clientWidth * 2 - 1, -(event.clientY / scope.domElement.clientHeight) * 2 + 1, scope.target.clone().project(scope.object).z)); + }); + const handleTouch = event => { + const touches = event.touches; + let touch; + if (touches.length === 1) { + touch = new THREE.Vector2(touches[0].clientX, touches[0].clientY); + } else if (touches.length === 2) { + touch = new THREE.Vector2((touches[0].clientX + touches[1].clientX) / 2, (touches[0].clientY + touches[1].clientY) / 2); + } + if (touch !== undefined) { + scope.cursorScreen.copy(new THREE.Vector3(touch.x / scope.domElement.clientWidth * 2 - 1, -(touch.y / scope.domElement.clientHeight) * 2 + 1, scope.target.clone().project(scope.object).z)); + } + }; + scope.domElement.addEventListener('touchstart', handleTouch); + scope.domElement.addEventListener('touchmove', handleTouch); + // + + // force an update at start + + this.update(); + } +} + +// This set of controls performs orbiting, dollying (zooming), and panning. +// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). +// This is very similar to OrbitControls, another set of touch behavior +// +// Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate +// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish +// Pan - left mouse, or arrow keys / touch: one-finger move + +class MapControls extends OrbitControls { + constructor(object, domElement) { + super(object, domElement); + this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up + + this.mouseButtons.LEFT = THREE.MOUSE.PAN; + this.mouseButtons.RIGHT = THREE.MOUSE.ROTATE; + this.touches.ONE = THREE.TOUCH.PAN; + this.touches.TWO = THREE.TOUCH.DOLLY_ROTATE; + } +} + +THREE.MapControls = MapControls; +THREE.OrbitControls = OrbitControls; } )(); diff --git a/examples/jsm/controls/OrbitControls.js b/examples/jsm/controls/OrbitControls.js index 4288a01dafe97a..5948c5c5f78a86 100644 --- a/examples/jsm/controls/OrbitControls.js +++ b/examples/jsm/controls/OrbitControls.js @@ -25,6 +25,9 @@ class OrbitControls extends EventDispatcher { super(); + if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' ); + if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); + this.object = object; this.domElement = domElement; this.domElement.style.touchAction = 'none'; // disable touch scroll @@ -95,10 +98,35 @@ class OrbitControls extends EventDispatcher { // the target DOM element for key events this._domElementKeyEvents = null; + // ZOOM-TO-CURSOR + this.cursorScreen = new Vector3(); + this.cursorWorld = new Vector3(); + this.enableZoomToCursor = false; + this.adjustmentAfterZoomNeeded = false; + // + // // public methods // + // ZOOM-TO-CURSOR + this.adjustAfterZoom = function () { + + const newCursorWorld = new Vector3( scope.cursorScreen.x, scope.cursorScreen.y, scope.target.clone().project( scope.object ).z ).clone().unproject( scope.object ); + const delta = new Vector3().subVectors( scope.cursorWorld, newCursorWorld ); + + scope.object.position.add( delta ); + scope.target.add( delta ); + + }; + + this.setCursorWorld = function () { + + scope.cursorWorld.copy( new Vector3( scope.cursorScreen.x, scope.cursorScreen.y, scope.target.clone().project( scope.object ).z ).unproject( scope.object ) ); + + }; + // + this.getPolarAngle = function () { return spherical.phi; @@ -163,6 +191,8 @@ class OrbitControls extends EventDispatcher { return function update() { + + const position = scope.object.position; offset.copy( position ).sub( scope.target ); @@ -275,6 +305,15 @@ class OrbitControls extends EventDispatcher { scope.dispatchEvent( _changeEvent ); + // ZOOM-TO-CURSOR + if ( scope.enableZoomToCursor && scope.adjustmentAfterZoomNeeded ) { + + scope.adjustmentAfterZoomNeeded = false; + this.adjustAfterZoom(); + + } + // + lastPosition.copy( scope.object.position ); lastQuaternion.copy( scope.object.quaternion ); zoomChanged = false; @@ -462,6 +501,10 @@ class OrbitControls extends EventDispatcher { function dollyOut( dollyScale ) { + // ZOOM-TO-CURSOR + if ( scope.enableZoomToCursor ) scope.setCursorWorld(); + // + if ( scope.object.isPerspectiveCamera ) { scale /= dollyScale; @@ -479,10 +522,23 @@ class OrbitControls extends EventDispatcher { } + // ZOOM-TO-CURSOR + if ( scope.enableZoomToCursor ) { + + if ( scope.object.isOrthographicCamera ) scope.adjustAfterZoom(); + else if ( scope.object.isPerspectiveCamera ) scope.adjustmentAfterZoomNeeded = true; + + } + // + } function dollyIn( dollyScale ) { + // ZOOM-TO-CURSOR + if ( scope.enableZoomToCursor ) scope.setCursorWorld(); + // + if ( scope.object.isPerspectiveCamera ) { scale *= dollyScale; @@ -500,6 +556,15 @@ class OrbitControls extends EventDispatcher { } + // ZOOM-TO-CURSOR + if ( scope.enableZoomToCursor ) { + + if ( scope.object.isOrthographicCamera ) scope.adjustAfterZoom(); + else if ( scope.object.isPerspectiveCamera ) scope.adjustmentAfterZoomNeeded = true; + + } + // + } // @@ -957,6 +1022,8 @@ class OrbitControls extends EventDispatcher { function onMouseMove( event ) { + if ( scope.enabled === false ) return; + switch ( state ) { case STATE.ROTATE: @@ -1209,6 +1276,52 @@ class OrbitControls extends EventDispatcher { scope.domElement.addEventListener( 'pointercancel', onPointerCancel ); scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } ); + // ZOOM-TO-CURSOR + scope.domElement.addEventListener( 'mousemove', event => { + + if ( ! scope.enableZoomToCursor ) return; + scope.cursorScreen.copy( + new Vector3( + ( ( event.clientX ) / scope.domElement.clientWidth ) * 2 - 1, + - ( ( event.clientY ) / scope.domElement.clientHeight ) * 2 + 1, + scope.target.clone().project( scope.object ).z + ) + ); + + } ); + + const handleTouch = event => { + + const touches = event.touches; + let touch; + if ( touches.length === 1 ) { + + touch = new Vector2( touches[ 0 ].clientX, touches[ 0 ].clientY ); + + } else if ( touches.length === 2 ) { + + touch = new Vector2( ( touches[ 0 ].clientX + touches[ 1 ].clientX ) / 2, ( touches[ 0 ].clientY + touches[ 1 ].clientY ) / 2 ); + + } + + if ( touch !== undefined ) { + + scope.cursorScreen.copy( + new Vector3( + ( ( touch.x ) / scope.domElement.clientWidth ) * 2 - 1, + - ( ( touch.y ) / scope.domElement.clientHeight ) * 2 + 1, + scope.target.clone().project( scope.object ).z + ) + ); + + } + + }; + + scope.domElement.addEventListener( 'touchstart', handleTouch ); + scope.domElement.addEventListener( 'touchmove', handleTouch ); + // + // force an update at start this.update();