itowns
Version:
A JS/WebGL framework for 3D geospatial data visualization
175 lines (170 loc) • 5.44 kB
JavaScript
import * as THREE from 'three';
import { MAIN_LOOP_EVENTS } from "../Core/MainLoop.js";
const MOVEMENTS = {
38: {
method: 'translateZ',
sign: -1
},
// FORWARD: up key
40: {
method: 'translateZ',
sign: 1
},
// BACKWARD: down key
37: {
method: 'translateX',
sign: -1
},
// STRAFE_LEFT: left key
39: {
method: 'translateX',
sign: 1
},
// STRAFE_RIGHT: right key
33: {
method: 'rotateZ',
sign: 1,
noSpeed: true
},
// UP: PageUp key
34: {
method: 'rotateZ',
sign: -1,
noSpeed: true
},
// DOWN: PageDown key
wheelup: {
method: 'translateZ',
sign: 1,
oneshot: true
},
// WHEEL up
wheeldown: {
method: 'translateZ',
sign: -1,
oneshot: true
} // WHEEL down
};
function onDocumentMouseDown(event) {
event.preventDefault();
this._isMouseDown = true;
const coords = this.view.eventToViewCoords(event);
this._onMouseDownMouseX = coords.x;
this._onMouseDownMouseY = coords.y;
}
function onTouchStart(event) {
event.preventDefault();
this._isMouseDown = true;
this._onMouseDownMouseX = event.touches[0].pageX;
this._onMouseDownMouseY = event.touches[0].pageY;
}
function onPointerMove(event) {
if (this._isMouseDown === true) {
const coords = this.view.eventToViewCoords(event);
// in rigor we have tan(theta) = tan(cameraFOV) * deltaH / H
// (where deltaH is the vertical amount we moved, and H the renderer height)
// we loosely approximate tan(x) by x
const pxToAngleRatio = THREE.MathUtils.degToRad(this._camera3D.fov) / this.view.mainLoop.gfxEngine.height;
this._camera3D.rotateY((coords.x - this._onMouseDownMouseX) * pxToAngleRatio);
this._camera3D.rotateX((coords.y - this._onMouseDownMouseY) * pxToAngleRatio);
this._onMouseDownMouseX = coords.x;
this._onMouseDownMouseY = coords.y;
this.view.notifyChange(this._camera3D, false);
}
}
function onDocumentMouseUp() {
this._isMouseDown = false;
}
function onKeyUp(e) {
const move = MOVEMENTS[e.keyCode];
if (move) {
this.moves.delete(move);
e.preventDefault();
}
}
function onKeyDown(e) {
const move = MOVEMENTS[e.keyCode];
if (move) {
this.moves.add(move);
this.view.notifyChange(this._camera3D, false);
e.preventDefault();
}
}
function onDocumentMouseWheel(event) {
const delta = -event.deltaY;
if (delta < 0) {
this.moves.add(MOVEMENTS.wheelup);
} else {
this.moves.add(MOVEMENTS.wheeldown);
}
this.view.notifyChange(this._camera3D, false);
}
/**
* First-Person controls (at least a possible declination of it).
*
* Bindings:
* - up + down keys: forward/backward
* - left + right keys: strafing movements
* - PageUp + PageDown: roll movement
* - mouse click+drag: pitch and yaw movements (as looking at a panorama, not as in FPS games for instance)
*/
class FlyControls extends THREE.EventDispatcher {
/**
* @param {View} view
* @param {object} options
* @param {boolean} options.focusOnClick - whether or not to focus the renderer domElement on click
* @param {boolean} options.focusOnMouseOver - whether or not to focus when the mouse is over the domElement
*/
constructor(view) {
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
super();
this.view = view;
this.options = options;
this._camera3D = view.camera3D;
this.moves = new Set();
this.moveSpeed = 10; // backward or forward move speed in m/s
this._onMouseDownMouseX = 0;
this._onMouseDownMouseY = 0;
this._isMouseDown = false;
view.domElement.addEventListener('mousedown', onDocumentMouseDown.bind(this), false);
view.domElement.addEventListener('touchstart', onTouchStart.bind(this), false);
const bindedPM = onPointerMove.bind(this);
view.domElement.addEventListener('mousemove', bindedPM, false);
view.domElement.addEventListener('touchmove', bindedPM, false);
view.domElement.addEventListener('mouseup', onDocumentMouseUp.bind(this), false);
view.domElement.addEventListener('touchend', onDocumentMouseUp.bind(this), false);
view.domElement.addEventListener('wheel', onDocumentMouseWheel.bind(this), false);
view.domElement.addEventListener('keyup', onKeyUp.bind(this), true);
view.domElement.addEventListener('keydown', onKeyDown.bind(this), true);
this.view.addFrameRequester(MAIN_LOOP_EVENTS.AFTER_CAMERA_UPDATE, this.update.bind(this));
// focus policy
if (options.focusOnMouseOver) {
view.domElement.addEventListener('mouseover', () => view.domElement.focus());
}
if (options.focusOnClick) {
view.domElement.addEventListener('click', () => view.domElement.focus());
}
}
isUserInteracting() {
return this.moves.size !== 0 || this._isMouseDown;
}
update(dt, updateLoopRestarted) {
// if we are in a keypressed state, then update position
// dt will not be relevant when we just started rendering, we consider a 1-frame move in this case
if (updateLoopRestarted) {
dt = 16;
}
for (const move of this.moves) {
this._camera3D[move.method](move.sign * (move.noSpeed ? 1 : this.moveSpeed) * dt / 1000);
}
if (this.moves.size > 0 || this._isMouseDown) {
this.view.notifyChange(this._camera3D);
for (const move of this.moves) {
if (move.oneshot) {
this.moves.delete(move);
}
}
}
}
}
export default FlyControls;