three-trackballcontrols
Version:
A module for using THREE.TrackballControls with nodejs
668 lines (443 loc) • 16.5 kB
JavaScript
/**
* @author Eberhard Graether / http://egraether.com/
* @author Mark Lundin / http://mark-lundin.com
* @author Simone Manini / http://daron1337.github.io
* @author Luca Antiga / http://lantiga.github.io
** three-trackballcontrols module
** @author Jon Lim / https://jonlim.ca
*/
const THREE = window.THREE || require('three');
const TrackballControls = function (object, domElement) {
// Pre-flight checks, with warnings / errors.
if (domElement === undefined) console.warn('THREE.TrackballControls: The second parameter "domElement" is now mandatory.');
if (domElement === document) console.error('THREE.TrackballControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.');
const scope = this;
const STATE = {
NONE: -1,
ROTATE: 0,
ZOOM: 1,
PAN: 2,
TOUCH_ROTATE: 3,
TOUCH_ZOOM_PAN: 4,
};
this.object = object;
this.domElement = domElement;
// API
this.enabled = true;
this.screen = { left: 0, top: 0, width: 0, height: 0 };
this.rotateSpeed = 1.0;
this.zoomSpeed = 1.2;
this.panSpeed = 0.3;
this.noRotate = false;
this.noZoom = false;
this.noPan = false;
this.staticMoving = false;
this.dynamicDampingFactor = 0.2;
this.minDistance = 0;
this.maxDistance = Infinity;
/**
* `KeyboardEvent.keyCode` values which should trigger the different
* interaction states. Each element can be a single code or an array
* of codes. All elements are required.
*/
this.keys = [
65, // A
83, // S
68, // D
];
/**
* `Event` for pointer interactions which should trigger different
* interaction states.
*/
this.mouseButtons = {
LEFT: THREE.MOUSE.ROTATE,
MIDDLE: THREE.MOUSE.ZOOM,
RIGHT: THREE.MOUSE.PAN,
};
// internals
this.target = new THREE.Vector3();
const EPS = 0.000001;
const lastPosition = new THREE.Vector3();
let _state = STATE.NONE,
_keyState = STATE.NONE,
_eye = new THREE.Vector3(),
_movePrev = new THREE.Vector2(),
_moveCurr = new THREE.Vector2(),
_lastAxis = new THREE.Vector3(),
_lastAngle = 0,
_zoomStart = new THREE.Vector2(),
_zoomEnd = new THREE.Vector2(),
_touchZoomDistanceStart = 0,
_touchZoomDistanceEnd = 0,
_panStart = new THREE.Vector2(),
_panEnd = new THREE.Vector2();
// for reset
this.target0 = this.target.clone();
this.position0 = this.object.position.clone();
this.up0 = this.object.up.clone();
this.zoom0 = this.object.zoom;
// events
var changeEvent = { type: 'change' };
var startEvent = { type: 'start' };
var endEvent = { type: 'end' };
// methods
this.handleResize = function () {
const box = scope.domElement.getBoundingClientRect();
// Adjustments come from similar code in the jquery offset() function.
const d = scope.domElement.ownerDocument.documentElement;
scope.screen.left = box.left + window.pageXOffset - d.clientLeft;
scope.screen.top = box.top + window.pageYOffset - d.clientTop;
scope.screen.width = box.width;
scope.screen.height = box.height;
};
const getMouseOnScreen = (function () {
const vector = new THREE.Vector2();
return function getMouseOnScreen(pageX, pageY) {
vector.set(
(pageX - scope.screen.left) / scope.screen.width,
(pageY - scope.screen.top) / scope.screen.height,
);
return vector;
};
}());
const getMouseOnCircle = (function () {
const vector = new THREE.Vector2();
return function getMouseOnCircle(pageX, pageY) {
vector.set(
((pageX - scope.screen.width * 0.5 - scope.screen.left) / (scope.screen.width * 0.5)),
((scope.screen.height + 2 * (scope.screen.top - pageY)) / scope.screen.width), // screen.width intentional
);
return vector;
};
}());
this.rotateCamera = (function () {
let axis = new THREE.Vector3(),
quaternion = new THREE.Quaternion(),
eyeDirection = new THREE.Vector3(),
objectUpDirection = new THREE.Vector3(),
objectSidewaysDirection = new THREE.Vector3(),
moveDirection = new THREE.Vector3(),
angle;
return function rotateCamera() {
moveDirection.set(_moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0);
angle = moveDirection.length();
if (angle) {
_eye.copy(scope.object.position).sub(scope.target);
eyeDirection.copy(_eye).normalize();
objectUpDirection.copy(scope.object.up).normalize();
objectSidewaysDirection.crossVectors(objectUpDirection, eyeDirection).normalize();
objectUpDirection.setLength(_moveCurr.y - _movePrev.y);
objectSidewaysDirection.setLength(_moveCurr.x - _movePrev.x);
moveDirection.copy(objectUpDirection.add(objectSidewaysDirection));
axis.crossVectors(moveDirection, _eye).normalize();
angle *= scope.rotateSpeed;
quaternion.setFromAxisAngle(axis, angle);
_eye.applyQuaternion(quaternion);
scope.object.up.applyQuaternion(quaternion);
_lastAxis.copy(axis);
_lastAngle = angle;
} else if (!scope.staticMoving && _lastAngle) {
_lastAngle *= Math.sqrt(1.0 - scope.dynamicDampingFactor);
_eye.copy(scope.object.position).sub(scope.target);
quaternion.setFromAxisAngle(_lastAxis, _lastAngle);
_eye.applyQuaternion(quaternion);
scope.object.up.applyQuaternion(quaternion);
}
_movePrev.copy(_moveCurr);
};
}());
this.zoomCamera = function () {
let factor;
if (_state === STATE.TOUCH_ZOOM_PAN) {
factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
_touchZoomDistanceStart = _touchZoomDistanceEnd;
_eye.multiplyScalar(factor);
} else {
factor = 1.0 + (_zoomEnd.y - _zoomStart.y) * scope.zoomSpeed;
if (factor !== 1.0 && factor > 0.0) {
_eye.multiplyScalar(factor);
}
if (scope.staticMoving) {
_zoomStart.copy(_zoomEnd);
} else {
_zoomStart.y += (_zoomEnd.y - _zoomStart.y) * this.dynamicDampingFactor;
}
}
};
this.panCamera = (function () {
let mouseChange = new THREE.Vector2(),
objectUp = new THREE.Vector3(),
pan = new THREE.Vector3();
return function panCamera() {
mouseChange.copy(_panEnd).sub(_panStart);
if (mouseChange.lengthSq()) {
mouseChange.multiplyScalar(_eye.length() * scope.panSpeed);
pan.copy(_eye).cross(scope.object.up).setLength(mouseChange.x);
pan.add(objectUp.copy(scope.object.up).setLength(mouseChange.y));
scope.object.position.add(pan);
scope.target.add(pan);
if (scope.staticMoving) {
_panStart.copy(_panEnd);
} else {
_panStart.add(mouseChange.subVectors(_panEnd, _panStart).multiplyScalar(scope.dynamicDampingFactor));
}
}
};
}());
this.checkDistances = function () {
if (!scope.noZoom || !scope.noPan) {
if (_eye.lengthSq() > scope.maxDistance * scope.maxDistance) {
scope.object.position.addVectors(scope.target, _eye.setLength(scope.maxDistance));
_zoomStart.copy(_zoomEnd);
}
if (_eye.lengthSq() < scope.minDistance * scope.minDistance) {
scope.object.position.addVectors(scope.target, _eye.setLength(scope.minDistance));
_zoomStart.copy(_zoomEnd);
}
}
};
this.update = function () {
_eye.subVectors(scope.object.position, scope.target);
if (!scope.noRotate) {
scope.rotateCamera();
}
if (!scope.noZoom) {
scope.zoomCamera();
}
if (!scope.noPan) {
scope.panCamera();
}
scope.object.position.addVectors(scope.target, _eye);
scope.checkDistances();
scope.object.lookAt(scope.target);
if (lastPosition.distanceToSquared(scope.object.position) > EPS) {
scope.dispatchEvent(changeEvent);
lastPosition.copy(scope.object.position);
}
};
this.reset = function () {
_state = STATE.NONE;
_keyState = STATE.NONE;
scope.target.copy(scope.target0);
scope.object.position.copy(scope.position0);
scope.object.up.copy(scope.up0);
_eye.subVectors(scope.object.position, scope.target);
scope.object.lookAt(scope.target);
scope.dispatchEvent(changeEvent);
lastPosition.copy(scope.object.position);
};
// Listeners
function onPointerDown( event ) {
if ( scope.enabled === false ) return;
switch ( event.pointerType ) {
case 'mouse':
case 'pen':
onMouseDown( event );
break;
// TODO touch
}
}
function onPointerMove(event) {
if (scope.enabled === false) return;
switch (event.pointerType) {
case 'mouse':
case 'pen':
onMouseMove(event);
break;
// TODO touch
}
}
function onPointerUp(event) {
if (scope.enabled === false) return;
switch (event.pointerType) {
case 'mouse':
case 'pen':
onMouseUp(event);
break;
// TODO touch
}
}
function keydown(event) {
if (scope.enabled === false) return;
window.removeEventListener('keydown', keydown);
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 onMouseDown(event) {
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;
}
}
const 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('pointermove', onPointerMove, false);
scope.domElement.ownerDocument.addEventListener('pointerup', onPointerUp, false);
scope.dispatchEvent(startEvent);
}
function onMouseMove(event) {
if (scope.enabled === false) return;
event.preventDefault();
event.stopPropagation();
const 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 onMouseUp(event) {
if (scope.enabled === false) return;
event.preventDefault();
event.stopPropagation();
_state = STATE.NONE;
scope.domElement.ownerDocument.removeEventListener('pointermove', onPointerMove);
scope.domElement.ownerDocument.removeEventListener('pointerup', onPointerUp);
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('pointerdown', onPointerDown, 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( 'pointermove', onPointerMove, false );
scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp, false );
window.removeEventListener('keydown', keydown, false);
window.removeEventListener('keyup', keyup, false);
};
this.domElement.addEventListener('contextmenu', contextmenu, false);
this.domElement.addEventListener('pointerdown', onPointerDown, 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);
this.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove, false );
this.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp, false );
window.addEventListener('keydown', keydown, false);
window.addEventListener('keyup', keyup, false);
this.handleResize();
// force an update at start
this.update();
};
TrackballControls.prototype = Object.create(THREE.EventDispatcher.prototype);
TrackballControls.prototype.constructor = TrackballControls;
export default TrackballControls;