diff --git a/examples/js/controls/OrbitControls.js b/examples/js/controls/OrbitControls.js index 500187b838faa30560bfe3b1349936c19a057b0d..bee9d00e7e9fae2ec0a063d1a4d76c55f63dfef8 100644 --- a/examples/js/controls/OrbitControls.js +++ b/examples/js/controls/OrbitControls.js @@ -7,1109 +7,724 @@ */ /*global THREE, console */ -( function () { +// This set of controls performs orbiting, dollying (zooming), and panning. It maintains +// the "up" direction as +Y, unlike the TrackballControls. Touch on tablet and phones is +// supported. +// +// Orbit - left mouse / touch: one finger move +// Zoom - middle mouse, or mousewheel / touch: two finger spread or squish +// Pan - right mouse, or arrow keys / touch: three finter swipe - function OrbitConstraint ( object ) { +THREE.OrbitControls = function ( object, domElement ) { - this.object = object; + this.object = object; + this.domElement = ( domElement !== undefined ) ? domElement : document; - // "target" sets the location of focus, where the object orbits around - // and where it pans with respect to. - this.target = new THREE.Vector3(); + // API - // Limits to how far you can dolly in and out ( PerspectiveCamera only ) - this.minDistance = 0; - this.maxDistance = Infinity; + // Set to false to disable this control + this.enabled = true; - // Limits to how far you can zoom in and out ( OrthographicCamera only ) - this.minZoom = 0; - this.maxZoom = Infinity; + // "target" sets the location of focus, where the control orbits around + // and where it pans with respect to. + this.target = new THREE.Vector3(); - // 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 + // center is old, deprecated; use "target" instead + this.center = this.target; - // How far you can orbit horizontally, upper and lower limits. - // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. - this.minAzimuthAngle = - Infinity; // radians - this.maxAzimuthAngle = Infinity; // radians + // This option actually enables dollying in and out; left as "zoom" for + // backwards compatibility + this.noZoom = false; + this.zoomSpeed = 1.0; - // 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.25; + // Limits to how far you can dolly in and out ( PerspectiveCamera only ) + this.minDistance = 0; + this.maxDistance = Infinity; - //////////// - // internals + // Limits to how far you can zoom in and out ( OrthographicCamera only ) + this.minZoom = 0; + this.maxZoom = Infinity; - var scope = this; + // Set to true to disable this control + this.noRotate = false; + this.rotateSpeed = 1.0; - var EPS = 0.000001; + // Set to true to disable this control + this.noPan = false; + this.keyPanSpeed = 7.0; // pixels moved per arrow key push - // Current position in spherical coordinate system. - var theta; - var phi; + // Set to true to automatically rotate around the target + this.autoRotate = false; + this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 - // Pending changes - var phiDelta = 0; - var thetaDelta = 0; - var scale = 1; - var panOffset = new THREE.Vector3(); - var zoomChanged = false; + // 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 - // API + // How far you can orbit horizontally, upper and lower limits. + // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. + this.minAzimuthAngle = - Infinity; // radians + this.maxAzimuthAngle = Infinity; // radians - this.getPolarAngle = function () { + // Set to true to disable use of the keys + this.noKeys = false; - return phi; + // The four arrow keys + this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; - }; + // Mouse buttons + this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }; - this.getAzimuthalAngle = function () { + //////////// + // internals - return theta; + var scope = this; - }; + var EPS = 0.000001; - this.rotateLeft = function ( angle ) { + var rotateStart = new THREE.Vector2(); + var rotateEnd = new THREE.Vector2(); + var rotateDelta = new THREE.Vector2(); - thetaDelta -= angle; + var panStart = new THREE.Vector2(); + var panEnd = new THREE.Vector2(); + var panDelta = new THREE.Vector2(); + var panOffset = new THREE.Vector3(); - }; + var offset = new THREE.Vector3(); - this.rotateUp = function ( angle ) { + var dollyStart = new THREE.Vector2(); + var dollyEnd = new THREE.Vector2(); + var dollyDelta = new THREE.Vector2(); - phiDelta -= angle; + var theta; + var phi; + var phiDelta = 0; + var thetaDelta = 0; + var scale = 1; + var pan = new THREE.Vector3(); - }; + var lastPosition = new THREE.Vector3(); + var lastQuaternion = new THREE.Quaternion(); - // pass in distance in world space to move left - this.panLeft = function() { + var STATE = { NONE : -1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 }; - var v = new THREE.Vector3(); + var state = STATE.NONE; - return function panLeft ( distance ) { + // for reset - var te = this.object.matrix.elements; + this.target0 = this.target.clone(); + this.position0 = this.object.position.clone(); + this.zoom0 = this.object.zoom; - // get X column of matrix - v.set( te[ 0 ], te[ 1 ], te[ 2 ] ); - v.multiplyScalar( - distance ); + // so camera.up is the orbit axis - panOffset.add( v ); + var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); + var quatInverse = quat.clone().inverse(); - }; + // events - }(); + var changeEvent = { type: 'change' }; + var startEvent = { type: 'start' }; + var endEvent = { type: 'end' }; - // pass in distance in world space to move up - this.panUp = function() { + this.rotateLeft = function ( angle ) { - var v = new THREE.Vector3(); + if ( angle === undefined ) { - return function panUp ( distance ) { + angle = getAutoRotationAngle(); - var te = this.object.matrix.elements; - - // get Y column of matrix - v.set( te[ 4 ], te[ 5 ], te[ 6 ] ); - v.multiplyScalar( distance ); - - panOffset.add( v ); - - }; - - }(); - - // pass in x,y of change desired in pixel space, - // right and down are positive - this.pan = function ( deltaX, deltaY, screenWidth, screenHeight ) { - - if ( scope.object instanceof THREE.PerspectiveCamera ) { - - // perspective - var position = scope.object.position; - var offset = position.clone().sub( scope.target ); - var 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 actually don't use screenWidth, since perspective camera is fixed to screen height - scope.panLeft( 2 * deltaX * targetDistance / screenHeight ); - scope.panUp( 2 * deltaY * targetDistance / screenHeight ); - - } else if ( scope.object instanceof THREE.OrthographicCamera ) { - - // orthographic - scope.panLeft( deltaX * ( scope.object.right - scope.object.left ) / screenWidth ); - scope.panUp( deltaY * ( scope.object.top - scope.object.bottom ) / screenHeight ); - - } else { - - // camera neither orthographic or perspective - console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); - - } - - }; - - this.dollyIn = function ( dollyScale ) { - - if ( scope.object instanceof THREE.PerspectiveCamera ) { - - scale /= dollyScale; - - } else if ( scope.object instanceof THREE.OrthographicCamera ) { - - scope.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom * dollyScale ) ); - scope.object.updateProjectionMatrix(); - zoomChanged = true; - - } else { - - console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); - - } - - }; - - this.dollyOut = function ( dollyScale ) { - - if ( scope.object instanceof THREE.PerspectiveCamera ) { - - scale *= dollyScale; - - } else if ( scope.object instanceof THREE.OrthographicCamera ) { - - scope.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / dollyScale ) ); - scope.object.updateProjectionMatrix(); - zoomChanged = true; - - } else { - - console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); - - } - - }; - - this.update = function() { - - var offset = new THREE.Vector3(); - - // so camera.up is the orbit axis - var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); - var quatInverse = quat.clone().inverse(); - - var lastPosition = new THREE.Vector3(); - var lastQuaternion = new THREE.Quaternion(); - - return function () { - - var position = this.object.position; - - offset.copy( position ).sub( this.target ); - - // rotate offset to "y-axis-is-up" space - offset.applyQuaternion( quat ); - - // angle from z-axis around y-axis - - theta = Math.atan2( offset.x, offset.z ); - - // angle from y-axis - - phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y ); - - theta += thetaDelta; - phi += phiDelta; - - // restrict theta to be between desired limits - theta = Math.max( this.minAzimuthAngle, Math.min( this.maxAzimuthAngle, theta ) ); - - // restrict phi to be between desired limits - phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) ); - - // restrict phi to be betwee EPS and PI-EPS - phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) ); - - var radius = offset.length() * scale; - - // restrict radius to be between desired limits - radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) ); - - // move target to panned location - this.target.add( panOffset ); - - offset.x = radius * Math.sin( phi ) * Math.sin( theta ); - offset.y = radius * Math.cos( phi ); - offset.z = radius * Math.sin( phi ) * Math.cos( theta ); - - // rotate offset back to "camera-up-vector-is-up" space - offset.applyQuaternion( quatInverse ); - - position.copy( this.target ).add( offset ); - - this.object.lookAt( this.target ); - - if ( this.enableDamping === true ) { - - thetaDelta *= ( 1 - this.dampingFactor ); - phiDelta *= ( 1 - this.dampingFactor ); - - } else { - - thetaDelta = 0; - phiDelta = 0; - - } - - scale = 1; - panOffset.set( 0, 0, 0 ); - - // 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( this.object.position ) > EPS || - 8 * ( 1 - lastQuaternion.dot( this.object.quaternion ) ) > EPS ) { - - lastPosition.copy( this.object.position ); - lastQuaternion.copy( this.object.quaternion ); - zoomChanged = false; - - return true; - - } - - return false; - - }; + } - }(); + thetaDelta -= angle; }; + this.rotateUp = function ( angle ) { - // This set of controls performs orbiting, dollying (zooming), and panning. It maintains - // the "up" direction as +Y, unlike the TrackballControls. Touch on tablet and phones is - // supported. - // - // Orbit - left mouse / touch: one finger move - // Zoom - middle mouse, or mousewheel / touch: two finger spread or squish - // Pan - right mouse, or arrow keys / touch: three finter swipe - - THREE.OrbitControls = function ( object, domElement ) { - - var constraint = new OrbitConstraint( object ); - - this.domElement = ( domElement !== undefined ) ? domElement : document; - - // API - - Object.defineProperty( this, 'constraint', { - - get: function() { - - return constraint; - - } - - } ); - - this.getPolarAngle = function () { - - return constraint.getPolarAngle(); - - }; - - this.getAzimuthalAngle = function () { - - return constraint.getAzimuthalAngle(); + if ( angle === undefined ) { - }; + angle = getAutoRotationAngle(); - // Set to false to disable this control - this.enabled = true; + } - // center is old, deprecated; use "target" instead - this.center = this.target; + phiDelta -= angle; - // 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; + // pass in distance in world space to move left + this.panLeft = function ( distance ) { - // Set to false to disable panning - this.enablePan = true; - this.keyPanSpeed = 7.0; // pixels moved per arrow key push + var te = this.object.matrix.elements; - // 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 round when fps is 60 + // get X column of matrix + panOffset.set( te[ 0 ], te[ 1 ], te[ 2 ] ); + panOffset.multiplyScalar( - distance ); - // Set to false to disable use of the keys - this.enableKeys = true; + pan.add( panOffset ); - // The four arrow keys - this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; + }; - // Mouse buttons - this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }; + // pass in distance in world space to move up + this.panUp = function ( distance ) { - //////////// - // internals + var te = this.object.matrix.elements; - var scope = this; + // get Y column of matrix + panOffset.set( te[ 4 ], te[ 5 ], te[ 6 ] ); + panOffset.multiplyScalar( distance ); - var rotateStart = new THREE.Vector2(); - var rotateEnd = new THREE.Vector2(); - var rotateDelta = new THREE.Vector2(); + pan.add( panOffset ); - var panStart = new THREE.Vector2(); - var panEnd = new THREE.Vector2(); - var panDelta = new THREE.Vector2(); + }; - var dollyStart = new THREE.Vector2(); - var dollyEnd = new THREE.Vector2(); - var dollyDelta = new THREE.Vector2(); + // pass in x,y of change desired in pixel space, + // right and down are positive + this.pan = function ( deltaX, deltaY ) { - var STATE = { NONE : - 1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 }; + var element = scope.domElement === document ? scope.domElement.body : scope.domElement; - var state = STATE.NONE; + if ( scope.object instanceof THREE.PerspectiveCamera ) { - // for reset + // perspective + var position = scope.object.position; + var offset = position.clone().sub( scope.target ); + var targetDistance = offset.length(); - this.target0 = this.target.clone(); - this.position0 = this.object.position.clone(); - this.zoom0 = this.object.zoom; + // half of the fov is center to top of screen + targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); - // events + // we actually don't use screenWidth, since perspective camera is fixed to screen height + scope.panLeft( 2 * deltaX * targetDistance / element.clientHeight ); + scope.panUp( 2 * deltaY * targetDistance / element.clientHeight ); - var changeEvent = { type: 'change' }; - var startEvent = { type: 'start' }; - var endEvent = { type: 'end' }; + } else if ( scope.object instanceof THREE.OrthographicCamera ) { - // pass in x,y of change desired in pixel space, - // right and down are positive - function pan( deltaX, deltaY ) { + // orthographic + scope.panLeft( deltaX * (scope.object.right - scope.object.left) / element.clientWidth ); + scope.panUp( deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight ); - var element = scope.domElement === document ? scope.domElement.body : scope.domElement; + } else { - constraint.pan( deltaX, deltaY, element.clientWidth, element.clientHeight ); + // camera neither orthographic or perspective + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); } - this.update = function () { - - if ( this.autoRotate && state === STATE.NONE ) { - - constraint.rotateLeft( getAutoRotationAngle() ); - - } - - if ( constraint.update() === true ) { - - this.dispatchEvent( changeEvent ); - - } - - }; - - this.reset = function () { - - state = STATE.NONE; - - this.target.copy( this.target0 ); - this.object.position.copy( this.position0 ); - this.object.zoom = this.zoom0; - - this.object.updateProjectionMatrix(); - this.dispatchEvent( changeEvent ); - - this.update(); - - }; - - function getAutoRotationAngle() { - - return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; + }; - } + this.dollyIn = function ( dollyScale ) { - function getZoomScale() { + if ( dollyScale === undefined ) { - return Math.pow( 0.95, scope.zoomSpeed ); + dollyScale = getZoomScale(); } - function onMouseDown( event ) { - - if ( scope.enabled === false ) return; - - event.preventDefault(); - - if ( event.button === scope.mouseButtons.ORBIT ) { - - if ( scope.enableRotate === false ) return; - - state = STATE.ROTATE; + if ( scope.object instanceof THREE.PerspectiveCamera ) { - rotateStart.set( event.clientX, event.clientY ); + scale /= dollyScale; - } else if ( event.button === scope.mouseButtons.ZOOM ) { + } else if ( scope.object instanceof THREE.OrthographicCamera ) { - if ( scope.enableZoom === false ) return; + scope.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom * dollyScale ) ); + scope.object.updateProjectionMatrix(); + scope.dispatchEvent( changeEvent ); - state = STATE.DOLLY; + } else { - dollyStart.set( event.clientX, event.clientY ); - - } else if ( event.button === scope.mouseButtons.PAN ) { - - if ( scope.enablePan === false ) return; - - state = STATE.PAN; - - panStart.set( event.clientX, event.clientY ); - - } - - if ( state !== STATE.NONE ) { - - document.addEventListener( 'mousemove', onMouseMove, false ); - document.addEventListener( 'mouseup', onMouseUp, false ); - scope.dispatchEvent( startEvent ); - - } + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); } - function onMouseMove( event ) { - - if ( scope.enabled === false ) return; - - event.preventDefault(); - - var element = scope.domElement === document ? scope.domElement.body : scope.domElement; - - if ( state === STATE.ROTATE ) { - - if ( scope.enableRotate === false ) return; - - rotateEnd.set( event.clientX, event.clientY ); - rotateDelta.subVectors( rotateEnd, rotateStart ); - - // rotating across whole screen goes 360 degrees around - constraint.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); - - // rotating up and down along whole screen attempts to go 360, but limited to 180 - constraint.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); - - rotateStart.copy( rotateEnd ); - - } else if ( state === STATE.DOLLY ) { - - if ( scope.enableZoom === false ) return; - - dollyEnd.set( event.clientX, event.clientY ); - dollyDelta.subVectors( dollyEnd, dollyStart ); - - if ( dollyDelta.y > 0 ) { - - constraint.dollyIn( getZoomScale() ); - - } else if ( dollyDelta.y < 0 ) { - - constraint.dollyOut( getZoomScale() ); - - } - - dollyStart.copy( dollyEnd ); - - } else if ( state === STATE.PAN ) { - - if ( scope.enablePan === false ) return; - - panEnd.set( event.clientX, event.clientY ); - panDelta.subVectors( panEnd, panStart ); - - pan( panDelta.x, panDelta.y ); - - panStart.copy( panEnd ); - - } - - if ( state !== STATE.NONE ) scope.update(); - - } + }; - function onMouseUp( /* event */ ) { + this.dollyOut = function ( dollyScale ) { - if ( scope.enabled === false ) return; + if ( dollyScale === undefined ) { - document.removeEventListener( 'mousemove', onMouseMove, false ); - document.removeEventListener( 'mouseup', onMouseUp, false ); - scope.dispatchEvent( endEvent ); - state = STATE.NONE; + dollyScale = getZoomScale(); } - function onMouseWheel( event ) { - - if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return; + if ( scope.object instanceof THREE.PerspectiveCamera ) { - event.preventDefault(); - event.stopPropagation(); + scale *= dollyScale; - var delta = 0; + } else if ( scope.object instanceof THREE.OrthographicCamera ) { - if ( event.wheelDelta !== undefined ) { + scope.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / dollyScale ) ); + scope.object.updateProjectionMatrix(); + scope.dispatchEvent( changeEvent ); - // WebKit / Opera / Explorer 9 + } else { - delta = event.wheelDelta; - - } else if ( event.detail !== undefined ) { - - // Firefox - - delta = - event.detail; - - } - - if ( delta > 0 ) { - - constraint.dollyOut( getZoomScale() ); - - } else if ( delta < 0 ) { - - constraint.dollyIn( getZoomScale() ); - - } - - scope.update(); - scope.dispatchEvent( startEvent ); - scope.dispatchEvent( endEvent ); + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); } - function onKeyDown( event ) { + }; - if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return; + this.update = function () { - switch ( event.keyCode ) { + var position = this.object.position; - case scope.keys.UP: - pan( 0, scope.keyPanSpeed ); - scope.update(); - break; + offset.copy( position ).sub( this.target ); - case scope.keys.BOTTOM: - pan( 0, - scope.keyPanSpeed ); - scope.update(); - break; + // rotate offset to "y-axis-is-up" space + offset.applyQuaternion( quat ); - case scope.keys.LEFT: - pan( scope.keyPanSpeed, 0 ); - scope.update(); - break; + // angle from z-axis around y-axis - case scope.keys.RIGHT: - pan( - scope.keyPanSpeed, 0 ); - scope.update(); - break; + theta = Math.atan2( offset.x, offset.z ); - } + // angle from y-axis - } + phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y ); - function touchstart( event ) { + if ( this.autoRotate && state === STATE.NONE ) { - if ( scope.enabled === false ) return; + this.rotateLeft( getAutoRotationAngle() ); - switch ( event.touches.length ) { + } - case 1: // one-fingered touch: rotate + theta += thetaDelta; + phi += phiDelta; - if ( scope.enableRotate === false ) return; + // restrict theta to be between desired limits + theta = Math.max( this.minAzimuthAngle, Math.min( this.maxAzimuthAngle, theta ) ); - state = STATE.TOUCH_ROTATE; + // restrict phi to be between desired limits + phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) ); - rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); - break; + // restrict phi to be betwee EPS and PI-EPS + phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) ); - case 2: // two-fingered touch: dolly + var radius = offset.length() * scale; - if ( scope.enableZoom === false ) return; + // restrict radius to be between desired limits + radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) ); - state = STATE.TOUCH_DOLLY; + // move target to panned location + this.target.add( pan ); - var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; - var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; - var distance = Math.sqrt( dx * dx + dy * dy ); - dollyStart.set( 0, distance ); - break; + offset.x = radius * Math.sin( phi ) * Math.sin( theta ); + offset.y = radius * Math.cos( phi ); + offset.z = radius * Math.sin( phi ) * Math.cos( theta ); - case 3: // three-fingered touch: pan + // rotate offset back to "camera-up-vector-is-up" space + offset.applyQuaternion( quatInverse ); - if ( scope.enablePan === false ) return; + position.copy( this.target ).add( offset ); - state = STATE.TOUCH_PAN; + this.object.lookAt( this.target ); - panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); - break; + thetaDelta = 0; + phiDelta = 0; + scale = 1; + pan.set( 0, 0, 0 ); - default: + // update condition is: + // min(camera displacement, camera rotation in radians)^2 > EPS + // using small-angle approximation cos(x/2) = 1 - x^2 / 8 - state = STATE.NONE; + if ( lastPosition.distanceToSquared( this.object.position ) > EPS + || 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS ) { - } + this.dispatchEvent( changeEvent ); - if ( state !== STATE.NONE ) scope.dispatchEvent( startEvent ); + lastPosition.copy( this.object.position ); + lastQuaternion.copy (this.object.quaternion ); } - function touchmove( event ) { - - if ( scope.enabled === false ) return; - - event.preventDefault(); - event.stopPropagation(); - - var element = scope.domElement === document ? scope.domElement.body : scope.domElement; - - switch ( event.touches.length ) { - - case 1: // one-fingered touch: rotate - - if ( scope.enableRotate === false ) return; - if ( state !== STATE.TOUCH_ROTATE ) return; - - rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); - rotateDelta.subVectors( rotateEnd, rotateStart ); + }; - // rotating across whole screen goes 360 degrees around - constraint.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); - // rotating up and down along whole screen attempts to go 360, but limited to 180 - constraint.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); - rotateStart.copy( rotateEnd ); + this.reset = function () { - scope.update(); - break; + state = STATE.NONE; - case 2: // two-fingered touch: dolly + this.target.copy( this.target0 ); + this.object.position.copy( this.position0 ); + this.object.zoom = this.zoom0; - if ( scope.enableZoom === false ) return; - if ( state !== STATE.TOUCH_DOLLY ) return; + this.object.updateProjectionMatrix(); + this.dispatchEvent( changeEvent ); - var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; - var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; - var distance = Math.sqrt( dx * dx + dy * dy ); + this.update(); - dollyEnd.set( 0, distance ); - dollyDelta.subVectors( dollyEnd, dollyStart ); + }; - if ( dollyDelta.y > 0 ) { + this.getPolarAngle = function () { - constraint.dollyOut( getZoomScale() ); + return phi; - } else if ( dollyDelta.y < 0 ) { + }; - constraint.dollyIn( getZoomScale() ); + this.getAzimuthalAngle = function () { - } + return theta - dollyStart.copy( dollyEnd ); + }; - scope.update(); - break; + function getAutoRotationAngle() { - case 3: // three-fingered touch: pan + return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; - if ( scope.enablePan === false ) return; - if ( state !== STATE.TOUCH_PAN ) return; + } - panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); - panDelta.subVectors( panEnd, panStart ); + function getZoomScale() { - pan( panDelta.x, panDelta.y ); + return Math.pow( 0.95, scope.zoomSpeed ); - panStart.copy( panEnd ); + } - scope.update(); - break; + function onMouseDown( event ) { - default: + if ( scope.enabled === false ) return; + event.preventDefault(); - state = STATE.NONE; + if ( event.button === scope.mouseButtons.ORBIT ) { + if ( scope.noRotate === true ) return; - } + state = STATE.ROTATE; - } + rotateStart.set( event.clientX, event.clientY ); - function touchend( /* event */ ) { + } else if ( event.button === scope.mouseButtons.ZOOM ) { + if ( scope.noZoom === true ) return; - if ( scope.enabled === false ) return; + state = STATE.DOLLY; - scope.dispatchEvent( endEvent ); - state = STATE.NONE; + dollyStart.set( event.clientX, event.clientY ); - } + } else if ( event.button === scope.mouseButtons.PAN ) { + if ( scope.noPan === true ) return; - function contextmenu( event ) { + state = STATE.PAN; - event.preventDefault(); + panStart.set( event.clientX, event.clientY ); } - this.dispose = function() { - - this.domElement.removeEventListener( 'contextmenu', contextmenu, false ); - this.domElement.removeEventListener( 'mousedown', onMouseDown, false ); - this.domElement.removeEventListener( 'mousewheel', onMouseWheel, false ); - this.domElement.removeEventListener( 'MozMousePixelScroll', onMouseWheel, false ); // firefox - - this.domElement.removeEventListener( 'touchstart', touchstart, false ); - this.domElement.removeEventListener( 'touchend', touchend, false ); - this.domElement.removeEventListener( 'touchmove', touchmove, false ); - - document.removeEventListener( 'mousemove', onMouseMove, false ); - document.removeEventListener( 'mouseup', onMouseUp, false ); - - window.removeEventListener( 'keydown', onKeyDown, false ); - + if ( state !== STATE.NONE ) { + document.addEventListener( 'mousemove', onMouseMove, false ); + document.addEventListener( 'mouseup', onMouseUp, false ); + scope.dispatchEvent( startEvent ); } - this.domElement.addEventListener( 'contextmenu', contextmenu, false ); - - this.domElement.addEventListener( 'mousedown', onMouseDown, false ); - this.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); - this.domElement.addEventListener( 'MozMousePixelScroll', onMouseWheel, false ); // firefox - - this.domElement.addEventListener( 'touchstart', touchstart, false ); - this.domElement.addEventListener( 'touchend', touchend, false ); - this.domElement.addEventListener( 'touchmove', touchmove, false ); - - window.addEventListener( 'keydown', onKeyDown, false ); - - // force an update at start - this.update(); - - }; - - THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); - THREE.OrbitControls.prototype.constructor = THREE.OrbitControls; - - Object.defineProperties( THREE.OrbitControls.prototype, { - - object: { - - get: function () { + } - return this.constraint.object; + function onMouseMove( event ) { - } + if ( scope.enabled === false ) return; - }, + event.preventDefault(); - target: { + var element = scope.domElement === document ? scope.domElement.body : scope.domElement; - get: function () { + if ( state === STATE.ROTATE ) { - return this.constraint.target; + if ( scope.noRotate === true ) return; - }, + rotateEnd.set( event.clientX, event.clientY ); + rotateDelta.subVectors( rotateEnd, rotateStart ); - set: function ( value ) { + // rotating across whole screen goes 360 degrees around + scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); - console.warn( 'THREE.OrbitControls: target is now immutable. Use target.set() instead.' ); - this.constraint.target.copy( value ); + // rotating up and down along whole screen attempts to go 360, but limited to 180 + scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); - } + rotateStart.copy( rotateEnd ); - }, + } else if ( state === STATE.DOLLY ) { - minDistance : { + if ( scope.noZoom === true ) return; - get: function () { + dollyEnd.set( event.clientX, event.clientY ); + dollyDelta.subVectors( dollyEnd, dollyStart ); - return this.constraint.minDistance; + if ( dollyDelta.y > 0 ) { - }, + scope.dollyIn(); - set: function ( value ) { + } else if ( dollyDelta.y < 0 ) { - this.constraint.minDistance = value; + scope.dollyOut(); } - }, - - maxDistance : { - - get: function () { - - return this.constraint.maxDistance; + dollyStart.copy( dollyEnd ); - }, - - set: function ( value ) { - - this.constraint.maxDistance = value; - - } + } else if ( state === STATE.PAN ) { - }, - - minZoom : { - - get: function () { - - return this.constraint.minZoom; - - }, - - set: function ( value ) { - - this.constraint.minZoom = value; - - } + if ( scope.noPan === true ) return; - }, + panEnd.set( event.clientX, event.clientY ); + panDelta.subVectors( panEnd, panStart ); - maxZoom : { + scope.pan( panDelta.x, panDelta.y ); - get: function () { + panStart.copy( panEnd ); - return this.constraint.maxZoom; - - }, + } - set: function ( value ) { + if ( state !== STATE.NONE ) scope.update(); - this.constraint.maxZoom = value; + } - } + function onMouseUp( /* event */ ) { - }, + if ( scope.enabled === false ) return; - minPolarAngle : { + document.removeEventListener( 'mousemove', onMouseMove, false ); + document.removeEventListener( 'mouseup', onMouseUp, false ); + scope.dispatchEvent( endEvent ); + state = STATE.NONE; - get: function () { + } - return this.constraint.minPolarAngle; + function onMouseWheel( event ) { - }, + if ( scope.enabled === false || scope.noZoom === true || state !== STATE.NONE ) return; - set: function ( value ) { + event.preventDefault(); + event.stopPropagation(); - this.constraint.minPolarAngle = value; - - } + var delta = 0; - }, + if ( event.wheelDelta !== undefined ) { // WebKit / Opera / Explorer 9 - maxPolarAngle : { + delta = event.wheelDelta; - get: function () { + } else if ( event.detail !== undefined ) { // Firefox - return this.constraint.maxPolarAngle; + delta = - event.detail; - }, + } - set: function ( value ) { + if ( delta > 0 ) { - this.constraint.maxPolarAngle = value; + scope.dollyOut(); - } + } else if ( delta < 0 ) { - }, + scope.dollyIn(); - minAzimuthAngle : { + } - get: function () { + scope.update(); + scope.dispatchEvent( startEvent ); + scope.dispatchEvent( endEvent ); - return this.constraint.minAzimuthAngle; + } - }, + function onKeyDown( event ) { - set: function ( value ) { + if ( scope.enabled === false || scope.noKeys === true || scope.noPan === true ) return; - this.constraint.minAzimuthAngle = value; + switch ( event.keyCode ) { - } + case scope.keys.UP: + scope.pan( 0, scope.keyPanSpeed ); + scope.update(); + break; - }, + case scope.keys.BOTTOM: + scope.pan( 0, - scope.keyPanSpeed ); + scope.update(); + break; - maxAzimuthAngle : { + case scope.keys.LEFT: + scope.pan( scope.keyPanSpeed, 0 ); + scope.update(); + break; - get: function () { + case scope.keys.RIGHT: + scope.pan( - scope.keyPanSpeed, 0 ); + scope.update(); + break; - return this.constraint.maxAzimuthAngle; + } - }, + } - set: function ( value ) { + function touchstart( event ) { - this.constraint.maxAzimuthAngle = value; + if ( scope.enabled === false ) return; - } + switch ( event.touches.length ) { - }, + case 1: // one-fingered touch: rotate - enableDamping : { + if ( scope.noRotate === true ) return; - get: function () { + state = STATE.TOUCH_ROTATE; - return this.constraint.enableDamping; + rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + break; - }, + case 2: // two-fingered touch: dolly - set: function ( value ) { + if ( scope.noZoom === true ) return; - this.constraint.enableDamping = value; + state = STATE.TOUCH_DOLLY; - } + var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + var distance = Math.sqrt( dx * dx + dy * dy ); + dollyStart.set( 0, distance ); + break; - }, + case 3: // three-fingered touch: pan - dampingFactor : { + if ( scope.noPan === true ) return; - get: function () { + state = STATE.TOUCH_PAN; - return this.constraint.dampingFactor; + panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + break; - }, + default: - set: function ( value ) { + state = STATE.NONE; - this.constraint.dampingFactor = value; + } - } + if ( state !== STATE.NONE ) scope.dispatchEvent( startEvent ); - }, + } - // backward compatibility + function touchmove( event ) { - noZoom: { + if ( scope.enabled === false ) return; - get: function () { + event.preventDefault(); + event.stopPropagation(); - console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); - return ! this.enableZoom; + var element = scope.domElement === document ? scope.domElement.body : scope.domElement; - }, + switch ( event.touches.length ) { - set: function ( value ) { + case 1: // one-fingered touch: rotate - console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); - this.enableZoom = ! value; + if ( scope.noRotate === true ) return; + if ( state !== STATE.TOUCH_ROTATE ) return; - } + rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + rotateDelta.subVectors( rotateEnd, rotateStart ); - }, + // rotating across whole screen goes 360 degrees around + scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); + // rotating up and down along whole screen attempts to go 360, but limited to 180 + scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); - noRotate: { + rotateStart.copy( rotateEnd ); - get: function () { + scope.update(); + break; - console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); - return ! this.enableRotate; + case 2: // two-fingered touch: dolly - }, + if ( scope.noZoom === true ) return; + if ( state !== STATE.TOUCH_DOLLY ) return; - set: function ( value ) { + var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + var distance = Math.sqrt( dx * dx + dy * dy ); - console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); - this.enableRotate = ! value; + dollyEnd.set( 0, distance ); + dollyDelta.subVectors( dollyEnd, dollyStart ); - } + if ( dollyDelta.y > 0 ) { - }, + scope.dollyOut(); - noPan: { + } else if ( dollyDelta.y < 0 ) { - get: function () { + scope.dollyIn(); - console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); - return ! this.enablePan; + } - }, + dollyStart.copy( dollyEnd ); - set: function ( value ) { + scope.update(); + break; - console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); - this.enablePan = ! value; + case 3: // three-fingered touch: pan - } + if ( scope.noPan === true ) return; + if ( state !== STATE.TOUCH_PAN ) return; - }, + panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + panDelta.subVectors( panEnd, panStart ); - noKeys: { + scope.pan( panDelta.x, panDelta.y ); - get: function () { + panStart.copy( panEnd ); - console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); - return ! this.enableKeys; + scope.update(); + break; - }, + default: - set: function ( value ) { + state = STATE.NONE; - console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); - this.enableKeys = ! value; + } - } + } - }, + function touchend( /* event */ ) { - staticMoving : { + if ( scope.enabled === false ) return; - get: function () { + scope.dispatchEvent( endEvent ); + state = STATE.NONE; - console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); - return ! this.constraint.enableDamping; + } - }, + function contextmenu( event ) { - set: function ( value ) { + event.preventDefault(); - console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); - this.constraint.enableDamping = ! value; + } - } + this.dispose = function() { - }, + this.domElement.removeEventListener( 'contextmenu', contextmenu, false ); + this.domElement.removeEventListener( 'mousedown', onMouseDown, false ); + this.domElement.removeEventListener( 'mousewheel', onMouseWheel, false ); + this.domElement.removeEventListener( 'MozMousePixelScroll', onMouseWheel, false ); // firefox - dynamicDampingFactor : { + this.domElement.removeEventListener( 'touchstart', touchstart, false ); + this.domElement.removeEventListener( 'touchend', touchend, false ); + this.domElement.removeEventListener( 'touchmove', touchmove, false ); - get: function () { + document.removeEventListener( 'mousemove', onMouseMove, false ); + document.removeEventListener( 'mouseup', onMouseUp, false ); - console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); - return this.constraint.dampingFactor; + window.removeEventListener( 'keydown', onKeyDown, false ); - }, + } - set: function ( value ) { + this.domElement.addEventListener( 'contextmenu', contextmenu, false ); + this.domElement.addEventListener( 'mousedown', onMouseDown, false ); + this.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); + this.domElement.addEventListener( 'MozMousePixelScroll', onMouseWheel, false ); // firefox - console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); - this.constraint.dampingFactor = value; + this.domElement.addEventListener( 'touchstart', touchstart, false ); + this.domElement.addEventListener( 'touchend', touchend, false ); + this.domElement.addEventListener( 'touchmove', touchmove, false ); - } + window.addEventListener( 'keydown', onKeyDown, false ); - } + // force an update at start + this.update(); - } ); +}; -}() ); +THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); +THREE.OrbitControls.prototype.constructor = THREE.OrbitControls;