Browse Source

Extract OrbitConstraint from OrbitControls

OrbitControls takes care of the DOM, input binding and event handling.

OrbitConstraint is responsible for keeping the camera on an orbit, maintaining its orientation towards the target and updating the camera and target position.

There is only one insignificant API change. Before, dolly / rotate functions could be called without parameters to engage autoRotate / autoSpeed. OrbitConstraint does not support that. Now, OrbitControls must provide these parameter explicitly to OrbitConstraint. As far as I can tell, those methods were only used internally and should not be part of the control API.

OrbitControls extends OrbitConstraint. In my ideal implementation, OrbitConstraint should be a property of the OrbitControls, but using inheritence help preserve the existing interface.

OrbitConstraint is not included in the `THREE` namespace. It is defined in a closure and private to this file. It could also the placed in `extras\constraints`, along with other contraints that could be extracted from the current controls, and maybe other type of constraints on the object position/displacement (constrain movement to plane, to path, collision).

These constraints should not necessarily be limited to cameras; they could apply to any objects. It's not clear to me how all this will shape up, but this is a nice place to pause and ask for feedback before going forward.
dubejf 10 years ago
parent
commit
6a2a28fe34
1 changed files with 460 additions and 420 deletions
  1. 460 420
      examples/js/controls/OrbitControls.js

+ 460 - 420
examples/js/controls/OrbitControls.js

@@ -7,700 +7,740 @@
  */
 /*global THREE, console */
 
-// 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 () {
 
-THREE.OrbitControls = function ( object, domElement ) {
+	function OrbitConstraint ( object ) {
 
-	this.object = object;
-	this.domElement = ( domElement !== undefined ) ? domElement : document;
+		this.object = object;
 
-	// API
+		// "target" sets the location of focus, where the object orbits around
+		// and where it pans with respect to.
+		this.target = new THREE.Vector3();
 
-	// Set to false to disable this control
-	this.enabled = true;
+		// Limits to how far you can dolly in and out ( PerspectiveCamera only )
+		this.minDistance = 0;
+		this.maxDistance = Infinity;
 
-	// "target" sets the location of focus, where the control orbits around
-	// and where it pans with respect to.
-	this.target = new THREE.Vector3();
+		// Limits to how far you can zoom in and out ( OrthographicCamera only )
+		this.minZoom = 0;
+		this.maxZoom = Infinity;
 
-	// center is old, deprecated; use "target" instead
-	this.center = this.target;
+		// 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
 
-	// This option actually enables dollying in and out; left as "zoom" for
-	// backwards compatibility
-	this.noZoom = false;
-	this.zoomSpeed = 1.0;
+		// 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
 
-	// 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();
 
-	// 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
+		// events
+		var changeEvent = { type: 'change' };
 
-	// 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 );
 
-		thetaDelta -= angle;
+				panOffset.add( v );
 
-	};
+			};
 
-	this.rotateUp = function ( angle ) {
+		}();
 
