useloadpcd
Version:
A .pcd file loader for React with React hooks and Three.js.
629 lines (489 loc) • 15.8 kB
text/typescript
import * as THREE from 'three';
class TrackballControls extends THREE.EventDispatcher {
object: any;
domElement: any;
enabled: boolean;
screen: {
left: number;
top: number;
width: number;
height: number;
};
rotateSpeed: number;
zoomSpeed: number;
panSpeed: number;
noRotate: boolean;
noZoom: boolean;
noPan: boolean;
staticMoving: boolean;
dynamicDampingFactor: number;
minDistance: number;
maxDistance: number;
keys: number[];
// internals
target: THREE.Vector3;
target0: THREE.Vector3;
position0: THREE.Vector3;
up0: THREE.Vector3;
[key: string]: any;
handleResize: () => void;
handleEvent: (event: Event) => void;
constructor(object: any, domElement: any) {
super();
const _this = this;
const STATE = {
NONE: -1,
ROTATE: 0,
ZOOM: 1,
PAN: 2,
TOUCH_ROTATE: 3,
TOUCH_ZOOM_PAN: 4
};
this.object = object;
this.domElement = domElement !== undefined ? domElement : document;
// 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 */];
// internals
this.target = new THREE.Vector3();
const EPS = 0.000001;
const lastPosition = new THREE.Vector3();
let _state = STATE.NONE;
let _prevState = STATE.NONE;
const _eye = new THREE.Vector3();
const _movePrev = new THREE.Vector2();
const _moveCurr = new THREE.Vector2();
const _lastAxis = new THREE.Vector3();
let _lastAngle = 0;
const _zoomStart = new THREE.Vector2();
const _zoomEnd = new THREE.Vector2();
let _touchZoomDistanceStart = 0;
let _touchZoomDistanceEnd = 0;
const _panStart = new THREE.Vector2();
const _panEnd = new THREE.Vector2();
// for reset
this.target0 = this.target.clone();
this.position0 = this.object.position.clone();
this.up0 = this.object.up.clone();
// events
const changeEvent = { type: 'change' };
const startEvent = { type: 'start' };
const endEvent = { type: 'end' };
// methods
this.handleResize = () => {
if (this.domElement === document) {
this.screen.left = 0;
this.screen.top = 0;
this.screen.width = window.innerWidth;
this.screen.height = window.innerHeight;
} else {
const box = this.domElement.getBoundingClientRect();
// adjustments come from similar code in the jquery offset() function
const d = this.domElement.ownerDocument.documentElement;
this.screen.left = box.left + window.pageXOffset - d.clientLeft;
this.screen.top = box.top + window.pageYOffset - d.clientTop;
this.screen.width = box.width;
this.screen.height = box.height;
}
};
this.handleEvent = event => {
if (typeof this[event.type] === 'function') {
this[event.type](event);
}
};
const getMouseOnScreen = (function wrapper() {
const vector = new THREE.Vector2();
return (pageX: number, pageY: number) => {
vector.set(
(pageX - _this.screen.left) / _this.screen.width,
(pageY - _this.screen.top) / _this.screen.height
);
return vector;
};
})();
const getMouseOnCircle = (function wrapper() {
const vector = new THREE.Vector2();
return (pageX: number, pageY: number) => {
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 wrapper() {
const axis = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
const eyeDirection = new THREE.Vector3();
const objectUpDirection = new THREE.Vector3();
const objectSidewaysDirection = new THREE.Vector3();
const moveDirection = new THREE.Vector3();
let 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 = () => {
let factor;
if (_state === STATE.TOUCH_ZOOM_PAN) {
factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
_touchZoomDistanceStart = _touchZoomDistanceEnd;
_eye.multiplyScalar(factor);
} else {
factor = 1.0 + (_zoomEnd.y - _zoomStart.y) * _this.zoomSpeed;
if (factor !== 1.0 && factor > 0.0) {
_eye.multiplyScalar(factor);
if (_this.staticMoving) {
_zoomStart.copy(_zoomEnd);
} else {
_zoomStart.y +=
(_zoomEnd.y - _zoomStart.y) *
this.dynamicDampingFactor;
}
}
}
};
this.panCamera = (function wrapper() {
const mouseChange = new THREE.Vector2();
const objectUp = new THREE.Vector3();
const pan = new THREE.Vector3();
return function panCamera() {
mouseChange.copy(_panEnd).sub(_panStart);
if (mouseChange.lengthSq()) {
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 = () => {
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 = () => {
_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);
_this.checkDistances();
_this.object.lookAt(_this.target);
if (lastPosition.distanceToSquared(_this.object.position) > EPS) {
_this.dispatchEvent(changeEvent);
lastPosition.copy(_this.object.position);
}
};
this.reset = () => {
_state = STATE.NONE;
_prevState = STATE.NONE;
_this.target.copy(_this.target0);
_this.object.position.copy(_this.position0);
_this.object.up.copy(_this.up0);
_eye.subVectors(_this.object.position, _this.target);
_this.object.lookAt(_this.target);
_this.dispatchEvent(changeEvent);
lastPosition.copy(_this.object.position);
};
// listeners
function keydown(event: KeyboardEvent) {
if (_this.enabled === false) return;
window.removeEventListener('keydown', keydown);
_prevState = _state;
if (_state !== STATE.NONE) {
return;
}
if (event.keyCode === _this.keys[STATE.ROTATE] && !_this.noRotate) {
_state = STATE.ROTATE;
} else if (
event.keyCode === _this.keys[STATE.ZOOM] &&
!_this.noZoom
) {
_state = STATE.ZOOM;
} else if (
event.keyCode === _this.keys[STATE.PAN] &&
!_this.noPan
) {
_state = STATE.PAN;
}
}
function keyup() {
if (_this.enabled === false) return;
_state = _prevState;
window.addEventListener('keydown', keydown, false);
}
function mousemove(event: MouseEvent) {
if (_this.enabled === false) return;
event.preventDefault();
event.stopPropagation();
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: MouseEvent) {
if (_this.enabled === false) return;
event.preventDefault();
event.stopPropagation();
_state = STATE.NONE;
document.removeEventListener('mousemove', mousemove);
document.removeEventListener('mouseup', mouseup);
_this.dispatchEvent(endEvent);
}
function mousedown(event: MouseEvent) {
if (_this.enabled === false) return;
event.preventDefault();
event.stopPropagation();
if (_state === STATE.NONE) {
_state = event.button;
}
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 mousewheel(event: any) {
if (_this.enabled === false) return;
event.preventDefault();
event.stopPropagation();
let delta = 0;
if (event.wheelDelta) {
// WebKit / Opera / Explorer 9
delta = event.wheelDelta / 40;
} else if (event.detail) {
// Firefox
delta = -event.detail / 3;
}
_zoomStart.y += delta * 0.01;
_this.dispatchEvent(startEvent);
_this.dispatchEvent(endEvent);
}
function touchstart(event: TouchEvent) {
if (_this.enabled === false) return;
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;
case 2:
_state = STATE.TOUCH_ZOOM_PAN;
const dx = event.touches[0].pageX - event.touches[1].pageX;
const dy = event.touches[0].pageY - event.touches[1].pageY;
_touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt(
dx * dx + dy * dy
);
const x =
(event.touches[0].pageX + event.touches[1].pageX) / 2;
const y =
(event.touches[0].pageY + event.touches[1].pageY) / 2;
_panStart.copy(getMouseOnScreen(x, y));
_panEnd.copy(_panStart);
break;
default:
_state = STATE.NONE;
}
_this.dispatchEvent(startEvent);
}
function touchmove(event: TouchEvent) {
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;
case 2:
const dx = event.touches[0].pageX - event.touches[1].pageX;
const dy = event.touches[0].pageY - event.touches[1].pageY;
_touchZoomDistanceEnd = Math.sqrt(dx * dx + dy * dy);
const x =
(event.touches[0].pageX + event.touches[1].pageX) / 2;
const y =
(event.touches[0].pageY + event.touches[1].pageY) / 2;
_panEnd.copy(getMouseOnScreen(x, y));
break;
default:
_state = STATE.NONE;
}
}
function touchend(event: TouchEvent) {
if (_this.enabled === false) return;
switch (event.touches.length) {
default:
// no touches
break;
case 1:
_moveCurr.copy(
getMouseOnCircle(
event.touches[0].pageX,
event.touches[0].pageY
)
);
_movePrev.copy(_moveCurr);
break;
case 2:
_touchZoomDistanceStart = _touchZoomDistanceEnd = 0;
const x =
(event.touches[0].pageX + event.touches[1].pageX) / 2;
const y =
(event.touches[0].pageY + event.touches[1].pageY) / 2;
_panEnd.copy(getMouseOnScreen(x, y));
_panStart.copy(_panEnd);
break;
}
_state = STATE.NONE;
_this.dispatchEvent(endEvent);
}
function contextmenu(event: Event) {
event.preventDefault();
}
this.dispose = () => {
this.domElement.removeEventListener(
'contextmenu',
contextmenu,
false
);
this.domElement.removeEventListener('mousedown', mousedown, false);
this.domElement.removeEventListener(
'mousewheel',
mousewheel,
false
);
this.domElement.removeEventListener(
'DOMMouseScroll',
mousewheel,
false
); // firefox
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('mousewheel', mousewheel, false);
this.domElement.addEventListener('DOMMouseScroll', mousewheel, false); // firefox
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();
}
}
export default TrackballControls;