scripts/three/controls.js
import {EventDispatcher, Vector2, Vector3} from 'three';
import $ from 'jquery';
import {EVENT_CAMERA_MOVED} from '../core/events.js';
/**
This file is a modified version of THREE.OrbitControls
Contributors:
* @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
*/
export const STATE = { NONE: -1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5 };
export class Controls extends EventDispatcher
{
constructor(object, domElement)
{
super();
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 control orbits around
// and where it pans with respect to.
this.target = new Vector3();
// center is old, deprecated; use "target" instead
this.center = this.target;
// This option actually enables dollying in and out; left as "zoom" for
// backwards compatibility
this.noZoom = false;
this.zoomSpeed = 1.0;
// Limits to how far you can dolly in and out
this.minDistance = 0;
this.maxDistance = 2500; //Infinity;
// Set to true to disable this control
this.noRotate = false;
this.rotateSpeed = 1.0;
// Set to true to disable this control
this.noPan = false;
this.keyPanSpeed = 40.0; // pixels moved per arrow key push
// Set to true to automatically rotate around the target
this.autoRotate = false;
this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
// 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 / 2; // radians
// Set to true to disable use of the keys
this.noKeys = false;
// The four arrow keys
this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
this.cameraMovedCallbacks = $.Callbacks();
this.needsUpdate = true;
// internals
// var window = $(window);
this.EPS = 0.000001;
this.rotateStart = new Vector2();
this.rotateEnd = new Vector2();
this.rotateDelta = new Vector2();
this.panStart = new Vector2();
this.panEnd = new Vector2();
this.panDelta = new Vector2();
this.dollyStart = new Vector2();
this.dollyEnd = new Vector2();
this.dollyDelta = new Vector2();
this.phiDelta = 0;
this.thetaDelta = 0;
this.scale = 1;
this.pan = new Vector3();
this.state = STATE.NONE;
this.mouseupevent = (event) => {this.onMouseUp(event);};
this.mousemoveevent = (event) => {this.onMouseMove(event);};
this.mousedownevent = (event) => {this.onMouseDown(event);};
this.mousewheelevent = (event) => {this.onMouseWheel(event);};
this.touchstartevent = (event) => {this.touchstart(event);};
this.touchendevent = (event) => {this.touchend(event);};
this.touchmoveevent = (event) => {this.touchmove(event);};
this.keydownevent = (event)=> {this.onKeyDown(event);};
this.domElement.addEventListener('contextmenu', (event) => { event.preventDefault(); }, false);
this.domElement.addEventListener('mousedown', this.mousedownevent, false);
this.domElement.addEventListener('mousewheel', this.mousewheelevent, false);
this.domElement.addEventListener('DOMMouseScroll', this.mousewheelevent, false); // firefox
this.domElement.addEventListener('touchstart', this.touchstartevent, false);
this.domElement.addEventListener('touchend', this.touchendevent, false);
this.domElement.addEventListener('touchmove', this.touchmoveevent, false);
window.addEventListener('keydown', this.keydownevent, false);
}
controlsActive()
{
return (this.state === STATE.NONE);
}
setPan(vec3)
{
this.pan = vec3;
}
panTo(vec3)
{
var newTarget = new Vector3(vec3.x, this.target.y, vec3.z);
var delta = this.target.clone().sub(newTarget);
this.pan.sub(delta);
this.update();
}
rotateLeft(angle)
{
if (angle === undefined)
{
angle = this.getAutoRotationAngle();
}
this.thetaDelta -= angle;
}
rotateUp(angle)
{
if (angle === undefined)
{
angle = this.getAutoRotationAngle();
}
this.phiDelta -= angle;
}
// pass in distance in world space to move left
panLeft(distance)
{
var panOffset = new Vector3();
var te = this.object.matrix.elements;
// get X column of matrix
panOffset.set(te[0], 0, te[2]);
panOffset.normalize();
panOffset.multiplyScalar(-distance);
this.pan.add(panOffset);
}
// pass in distance in world space to move up
panUp(distance)
{
var panOffset = new Vector3();
var te = this.object.matrix.elements;
// get Y column of matrix
panOffset.set(te[4], 0, te[6]);
panOffset.normalize();
panOffset.multiplyScalar(distance);
this.pan.add(panOffset);
}
// main entry point; pass in Vector2 of change desired in pixel space,
// right and down are positive
// Avoid the method name 'pan' this is conflicting with a variable name
// pan(delta)
updatePan(delta)
{
var element = (this.domElement === $(document)) ? this.domElement.body : this.domElement;
if (this.object.fov !== undefined)
{
// perspective
var position = this.object.position;
var offset = position.clone().sub(this.target);
var targetDistance = offset.length();
// half of the fov is center to top of screen
targetDistance *= Math.tan((this.object.fov / 2) * Math.PI / 180.0);
// we actually don't use screenWidth, since perspective camera is fixed to screen height
this.panLeft(2 * delta.x * targetDistance / element.clientHeight);
this.panUp(2 * delta.y * targetDistance / element.clientHeight);
}
else if (this.object.top !== undefined)
{
// orthographic
this.panLeft(delta.x * (this.object.right - this.object.left) / element.clientWidth);
this.panUp(delta.y * (this.object.top - this.object.bottom) / element.clientHeight);
}
else
{
// camera neither orthographic or perspective - warn user
console.warn('WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.');
}
this.update();
}
panXY(x, y)
{
// this.pan(new Vector2(x, y));
this.updatePan(new Vector2(x, y));
}
dollyIn(dollyScale)
{
if (dollyScale === undefined)
{
dollyScale = this.getZoomScale();
}
this.scale /= dollyScale;
}
dollyOut(dollyScale)
{
if (dollyScale === undefined)
{
dollyScale = this.getZoomScale();
}
this.scale *= dollyScale;
}
update()
{
var position = this.object.position;
var offset = position.clone().sub(this.target);
// angle from z-axis around y-axis
var theta = Math.atan2(offset.x, offset.z);
// angle from y-axis
var 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(this.EPS, Math.min(Math.PI - this.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));
// move target to panned location
this.target.add(this.pan);
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.target).add(offset);
this.object.lookAt(this.target);
this.thetaDelta = 0;
this.phiDelta = 0;
this.scale = 1;
this.pan.set(0, 0, 0);
// this.cameraMovedCallbacks.fire();
this.dispatchEvent({type:EVENT_CAMERA_MOVED});
this.needsUpdate = true;
}
getAutoRotationAngle()
{
return 2 * Math.PI / 60 / 60 * this.autoRotateSpeed;
}
getZoomScale()
{
return Math.pow(0.95, this.zoomSpeed);
}
onMouseDown(event)
{
if (this.enabled === false) { return; }
event.preventDefault();
if (event.button === 0)
{
if (this.noRotate === true) { return; }
this.state = STATE.ROTATE;
this.rotateStart.set(event.clientX, event.clientY);
}
else if (event.button === 1)
{
if (this.noZoom === true) { return; }
this.state = STATE.DOLLY;
this.dollyStart.set(event.clientX, event.clientY);
}
else if (event.button === 2)
{
if (this.noPan === true) { return; }
this.state = STATE.PAN;
this.panStart.set(event.clientX, event.clientY);
}
// Greggman fix: https://github.com/greggman/three.js/commit/fde9f9917d6d8381f06bf22cdff766029d1761be
this.domElement.addEventListener('mousemove', this.mousemoveevent, false);
this.domElement.addEventListener('mouseup', this.mouseupevent, false);
}
onMouseMove(event)
{
if (this.enabled === false)
{
return;
}
event.preventDefault();
var element = this.domElement === $(document) ? this.domElement.body : this.domElement;
if (this.state === STATE.ROTATE)
{
if (this.noRotate === true)
{
return;
}
this.rotateEnd.set(event.clientX, event.clientY);
this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart);
// rotating across whole screen goes 360 degrees around
this.rotateLeft(2 * Math.PI * this.rotateDelta.x / element.clientWidth * this.rotateSpeed);
// rotating up and down along whole screen attempts to go 360, but limited to 180
this.rotateUp(2 * Math.PI * this.rotateDelta.y / element.clientHeight * this.rotateSpeed);
this.rotateStart.copy(this.rotateEnd);
}
else if (this.state === STATE.DOLLY)
{
if (this.noZoom === true)
{
return;
}
this.dollyEnd.set(event.clientX, event.clientY);
this.dollyDelta.subVectors(this.dollyEnd, this.dollyStart);
if (this.dollyDelta.y > 0)
{
this.dollyIn();
}
else
{
this.dollyOut();
}
this.dollyStart.copy(this.dollyEnd);
}
else if (this.state === STATE.PAN)
{
if (this.noPan === true)
{
return;
}
this.panEnd.set(event.clientX, event.clientY);
this.panDelta.subVectors(this.panEnd, this.panStart);
// this.pan(this.panDelta);
this.updatePan(this.panDelta);
this.panStart.copy(this.panEnd);
}
// Greggman fix: https://github.com/greggman/three.js/commit/fde9f9917d6d8381f06bf22cdff766029d1761be
this.update();
}
onMouseUp( /* event */)
{
if (this.enabled === false)
{
return;
}
// Greggman fix: https://github.com/greggman/three.js/commit/fde9f9917d6d8381f06bf22cdff766029d1761be
this.domElement.removeEventListener('mousemove', this.mousemoveevent, false);
this.domElement.removeEventListener('mouseup', this.mouseupevent, false);
this.state = STATE.NONE;
}
onMouseWheel(event)
{
if (this.enabled === false || this.noZoom === true)
{
return;
}
var delta = 0;
if (event.wheelDelta)
{
// WebKit / Opera / Explorer 9
delta = event.wheelDelta;
}
else if (event.detail)
{
// Firefox
delta = - event.detail;
}
if (delta > 0)
{
this.dollyOut();
}
else
{
this.dollyIn();
}
this.update();
}
onKeyDown(event)
{
if (this.enabled === false)
{
return;
}
if (this.noKeys === true)
{
return;
}
if (this.noPan === true)
{
return;
}
switch (event.keyCode)
{
case this.keys.UP:
// this.pan(new Vector2(0, this.keyPanSpeed));
this.updatePan(new Vector2(0, this.keyPanSpeed));
break;
case this.keys.BOTTOM:
// this.pan(new Vector2(0, -this.keyPanSpeed));
this.updatePan(new Vector2(0, -this.keyPanSpeed));
break;
case this.keys.LEFT:
// this.pan(new Vector2(this.keyPanSpeed, 0));
this.updatePan(new Vector2(this.keyPanSpeed, 0));
break;
case this.keys.RIGHT:
// this.pan(new Vector2(-this.keyPanSpeed, 0));
this.updatePan(new Vector2(-this.keyPanSpeed, 0));
break;
}
}
touchstart(event)
{
if (this.enabled === false)
{
return;
}
switch (event.touches.length)
{
case 1: // one-fingered touch: rotate
if (this.noRotate === true)
{
return;
}
this.state = STATE.TOUCH_ROTATE;
this.rotateStart.set(event.touches[0].pageX, event.touches[0].pageY);
break;
case 2: // two-fingered touch: dolly
if (this.noZoom === true)
{
return;
}
this.state = STATE.TOUCH_DOLLY;
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);
this.dollyStart.set(0, distance);
break;
case 3: // three-fingered touch: pan
if (this.noPan === true)
{
return;
}
this.state = STATE.TOUCH_PAN;
this.panStart.set(event.touches[0].pageX, event.touches[0].pageY);
break;
default:
this.state = STATE.NONE;
}
}
touchmove(event)
{
if (this.enabled === false)
{
return;
}
event.preventDefault();
event.stopPropagation();
var element = this.domElement === $(document) ? this.domElement.body : this.domElement;
switch (event.touches.length)
{
case 1: // one-fingered touch: rotate
if (this.noRotate === true)
{
return;
}
if (this.state !== STATE.TOUCH_ROTATE)
{
return;
}
this.rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY);
this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart);
// rotating across whole screen goes 360 degrees around
this.rotateLeft(2 * Math.PI * this.rotateDelta.x / element.clientWidth * this.rotateSpeed);
// rotating up and down along whole screen attempts to go 360, but limited to 180
this.rotateUp(2 * Math.PI * this.rotateDelta.y / element.clientHeight * this.rotateSpeed);
this.rotateStart.copy(this.rotateEnd);
break;
case 2: // two-fingered touch: dolly
if (this.noZoom === true)
{
return;
}
if (this.state !== STATE.TOUCH_DOLLY)
{
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);
this.dollyEnd.set(0, distance);
this.dollyDelta.subVectors(this.dollyEnd, this.dollyStart);
if (this.dollyDelta.y > 0)
{
this.dollyOut();
}
else
{
this.dollyIn();
}
this.dollyStart.copy(this.dollyEnd);
break;
case 3: // three-fingered touch: pan
if (this.noPan === true)
{
return;
}
if (this.state !== STATE.TOUCH_PAN)
{
return;
}
this.panEnd.set(event.touches[0].pageX, event.touches[0].pageY);
this.panDelta.subVectors(this.panEnd, this.panStart);
this.pan(this.panDelta);
this.panStart.copy(this.panEnd);
break;
default:
this.state = STATE.NONE;
}
}
touchend( /* event */)
{
if (this.enabled === false)
{
return;
}
this.state = STATE.NONE;
}
}