/** * @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 */ THREE.OrbitControls = function ( object, preview ) { this.object = object; this.preview = preview this.domElement = preview.canvas; this.enabled = true; this.isEnabled = function() { return this.enabled// && Toolbox.selected.navigate }; this.target = new THREE.Vector3(); this.minDistance = 0; this.maxDistance = Infinity; this.minZoom = 0; this.maxZoom = Infinity; this.minPolarAngle = 0; // radians this.maxPolarAngle = Math.PI; // radians this.minAzimuthAngle = - Infinity; // radians this.maxAzimuthAngle = Infinity; // radians this.enableDamping = false; this.dampingFactor = 0.25; this.enableZoom = true; this.zoomSpeed = 1.0; this.enableRotate = true; this.rotateSpeed = 1.0; this.enablePan = true; this.keyPanSpeed = 7.0; // pixels moved per arrow key push this.autoRotate = false; this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 this.enableKeys = true; // 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 }; // for reset this.target0 = this.target.clone(); this.position0 = this.object.position.clone(); this.zoom0 = this.object.zoom; var updateHandlers = []; this.getPolarAngle = function () { return spherical.phi; }; this.getAzimuthalAngle = function () { return spherical.theta; }; 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.updateSceneScale = function() { if (scope.preview.isOrtho === true && scope.preview.camOrtho.axis && scope.preview.background.image !== false) { scope.preview.updateBackground() } Transformer.update() Blockbench.dispatchEvent('update_camera_position', {preview: scope.preview}) }; this.onUpdate = function(call) { if (typeof call == 'function') { updateHandlers.push(call); } } // this method is exposed, but perhaps it would be better if we can make it private... 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().invert(); var lastPosition = new THREE.Vector3(); var lastQuaternion = new THREE.Quaternion(); 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 ) { let auto_rot_angle = getAutoRotationAngle() scope.autoRotateProgress += auto_rot_angle; scope.rotateLeft( auto_rot_angle ); } spherical.theta += sphericalDelta.theta; spherical.phi += sphericalDelta.phi; // restrict theta to be between desired limits spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, 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 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 ); } else { sphericalDelta.set( 0, 0, 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( 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; updateHandlers.forEach(call => call(changeEvent)); 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 ); document.removeEventListener( 'mousemove', onMouseMove, false ); document.removeEventListener( 'mouseup', onMouseUp, false ); window.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_DOLLY_PAN: 4 }; var state = STATE.NONE; var EPS = 0.000001; // current position in spherical coordinates var spherical = new THREE.Spherical(); var sphericalDelta = new THREE.Spherical(); var scale = 1; var panOffset = new THREE.Vector3(); var zoomChanged = false; var rotateStart = new THREE.Vector2(); var rotateEnd = new THREE.Vector2(); var rotateDelta = new THREE.Vector2(); 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(); function getAutoRotationAngle() { return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; } function getZoomScale(modifier = 1) { return Math.pow( 0.95, scope.zoomSpeed * modifier); } this.rotateLeft = function( angle ) { sphericalDelta.theta -= angle; } this.rotateUp = function( angle ) { sphericalDelta.phi -= angle; } var panLeft = function () { var v = new THREE.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 THREE.Vector3(); return function panUp( distance, objectMatrix ) { v.setFromMatrixColumn( objectMatrix, 1 ); // get Y column of objectMatrix v.multiplyScalar( distance ); panOffset.add( v ); }; }(); // deltaX and deltaY are in pixels; right and down are positive var pan = function () { var offset = new THREE.Vector3(); return function pan( deltaX, deltaY ) { var element = scope.domElement === document ? scope.domElement.body : scope.domElement; if ( scope.object instanceof THREE.PerspectiveCamera ) { // 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 actually don't use screenWidth, since perspective camera is fixed to screen height panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); } else if ( scope.object instanceof THREE.OrthographicCamera ) { // 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 dollyIn( dollyScale ) { if ( scope.object instanceof THREE.PerspectiveCamera ) { scale /= dollyScale; } else if ( scope.object instanceof THREE.OrthographicCamera ) { 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; } scope.updateSceneScale() } function dollyOut( dollyScale ) { if ( scope.object instanceof THREE.PerspectiveCamera ) { scale *= dollyScale; } else if ( scope.object instanceof THREE.OrthographicCamera ) { 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; } scope.updateSceneScale() } this.dollyIn = dollyIn; this.dollyOut = dollyOut; // 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 ); var element = scope.domElement === document ? scope.domElement.body : scope.domElement; // 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 ); rotateStart.copy( rotateEnd ); scope.update(); scope.updateSceneScale(); } function handleMouseMoveDolly( event ) { dollyEnd.set( event.clientX, event.clientY ); dollyDelta.subVectors( dollyEnd, dollyStart ); if ( dollyDelta.y > 0 ) { dollyIn( getZoomScale(0.12 * dollyDelta.y) ); } else if ( dollyDelta.y < 0 ) { dollyOut( getZoomScale(0.12 * -dollyDelta.y) ); } dollyStart.copy( dollyEnd ); scope.update(); scope.updateSceneScale(); } function handleMouseMovePan( event ) { panEnd.set( event.clientX, event.clientY ); panDelta.subVectors( panEnd, panStart ); pan( panDelta.x, panDelta.y ); panStart.copy( panEnd ); scope.update(); scope.updateSceneScale(); } function handleMouseUp( event ) { } function handleMouseWheel( event ) { if ( event.deltaY < 0 ) { dollyOut( getZoomScale() ); } else if ( event.deltaY > 0 ) { dollyIn( getZoomScale() ); } scope.update(); scope.updateSceneScale(); } function handleKeyDown( event ) { switch ( event.keyCode ) { case scope.keys.UP: pan( 0, scope.keyPanSpeed ); scope.update(); break; case scope.keys.BOTTOM: pan( 0, - scope.keyPanSpeed ); scope.update(); break; case scope.keys.LEFT: pan( scope.keyPanSpeed, 0 ); scope.update(); break; case scope.keys.RIGHT: pan( - scope.keyPanSpeed, 0 ); scope.update(); break; } } function handleTouchStartRotate( event ) { if (scope.enableRotate) { rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); } } function handleTouchStartDollyPan( event ) { if (scope.enableZoom) { 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 ); } if (scope.enablePan) { 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 handleTouchMoveRotate( event ) { rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); rotateDelta.subVectors( rotateEnd, rotateStart ); var element = scope.domElement === document ? scope.domElement.body : scope.domElement; // 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 ); rotateStart.copy( rotateEnd ); scope.update(); scope.updateSceneScale() } function handleTouchMoveDollyPan( event ) { if (scope.enableZoom) { 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 ); dollyEnd.set( 0, distance ); dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); dollyIn( dollyDelta.y ); dollyStart.copy( dollyEnd ); } if (scope.enablePan) { var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); panEnd.set( x, y ); panDelta.subVectors( panEnd, panStart ).multiplyScalar( 1 ); pan( panDelta.x, panDelta.y ); panStart.copy( panEnd ); } scope.update(); /* 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 ); dollyEnd.set( 0, distance ); dollyDelta.subVectors( dollyEnd, dollyStart ); if ( dollyDelta.y > 0 ) { dollyOut( getZoomScale() ); } else if ( dollyDelta.y < 0 ) { dollyIn( getZoomScale() ); } dollyStart.copy( dollyEnd ); scope.update();*/ } // // event handlers - FSM: listen for events and reset state // function onMouseDown( event ) { if (scope.isEnabled() === false || Transformer.dragging) return; event.preventDefault(); scope.hasMoved = false if ( Keybinds.extra.preview_rotate.keybind.isTriggered(event) ) { if ( scope.enableRotate === false ) return; if (event.which === 1 && Canvas.raycast(event) && display_mode === false) { return; } handleMouseDownRotate( event ); state = STATE.ROTATE; } else if ( Keybinds.extra.preview_drag.keybind.isTriggered(event) ) { if ( scope.enablePan === false ) return; if (event.which === 1 && Canvas.raycast(event) && display_mode === false) { return; } handleMouseDownPan( event ); state = STATE.PAN; } else if ( Keybinds.extra.preview_zoom.keybind.isTriggered(event) ) { if ( scope.enableZoom === false ) return; if (event.which === 1 && Canvas.raycast(event) && display_mode === false) { return; } handleMouseDownDolly( event ); state = STATE.DOLLY; } if ( state !== STATE.NONE ) { document.addEventListener( 'mousemove', onMouseMove, false ); document.addEventListener( 'mouseup', onMouseUp, false ); scope.dispatchEvent( startEvent ); } } function onMouseMove( event ) { if (scope.isEnabled() === false || Transformer.dragging) return; event.preventDefault(); scope.hasMoved = true if ( state === STATE.ROTATE ) { if ( scope.enableRotate === false ) return; handleMouseMoveRotate( event ); } else if ( state === STATE.DOLLY ) { if ( scope.enableZoom === false ) return; handleMouseMoveDolly( event ); } else if ( state === STATE.PAN ) { if ( scope.enablePan === false ) return; handleMouseMovePan( event ); } } function onMouseUp( event ) { if ( scope.isEnabled() === false ) return; handleMouseUp( event ); document.removeEventListener( 'mousemove', onMouseMove, false ); document.removeEventListener( 'mouseup', onMouseUp, false ); scope.dispatchEvent( endEvent ); state = STATE.NONE; } this.stopMovement = function(event) { onMouseUp() } function onMouseWheel( event ) { if ( scope.isEnabled() === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return; event.preventDefault(); event.stopPropagation(); handleMouseWheel( event ); } function onKeyDown( event ) { if ( scope.isEnabled() === false || scope.enableKeys === false || scope.enablePan === false ) return; handleKeyDown( event ); } function onTouchStart( event ) { if ( scope.isEnabled() === false ) return; event.preventDefault(); switch ( event.touches.length ) { case 1: // one-fingered touch: rotate if ( scope.enableRotate === false ) return; if (event.touches[0].touchType == 'stylus' && Modes.paint) return; handleTouchStartRotate( event ); state = STATE.TOUCH_ROTATE; break; case 2: // two-fingered touch: dolly-pan //if ( scope.enableZoom === false && scope.enablePan === false ) return; handleTouchStartDollyPan( event ); state = STATE.TOUCH_DOLLY_PAN; break; default: state = STATE.NONE; } if ( state !== STATE.NONE ) { scope.dispatchEvent( startEvent ); } } function onTouchMove( event ) { if ( scope.isEnabled() === false ) return; if ( Transformer.dragging || Painter.painting ) return; event.preventDefault(); event.stopPropagation(); switch ( event.touches.length ) { case 1: // one-fingered touch: rotate //if ( scope.enableRotate === false ) return; if ( state !== STATE.TOUCH_ROTATE ) return; // is this needed? handleTouchMoveRotate( event ); break; case 2: // two-fingered touch: dolly-pan //if ( scope.enableZoom === false && scope.enablePan === false ) return; if ( state !== STATE.TOUCH_DOLLY_PAN ) return; // is this needed? handleTouchMoveDollyPan( event ); break; default: state = STATE.NONE; } } function onTouchEnd( event ) { if ( scope.isEnabled() === false ) return; scope.dispatchEvent( endEvent ); state = STATE.NONE; } scope.domElement.addEventListener( 'mousedown', onMouseDown, false ); scope.domElement.addEventListener( 'wheel', onMouseWheel, false ); scope.domElement.addEventListener( 'touchstart', onTouchStart, false ); scope.domElement.addEventListener( 'touchend', onTouchEnd, false ); scope.domElement.addEventListener( 'touchmove', onTouchMove, false ); window.addEventListener( 'keydown', onKeyDown, false ); this.update(); }; THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); THREE.OrbitControls.prototype.constructor = THREE.OrbitControls;