|
/** |
|
* @author qiao / https://github.com/qiao |
|
* @author mrdoob / http://mrdoob.com |
|
* @author alteredq / http://alteredqualia.com/ |
|
* @author WestLangley / http://github.com/WestLangley |
|
* @author David Geo Holmes (TypeScript migration) |
|
*/ |
|
|
|
const EPS = 0.000001; |
|
// 65 /*A*/, 83 /*S*/, 68 /*D*/ |
|
const keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40, ROTATE: 65, ZOOM: 83, PAN: 68 }; |
|
|
|
export default class OrbitControls { |
|
private object: THREE.Object3D; |
|
private domElement; |
|
|
|
public enabled = true; |
|
public center = new THREE.Vector3(); |
|
public userZoom = true; |
|
public userZoomSpeed = 1.0; |
|
public userRotate = true; |
|
public userRotateSpeed = 1.0; |
|
|
|
public userPan = true; |
|
public userPanSpeed = 2.0; |
|
|
|
public autoRotate = false; |
|
public autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 |
|
|
|
public minPolarAngle = 0; // radians |
|
public maxPolarAngle = Math.PI; // radians |
|
|
|
public minDistance = 0; |
|
public maxDistance = Infinity; |
|
|
|
private phiDelta = 0; |
|
private thetaDelta = 0; |
|
private scale = 1; |
|
private lastPosition = new THREE.Vector3(); |
|
|
|
constructor( object: THREE.Object3D , domElement: HTMLCanvasElement ) { |
|
|
|
this.object = object; |
|
this.domElement = ( domElement !== undefined ) ? domElement : document; |
|
|
|
var PIXELS_PER_ROUND = 1800; |
|
|
|
var rotateStart = new THREE.Vector2(); |
|
var rotateEnd = new THREE.Vector2(); |
|
var rotateDelta = new THREE.Vector2(); |
|
|
|
var zoomStart = new THREE.Vector2(); |
|
var zoomEnd = new THREE.Vector2(); |
|
var zoomDelta = new THREE.Vector2(); |
|
|
|
var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2 }; |
|
var state = STATE.NONE; |
|
|
|
// events |
|
|
|
var changeEvent = { type: 'change' }; |
|
|
|
this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); |
|
|
|
const onMouseDown = (event: MouseEvent) => { |
|
|
|
if (this.enabled === false ) return; |
|
if (this.userRotate === false ) return; |
|
|
|
event.preventDefault(); |
|
|
|
if ( state === STATE.NONE ) |
|
{ |
|
if ( event.button === 0 ) |
|
state = STATE.ROTATE; |
|
if ( event.button === 1 ) |
|
state = STATE.ZOOM; |
|
if ( event.button === 2 ) |
|
state = STATE.PAN; |
|
} |
|
|
|
|
|
if ( state === STATE.ROTATE ) { |
|
//state = STATE.ROTATE; |
|
rotateStart.set( event.clientX, event.clientY ); |
|
} |
|
else if ( state === STATE.ZOOM ) { |
|
//state = STATE.ZOOM; |
|
zoomStart.set( event.clientX, event.clientY ); |
|
} |
|
else if ( state === STATE.PAN ) { |
|
//state = STATE.PAN; |
|
} |
|
|
|
document.addEventListener( 'mousemove', onMouseMove, false ); |
|
document.addEventListener( 'mouseup', onMouseUp, false ); |
|
} |
|
|
|
this.domElement.addEventListener( 'mousedown', onMouseDown, false ); |
|
|
|
const onMouseMove = (event: MouseEvent) => { |
|
|
|
if (this.enabled === false ) return; |
|
|
|
event.preventDefault(); |
|
|
|
if ( state === STATE.ROTATE ) { |
|
|
|
rotateEnd.set( event.clientX, event.clientY ); |
|
rotateDelta.subVectors( rotateEnd, rotateStart ); |
|
|
|
this.rotateLeft( 2 * Math.PI * rotateDelta.x / PIXELS_PER_ROUND * this.userRotateSpeed ); |
|
this.rotateUp( 2 * Math.PI * rotateDelta.y / PIXELS_PER_ROUND * this.userRotateSpeed ); |
|
|
|
rotateStart.copy( rotateEnd ); |
|
} |
|
else if ( state === STATE.ZOOM ) { |
|
|
|
zoomEnd.set( event.clientX, event.clientY ); |
|
zoomDelta.subVectors( zoomEnd, zoomStart ); |
|
|
|
if ( zoomDelta.y > 0 ) { |
|
|
|
this.zoomIn(); |
|
|
|
} |
|
else { |
|
this.zoomOut(); |
|
} |
|
|
|
zoomStart.copy( zoomEnd ); |
|
|
|
} else if ( state === STATE.PAN ) { |
|
|
|
const movementX: number = event['movementX'] || event['mozMovementX'] || event['webkitMovementX'] || 0; |
|
const movementY = event['movementY'] || event['mozMovementY'] || event['webkitMovementY'] || 0; |
|
|
|
this.pan( new THREE.Vector3( - movementX, movementY, 0 ) ); |
|
|
|
} |
|
|
|
} |
|
|
|
const onMouseUp = (event: MouseEvent) => { |
|
if (this.enabled === false) return; |
|
if (this.userRotate === false) return; |
|
document.removeEventListener( 'mousemove', onMouseMove, false ); |
|
document.removeEventListener( 'mouseup', onMouseUp, false ); |
|
state = STATE.NONE; |
|
} |
|
|
|
const onMouseWheel = (event: MouseWheelEvent) => { |
|
|
|
if (this.enabled === false ) return; |
|
if (this.userZoom === false ) return; |
|
|
|
let delta = 0; |
|
|
|
if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9 |
|
delta = event.wheelDelta; |
|
} |
|
else if ( event.detail ) { // Firefox |
|
delta = - event.detail; |
|
} |
|
|
|
if ( delta > 0 ) { |
|
this.zoomOut(); |
|
} |
|
else { |
|
this.zoomIn(); |
|
} |
|
} |
|
|
|
this.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); |
|
this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox |
|
|
|
const onKeyDown = (event: KeyboardEvent) => { |
|
|
|
if (this.enabled === false) return; |
|
if (this.userPan === false) return; |
|
|
|
switch ( event.keyCode ) { |
|
|
|
/*case scope.keys.UP: |
|
scope.pan( new THREE.Vector3( 0, 1, 0 ) ); |
|
break; |
|
case scope.keys.BOTTOM: |
|
scope.pan( new THREE.Vector3( 0, - 1, 0 ) ); |
|
break; |
|
case scope.keys.LEFT: |
|
scope.pan( new THREE.Vector3( - 1, 0, 0 ) ); |
|
break; |
|
case scope.keys.RIGHT: |
|
scope.pan( new THREE.Vector3( 1, 0, 0 ) ); |
|
break; |
|
*/ |
|
case keys.ROTATE: |
|
state = STATE.ROTATE; |
|
break; |
|
case keys.ZOOM: |
|
state = STATE.ZOOM; |
|
break; |
|
case keys.PAN: |
|
state = STATE.PAN; |
|
break; |
|
|
|
} |
|
} |
|
|
|
const onKeyUp = (event: KeyboardEvent) => { |
|
switch (event.keyCode) { |
|
case keys.ROTATE: |
|
case keys.ZOOM: |
|
case keys.PAN: |
|
state = STATE.NONE; |
|
break; |
|
} |
|
} |
|
|
|
window.addEventListener( 'keydown', onKeyDown, false ); |
|
window.addEventListener( 'keyup', onKeyUp, false ); |
|
} |
|
|
|
private getAutoRotationAngle(): number { |
|
return 2 * Math.PI / 60 / 60 * this.autoRotateSpeed; |
|
} |
|
|
|
private getZoomScale(): number { |
|
return Math.pow( 0.95, this.userZoomSpeed ); |
|
} |
|
|
|
rotateLeft(angle?: number): void { |
|
if ( angle === undefined ) { |
|
angle = this.getAutoRotationAngle(); |
|
} |
|
this.thetaDelta -= angle; |
|
} |
|
|
|
rotateRight(angle?: number): void { |
|
if ( angle === undefined ) { |
|
angle = this.getAutoRotationAngle(); |
|
} |
|
this.thetaDelta += angle; |
|
} |
|
|
|
rotateUp(angle?: number) { |
|
if ( angle === undefined ) { |
|
angle = this.getAutoRotationAngle(); |
|
} |
|
this.phiDelta -= angle; |
|
} |
|
|
|
rotateDown(angle?: number) { |
|
if ( angle === undefined ) { |
|
angle = this.getAutoRotationAngle(); |
|
} |
|
this.phiDelta += angle; |
|
} |
|
|
|
zoomIn(zoomScale?: number): void { |
|
if ( zoomScale === undefined ) { |
|
zoomScale = this.getZoomScale(); |
|
} |
|
this.scale /= zoomScale; |
|
} |
|
|
|
zoomOut(zoomScale?: number): void { |
|
if (zoomScale === undefined) { |
|
zoomScale = this.getZoomScale(); |
|
} |
|
this.scale *= zoomScale; |
|
} |
|
|
|
pan( distance: THREE.Vector3 ) { |
|
distance.transformDirection( this.object.matrix ); |
|
distance.multiplyScalar( this.userPanSpeed ); |
|
this.object.position.add( distance ); |
|
this.center.add( distance ); |
|
} |
|
|
|
update() { |
|
const position = this.object.position; |
|
const offset = position.clone().sub( this.center ); |
|
|
|
// angle from z-axis around y-axis |
|
|
|
let theta = Math.atan2( offset.x, offset.z ); |
|
|
|
// angle from y-axis |
|
|
|
let phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y ); |
|
|
|
if (this.autoRotate) { |
|
this.rotateLeft(this.getAutoRotationAngle()); |
|
} |
|
|
|
theta += this.thetaDelta; |
|
phi += this.phiDelta; |
|
|
|
// restrict phi to be between desired limits |
|
phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) ); |
|
|
|
// restrict phi to be betwee EPS and PI-EPS |
|
phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) ); |
|
|
|
var radius = offset.length() * this.scale; |
|
|
|
// restrict radius to be between desired limits |
|
radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) ); |
|
|
|
offset.x = radius * Math.sin( phi ) * Math.sin( theta ); |
|
offset.y = radius * Math.cos( phi ); |
|
offset.z = radius * Math.sin( phi ) * Math.cos( theta ); |
|
|
|
position.copy( this.center ).add( offset ); |
|
|
|
this.object.lookAt( this.center ); |
|
|
|
this.thetaDelta = 0; |
|
this.phiDelta = 0; |
|
this.scale = 1; |
|
|
|
if ( this.lastPosition.distanceTo( this.object.position ) > 0 ) { |
|
// this.dispatchEvent( changeEvent ); |
|
this.lastPosition.copy( this.object.position ); |
|
} |
|
} |
|
} |