-		if ( angle === undefined ) {
+		// pass in x,y of change desired in pixel space,
+		// right and down are positive
+		this.pan = function ( deltaX, deltaY, screenWidth, screenHeight ) {
 
-			angle = getAutoRotationAngle();
+			if ( scope.object instanceof THREE.PerspectiveCamera ) {
 
-		}
+				// perspective
+				var position = scope.object.position;
+				var offset = position.clone().sub( scope.target );
+				var targetDistance = offset.length();
 
-		phiDelta -= angle;
+				// 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 );
 
-	// pass in distance in world space to move left
-	this.panLeft = function ( distance ) {
+			} else if ( scope.object instanceof THREE.OrthographicCamera ) {
 
-		var te = this.object.matrix.elements;
+				// orthographic
+				scope.panLeft( deltaX * ( scope.object.right - scope.object.left ) / screenWidth );
+				scope.panUp( deltaY * ( scope.object.top - scope.object.bottom ) / screenHeight );
 
-		// get X column of matrix
-		panOffset.set( te[ 0 ], te[ 1 ], te[ 2 ] );
-		panOffset.multiplyScalar( - distance );
+			} else {
 
-		pan.add( panOffset );
+				// camera neither orthographic or perspective
+				console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
 
-	};
+			}
 
-	// pass in distance in world space to move up
-	this.panUp = function ( distance ) {
+		};
 
-		var te = this.object.matrix.elements;
+		this.dollyIn = function ( dollyScale ) {
 
-		// get Y column of matrix
-		panOffset.set( te[ 4 ], te[ 5 ], te[ 6 ] );
-		panOffset.multiplyScalar( distance );
+			if ( scope.object instanceof THREE.PerspectiveCamera ) {
 
-		pan.add( panOffset );
+				scale /= dollyScale;
 
-	};
+			} else if ( scope.object instanceof THREE.OrthographicCamera ) {
 
-	// pass in x,y of change desired in pixel space,
-	// right and down are positive
-	this.pan = function ( deltaX, deltaY ) {
+				scope.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom * dollyScale ) );
+				scope.object.updateProjectionMatrix();
+				scope.dispatchEvent( changeEvent );
 
-		var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
+			} else {
 
-		if ( scope.object instanceof THREE.PerspectiveCamera ) {
+				console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
 
-			// 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 / element.clientHeight );
-			scope.panUp( 2 * deltaY * targetDistance / element.clientHeight );
+		this.dollyOut = function ( dollyScale ) {
 
-		} else if ( scope.object instanceof THREE.OrthographicCamera ) {
+			if ( scope.object instanceof THREE.PerspectiveCamera ) {
 
-			// orthographic
-			scope.panLeft( deltaX * (scope.object.right - scope.object.left) / element.clientWidth );
-			scope.panUp( deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight );
+				scale *= dollyScale;
 
-		} else {
+			} else if ( scope.object instanceof THREE.OrthographicCamera ) {
 
-			// camera neither orthographic or perspective
-			console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
+				scope.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / dollyScale ) );
+				scope.object.updateProjectionMatrix();
+				scope.dispatchEvent( changeEvent );
 
-		}
+			} else {
 
-	};
+				console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
 
-	this.dollyIn = function ( dollyScale ) {
+			}
 
-		if ( dollyScale === undefined ) {
+		};
 
-			dollyScale = getZoomScale();
+		this.update = function() {
 
-		}
+			var offset = new THREE.Vector3();
 
-		if ( scope.object instanceof THREE.PerspectiveCamera ) {
+			// 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();
 
-			scale /= dollyScale;
+			var lastPosition = new THREE.Vector3();
+			var lastQuaternion = new THREE.Quaternion();
 
-		} else if ( scope.object instanceof THREE.OrthographicCamera ) {
+			return function () {
 
-			scope.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom * dollyScale ) );
-			scope.object.updateProjectionMatrix();
-			scope.dispatchEvent( changeEvent );
+				var position = this.object.position;
 
-		} else {
+				offset.copy( position ).sub( this.target );
 
-			console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+				// 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 );
 
-	this.dollyOut = function ( dollyScale ) {
+				// angle from y-axis
 
-		if ( dollyScale === undefined ) {
+				phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y );
 
-			dollyScale = getZoomScale();
+				theta += thetaDelta;
+				phi += phiDelta;
 
-		}
+				// restrict theta to be between desired limits
+				theta = Math.max( this.minAzimuthAngle, Math.min( this.maxAzimuthAngle, theta ) );
 
-		if ( scope.object instanceof THREE.PerspectiveCamera ) {
+				// restrict phi to be between desired limits
+				phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) );
 
-			scale *= dollyScale;
+				// restrict phi to be betwee EPS and PI-EPS
+				phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) );
 
-		} else if ( scope.object instanceof THREE.OrthographicCamera ) {
+				var radius = offset.length() * scale;
 
-			scope.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / dollyScale ) );
-			scope.object.updateProjectionMatrix();
-			scope.dispatchEvent( changeEvent );
+				// restrict radius to be between desired limits
+				radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) );
 
