/** * @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 */ // 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 arrow keys / touch: two-finger move three.orbitcontrols = function ( object, domelement ) { this.object = object; this.domelement = ( domelement !== undefined ) ? domelement : document; // set to false to disable this control this.enabled = true; // "target" sets the location of focus, where the object orbits around this.target = new three.vector3(); // how far you can dolly in and out ( perspectivecamera only ) this.mindistance = 0; this.maxdistance = infinity; // how far you can zoom in and out ( orthographiccamera only ) this.minzoom = 0; this.maxzoom = infinity; // how far you can orbit vertically, upper and lower limits. // range is 0 to math.pi radians. this.minpolarangle = 0; // radians this.maxpolarangle = math.pi; // radians // how far you can orbit horizontally, upper and lower limits. // if set, must be a sub-interval of the interval [ - math.pi, math.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.25; // 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 = false; // if true, pan in screen-space 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 = { 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; // // 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 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 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() ); } 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 ); 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 ); 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() { 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 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 ) { 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 three.vector3(); return function pan( deltax, deltay ) { var element = scope.domelement === document ? scope.domelement.body : 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 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; } } 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; } } // // event callbacks - update the object state // function handlemousedownrotate( event ) { //console.log( 'handlemousedownrotate' ); rotatestart.set( event.clientx, event.clienty ); } function handlemousedowndolly( event ) { //console.log( 'handlemousedowndolly' ); dollystart.set( event.clientx, event.clienty ); } function handlemousedownpan( event ) { //console.log( 'handlemousedownpan' ); panstart.set( event.clientx, event.clienty ); } function handlemousemoverotate( event ) { //console.log( 'handlemousemoverotate' ); rotateend.set( event.clientx, event.clienty ); rotatedelta.subvectors( rotateend, rotatestart ).multiplyscalar( scope.rotatespeed ); var element = scope.domelement === document ? scope.domelement.body : scope.domelement; // rotating across whole screen goes 360 degrees around rotateleft( 2 * math.pi * rotatedelta.x / element.clientwidth ); // rotating up and down along whole screen attempts to go 360, but limited to 180 rotateup( 2 * math.pi * rotatedelta.y / element.clientheight ); rotatestart.copy( rotateend ); scope.update(); } function handlemousemovedolly( event ) { //console.log( 'handlemousemovedolly' ); dollyend.set( event.clientx, event.clienty ); dollydelta.subvectors( dollyend, dollystart ); if ( dollydelta.y > 0 ) { dollyin( getzoomscale() ); } else if ( dollydelta.y < 0 ) { dollyout( getzoomscale() ); } dollystart.copy( dollyend ); scope.update(); } function handlemousemovepan( event ) { //console.log( 'handlemousemovepan' ); panend.set( event.clientx, event.clienty ); pandelta.subvectors( panend, panstart ).multiplyscalar( scope.panspeed ); pan( pandelta.x, pandelta.y ); panstart.copy( panend ); scope.update(); } function handlemouseup( event ) { // console.log( 'handlemouseup' ); } function handlemousewheel( event ) { // console.log( 'handlemousewheel' ); if ( event.deltay < 0 ) { dollyout( getzoomscale() ); } else if ( event.deltay > 0 ) { dollyin( getzoomscale() ); } scope.update(); } function handlekeydown( event ) { //console.log( 'handlekeydown' ); 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 ) { //console.log( 'handletouchstartrotate' ); rotatestart.set( event.touches[ 0 ].pagex, event.touches[ 0 ].pagey ); } function handletouchstartdollypan( event ) { //console.log( 'handletouchstartdollypan' ); 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 ) { //console.log( 'handletouchmoverotate' ); rotateend.set( event.touches[ 0 ].pagex, event.touches[ 0 ].pagey ); rotatedelta.subvectors( rotateend, rotatestart ).multiplyscalar( scope.rotatespeed ); var element = scope.domelement === document ? scope.domelement.body : scope.domelement; // rotating across whole screen goes 360 degrees around rotateleft( 2 * math.pi * rotatedelta.x / element.clientwidth ); // rotating up and down along whole screen attempts to go 360, but limited to 180 rotateup( 2 * math.pi * rotatedelta.y / element.clientheight ); rotatestart.copy( rotateend ); scope.update(); } function handletouchmovedollypan( event ) { //console.log( 'handletouchmovedollypan' ); 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( scope.panspeed ); pan( pandelta.x, pandelta.y ); panstart.copy( panend ); } scope.update(); } function handletouchend( event ) { //console.log( 'handletouchend' ); } // // event handlers - fsm: listen for events and reset state // function onmousedown( event ) { if ( scope.enabled === false ) return; event.preventdefault(); switch ( event.button ) { case scope.mousebuttons.orbit: if ( scope.enablerotate === false ) return; handlemousedownrotate( event ); state = state.rotate; break; case scope.mousebuttons.zoom: if ( scope.enablezoom === false ) return; handlemousedowndolly( event ); state = state.dolly; break; case scope.mousebuttons.pan: if ( scope.enablepan === false ) return; handlemousedownpan( event ); state = state.pan; break; } if ( state !== state.none ) { document.addeventlistener( 'mousemove', onmousemove, false ); document.addeventlistener( 'mouseup', onmouseup, false ); scope.dispatchevent( startevent ); } } function onmousemove( event ) { if ( scope.enabled === false ) return; event.preventdefault(); switch ( state ) { case state.rotate: if ( scope.enablerotate === false ) return; handlemousemoverotate( event ); break; case state.dolly: if ( scope.enablezoom === false ) return; handlemousemovedolly( event ); break; case state.pan: if ( scope.enablepan === false ) return; handlemousemovepan( event ); break; } } function onmouseup( event ) { if ( scope.enabled === false ) return; handlemouseup( event ); document.removeeventlistener( 'mousemove', onmousemove, false ); document.removeeventlistener( 'mouseup', onmouseup, false ); scope.dispatchevent( endevent ); state = state.none; } function onmousewheel( event ) { if ( scope.enabled === false || scope.enablezoom === false || ( state !== state.none && state !== state.rotate ) ) return; event.preventdefault(); event.stoppropagation(); scope.dispatchevent( startevent ); handlemousewheel( event ); scope.dispatchevent( endevent ); } function onkeydown( event ) { if ( scope.enabled === false || scope.enablekeys === false || scope.enablepan === false ) return; handlekeydown( event ); } function ontouchstart( event ) { if ( scope.enabled === false ) return; event.preventdefault(); switch ( event.touches.length ) { case 1: // one-fingered touch: rotate if ( scope.enablerotate === false ) 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.enabled === false ) 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.enabled === false ) return; handletouchend( event ); scope.dispatchevent( endevent ); state = state.none; } function oncontextmenu( event ) { if ( scope.enabled === false ) return; event.preventdefault(); } // scope.domelement.addeventlistener( 'contextmenu', oncontextmenu, false ); 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 ); // 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, { center: { get: function () { console.warn( 'three.orbitcontrols: .center has been renamed to .target' ); return this.target; } }, // backward compatibility nozoom: { get: function () { console.warn( 'three.orbitcontrols: .nozoom has been deprecated. use .enablezoom instead.' ); return ! this.enablezoom; }, set: function ( value ) { console.warn( 'three.orbitcontrols: .nozoom has been deprecated. use .enablezoom instead.' ); this.enablezoom = ! value; } }, norotate: { get: function () { console.warn( 'three.orbitcontrols: .norotate has been deprecated. use .enablerotate instead.' ); return ! this.enablerotate; }, set: function ( value ) { console.warn( 'three.orbitcontrols: .norotate has been deprecated. use .enablerotate instead.' ); this.enablerotate = ! value; } }, nopan: { get: function () { console.warn( 'three.orbitcontrols: .nopan has been deprecated. use .enablepan instead.' ); return ! this.enablepan; }, set: function ( value ) { console.warn( 'three.orbitcontrols: .nopan has been deprecated. use .enablepan instead.' ); this.enablepan = ! value; } }, nokeys: { get: function () { console.warn( 'three.orbitcontrols: .nokeys has been deprecated. use .enablekeys instead.' ); return ! this.enablekeys; }, set: function ( value ) { console.warn( 'three.orbitcontrols: .nokeys has been deprecated. use .enablekeys instead.' ); this.enablekeys = ! value; } }, staticmoving: { get: function () { console.warn( 'three.orbitcontrols: .staticmoving has been deprecated. use .enabledamping instead.' ); return ! this.enabledamping; }, set: function ( value ) { console.warn( 'three.orbitcontrols: .staticmoving has been deprecated. use .enabledamping instead.' ); this.enabledamping = ! value; } }, dynamicdampingfactor: { get: function () { console.warn( 'three.orbitcontrols: .dynamicdampingfactor has been renamed. use .dampingfactor instead.' ); return this.dampingfactor; }, set: function ( value ) { console.warn( 'three.orbitcontrols: .dynamicdampingfactor has been renamed. use .dampingfactor instead.' ); this.dampingfactor = value; } } } );