Skip to content
Snippets Groups Projects
3d-force-graph.js 1.6 MiB
Newer Older
Amira Abdel-Rahman's avatar
Amira Abdel-Rahman committed
60001 60002 60003 60004 60005 60006 60007 60008 60009 60010 60011 60012 60013 60014 60015 60016 60017 60018 60019 60020 60021 60022 60023 60024 60025 60026 60027 60028 60029 60030 60031 60032 60033 60034 60035 60036 60037 60038 60039 60040 60041 60042 60043 60044 60045 60046 60047 60048 60049 60050 60051 60052 60053 60054 60055 60056 60057 60058 60059 60060 60061 60062 60063 60064 60065 60066 60067 60068 60069 60070 60071 60072 60073 60074 60075 60076 60077 60078 60079 60080 60081 60082 60083 60084 60085 60086 60087 60088 60089 60090 60091 60092 60093 60094 60095 60096 60097 60098 60099 60100 60101 60102 60103 60104 60105 60106 60107 60108 60109 60110 60111 60112 60113 60114 60115 60116 60117 60118 60119 60120 60121 60122 60123 60124 60125 60126 60127 60128 60129 60130 60131 60132 60133 60134 60135 60136 60137 60138 60139 60140 60141 60142 60143 60144 60145 60146 60147 60148 60149 60150 60151 60152 60153 60154 60155 60156 60157 60158 60159 60160 60161 60162 60163 60164 60165 60166 60167 60168 60169 60170 60171 60172 60173 60174 60175 60176 60177 60178 60179 60180 60181 60182 60183 60184 60185 60186 60187 60188 60189 60190 60191 60192 60193 60194 60195 60196 60197 60198 60199 60200 60201 60202 60203 60204 60205 60206 60207 60208 60209 60210 60211 60212 60213 60214 60215 60216 60217 60218 60219 60220 60221 60222 60223 60224 60225 60226 60227 60228 60229 60230 60231 60232 60233 60234 60235 60236 60237 60238 60239 60240 60241 60242 60243 60244 60245 60246 60247 60248 60249 60250 60251 60252 60253 60254 60255 60256 60257 60258 60259 60260 60261 60262 60263 60264 60265 60266 60267 60268 60269 60270 60271 60272 60273 60274 60275 60276 60277 60278 60279 60280 60281 60282 60283 60284 60285 60286 60287 60288 60289 60290 60291 60292 60293 60294 60295 60296 60297 60298 60299 60300 60301 60302 60303 60304 60305 60306 60307 60308 60309 60310 60311 60312 60313 60314 60315 60316 60317 60318 60319 60320 60321 60322 60323 60324 60325 60326 60327 60328 60329 60330 60331 60332 60333 60334 60335 60336 60337 60338 60339 60340 60341 60342 60343 60344 60345 60346 60347 60348 60349 60350 60351 60352 60353 60354 60355 60356 60357 60358 60359 60360 60361 60362 60363 60364 60365 60366 60367 60368 60369 60370 60371 60372 60373 60374 60375 60376 60377 60378 60379 60380 60381 60382 60383 60384 60385 60386 60387 60388 60389 60390 60391 60392 60393 60394 60395 60396 60397 60398 60399 60400 60401 60402 60403 60404 60405 60406 60407 60408 60409 60410 60411 60412 60413 60414 60415 60416 60417 60418 60419 60420 60421 60422 60423 60424 60425 60426 60427 60428 60429 60430 60431 60432 60433 60434 60435 60436 60437 60438 60439 60440 60441 60442 60443 60444 60445 60446 60447 60448 60449 60450 60451 60452 60453 60454 60455 60456 60457 60458 60459 60460 60461 60462 60463 60464 60465 60466 60467 60468 60469 60470 60471 60472 60473 60474 60475 60476 60477 60478 60479 60480 60481 60482 60483 60484 60485 60486 60487 60488 60489 60490 60491 60492 60493 60494 60495 60496 60497 60498 60499 60500 60501 60502 60503 60504 60505 60506 60507 60508 60509 60510 60511 60512 60513 60514 60515 60516 60517 60518 60519 60520 60521 60522 60523 60524 60525 60526 60527 60528 60529 60530 60531 60532 60533 60534 60535 60536 60537 60538 60539 60540 60541 60542 60543 60544 60545 60546 60547 60548 60549 60550 60551 60552 60553 60554 60555 60556 60557 60558 60559 60560 60561 60562 60563 60564 60565 60566 60567 60568 60569 60570 60571 60572 60573 60574 60575 60576 60577 60578 60579 60580 60581 60582 60583 60584 60585 60586 60587 60588 60589 60590 60591 60592 60593 60594 60595 60596 60597 60598 60599 60600 60601 60602 60603 60604 60605 60606 60607 60608 60609 60610 60611 60612 60613 60614 60615 60616 60617 60618 60619 60620 60621 60622 60623 60624 60625 60626 60627 60628 60629 60630 60631 60632 60633 60634 60635 60636 60637 60638 60639 60640 60641 60642 60643 60644 60645 60646 60647 60648 60649 60650 60651 60652 60653 60654 60655 60656 60657 60658 60659 60660 60661 60662 60663 60664 60665 60666 60667 60668 60669 60670 60671 60672 60673 60674 60675 60676 60677 60678 60679 60680 60681 60682 60683 60684 60685 60686 60687 60688 60689 60690 60691 60692 60693 60694 60695 60696 60697 60698 60699 60700 60701 60702 60703 60704 60705 60706 60707 60708 60709 60710 60711 60712 60713 60714 60715 60716 60717 60718 60719 60720 60721 60722 60723 60724 60725 60726 60727 60728 60729 60730 60731 60732 60733 60734 60735 60736 60737 60738 60739 60740 60741 60742 60743 60744 60745 60746 60747 60748 60749 60750 60751 60752 60753 60754 60755 60756 60757 60758 60759 60760 60761 60762 60763 60764 60765 60766 60767 60768 60769 60770 60771 60772 60773 60774 60775 60776 60777 60778 60779 60780 60781 60782 60783 60784 60785 60786 60787 60788 60789 60790 60791 60792 60793 60794 60795 60796 60797 60798 60799 60800 60801 60802 60803 60804 60805 60806 60807 60808 60809 60810 60811 60812 60813 60814 60815 60816 60817 60818 60819 60820 60821 60822 60823 60824 60825 60826 60827 60828 60829 60830 60831 60832 60833 60834 60835 60836 60837 60838 60839 60840 60841 60842 60843 60844 60845 60846 60847 60848 60849 60850 60851 60852 60853 60854 60855 60856 60857 60858 60859 60860 60861 60862 60863 60864 60865 60866 60867 60868 60869 60870 60871 60872 60873 60874 60875 60876 60877 60878 60879 60880 60881 60882 60883 60884 60885 60886 60887 60888 60889 60890 60891 60892 60893 60894 60895 60896 60897 60898 60899 60900 60901 60902 60903 60904 60905 60906 60907 60908 60909 60910 60911 60912 60913 60914 60915 60916 60917 60918 60919 60920 60921 60922 60923 60924 60925 60926 60927 60928 60929 60930 60931 60932 60933 60934 60935 60936 60937 60938 60939 60940 60941 60942 60943 60944 60945 60946 60947 60948 60949 60950 60951 60952 60953 60954 60955 60956 60957 60958 60959 60960 60961 60962 60963 60964 60965 60966 60967 60968 60969 60970 60971 60972 60973 60974 60975 60976 60977 60978 60979 60980 60981 60982 60983 60984 60985 60986 60987 60988 60989 60990 60991 60992 60993 60994 60995 60996 60997 60998 60999 61000
  		if ( _keyState !== STATE.NONE ) {

  			return;

  		} else if ( event.keyCode === scope.keys[ STATE.ROTATE ] && ! scope.noRotate ) {

  			_keyState = STATE.ROTATE;

  		} else if ( event.keyCode === scope.keys[ STATE.ZOOM ] && ! scope.noZoom ) {

  			_keyState = STATE.ZOOM;

  		} else if ( event.keyCode === scope.keys[ STATE.PAN ] && ! scope.noPan ) {

  			_keyState = STATE.PAN;

  		}

  	}

  	function keyup() {

  		if ( scope.enabled === false ) return;

  		_keyState = STATE.NONE;

  		window.addEventListener( 'keydown', keydown, false );

  	}

  	function mousedown( event ) {

  		if ( scope.enabled === false ) return;

  		event.preventDefault();
  		event.stopPropagation();

  		if ( _state === STATE.NONE ) {

  			switch ( event.button ) {

  				case scope.mouseButtons.LEFT:
  					_state = STATE.ROTATE;
  					break;

  				case scope.mouseButtons.MIDDLE:
  					_state = STATE.ZOOM;
  					break;

  				case scope.mouseButtons.RIGHT:
  					_state = STATE.PAN;
  					break;

  				default:
  					_state = STATE.NONE;

  			}

  		}

  		var state = ( _keyState !== STATE.NONE ) ? _keyState : _state;

  		if ( state === STATE.ROTATE && ! scope.noRotate ) {

  			_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
  			_movePrev.copy( _moveCurr );

  		} else if ( state === STATE.ZOOM && ! scope.noZoom ) {

  			_zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
  			_zoomEnd.copy( _zoomStart );

  		} else if ( state === STATE.PAN && ! scope.noPan ) {

  			_panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
  			_panEnd.copy( _panStart );

  		}

  		scope.domElement.ownerDocument.addEventListener( 'mousemove', mousemove, false );
  		scope.domElement.ownerDocument.addEventListener( 'mouseup', mouseup, false );

  		scope.dispatchEvent( startEvent );

  	}

  	function mousemove( event ) {

  		if ( scope.enabled === false ) return;

  		event.preventDefault();
  		event.stopPropagation();

  		var state = ( _keyState !== STATE.NONE ) ? _keyState : _state;

  		if ( state === STATE.ROTATE && ! scope.noRotate ) {

  			_movePrev.copy( _moveCurr );
  			_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );

  		} else if ( state === STATE.ZOOM && ! scope.noZoom ) {

  			_zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );

  		} else if ( state === STATE.PAN && ! scope.noPan ) {

  			_panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );

  		}

  	}

  	function mouseup( event ) {

  		if ( scope.enabled === false ) return;

  		event.preventDefault();
  		event.stopPropagation();

  		_state = STATE.NONE;

  		scope.domElement.ownerDocument.removeEventListener( 'mousemove', mousemove );
  		scope.domElement.ownerDocument.removeEventListener( 'mouseup', mouseup );
  		scope.dispatchEvent( endEvent );

  	}

  	function mousewheel( event ) {

  		if ( scope.enabled === false ) return;

  		if ( scope.noZoom === true ) return;

  		event.preventDefault();
  		event.stopPropagation();

  		switch ( event.deltaMode ) {

  			case 2:
  				// Zoom in pages
  				_zoomStart.y -= event.deltaY * 0.025;
  				break;

  			case 1:
  				// Zoom in lines
  				_zoomStart.y -= event.deltaY * 0.01;
  				break;

  			default:
  				// undefined, 0, assume pixels
  				_zoomStart.y -= event.deltaY * 0.00025;
  				break;

  		}

  		scope.dispatchEvent( startEvent );
  		scope.dispatchEvent( endEvent );

  	}

  	function touchstart( event ) {

  		if ( scope.enabled === false ) return;

  		event.preventDefault();

  		switch ( event.touches.length ) {

  			case 1:
  				_state = STATE.TOUCH_ROTATE;
  				_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
  				_movePrev.copy( _moveCurr );
  				break;

  			default: // 2 or more
  				_state = STATE.TOUCH_ZOOM_PAN;
  				var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
  				var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
  				_touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );

  				var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
  				var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
  				_panStart.copy( getMouseOnScreen( x, y ) );
  				_panEnd.copy( _panStart );
  				break;

  		}

  		scope.dispatchEvent( startEvent );

  	}

  	function touchmove( event ) {

  		if ( scope.enabled === false ) return;

  		event.preventDefault();
  		event.stopPropagation();

  		switch ( event.touches.length ) {

  			case 1:
  				_movePrev.copy( _moveCurr );
  				_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
  				break;

  			default: // 2 or more
  				var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
  				var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
  				_touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy );

  				var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
  				var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
  				_panEnd.copy( getMouseOnScreen( x, y ) );
  				break;

  		}

  	}

  	function touchend( event ) {

  		if ( scope.enabled === false ) return;

  		switch ( event.touches.length ) {

  			case 0:
  				_state = STATE.NONE;
  				break;

  			case 1:
  				_state = STATE.TOUCH_ROTATE;
  				_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
  				_movePrev.copy( _moveCurr );
  				break;

  		}

  		scope.dispatchEvent( endEvent );

  	}

  	function contextmenu( event ) {

  		if ( scope.enabled === false ) return;

  		event.preventDefault();

  	}

  	this.dispose = function () {

  		scope.domElement.removeEventListener( 'contextmenu', contextmenu, false );
  		scope.domElement.removeEventListener( 'mousedown', mousedown, false );
  		scope.domElement.removeEventListener( 'wheel', mousewheel, false );

  		scope.domElement.removeEventListener( 'touchstart', touchstart, false );
  		scope.domElement.removeEventListener( 'touchend', touchend, false );
  		scope.domElement.removeEventListener( 'touchmove', touchmove, false );

  		scope.domElement.ownerDocument.removeEventListener( 'mousemove', mousemove, false );
  		scope.domElement.ownerDocument.removeEventListener( 'mouseup', mouseup, false );

  		window.removeEventListener( 'keydown', keydown, false );
  		window.removeEventListener( 'keyup', keyup, false );

  	};

  	this.domElement.addEventListener( 'contextmenu', contextmenu, false );
  	this.domElement.addEventListener( 'mousedown', mousedown, false );
  	this.domElement.addEventListener( 'wheel', mousewheel, false );

  	this.domElement.addEventListener( 'touchstart', touchstart, false );
  	this.domElement.addEventListener( 'touchend', touchend, false );
  	this.domElement.addEventListener( 'touchmove', touchmove, false );

  	window.addEventListener( 'keydown', keydown, false );
  	window.addEventListener( 'keyup', keyup, false );

  	this.handleResize();

  	// force an update at start
  	this.update();

  };

  TrackballControls.prototype = Object.create( EventDispatcher.prototype );
  TrackballControls.prototype.constructor = TrackballControls;

  /**
   * @author qiao / https://github.com/qiao
   * @author mrdoob / http://mrdoob.com
   * @author alteredq / http://alteredqualia.com/
   * @author WestLangley / http://github.com/WestLangley
   * @author erich666 / http://erichaines.com
   * @author ScieCode / http://github.com/sciecode
   */

  // 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

  var OrbitControls = function ( object, domElement ) {

  	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;

  	// Set to false to disable this control
  	this.enabled = true;

  	// "target" sets the location of focus, where the object orbits around
  	this.target = new 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 round when fps is 60

  	// Set to false to disable use of the keys
  	this.enableKeys = true;

  	// The four arrow keys
  	this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };

  	// Mouse buttons
  	this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };

  	// Touch fingers
  	this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };

  	// for reset
  	this.target0 = this.target.clone();
  	this.position0 = this.object.position.clone();
  	this.zoom0 = this.object.zoom;

  	//
  	// public methods
  	//

  	this.getPolarAngle = function () {

  		return spherical.phi;

  	};

  	this.getAzimuthalAngle = function () {

  		return spherical.theta;

  	};

  	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 () {

  		var offset = new Vector3();

  		// so camera.up is the orbit axis
  		var quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) );
  		var quatInverse = quat.clone().inverse();

  		var lastPosition = new Vector3();
  		var lastQuaternion = new Quaternion();

  		var twoPI = 2 * Math.PI;

  		return function update() {

  			var 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

  			var min = scope.minAzimuthAngle;
  			var 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, false );
  		scope.domElement.removeEventListener( 'mousedown', onMouseDown, false );
  		scope.domElement.removeEventListener( 'wheel', onMouseWheel, false );

  		scope.domElement.removeEventListener( 'touchstart', onTouchStart, false );
  		scope.domElement.removeEventListener( 'touchend', onTouchEnd, false );
  		scope.domElement.removeEventListener( 'touchmove', onTouchMove, false );

  		scope.domElement.ownerDocument.removeEventListener( 'mousemove', onMouseMove, false );
  		scope.domElement.ownerDocument.removeEventListener( 'mouseup', onMouseUp, false );

  		scope.domElement.removeEventListener( 'keydown', onKeyDown, false );

  		//scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?

  	};

  	//
  	// internals
  	//

  	var scope = this;

  	var changeEvent = { type: 'change' };
  	var startEvent = { type: 'start' };
  	var endEvent = { type: 'end' };

  	var STATE = {
  		NONE: - 1,
  		ROTATE: 0,
  		DOLLY: 1,
  		PAN: 2,
  		TOUCH_ROTATE: 3,
  		TOUCH_PAN: 4,
  		TOUCH_DOLLY_PAN: 5,
  		TOUCH_DOLLY_ROTATE: 6
  	};

  	var state = STATE.NONE;

  	var EPS = 0.000001;

  	// current position in spherical coordinates
  	var spherical = new Spherical();
  	var sphericalDelta = new Spherical();

  	var scale = 1;
  	var panOffset = new Vector3();
  	var zoomChanged = false;

  	var rotateStart = new Vector2();
  	var rotateEnd = new Vector2();
  	var rotateDelta = new Vector2();

  	var panStart = new Vector2();
  	var panEnd = new Vector2();
  	var panDelta = new Vector2();

  	var dollyStart = new Vector2();
  	var dollyEnd = new Vector2();
  	var dollyDelta = new Vector2();

  	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;

  	}

  	var panLeft = function () {

  		var v = new Vector3();

  		return function panLeft( distance, objectMatrix ) {

  			v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
  			v.multiplyScalar( - distance );

  			panOffset.add( v );

  		};

  	}();

  	var panUp = function () {

  		var v = new 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
  	var pan = function () {

  		var offset = new Vector3();

  		return function pan( deltaX, deltaY ) {

  			var element = scope.domElement;

  			if ( scope.object.isPerspectiveCamera ) {

  				// perspective
  				var position = scope.object.position;
  				offset.copy( position ).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 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 );

  		var 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 ) {

  		var needsUpdate = false;

  		switch ( event.keyCode ) {

  			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( event ) {

  		if ( event.touches.length == 1 ) {

  			rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );

  		} else {

  			var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
  			var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );

  			rotateStart.set( x, y );

  		}

  	}

  	function handleTouchStartPan( event ) {

  		if ( event.touches.length == 1 ) {

  			panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );

  		} else {

  			var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
  			var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );

  			panStart.set( x, y );

  		}

  	}

  	function handleTouchStartDolly( event ) {

  		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 );

  	}

  	function handleTouchStartDollyPan( event ) {

  		if ( scope.enableZoom ) handleTouchStartDolly( event );

  		if ( scope.enablePan ) handleTouchStartPan( event );

  	}

  	function handleTouchStartDollyRotate( event ) {

  		if ( scope.enableZoom ) handleTouchStartDolly( event );

  		if ( scope.enableRotate ) handleTouchStartRotate( event );

  	}

  	function handleTouchMoveRotate( event ) {

  		if ( event.touches.length == 1 ) {

  			rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );

  		} else {

  			var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
  			var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );

  			rotateEnd.set( x, y );

  		}

  		rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );

  		var 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 ( event.touches.length == 1 ) {

  			panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );

  		} else {

  			var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );