/**
|
* @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
|
*/
|
export default function (THREE) {
|
|
let {
|
EventDispatcher,
|
MOUSE,
|
Quaternion,
|
Vector2,
|
Vector3,
|
global: window,
|
} = THREE;
|
let document = window.document;
|
|
|
var TrackballControls = function (object, domElement) {
|
|
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.');
|
|
var _this = this;
|
var 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;
|
|
this.keys = [65 /*A*/, 83 /*S*/, 68 /*D*/];
|
|
this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.ZOOM, RIGHT: MOUSE.PAN };
|
|
// internals
|
|
this.target = new Vector3();
|
|
var EPS = 0.000001;
|
|
var lastPosition = new Vector3();
|
var lastZoom = 1;
|
|
var _state = STATE.NONE,
|
_keyState = STATE.NONE,
|
|
_eye = new Vector3(),
|
|
_movePrev = new Vector2(),
|
_moveCurr = new Vector2(),
|
|
_lastAxis = new Vector3(),
|
_lastAngle = 0,
|
|
_zoomStart = new Vector2(),
|
_zoomEnd = new Vector2(),
|
|
_touchZoomDistanceStart = 0,
|
_touchZoomDistanceEnd = 0,
|
|
_panStart = new Vector2(),
|
_panEnd = new 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 () {
|
|
var box = this.domElement.getBoundingClientRect();
|
// adjustments come from similar code in the jquery offset() function
|
// var d = this.domElement.ownerDocument.documentElement;
|
var d = document.documentElement;
|
this.screen.left = box.left + window.scrollX - d.clientLeft;
|
this.screen.top = box.top + window.scrollY - d.clientTop;
|
this.screen.width = box.width;
|
this.screen.height = box.height;
|
|
};
|
|
var getMouseOnScreen = (function () {
|
|
var vector = new Vector2();
|
|
return function getMouseOnScreen(pageX, pageY) {
|
|
vector.set(
|
(pageX - _this.screen.left) / _this.screen.width,
|
(pageY - _this.screen.top) / _this.screen.height
|
);
|
|
return vector;
|
|
};
|
|
}());
|
|
var getMouseOnCircle = (function () {
|
|
var vector = new Vector2();
|
|
return function getMouseOnCircle(pageX, pageY) {
|
|
vector.set(
|
((pageX - _this.screen.width * 0.5 - _this.screen.left) / (_this.screen.width * 0.5)),
|
((_this.screen.height + 2 * (_this.screen.top - pageY)) / _this.screen.width) // screen.width intentional
|
);
|
|
return vector;
|
|
};
|
|
}());
|
|
this.rotateCamera = (function () {
|
|
var axis = new Vector3(),
|
quaternion = new Quaternion(),
|
eyeDirection = new Vector3(),
|
objectUpDirection = new Vector3(),
|
objectSidewaysDirection = new Vector3(),
|
moveDirection = new Vector3(),
|
angle;
|
|
return function rotateCamera() {
|
|
moveDirection.set(_moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0);
|
angle = moveDirection.length();
|
|
if (angle) {
|
|
_eye.copy(_this.object.position).sub(_this.target);
|
|
eyeDirection.copy(_eye).normalize();
|
objectUpDirection.copy(_this.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 *= _this.rotateSpeed;
|
quaternion.setFromAxisAngle(axis, angle);
|
|
_eye.applyQuaternion(quaternion);
|
_this.object.up.applyQuaternion(quaternion);
|
|
_lastAxis.copy(axis);
|
_lastAngle = angle;
|
|
} else if (!_this.staticMoving && _lastAngle) {
|
|
_lastAngle *= Math.sqrt(1.0 - _this.dynamicDampingFactor);
|
_eye.copy(_this.object.position).sub(_this.target);
|
quaternion.setFromAxisAngle(_lastAxis, _lastAngle);
|
_eye.applyQuaternion(quaternion);
|
_this.object.up.applyQuaternion(quaternion);
|
|
}
|
|
_movePrev.copy(_moveCurr);
|
|
};
|
|
}());
|
|
|
this.zoomCamera = function () {
|
|
var factor;
|
|
if (_state === STATE.TOUCH_ZOOM_PAN) {
|
|
factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
|
_touchZoomDistanceStart = _touchZoomDistanceEnd;
|
|
if (_this.object.isPerspectiveCamera) {
|
|
_eye.multiplyScalar(factor);
|
|
} else if (_this.object.isOrthographicCamera) {
|
|
_this.object.zoom *= factor;
|
_this.object.updateProjectionMatrix();
|
|
} else {
|
|
console.warn('THREE.TrackballControls: Unsupported camera type');
|
|
}
|
|
} else {
|
|
factor = 1.0 + (_zoomEnd.y - _zoomStart.y) * _this.zoomSpeed;
|
|
if (factor !== 1.0 && factor > 0.0) {
|
|
if (_this.object.isPerspectiveCamera) {
|
|
_eye.multiplyScalar(factor);
|
|
} else if (_this.object.isOrthographicCamera) {
|
|
_this.object.zoom /= factor;
|
_this.object.updateProjectionMatrix();
|
|
} else {
|
|
console.warn('THREE.TrackballControls: Unsupported camera type');
|
|
}
|
|
}
|
|
if (_this.staticMoving) {
|
|
_zoomStart.copy(_zoomEnd);
|
|
} else {
|
|
_zoomStart.y += (_zoomEnd.y - _zoomStart.y) * this.dynamicDampingFactor;
|
|
}
|
|
}
|
|
};
|
|
this.panCamera = (function () {
|
|
var mouseChange = new Vector2(),
|
objectUp = new Vector3(),
|
pan = new Vector3();
|
|
return function panCamera() {
|
|
mouseChange.copy(_panEnd).sub(_panStart);
|
|
if (mouseChange.lengthSq()) {
|
|
if (_this.object.isOrthographicCamera) {
|
|
var scale_x = (_this.object.right - _this.object.left) / _this.object.zoom / _this.domElement.clientWidth;
|
var scale_y = (_this.object.top - _this.object.bottom) / _this.object.zoom / _this.domElement.clientWidth;
|
|
mouseChange.x *= scale_x;
|
mouseChange.y *= scale_y;
|
|
}
|
|
mouseChange.multiplyScalar(_eye.length() * _this.panSpeed);
|
|
pan.copy(_eye).cross(_this.object.up).setLength(mouseChange.x);
|
pan.add(objectUp.copy(_this.object.up).setLength(mouseChange.y));
|
|
_this.object.position.add(pan);
|
_this.target.add(pan);
|
|
if (_this.staticMoving) {
|
|
_panStart.copy(_panEnd);
|
|
} else {
|
|
_panStart.add(mouseChange.subVectors(_panEnd, _panStart).multiplyScalar(_this.dynamicDampingFactor));
|
|
}
|
|
}
|
|
};
|
|
}());
|
|
this.checkDistances = function () {
|
|
if (!_this.noZoom || !_this.noPan) {
|
|
if (_eye.lengthSq() > _this.maxDistance * _this.maxDistance) {
|
|
_this.object.position.addVectors(_this.target, _eye.setLength(_this.maxDistance));
|
_zoomStart.copy(_zoomEnd);
|
|
}
|
|
if (_eye.lengthSq() < _this.minDistance * _this.minDistance) {
|
|
_this.object.position.addVectors(_this.target, _eye.setLength(_this.minDistance));
|
_zoomStart.copy(_zoomEnd);
|
|
}
|
|
}
|
|
};
|
|
this.update = function () {
|
|
_eye.subVectors(_this.object.position, _this.target);
|
|
if (!_this.noRotate) {
|
|
_this.rotateCamera();
|
|
}
|
|
if (!_this.noZoom) {
|
|
_this.zoomCamera();
|
|
}
|
|
if (!_this.noPan) {
|
|
_this.panCamera();
|
|
}
|
|
_this.object.position.addVectors(_this.target, _eye);
|
|
if (_this.object.isPerspectiveCamera) {
|
|
_this.checkDistances();
|
|
_this.object.lookAt(_this.target);
|
|
if (lastPosition.distanceToSquared(_this.object.position) > EPS) {
|
|
_this.dispatchEvent(changeEvent);
|
|
lastPosition.copy(_this.object.position);
|
|
}
|
|
} else if (_this.object.isOrthographicCamera) {
|
|
_this.object.lookAt(_this.target);
|
|
if (lastPosition.distanceToSquared(_this.object.position) > EPS || lastZoom !== _this.object.zoom) {
|
|
_this.dispatchEvent(changeEvent);
|
|
lastPosition.copy(_this.object.position);
|
lastZoom = _this.object.zoom;
|
|
}
|
|
} else {
|
|
console.warn('THREE.TrackballControls: Unsupported camera type');
|
|
}
|
|
};
|
|
this.reset = function () {
|
|
_state = STATE.NONE;
|
_keyState = STATE.NONE;
|
|
_this.target.copy(_this.target0);
|
_this.object.position.copy(_this.position0);
|
_this.object.up.copy(_this.up0);
|
_this.object.zoom = _this.zoom0;
|
|
_this.object.updateProjectionMatrix();
|
|
_eye.subVectors(_this.object.position, _this.target);
|
|
_this.object.lookAt(_this.target);
|
|
_this.dispatchEvent(changeEvent);
|
|
lastPosition.copy(_this.object.position);
|
lastZoom = _this.object.zoom;
|
|
};
|
|
// listeners
|
|
function keydown(event) {
|
|
if (_this.enabled === false) return;
|
|
window.removeEventListener('keydown', keydown);
|
|
if (_keyState !== STATE.NONE) {
|
|
return;
|
|
} else if (event.keyCode === _this.keys[STATE.ROTATE] && !_this.noRotate) {
|
|
_keyState = STATE.ROTATE;
|
|
} else if (event.keyCode === _this.keys[STATE.ZOOM] && !_this.noZoom) {
|
|
_keyState = STATE.ZOOM;
|
|
} else if (event.keyCode === _this.keys[STATE.PAN] && !_this.noPan) {
|
|
_keyState = STATE.PAN;
|
|
}
|
|
}
|
|
function keyup() {
|
|
if (_this.enabled === false) return;
|
|
_keyState = STATE.NONE;
|
|
window.addEventListener('keydown', keydown, false);
|
|
}
|
|
function mousedown(event) {
|
|
if (_this.enabled === false) return;
|
|
event.preventDefault();
|
event.stopPropagation();
|
|
if (_state === STATE.NONE) {
|
|
switch (event.button) {
|
|
case _this.mouseButtons.LEFT:
|
_state = STATE.ROTATE;
|
break;
|
|
case _this.mouseButtons.MIDDLE:
|
_state = STATE.ZOOM;
|
break;
|
|
case _this.mouseButtons.RIGHT:
|
_state = STATE.PAN;
|
break;
|
|
default:
|
_state = STATE.NONE;
|
|
}
|
|
}
|
|
var state = (_keyState !== STATE.NONE) ? _keyState : _state;
|
|
if (state === STATE.ROTATE && !_this.noRotate) {
|
|
_moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));
|
_movePrev.copy(_moveCurr);
|
|
} else if (state === STATE.ZOOM && !_this.noZoom) {
|
|
_zoomStart.copy(getMouseOnScreen(event.pageX, event.pageY));
|
_zoomEnd.copy(_zoomStart);
|
|
} else if (state === STATE.PAN && !_this.noPan) {
|
|
_panStart.copy(getMouseOnScreen(event.pageX, event.pageY));
|
_panEnd.copy(_panStart);
|
|
}
|
|
document.addEventListener('mousemove', mousemove, false);
|
document.addEventListener('mouseup', mouseup, false);
|
|
_this.dispatchEvent(startEvent);
|
|
}
|
|
function mousemove(event) {
|
|
if (_this.enabled === false) return;
|
|
event.preventDefault();
|
event.stopPropagation();
|
|
var state = (_keyState !== STATE.NONE) ? _keyState : _state;
|
|
if (state === STATE.ROTATE && !_this.noRotate) {
|
|
_movePrev.copy(_moveCurr);
|
_moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY));
|
|
} else if (state === STATE.ZOOM && !_this.noZoom) {
|
|
_zoomEnd.copy(getMouseOnScreen(event.pageX, event.pageY));
|
|
} else if (state === STATE.PAN && !_this.noPan) {
|
|
_panEnd.copy(getMouseOnScreen(event.pageX, event.pageY));
|
|
}
|
|
}
|
|
function mouseup(event) {
|
|
if (_this.enabled === false) return;
|
|
event.preventDefault();
|
event.stopPropagation();
|
|
_state = STATE.NONE;
|
|
document.removeEventListener('mousemove', mousemove);
|
document.removeEventListener('mouseup', mouseup);
|
_this.dispatchEvent(endEvent);
|
|
}
|
|
function mousewheel(event) {
|
|
if (_this.enabled === false) return;
|
|
if (_this.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;
|
|
}
|
|
_this.dispatchEvent(startEvent);
|
_this.dispatchEvent(endEvent);
|
|
}
|
|
function touchstart(event) {
|
|
if (_this.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;
|
|
}
|
|
_this.dispatchEvent(startEvent);
|
|
}
|
|
function touchmove(event) {
|
|
if (_this.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 (_this.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;
|
|
}
|
|
_this.dispatchEvent(endEvent);
|
|
}
|
|
function contextmenu(event) {
|
|
if (_this.enabled === false) return;
|
|
event.preventDefault();
|
|
}
|
|
this.dispose = function () {
|
|
this.domElement.removeEventListener('contextmenu', contextmenu, false);
|
this.domElement.removeEventListener('mousedown', mousedown, false);
|
this.domElement.removeEventListener('wheel', mousewheel, false);
|
|
this.domElement.removeEventListener('touchstart', touchstart, false);
|
this.domElement.removeEventListener('touchend', touchend, false);
|
this.domElement.removeEventListener('touchmove', touchmove, false);
|
|
document.removeEventListener('mousemove', mousemove, false);
|
document.removeEventListener('mouseup', mouseup, false);
|
|
window.removeEventListener('keydown', keydown, false);
|
window.removeEventListener('keyup', keyup, false);
|
|
};
|
|
this.domElement.addEventListener('contextmenu', contextmenu, false);
|
this.domElement.addEventListener('mousedown', mousedown, 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);
|
|
window.addEventListener('keydown', keydown, false);
|
window.addEventListener('keyup', keyup, false);
|
|
this.handleResize();
|
|
// force an update at start
|
this.update();
|
|
};
|
|
TrackballControls.prototype = Object.create(EventDispatcher.prototype);
|
TrackballControls.prototype.constructor = TrackballControls;
|
|
return { TrackballControls };
|
}
|