-		} else {
+				// move target to panned location
+				this.target.add( panOffset );
 
-			console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+				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.update = function () {
+				this.object.lookAt( this.target );
 
-		var position = this.object.position;
+				thetaDelta = 0;
+				phiDelta = 0;
+				scale = 1;
+				panOffset.set( 0, 0, 0 );
 
-		offset.copy( position ).sub( this.target );
+				// update condition is:
+				// min(camera displacement, camera rotation in radians)^2 > EPS
+				// using small-angle approximation cos(x/2) = 1 - x^2 / 8
 
-		// rotate offset to "y-axis-is-up" space
-		offset.applyQuaternion( quat );
+				if ( lastPosition.distanceToSquared( this.object.position ) > EPS ||
+				    8 * ( 1 - lastQuaternion.dot( this.object.quaternion) ) > EPS ) {
 
-		// angle from z-axis around y-axis
+					lastPosition.copy( this.object.position );
+					lastQuaternion.copy( this.object.quaternion );
 
-		theta = Math.atan2( offset.x, offset.z );
+					return true;
 
-		// angle from y-axis
+				}
 
-		phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y );
+				return false;
 
-		if ( this.autoRotate && state === STATE.NONE ) {
+			};
 
-			this.rotateLeft( getAutoRotationAngle() );
+		}();
 
-		}
+	};
 
-		theta += thetaDelta;
-		phi += phiDelta;
+	OrbitConstraint.prototype = Object.create( THREE.EventDispatcher.prototype );
+	OrbitConstraint.prototype.constructor = OrbitConstraint;
 
-		// 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 ) );
+	// 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
 
-		var radius = offset.length() * scale;
+	THREE.OrbitControls = function ( object, domElement ) {
 
-		// restrict radius to be between desired limits
-		radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) );
+		OrbitConstraint.call( this, object );
 
-		// move target to panned location
-		this.target.add( pan );
+		this.domElement = ( domElement !== undefined ) ? domElement : document;
 
-		offset.x = radius * Math.sin( phi ) * Math.sin( theta );
-		offset.y = radius * Math.cos( phi );
-		offset.z = radius * Math.sin( phi ) * Math.cos( theta );
+		// API
 
-		// rotate offset back to "camera-up-vector-is-up" space
-		offset.applyQuaternion( quatInverse );
+		// Set to false to disable this control
+		this.enabled = true;
 
-		position.copy( this.target ).add( offset );
+		// center is old, deprecated; use "target" instead
+		this.center = this.target;
 
-		this.object.lookAt( this.target );
+		// This option actually enables dollying in and out; left as "zoom" for
+		// backwards compatibility
+		this.noZoom = false;
+		this.zoomSpeed = 1.0;
 
-		thetaDelta = 0;
-		phiDelta = 0;
-		scale = 1;
-		pan.set( 0, 0, 0 );
+		// Set to true to disable this control
+		this.noRotate = false;
+		this.rotateSpeed = 1.0;
 
-		// update condition is:
-		// min(camera displacement, camera rotation in radians)^2 > EPS
-		// using small-angle approximation cos(x/2) = 1 - x^2 / 8
+		// Set to true to disable this control
+		this.noPan = false;
+		this.keyPanSpeed = 7.0;	// pixels moved per arrow key push
 
-		if ( lastPosition.distanceToSquared( this.object.position ) > EPS
-		    || 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS ) {
+		// Set to true to automatically rotate around the target
+		this.autoRotate = false;
+		this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
 
-			this.dispatchEvent( changeEvent );
+		// Set to true to disable use of the keys
+		this.noKeys = false;
 
-			lastPosition.copy( this.object.position );
-			lastQuaternion.copy (this.object.quaternion );
+		// 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 };
 
-	};
+		////////////
+		// internals
 
+		var scope = this;
 
-	this.reset = function () {
+		var rotateStart = new THREE.Vector2();
+		var rotateEnd = new THREE.Vector2();
+		var rotateDelta = new THREE.Vector2();
 
-		state = STATE.NONE;
+		var panStart = new THREE.Vector2();
+		var panEnd = new THREE.Vector2();
+		var panDelta = new THREE.Vector2();
 
-		this.target.copy( this.target0 );
-		this.object.position.copy( this.position0 );
-		this.object.zoom = this.zoom0;
+		var dollyStart = new THREE.Vector2();
+		var dollyEnd = new THREE.Vector2();
+		var dollyDelta = new THREE.Vector2();
 
-		this.object.updateProjectionMatrix();
-		this.dispatchEvent( changeEvent );
+		var STATE = { NONE : -1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 };
 
-		this.update();
+		var state = STATE.NONE;
 
-	};
+		// for reset
 
