viewer/cameracontrol.js
import * as mat4 from "./glmatrix/mat4.js";
import * as vec4 from "./glmatrix/vec4.js";
import * as vec3 from "./glmatrix/vec3.js";
import * as vec2 from "./glmatrix/vec2.js";
export const DRAG_ORBIT = 0xfe01;
export const DRAG_PAN = 0xfe02;
export const DRAG_SECTION = 0xfe03;
export const CLICK_SELECT = 0xfe11;
export const CLICK_MEASURE_PATH = 0xfe12;
export const CLICK_MEASURE_DIST = 0xfe13;
/**
Controls the camera with user input.
*/
export class CameraControl {
constructor(viewer) {
this.viewer = viewer;
this.mousePanSensitivity = 1; // 0.5;
this.mouseOrbitSensitivity = 0.5;
this.canvasPickTolerance = 4;
this.canvas = viewer.canvas;
this.camera = viewer.camera;
this.mousePos = vec2.create();
this.mouseDownPos = vec2.create();
this.over = false; // True when mouse over canvas
this.lastX = 0; // Last canvas pos while dragging
this.lastY = 0;
this.keysDown = new Map();
this.keyMapping = {
"ArrowRight": "x_neg",
"ArrowLeft": "x_pos",
"ArrowUp": "z_neg",
"ArrowDown": "z_pos",
"PageUp": "y_pos",
"PageDown": "y_neg",
"w": "z_neg",
"a": "x_pos",
"s": "z_pos",
"d": "x_neg",
"q": "y_pos",
"z": "y_neg"
};
this.axoKeyMapping = {
"1": "z_pos",
"2": "z_neg",
"3": "x_pos",
"4": "x_neg",
"5": "y_pos",
"6": "y_neg",
}
this.mouseDown = false;
this.dragMode = DRAG_ORBIT;
this.clickMode = CLICK_SELECT;
this._tmp_topleftfront_0 = vec3.create();
this._tmp_topleftfront_1 = vec3.create();
this.canvas.oncontextmenu = (e) => {
e.preventDefault();
};
this.canvas.addEventListener("mousedown", this.canvasMouseDownHandler = (e) => {
this.canvasMouseDown(e);
});
this.canvas.addEventListener("mouseup", this.canvasMouseUpHandler = (e) => {
this.canvasMouseUp(e);
});
this.documentMouseUpHandler = (e) => {
this.documentMouseUp(e);
};
document.addEventListener("mouseup", this.documentMouseUpHandler);
this.canvasKeyUpHandler = (e) => {
this.canvasKeyProcess(e, false);
};
this.canvas.addEventListener("keyup", this.canvasKeyUpHandler);
this.canvasKeyDownHandler = (e) => {
this.canvasKeyProcess(e, true);
};
this.canvas.addEventListener("keydown", this.canvasKeyDownHandler);
this.canvas.addEventListener("mouseenter", this.canvasMouseEnterHandler = (e) => {
this.over = true;
e.preventDefault();
});
this.canvas.addEventListener("mouseleave", this.canvasMouseLeaveHandler = (e) => {
this.over = false;
e.preventDefault();
});
this.canvas.addEventListener("mousemove", this.canvasMouseMoveHandler = (e) => {
this.canvasMouseMove(e);
});
this.canvas.addEventListener("wheel", this.canvasMouseWheelHandler = (e) => {
this.canvasWheel(e);
});
this.canvas.addEventListener("touchstart", this.touchStartHandler = (e) => {
this.canvasMouseDown(e);
});
this.canvas.addEventListener("touchend", this.touchEndHandler = (e) => {
this.canvasMouseUp(e);
});
this.canvas.addEventListener("touchmove", this.touchMoveHandler = (e) => {
this.canvasMouseMove(e);
});
window.setInterval(this.keyTick.bind(this), 10);
}
/**
* @private
*/
getCanvasPosFromEvent(event, canvasPos) {
if (!event) {
event = window.event;
canvasPos[0] = event.x;
canvasPos[1] = event.y;
} else {
let pageX = null, pageY = null;
if (window.TouchEvent && event instanceof TouchEvent) {
if (event.touches.length == 0) {
return;
}
if (event.touches.length == 2) {
let coords = Array.from(event.touches).map(t => vec2.fromValues(t.pageX, t.pageY));
this.pinchDistance = vec2.length(vec2.sub(vec2.create(), ...coords));
let avg = vec2.add(vec2.create(), ...coords);
vec2.scale(avg, avg, 0.5);
pageX = avg[0];
pageY = avg[1];
} else {
pageX = event.touches[0].pageX;
pageY = event.touches[0].pageY;
}
} else {
pageX = event.pageX;
pageY = event.pageY;
}
const rect = event.target.getBoundingClientRect();
const totalOffsetLeft = rect.left;
const totalOffsetTop = rect.top;
canvasPos[0] = pageX - totalOffsetLeft;
canvasPos[1] = pageY - totalOffsetTop;
}
return canvasPos;
}
/**
* @private
*/
getZoomRate() {
var modelBounds = this.viewer.modelBounds;
if (modelBounds) {
var xsize = modelBounds[3] - modelBounds[0];
var ysize = modelBounds[4] - modelBounds[1];
var zsize = modelBounds[5] - modelBounds[2];
var max = (xsize > ysize ? xsize : ysize);
max = (zsize > max ? zsize : max);
return max / 20;
} else {
return 1;
}
}
/**
* @private
*/
canvasMouseDown(e) {
this.lastPan = +new Date();
this.getCanvasPosFromEvent(e, this.mousePos);
this.lastX = this.mousePos[0];
this.lastY = this.mousePos[1];
this.mouseDown = true;
this.mouseDownTime = e.timeStamp;
this.mouseDownPos.set(this.mousePos);
let handleSection = () => {
this.mouseDownTime = 0;
if (this.viewer.enableSectionPlane({canvasPos:[this.lastX, this.lastY]})) {
this.dragMode = DRAG_SECTION;
} else {
this.dragMode = DRAG_ORBIT;
}
this.viewer.removeSectionPlaneWidget();
}
let handleOrbit = () => {
this.dragMode = e.shiftKey ? DRAG_PAN : DRAG_ORBIT;
let picked = this.viewer.pick({canvasPos:[this.lastX, this.lastY], select:false});
if (picked && picked.coordinates && picked.object) {
this.viewer.camera.center = picked.coordinates;
} else {
// Check if we can 'see' the previous center. If not, pick
// a new point.
let center_vp = vec3.transformMat4(vec3.create(), this.viewer.camera.center, this.viewer.camera.viewProjMatrix);
let isv = true;
for (let i = 0; i < 3; ++i) {
if (center_vp[i] < -1. || center_vp[i] > 1.) {
isv = false;
break;
}
}
if (!isv) {
let [x,y] = this.mousePos;
vec3.set(center_vp, x / this.viewer.width * 2 - 1, - y / this.viewer.height * 2 + 1, 1.);
vec3.transformMat4(center_vp, center_vp, this.camera.viewProjMatrixInverted);
vec3.subtract(center_vp, center_vp, this.camera.eye);
vec3.normalize(center_vp, center_vp);
vec3.scale(center_vp, center_vp, this.getZoomRate() * 10.);
vec3.add(center_vp, center_vp, this.camera.eye);
console.log("new center", center_vp);
this.viewer.camera.center = center_vp;
}
}
};
let handlePan = () => {
this.dragMode = DRAG_PAN;
}
if (window.TouchEvent && e instanceof TouchEvent) {
if (e.touches.length == 1) {
handleOrbit();
} else if (e.touches.length == 2) {
this.lastPinchDistance = this.pinchDistance;
handlePan();
} else if (e.touches.length == 3) {
handleSection();
}
} else {
if (e.which == 1 && e.ctrlKey && !e.altKey) {
handleSection();
} else if (e.which == 1) {
handleOrbit();
} else if (e.which == 2) {
handlePan();
}
}
this.over = true;
if (this.dragMode == DRAG_PAN || e.shiftKey) {
e.preventDefault();
}
}
/**
* @private
*/
canvasMouseUp(e) {
this.camera.orbitting = false;
this.viewer.overlay.update();
this.getCanvasPosFromEvent(e, this.mousePos);
let dt = e.timeStamp - this.mouseDownTime;
this.mouseDown = false;
let handleMeasurement = () => {
this.viewer.setMeasurementPoint({
canvasPos:[this.lastX, this.lastY],
commit: true,
mode: this.clickMode
});
}
const handleClick = () => {
if (dt < 500. && this.closeEnoughCanvas(this.mouseDownPos, this.mousePos)) {
if (this.viewer.activeMeasurement || this.clickMode == CLICK_MEASURE_PATH || this.clickMode == CLICK_MEASURE_DIST) {
return handleMeasurement();
}
var viewObject = this.viewer.pick({
canvasPos: this.mousePos,
select: true, // e.which == 3,
shiftKey: (e.which == 1 || e.which == 0) ? e.shiftKey : this.viewer.selectedElements.size > 0, // e.which == 0 on touch events
onlyAdd: e.which == 3 && this.viewer.selectedElements.size > 0
});
if (viewObject && viewObject.object) {
console.log("Picked", viewObject.object);
}
this.viewer.drawScene();
}
}
if ((window.TouchEvent && e instanceof TouchEvent && this.dragMode == DRAG_ORBIT) || (e instanceof MouseEvent && e.which == 1)) {
handleClick();
}
e.preventDefault();
}
/**
* @private
*/
canvasWheel(e) {
this.getCanvasPosFromEvent(e, this.mousePos);
var delta = Math.max(-1, Math.min(1, -e.deltaY * 40));
if (delta === 0) {
return;
}
var d = delta / Math.abs(delta);
var zoom = -d * this.getEyeLookDist() / 20.;
this.camera.zoom(zoom, this.mousePos);
e.preventDefault();
}
keyTick() {
let f;
if (this.keysDown.size) {
f = this.getEyeLookDist() / 600;
}
let vec = [0., 0., 0.];
this.keysDown.forEach((v, action) => {
if (v) {
let axis = action.charCodeAt(0) - 120;
let direction = action.charAt(2) == 'p';
vec[axis] += direction ? +f : -f;
}
});
if (this.keysDown.size) {
this.camera.pan(vec);
}
}
moveToAxo(axo) {
this._tmp_topleftfront_0[0] = this._tmp_topleftfront_1[0] = (this.viewer.modelBounds[0] + this.viewer.modelBounds[3]) / 2;
this._tmp_topleftfront_0[1] = this._tmp_topleftfront_1[1] = (this.viewer.modelBounds[1] + this.viewer.modelBounds[4]) / 2;
this._tmp_topleftfront_0[2] = this._tmp_topleftfront_1[2] = (this.viewer.modelBounds[2] + this.viewer.modelBounds[5]) / 2;
let axis = axo.charCodeAt(0) - 120;
let direction = axo.charAt(2) == 'p';
this._tmp_topleftfront_0[axis] = this.viewer.modelBounds[axis + (direction ? 3 : 0)];
this.camera.calcViewFit(null, null, this._tmp_topleftfront_0, this._tmp_topleftfront_1);
this.camera.interpolateView(this._tmp_topleftfront_0, this._tmp_topleftfront_1);
}
canvasKeyProcess(e, state) {
let axo = this.axoKeyMapping[e.key];
if (axo && state == false) {
this.moveToAxo(axo);
return;
}
let action = this.keyMapping[e.key];
if (action) {
if (state) {
this.keysDown.set(action, state);
} else {
this.keysDown.delete(action);
}
} else if (e.key == "Control" || e.key == "Alt") {
if (state == (e.key == "Control")) {
if (this.viewer.sectionPlaneIsDisabled) {
this.viewer.positionSectionPlaneWidget({canvasPos: [this.lastX, this.lastY]});
}
} else {
this.viewer.removeSectionPlaneWidget();
}
} else if (e.key == "Home") {
this.camera.viewFit({animate:true});
this.viewer.dirty = 2;
} else if (e.key == "Insert") {
// Should show the model from the side
this.camera.target = [0, 0, 0];
this.camera.eye = [1, 0, 0];
this.camera.viewFit({aabb: this.camera.modelBounds, animate: true});
} else if (e.key == "Escape") {
//@nb works when no measurement is present
this.viewer.destroyActiveMeasurement();
this.clickMode = CLICK_SELECT;
} else if (e.key == "Enter") {
this.viewer.commitActiveMeasurement();
}
}
/**
* @private
*/
closeEnoughCanvas(p, q) {
return p[0] >= (q[0] - this.canvasPickTolerance) &&
p[0] <= (q[0] + this.canvasPickTolerance) &&
p[1] >= (q[1] - this.canvasPickTolerance) &&
p[1] <= (q[1] + this.canvasPickTolerance);
}
/**
* @private
*/
canvasMouseMove(e) {
if (!this.over) {
return;
}
if (this.mouseDown || (e.ctrlKey && !e.altKey) || this.viewer.activeMeasurement) {
this.getCanvasPosFromEvent(e, this.mousePos);
if (this.viewer.activeMeasurement && !this.mouseDown) {
this.viewer.setMeasurementPoint({canvasPos: this.mousePos, commit: false});
} else if (this.dragMode == DRAG_SECTION) {
this.viewer.moveSectionPlane({canvasPos: this.mousePos});
} else if (e.ctrlKey) {
this.viewer.positionSectionPlaneWidget({canvasPos: this.mousePos});
} else {
var x = this.mousePos[0];
var y = this.mousePos[1];
var xDelta = (x - this.lastX);
var yDelta = (y - this.lastY);
this.lastX = x;
this.lastY = y;
if (this.dragMode == DRAG_ORBIT) {
let f = 0.5;
if (xDelta !== 0) {
this.camera.orbitYaw(-xDelta * this.mouseOrbitSensitivity * f);
}
if (yDelta !== 0) {
this.camera.orbitPitch(yDelta * this.mouseOrbitSensitivity * f);
}
this.camera.orbitting = true;
} else if (this.dragMode == DRAG_PAN) {
// tfk: using the elapsed time didn't seem to make navigation smoother.
// let now = +new Date();
// let elapsed = now - this.lastPan;
// elapsed /= 20.;
let dist = this.getEyeLookDist();
if (window.TouchEvent && e instanceof TouchEvent && e.touches.length == 2) {
let factor = Math.pow(this.pinchDistance / this.lastPinchDistance, 0.5);
this.camera.zoom(dist - dist * factor, this.mousePos);
this.lastPinchDistance = this.pinchDistance;
}
var f = dist / 600;
// f *= elapsed;
this.camera.pan([xDelta * f, yDelta * f, 0.0]);
// this.lastPan = now;
}
}
}
e.preventDefault();
}
/**
* @private
*/
documentMouseUp(e) {
this.mouseDown = false;
// Potential end-of-pan
if (this.dragMode == DRAG_PAN) {
this.camera.updateLowVolumeListeners();
}
this.dragMode = DRAG_ORBIT;
}
getEyeLookDist() {
let d = this.viewer.lastRecordedDepth;
if (!this.mouseDown && !this.keysDown.size && ((+new Date()) - this.viewer.recordedDepthAt) > 500) {
// Reread depth at mouse coordinates for sensitivity measures
this.viewer.pick({canvasPos: this.mousePos, select: false});
}
if (d === null) {
return this.getZoomRate() * 20.;
} else {
// Always add a bit so that we can zoom past a window
return d + this.getZoomRate();
}
}
/**
* @private
*/
cleanup() {
var canvas = this.canvas;
document.removeEventListener("mouseup", this.documentMouseUpHandler);
canvas.removeEventListener("mousedown", this.canvasMouseDownHandler);
canvas.removeEventListener("mouseup", this.canvasMouseUpHandler);
document.removeEventListener("mouseup", this.documentMouseUpHandler);
canvas.removeEventListener("keyup", this.canvasKeyUpHandler);
canvas.removeEventListener("keydown", this.canvastKeyDownHandler);
canvas.removeEventListener("mouseenter", this.canvasMouseEnterHandler);
canvas.removeEventListener("mouseleave", this.canvasMouseLeaveHandler);
canvas.removeEventListener("mousemove", this.canvasMouseMoveHandler);
canvas.removeEventListener("wheel", this.canvasMouseWheelHandler);
}
}