-	this.getPolarAngle = function () {
+		this.target0 = this.target.clone();
+		this.position0 = this.object.position.clone();
+		this.zoom0 = this.object.zoom;
 
-		return phi;
+		// events
 
-	};
+		var changeEvent = { type: 'change' };
+		var startEvent = { type: 'start' };
+		var endEvent = { type: 'end' };
 
-	this.getAzimuthalAngle = function () {
+		// pass in x,y of change desired in pixel space,
+		// right and down are positive
+		var _pan = this.pan;
+		this.pan = function ( deltaX, deltaY ) {
 
-		return theta
+			var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
 
-	};
+			_pan.call( this, deltaX, deltaY, element.clientWidth, element.clientHeight );
 
-	function getAutoRotationAngle() {
+		};
 
-		return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
+		var _update = this.update;
+		this.update = function() {
 
-	}
+			if ( this.autoRotate && state === STATE.NONE ) {
 
-	function getZoomScale() {
+				this.rotateLeft( getAutoRotationAngle() );
 
-		return Math.pow( 0.95, scope.zoomSpeed );
+			}
 
-	}
+			if ( _update.call( this ) === true ) {
 
-	function onMouseDown( event ) {
+				this.dispatchEvent( changeEvent );
 
-		if ( scope.enabled === false ) return;
-		event.preventDefault();
+			}
 
-		if ( event.button === scope.mouseButtons.ORBIT ) {
-			if ( scope.noRotate === true ) return;
+		};
 
-			state = STATE.ROTATE;
+		this.reset = function () {
 
-			rotateStart.set( event.clientX, event.clientY );
+			state = STATE.NONE;
 
-		} else if ( event.button === scope.mouseButtons.ZOOM ) {
-			if ( scope.noZoom === true ) return;
+			this.target.copy( this.target0 );
+			this.object.position.copy( this.position0 );
+			this.object.zoom = this.zoom0;
 
-			state = STATE.DOLLY;
+			this.object.updateProjectionMatrix();
+			this.dispatchEvent( changeEvent );
 
-			dollyStart.set( event.clientX, event.clientY );
+			this.update();
 
-		} else if ( event.button === scope.mouseButtons.PAN ) {
-			if ( scope.noPan === true ) return;
+		};
 
-			state = STATE.PAN;
+		function getAutoRotationAngle() {
 
-			panStart.set( event.clientX, event.clientY );
+			return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
 
 		}
 
-		if ( state !== STATE.NONE ) {
-			document.addEventListener( 'mousemove', onMouseMove, false );
-			document.addEventListener( 'mouseup', onMouseUp, false );
-			scope.dispatchEvent( startEvent );
+		function getZoomScale() {
+
+			return Math.pow( 0.95, scope.zoomSpeed );
+
 		}
 
-	}
+		function onMouseDown( event ) {
 
-	function onMouseMove( event ) {
+			if ( scope.enabled === false ) return;
+			event.preventDefault();
 
-		if ( scope.enabled === false ) return;
+			if ( event.button === scope.mouseButtons.ORBIT ) {
+				if ( scope.noRotate === true ) return;
 
-		event.preventDefault();
+				state = STATE.ROTATE;
 
-		var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
+				rotateStart.set( event.clientX, event.clientY );
 
-		if ( state === STATE.ROTATE ) {
+			} else if ( event.button === scope.mouseButtons.ZOOM ) {
+				if ( scope.noZoom === true ) return;
 
-			if ( scope.noRotate === true ) return;
+				state = STATE.DOLLY;
 
-			rotateEnd.set( event.clientX, event.clientY );
-			rotateDelta.subVectors( rotateEnd, rotateStart );
+				dollyStart.set( event.clientX, event.clientY );
 
-			// rotating across whole screen goes 360 degrees around
-			scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
+			} else if ( event.button === scope.mouseButtons.PAN ) {
+				if ( scope.noPan === true ) return;
 
-			// 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 );
+				state = STATE.PAN;
 
-			rotateStart.copy( rotateEnd );
+				panStart.set( event.clientX, event.clientY );
 
-		} else if ( state === STATE.DOLLY ) {
+			}
 
-			if ( scope.noZoom === true ) return;
+			if ( state !== STATE.NONE ) {
 
-			dollyEnd.set( event.clientX, event.clientY );
-			dollyDelta.subVectors( dollyEnd, dollyStart );
+				document.addEventListener( 'mousemove', onMouseMove, false );
+				document.addEventListener( 'mouseup', onMouseUp, false );
+				scope.dispatchEvent( startEvent );
 
-			if ( dollyDelta.y > 0 ) {
+			}
 
-				scope.dollyIn();
+		}
 
-			} else if ( dollyDelta.y < 0 ) {
+		function onMouseMove( event ) {
 
-				scope.dollyOut();
+			if ( scope.enabled === false ) return;
 
-			}
+			event.preventDefault();
 
-			dollyStart.copy( dollyEnd );
+			var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
 
-		} else if ( state === STATE.PAN ) {
+			if ( state === STATE.ROTATE ) {
 
-			if ( scope.noPan === true ) return;
+				if ( scope.noRotate === true ) return;
 
-			panEnd.set( event.clientX, event.clientY );
-			panDelta.subVectors( panEnd, panStart );
+				rotateEnd.set( event.clientX, event.clientY );
+				rotateDelta.subVectors( rotateEnd, rotateStart );
 
-			scope.pan( panDelta.x, panDelta.y );
+				// rotating across whole screen goes 360 degrees around
+				scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
 
-			panStart.copy( panEnd );
+				// 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 );
 
-		if ( state !== STATE.NONE ) scope.update();
+			} else if ( state === STATE.DOLLY ) {
 
-	}
+				if ( scope.noZoom === true ) return;
 
-	function onMouseUp( /* event */ ) {
+				dollyEnd.set( event.clientX, event.clientY );
+				dollyDelta.subVectors( dollyEnd, dollyStart );
 
-		if ( scope.enabled === false ) return;
+				if ( dollyDelta.y > 0 ) {
 
-		document.removeEventListener( 'mousemove', onMouseMove, false );
-		document.removeEventListener( 'mouseup', onMouseUp, false );
-		scope.dispatchEvent( endEvent );
-		state = STATE.NONE;
+					scope.dollyIn( getZoomScale() );
 
-	}
+				} else if ( dollyDelta.y < 0 ) {
 
-	function onMouseWheel( event ) {
+					scope.dollyOut( getZoomScale() );
 
-		if ( scope.enabled === false || scope.noZoom === true || state !== STATE.NONE ) return;
+				}
 
-		event.preventDefault();
-		event.stopPropagation();
+				dollyStart.copy( dollyEnd );
 
-		var delta = 0;
+			} else if ( state === STATE.PAN ) {
 
-		if ( event.wheelDelta !== undefined ) { // WebKit / Opera / Explorer 9
+				if ( scope.noPan === true ) return;
 
-			delta = event.wheelDelta;
+				panEnd.set( event.clientX, event.clientY );
+				panDelta.subVectors( panEnd, panStart );
 
-		} else if ( event.detail !== undefined ) { // Firefox
+				scope.pan( panDelta.x, panDelta.y );
 
-			delta = - event.detail;
+				panStart.copy( panEnd );
 
-		}
+			}
+
+			if ( state !== STATE.NONE ) scope.update();
 
-		if ( delta > 0 ) {
+		}
 
-			scope.dollyOut();
+		function onMouseUp( /* event */ ) {
 
-		} else if ( delta < 0 ) {
+			if ( scope.enabled === false ) return;
 
-			scope.dollyIn();
+			document.removeEventListener( 'mousemove', onMouseMove, false );
+			document.removeEventListener( 'mouseup', onMouseUp, false );
+			scope.dispatchEvent( endEvent );
+			state = STATE.NONE;
 
 		}
 
-		scope.update();
-		scope.dispatchEvent( startEvent );
-		scope.dispatchEvent( endEvent );
+		function onMouseWheel( event ) {
 
-	}
+			if ( scope.enabled === false || scope.noZoom === true || state !== STATE.NONE ) return;
 
-	function onKeyDown( event ) {
+			event.preventDefault();
+			event.stopPropagation();
 
-		if ( scope.enabled === false || scope.noKeys === true || scope.noPan === true ) return;
+			var delta = 0;
 
-		switch ( event.keyCode ) {
+			if ( event.wheelDelta !== undefined ) { // WebKit / Opera / Explorer 9
 
-			case scope.keys.UP:
-				scope.pan( 0, scope.keyPanSpeed );
-				scope.update();
-				break;
+				delta = event.wheelDelta;
 
-			case scope.keys.BOTTOM:
-				scope.pan( 0, - scope.keyPanSpeed );
-				scope.update();
-				break;
+			} else if ( event.detail !== undefined ) { // Firefox
 
-			case scope.keys.LEFT:
-				scope.pan( scope.keyPanSpeed, 0 );
-				scope.update();
-				break;
+				delta = - event.detail;
 
-			case scope.keys.RIGHT:
-				scope.pan( - scope.keyPanSpeed, 0 );
-				scope.update();
-				break;
+			}
+
+			if ( delta > 0 ) {
+
+				scope.dollyOut( getZoomScale() );
+
+			} else if ( delta < 0 ) {
+
+				scope.dollyIn( getZoomScale() );
+
+			}
+
+			scope.update();
+			scope.dispatchEvent( startEvent );
+			scope.dispatchEvent( endEvent );
 
 		}
 
-	}
+		function onKeyDown( event ) {
 
-	function touchstart( event ) {
+			if ( scope.enabled === false || scope.noKeys === true || scope.noPan === true ) return;
 
-		if ( scope.enabled === false ) return;
+			switch ( event.keyCode ) {
 
-		switch ( event.touches.length ) {
+				case scope.keys.UP:
+					scope.pan( 0, scope.keyPanSpeed );
+					scope.update();
+					break;
 
-			case 1:	// one-fingered touch: rotate
+				case scope.keys.BOTTOM:
+					scope.pan( 0, - scope.keyPanSpeed );
+					scope.update();
+					break;
 
-				if ( scope.noRotate === true ) return;
+				case scope.keys.LEFT:
+					scope.pan( scope.keyPanSpeed, 0 );
+					scope.update();
+					break;
 
-				state = STATE.TOUCH_ROTATE;
+				case scope.keys.RIGHT:
+					scope.pan( - scope.keyPanSpeed, 0 );
+					scope.update();
+					break;
 
-				rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
-				break;
+			}
 
-			case 2:	// two-fingered touch: dolly
+		}
 
-				if ( scope.noZoom === true ) return;
+		function touchstart( event ) {
 
-				state = STATE.TOUCH_DOLLY;
+			if ( scope.enabled === false ) return;
 
-				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;
+			switch ( event.touches.length ) {
 
-			case 3: // three-fingered touch: pan
+				case 1:	// one-fingered touch: rotate
 
-				if ( scope.noPan === true ) return;
+					if ( scope.noRotate === true ) return;
 
-				state = STATE.TOUCH_PAN;
+					state = STATE.TOUCH_ROTATE;
 
-				panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
-				break;
+					rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+					break;
 
-			default:
+				case 2:	// two-fingered touch: dolly
 
-				state = STATE.NONE;
+					if ( scope.noZoom === true ) return;
 
-		}
+					state = STATE.TOUCH_DOLLY;
 
-		if ( state !== STATE.NONE ) scope.dispatchEvent( startEvent );
+					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
 
-	function touchmove( event ) {
+					if ( scope.noPan === true ) return;
 
-		if ( scope.enabled === false ) return;
+					state = STATE.TOUCH_PAN;
 
-		event.preventDefault();
-		event.stopPropagation();
+					panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+					break;
 
-		var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
+				default:
 
-		switch ( event.touches.length ) {
+					state = STATE.NONE;
 
-			case 1: // one-fingered touch: rotate
+			}
 
-				if ( scope.noRotate === true ) return;
-				if ( state !== STATE.TOUCH_ROTATE ) return;
+			if ( state !== STATE.NONE ) scope.dispatchEvent( startEvent );
 
-				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 );
+		function touchmove( event ) {
 
-				rotateStart.copy( rotateEnd );
+			if ( scope.enabled === false ) return;
 
-				scope.update();
-				break;
+			event.preventDefault();
+			event.stopPropagation();
 
-			case 2: // two-fingered touch: dolly
+			var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
 
-				if ( scope.noZoom === true ) return;
-				if ( state !== STATE.TOUCH_DOLLY ) return;
+			switch ( event.touches.length ) {
 
-				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 );
+				case 1: // one-fingered touch: rotate
 
-				dollyEnd.set( 0, distance );
-				dollyDelta.subVectors( dollyEnd, dollyStart );
+					if ( scope.noRotate === true ) return;
+					if ( state !== STATE.TOUCH_ROTATE ) return;
 
-				if ( dollyDelta.y > 0 ) {
+					rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+					rotateDelta.subVectors( rotateEnd, rotateStart );
 
-					scope.dollyOut();
+					// 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 );
 
-				} else if ( dollyDelta.y < 0 ) {
+					rotateStart.copy( rotateEnd );
 
-					scope.dollyIn();
+					scope.update();
+					break;
 
-				}
+				case 2: // two-fingered touch: dolly
 
-				dollyStart.copy( dollyEnd );
+					if ( scope.noZoom === true ) return;
+					if ( state !== STATE.TOUCH_DOLLY ) return;
 
-				scope.update();
-				break;
+					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 );
 
-			case 3: // three-fingered touch: pan
+					dollyEnd.set( 0, distance );
+					dollyDelta.subVectors( dollyEnd, dollyStart );
 
-				if ( scope.noPan === true ) return;
-				if ( state !== STATE.TOUCH_PAN ) return;
+					if ( dollyDelta.y > 0 ) {
 
-				panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
-				panDelta.subVectors( panEnd, panStart );
+						scope.dollyOut( getZoomScale() );
 
-				scope.pan( panDelta.x, panDelta.y );
+					} else if ( dollyDelta.y < 0 ) {
 
-				panStart.copy( panEnd );
+						scope.dollyIn( getZoomScale() );
 
-				scope.update();
-				break;
+					}
 
-			default:
+					dollyStart.copy( dollyEnd );
 
-				state = STATE.NONE;
+					scope.update();
+					break;
+
+				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 );
+
+					scope.pan( panDelta.x, panDelta.y );
+
+					panStart.copy( panEnd );
+
+					scope.update();
+					break;
+
+				default:
+
+					state = STATE.NONE;
+
+			}
 
 		}
 
-	}
+		function touchend( /* event */ ) {
 
-	function touchend( /* event */ ) {
+			if ( scope.enabled === false ) return;
 
-		if ( scope.enabled === false ) return;
+			scope.dispatchEvent( endEvent );
+			state = STATE.NONE;
 
-		scope.dispatchEvent( endEvent );
-		state = STATE.NONE;
+		}
 
-	}
+		this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false );
+		this.domElement.addEventListener( 'mousedown', onMouseDown, false );
+		this.domElement.addEventListener( 'mousewheel', onMouseWheel, false );
+		this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox
 
-	this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false );
-	this.domElement.addEventListener( 'mousedown', onMouseDown, false );
-	this.domElement.addEventListener( 'mousewheel', onMouseWheel, false );
-	this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox
+		this.domElement.addEventListener( 'touchstart', touchstart, false );
+		this.domElement.addEventListener( 'touchend', touchend, false );
+		this.domElement.addEventListener( 'touchmove', touchmove, false );
 
-	this.domElement.addEventListener( 'touchstart', touchstart, false );
-	this.domElement.addEventListener( 'touchend', touchend, false );
-	this.domElement.addEventListener( 'touchmove', touchmove, false );
+		window.addEventListener( 'keydown', onKeyDown, false );
 
-	window.addEventListener( 'keydown', onKeyDown, false );
+		// force an update at start
+		this.update();
 
-	// force an update at start
-	this.update();
+	};
 
-};
+	THREE.OrbitControls.prototype = Object.create( OrbitConstraint.prototype );
+	THREE.OrbitControls.prototype.constructor = THREE.OrbitControls;
 
-THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype );
-THREE.OrbitControls.prototype.constructor = THREE.OrbitControls;
+}());