xeokit-viewer
Version:
BIM viewer built on xeokit and Bootstrap
1,338 lines (1,185 loc) • 71.7 kB
JavaScript
import {math} from '../math/math.js';
import {Component} from '../Component.js';
import {CameraFlightAnimation} from './CameraFlightAnimation.js';
/**
* @desc Controls a {@link Camera} with keyboard, mouse and touch input.
*
* Located for convenience at {@link Viewer#cameraControl}.
*
* Fires these events:
*
* @emits "hover" - pointer hovers over a new object
* @emits "hoverSurface" - Hover continues over an object surface - fired continuously as mouse moves over an object
* @emits "hoverOut" - Hover has left the last object we were hovering over
* @emits "hoverOff" - Hover continues over empty space - fired continuously as mouse moves over nothing
* @emits "picked" - Clicked or tapped object
* @emits "pickedSurface" - Clicked or tapped object, with event containing surface intersection details
* @emits "doublePicked" - Double-clicked or double-tapped object
* @emits "doublePickedSurface" - Double-clicked or double-tapped object, with event containing surface intersection details
* @emits "pickedNothing" - Clicked or tapped, but not on any objects
* @emits "doublePickedNothing" - Double-clicked or double-tapped, but not on any objects
*/
class CameraControl extends Component {
/**
* @private
*/
get type() {
return "CameraControl";
}
/**
* @private
* @constructor
*/
constructor(owner, cfg = {}) {
super(owner, cfg);
const self = this;
this._pivoter = new (function () { // Pivots the Camera around an arbitrary World-space position
// Pivot math by: http://www.derschmale.com/
const scene = self.scene;
const camera = scene.camera;
const canvas = scene.canvas;
const pivotPoint = new Float32Array(3);
let cameraOffset;
let azimuth = 0;
let polar = 0;
let radius = 0;
let pivoting = false; // True while pivoting
const spot = document.createElement("div");
spot.innerText = " ";
spot.style.color = "#ffffff";
spot.style.position = "absolute";
spot.style.width = "25px";
spot.style.height = "25px";
spot.style.left = "0px";
spot.style.top = "0px";
spot.style["border-radius"] = "15px";
spot.style["border"] = "2px solid #ffffff";
spot.style["background"] = "black";
spot.style.visibility = "hidden";
spot.style["box-shadow"] = "5px 5px 15px 1px #000000";
spot.style["z-index"] = 0;
spot.style["pointer-events"] = "none";
document.body.appendChild(spot);
(function () {
const viewPos = math.vec4();
const projPos = math.vec4();
const canvasPos = math.vec2();
let distDirty = true;
camera.on("viewMatrix", function () {
distDirty = true;
});
camera.on("projMatrix", function () {
distDirty = true;
});
scene.on("tick", function () {
if (pivoting && distDirty) {
math.transformPoint3(camera.viewMatrix, pivotPoint, viewPos);
viewPos[3] = 1;
math.transformPoint4(camera.projMatrix, viewPos, projPos);
const aabb = canvas.boundary;
canvasPos[0] = Math.floor((1 + projPos[0] / projPos[3]) * aabb[2] / 2);
canvasPos[1] = Math.floor((1 - projPos[1] / projPos[3]) * aabb[3] / 2);
const canvasElem = canvas.canvas;
const rect = canvasElem.getBoundingClientRect();
spot.style.left = (Math.floor(rect.left + canvasPos[0]) - 12) + "px";
spot.style.top = (Math.floor(rect.top + canvasPos[1]) - 12) + "px";
spot.style.visibility = "visible";
distDirty = false;
}
});
})();
this.startPivot = function (worldPos) {
if (worldPos) { // Use last pivotPoint by default
pivotPoint.set(worldPos);
}
let lookat = math.lookAtMat4v(camera.eye, camera.look, camera.worldUp);
cameraOffset = math.transformPoint3(lookat, pivotPoint);
cameraOffset[2] += math.distVec3(camera.eye, pivotPoint);
lookat = math.inverseMat4(lookat);
const offset = math.transformVec3(lookat, cameraOffset);
const diff = math.vec3();
math.subVec3(camera.eye, pivotPoint, diff);
math.addVec3(diff, offset);
if (camera.worldUp[2] === 1) {
const t = diff[1];
diff[1] = diff[2];
diff[2] = t;
}
radius = math.lenVec3(diff);
polar = Math.acos(diff[1] / radius);
azimuth = Math.atan2(diff[0], diff[2]);
pivoting = true;
};
this.getPivoting = function () {
return pivoting;
};
this.setPivotPos = function (worldPos) {
pivotPoint.set(worldPos);
};
this.getPivotPos = function () {
return pivotPoint;
};
this.continuePivot = function (yawInc, pitchInc) {
if (!pivoting) {
return;
}
if (yawInc === 0 && pitchInc === 0) {
return;
}
if (camera.worldUp[2] === 1) {
dx = -dx;
}
var dx = -yawInc;
const dy = -pitchInc;
azimuth += -dx * .01;
polar += dy * .01;
polar = math.clamp(polar, .001, Math.PI - .001);
const pos = [
radius * Math.sin(polar) * Math.sin(azimuth),
radius * Math.cos(polar),
radius * Math.sin(polar) * Math.cos(azimuth)
];
if (camera.worldUp[2] === 1) {
const t = pos[1];
pos[1] = pos[2];
pos[2] = t;
}
// Preserve the eye->look distance, since in xeokit "look" is the point-of-interest, not the direction vector.
const eyeLookLen = math.lenVec3(math.subVec3(camera.look, camera.eye, math.vec3()));
math.addVec3(pos, pivotPoint);
let lookat = math.lookAtMat4v(pos, pivotPoint, camera.worldUp);
lookat = math.inverseMat4(lookat);
const offset = math.transformVec3(lookat, cameraOffset);
lookat[12] -= offset[0];
lookat[13] -= offset[1];
lookat[14] -= offset[2];
const zAxis = [lookat[8], lookat[9], lookat[10]];
camera.eye = [lookat[12], lookat[13], lookat[14]];
math.subVec3(camera.eye, math.mulVec3Scalar(zAxis, eyeLookLen), camera.look);
camera.up = [lookat[4], lookat[5], lookat[6]];
spot.style.visibility = "visible";
};
this.hidePivot = function () {
spot.style.visibility = "hidden";
};
this.endPivot = function () {
pivoting = false;
};
})();
this._cameraFlight = new CameraFlightAnimation(this, {
duration: 0.5
});
this.planView = cfg.planView;
this.firstPerson = cfg.firstPerson;
this.walking = cfg.walking;
this.keyboardLayout = cfg.keyboardLayout;
this.doublePickFlyTo = cfg.doublePickFlyTo;
this.panRightClick = cfg.panRightClick;
this.active = cfg.active;
this.pivoting = cfg.pivoting;
this.panToPointer = cfg.panToPointer;
this.panToPivot = cfg.panToPivot;
this.inertia = cfg.inertia;
this.pointerEnabled = true;
this._initEvents(); // Set up all the mouse/touch/kb handlers
}
/**
* Sets if this CameraControl is active or not.
*
* Default value is ````true````.
*
* @param {Boolean} value Set ````true```` to activate this CameraControl.
*/
set active(value) {
this._active = value !== false;
}
/**
* Gets if this CameraControl is active or not.
*
* Default value is ````true````.
*
* @returns {Boolean} Returns ````true```` if this CameraControl is active.
*/
get active() {
return this._active;
}
/**
* Sets whether dragging will pivot the {@link Camera} about the current 3D pivot point.
*
* The pivot point is indicated by {@link CameraControl#pivotPos}.
*
* When in pivoting mode, clicking on an {@link Entity} will set {@link CameraControl#pivotPos} to the clicked position on the surface of the Entity.
*
* Default value is ````false````.
*
* @param {Boolean} value Set ````true```` to enable pivoting.
*/
set pivoting(value) {
this._pivoting = !!value;
}
/**
* Sets whether dragging will pivot the {@link Camera} about the current 3D pivot point.
*
* The pivot point is indicated by {@link CameraControl#pivotPos}.
*
* When in pivoting mode, clicking on an {@link Entity} will set {@link CameraControl#pivotPos} to the clicked position on the surface of the Entity.
*
* Default value is ````false````.
*
* @returns {Boolean} Returns ````true```` to enable pivoting.
*/
get pivoting() {
return this._pivoting;
}
/**
* Sets the current World-space 3D pivot position.
*
* @param {Number[]} worldPos The new World-space 3D pivot position.
*/
set pivotPos(worldPos) {
this._pivoter.setPivotPos(worldPos);
}
/**
* Gets the current World-space 3D pivot position.
*
* @return {Number[]} worldPos The current World-space 3D pivot position.
*/
get pivotPos() {
return this._pivoter.getPivotPos();
}
/**
* Sets whether scrolling the mouse wheel, when the mouse is over an {@link Entity}, will zoom the {@link Camera} towards the hovered point on the Entity's surface.
*
* Default value is ````false````.
*
* @param {Boolean} value Set ````true```` to enable pan-to-pointer behaviour.
*/
set panToPointer(value) {
this._panToPointer = !!value;
if (this._panToPointer) {
this._panToPivot = false;
}
}
/**
* Gets whether scrolling the mouse wheel, when the mouse is over an {@link Entity}, will zoom the {@link Camera} towards the hovered point on the Entity's surface.
*
* Default value is ````false````.
*
* @returns {Boolean} Returns ````true```` if pan-to-pointer behaviour is enabled.
*/
get panToPointer() {
return this._panToPointer;
}
/**
* Sets whether scrolling the mouse wheel, when mouse is over an {@link Entity}, will zoom the {@link Camera} towards the pivot point.
*
* Default value is ````false````.
*
* @param {Boolean} value Set ````true```` to enable pan-to-pivot behaviour.
*/
set panToPivot(value) {
this._panToPivot = !!value;
if (this._panToPivot) {
this._panToPointer = false;
}
}
/**
* Gets whether scrolling the mouse wheel, when mouse is over an {@link Entity}, will zoom the {@link Camera} towards the pivot point.
*
* Default value is ````false````.
*
* @returns {Boolean} Returns ````true```` if enable pan-to-pivot behaviour is enabled.
*/
get panToPivot() {
return this._panToPivot;
}
/**
* Sets whether this CameraControl is in plan-view mode.
*
* When in plan-view mode, rotation is disabled.
*
* Default value is ````false````.
*
* @param {Boolean} value Set ````true```` to enable plan-view mode.
*/
set planView(value) {
this._planView = !!value;
}
/**
* Gets whether this CameraControl is in plan-view mode.
*
* When in plan-view mode, rotation is disabled.
*
* Default value is ````false````.
*
* @returns {Boolean} Returns ````true```` if plan-view mode is enabled.
*/
get planView() {
return this._planView;
}
/**
* Sets whether this CameraControl is in first-person mode.
*
* In "first person" mode (disabled by default) the look position rotates about the eye position. Otherwise, {@link Camera#eye} rotates about {@link Camera#look}.
*
* Default value is ````false````.
*
* @param {Boolean} value Set ````true```` to enable first-person mode.
*/
set firstPerson(value) {
this._firstPerson = !!value;
if (this._firstPerson) {
this._pivoter.hidePivot();
this._pivoter.endPivot();
}
}
/**
* Gets whether this CameraControl is in first-person mode.
*
* In "first person" mode (disabled by default) the look position rotates about the eye position. Otherwise, {@link Camera#eye} rotates about {@link Camera#look}.
*
* Default value is ````false````.
*
* @returns {Boolean} Returns ````true```` if first-person mode is enabled.
*/
get firstPerson() {
return this._firstPerson;
}
/**
* Sets whether this CameraControl is in walking mode.
*
* When set ````true````, this constrains {@link Camera#eye} movement to the horizontal X-Z plane. When doing a walkthrough,
* this is useful to allow us to look upwards or downwards as we move, while keeping us moving in the horizontal plane.
*
* This only has an effect when {@link CameraControl#firstPerson} is ````true````.
*
* Default value is ````false````.
*
* @param {Boolean} value Set ````true```` to enable walking mode.
*/
set walking(value) {
this._walking = !!value;
}
/**
* Gets whether this CameraControl is in walking mode.
*
* When set ````true````, this constrains {@link Camera#eye} movement to the horizontal X-Z plane. When doing a walkthrough,
* this is useful to allow us to look upwards or downwards as we move, while keeping us moving in the horizontal plane.
*
* This only has an effect when {@link CameraControl#firstPerson} is ````true````.
*
* Default value is ````false````.
*
* @returns {Boolean} Returns ````true```` when in walking mode.
*/
get walking() {
return this._walking;
}
/**
* Sets whether double-picking an {@link Entity} causes the {@link Camera} to fly to its boundary.
*
* Default value is ````false````.
*
* @param {Boolean} value Set ````true```` to enable double-pick-fly-to mode.
*/
set doublePickFlyTo(value) {
this._doublePickFlyTo = value !== false;
}
/**
* Gets whether double-picking an {@link Entity} causes the {@link Camera} to fly to its boundary.
*
* Default value is ````false````.
*
* @returns {Boolean} Returns ````true```` when double-pick-fly-to mode is enabled.
*/
get doublePickFlyTo() {
return this._doublePickFlyTo;
}
/**
* Sets whether right-clicking pans the {@link Camera}.
*
* Default value is ````true````.
*
* @param {Boolean} value Set ````false```` to disable pan on right-click.
*/
set panRightClick(value) {
this._panRightClick = value !== false;
}
/**
* Gets whether right-clicking pans the {@link Camera}.
*
* Default value is ````true````.
*
* @returns {Boolean} Returns ````false```` when pan on right-click is disabled.
*/
get panRightClick() {
return this._panRightClick;
}
/**
* Sets a factor in range ````[0..1]```` indicating how much the camera keeps moving after you finish panning or rotating it.
*
* A value of ````0.0```` causes it to immediately stop, ````0.5```` causes its movement to decay 50% on each tick,
* while ````1.0```` causes no decay, allowing it continue moving, by the current rate of pan or rotation.
*
* You may choose an inertia of zero when you want be able to precisely position or rotate the camera,
* without interference from inertia. ero inertia can also mean that less frames are rendered while
* you are positioning the camera.
*
* Default value is ````0.5````.
*
* @param {Number} value New inertial factor.
*/
set inertia(value) {
this._inertia = value === undefined ? 0.5 : value;
}
/**
* Gets the inertia factor.
*
* Default value is ````0.5````.
*
* @returns {Number} The inertia factor.
*/
get inertia() {
return this._inertia;
}
/**
* Sets whether canvas pointer events are enabled.
*
* Default value is ````true````.
*
* @param {Boolean} value Set ````true```` to enable drag events.
*/
set pointerEnabled(value) {
this._pointerEnabled = !!value;
}
/**
* Gets whether canvas pointer events are enabled.
*
* Default value is ````true````.
*
* @returns {Boolean} Returns ````true```` to enable drag events.
*/
get pointerEnabled() {
return this._pointerEnabled;
}
/**
* @private
*/
set keyboardLayout(value) {
this._keyboardLayout = value || "qwerty";
}
/**
* @private
*/
get keyboardLayout() {
return this._keyboardLayout;
}
_initEvents() {
const self = this;
const scene = this.scene;
const input = scene.input;
const camera = scene.camera;
const canvas = this.scene.canvas.canvas;
let over = false;
const mouseOrbitRate = 0.4;
const mousePanRate = 0.4;
const mouseZoomRate = 0.8;
const keyboardOrbitRate = .02;
const keyboardPanRate = .02;
const keyboardZoomRate = .02;
const touchRotateRate = 0.3;
const touchPanRate = 0.2;
const touchZoomRate = 0.05;
canvas.oncontextmenu = function (e) {
e.preventDefault();
};
const getCanvasPosFromEvent = function (event, canvasPos) {
if (!event) {
event = window.event;
canvasPos[0] = event.x;
canvasPos[1] = event.y;
} else {
let element = event.target;
let totalOffsetLeft = 0;
let totalOffsetTop = 0;
while (element.offsetParent) {
totalOffsetLeft += element.offsetLeft;
totalOffsetTop += element.offsetTop;
element = element.offsetParent;
}
canvasPos[0] = event.pageX - totalOffsetLeft;
canvasPos[1] = event.pageY - totalOffsetTop;
}
return canvasPos;
};
const pickCursorPos = [0, 0];
let needPickEntity = false;
let needPickSurface = false;
let lastPickedEntityId;
let pickResult;
let picked = false;
let pickedSurface = false;
function updatePick() {
if (!self._pointerEnabled) {
return;
}
if (!needPickEntity && !needPickSurface) {
return;
}
picked = false;
pickedSurface = false;
if (needPickSurface || self.hasSubs("hoverSurface")) {
pickResult = scene.pick({
pickSurface: true,
canvasPos: pickCursorPos
});
} else { // needPickEntity == true
pickResult = scene.pick({
canvasPos: pickCursorPos
});
}
if (pickResult) {
picked = true;
const pickedEntityId = pickResult.entity.id;
if (lastPickedEntityId !== pickedEntityId) {
if (lastPickedEntityId !== undefined) {
/**
* Fired whenever the pointer no longer hovers over an {@link Entity}.
* @event hoverOut
* @param entity The Entity
*/
self.fire("hoverOut", {
entity: scene.objects[lastPickedEntityId]
}, true);
}
/**
* Fired when the pointer is over a new {@link Entity}.
* @event hoverEnter
* @param pickResult A pick pickResult result containing the ID of the Entity - see {@link Scene/pick:method"}}Scene#pick(){{/crossLink}}.
*/
self.fire("hoverEnter", pickResult, true);
lastPickedEntityId = pickedEntityId;
}
/**
* Fired continuously while the pointer is moving while hovering over an {@link Entity}.
* @event hover
* @param pickResult A pick pickResult result containing the ID of the Entity - see {@link Scene/pick:method"}}Scene#pick(){{/crossLink}}.
*/
self.fire("hover", pickResult, true);
if (pickResult.worldPos) {
pickedSurface = true;
/**
* Fired while the pointer hovers over the surface of an {@link Entity}.
*
* This event provides 3D information about the point on the surface that the pointer is
* hovering over.
*
* @event hoverSurface
* @param pickResult A surface pick pickResult result, containing the ID of the Entity and 3D info on the
* surface position - see {@link Scene/pick:method"}}Scene#pick(){{/crossLink}}.
*/
self.fire("hoverSurface", pickResult, true);
}
} else {
if (lastPickedEntityId !== undefined) {
/**
* Fired whenever the pointer no longer hovers over an {@link Entity}.
* @event hoverOut
* @param entity The Entity
*/
self.fire("hoverOut", {
entity: scene.objects[lastPickedEntityId]
}, true);
lastPickedEntityId = undefined;
}
/**
* Fired continuously while the pointer is moving but not hovering over anything.
*
* @event hoverOff
*/
self.fire("hoverOff", {
canvasPos: pickCursorPos
}, true);
}
needPickEntity = false;
needPickSurface = false;
}
scene.on("tick", updatePick);
//------------------------------------------------------------------------------------
// Mouse, touch and keyboard camera control
//------------------------------------------------------------------------------------
(function () {
let rotateVx = 0;
let rotateVy = 0;
let panVx = 0;
let panVy = 0;
let panVz = 0;
let vZoom = 0;
const mousePos = math.vec2();
let panToMouse = false;
let ctrlDown = false;
let altDown = false;
let shiftDown = false;
const keyDown = {};
const EPSILON = 0.001;
const getEyeLookDist = (function () {
const vec = new Float32Array(3);
return function () {
return math.lenVec3(math.subVec3(camera.look, camera.eye, vec));
};
})();
const getInverseProjectMat = (function () {
let projMatDirty = true;
camera.on("projMatrix", function () {
projMatDirty = true;
});
const inverseProjectMat = math.mat4();
return function () {
if (projMatDirty) {
math.inverseMat4(camera.projMatrix, inverseProjectMat);
}
return inverseProjectMat;
}
})();
const getTransposedProjectMat = (function () {
let projMatDirty = true;
camera.on("projMatrix", function () {
projMatDirty = true;
});
const transposedProjectMat = math.mat4();
return function () {
if (projMatDirty) {
math.transposeMat4(camera.projMatrix, transposedProjectMat);
}
return transposedProjectMat;
}
})();
const getInverseViewMat = (function () {
let viewMatDirty = true;
camera.on("viewMatrix", function () {
viewMatDirty = true;
});
const inverseViewMat = math.mat4();
return function () {
if (viewMatDirty) {
math.inverseMat4(camera.viewMatrix, inverseViewMat);
}
return inverseViewMat;
}
})();
const getSceneDiagSize = (function () {
let sceneSizeDirty = true;
let diag = 1; // Just in case
scene.on("boundary", function () {
sceneSizeDirty = true;
});
return function () {
if (sceneSizeDirty) {
diag = math.getAABB3Diag(scene.aabb);
}
return diag;
};
})();
const panToMousePos = (function () {
const cp = math.vec4();
const viewPos = math.vec4();
const worldPos = math.vec4();
const eyeCursorVec = math.vec3();
const unproject = function (inverseProjMat, inverseViewMat, mousePos, z, viewPos, worldPos) {
const canvas = scene.canvas.canvas;
const halfCanvasWidth = canvas.offsetWidth / 2.0;
const halfCanvasHeight = canvas.offsetHeight / 2.0;
cp[0] = (mousePos[0] - halfCanvasWidth) / halfCanvasWidth;
cp[1] = (mousePos[1] - halfCanvasHeight) / halfCanvasHeight;
cp[2] = z;
cp[3] = 1.0;
math.mulMat4v4(inverseProjMat, cp, viewPos);
math.mulVec3Scalar(viewPos, 1.0 / viewPos[3]); // Normalize homogeneous coord
viewPos[3] = 1.0;
viewPos[1] *= -1; // TODO: Why is this reversed?
math.mulMat4v4(inverseViewMat, viewPos, worldPos);
};
return function (mousePos, factor) {
const lastHoverDistance = 0;
const inverseProjMat = getInverseProjectMat();
const inverseViewMat = getInverseViewMat();
// Get last two columns of projection matrix
const transposedProjectMat = getTransposedProjectMat();
const Pt3 = transposedProjectMat.subarray(8, 12);
const Pt4 = transposedProjectMat.subarray(12);
const D = [0, 0, -(lastHoverDistance || getSceneDiagSize()), 1];
const Z = math.dotVec4(D, Pt3) / math.dotVec4(D, Pt4);
unproject(inverseProjMat, inverseViewMat, mousePos, Z, viewPos, worldPos);
math.subVec3(worldPos, camera.eye, eyeCursorVec);
math.normalizeVec3(eyeCursorVec);
const px = eyeCursorVec[0] * factor;
const py = eyeCursorVec[1] * factor;
const pz = eyeCursorVec[2] * factor;
const eye = camera.eye;
const look = camera.look;
camera.eye = [eye[0] + px, eye[1] + py, eye[2] + pz];
camera.look = [look[0] + px, look[1] + py, look[2] + pz];
};
})();
const panToWorldPos = (function () {
const eyeCursorVec = math.vec3();
return function (worldPos, factor) {
math.subVec3(worldPos, camera.eye, eyeCursorVec);
math.normalizeVec3(eyeCursorVec);
const px = eyeCursorVec[0] * factor;
const py = eyeCursorVec[1] * factor;
const pz = eyeCursorVec[2] * factor;
const eye = camera.eye;
const look = camera.look;
camera.eye = [eye[0] + px, eye[1] + py, eye[2] + pz];
camera.look = [look[0] + px, look[1] + py, look[2] + pz];
};
})();
scene.on("tick", function () {
const cameraInertia = self._inertia;
if (Math.abs(rotateVx) < EPSILON) {
rotateVx = 0;
}
if (Math.abs(rotateVy) < EPSILON) {
rotateVy = 0;
}
if (rotateVy !== 0 || rotateVx !== 0) {
if (self._pivoter.getPivoting()) {
self._pivoter.continuePivot(rotateVy, rotateVx);
} else {
if (rotateVx !== 0) {
if (self._firstPerson) {
camera.pitch(-rotateVx);
} else {
camera.orbitPitch(rotateVx);
}
}
if (rotateVy !== 0) {
if (self._firstPerson) {
camera.yaw(rotateVy);
} else {
camera.orbitYaw(rotateVy);
}
}
}
rotateVx *= cameraInertia;
rotateVy *= cameraInertia;
} else {
self._pivoter.hidePivot();
}
if (Math.abs(panVx) < EPSILON) {
panVx = 0;
}
if (Math.abs(panVy) < EPSILON) {
panVy = 0;
}
if (Math.abs(panVz) < EPSILON) {
panVz = 0;
}
if (panVx !== 0 || panVy !== 0 || panVz !== 0) {
const f = getEyeLookDist() / 80;
if (self._walking) {
let y = camera.eye[1];
camera.pan([panVx * f, panVy * f, panVz * f]);
let eye = camera.eye;
eye[1] = y;
camera.eye = eye;
} else {
camera.pan([panVx * f, panVy * f, panVz * f]);
}
}
panVx *= cameraInertia;
panVy *= cameraInertia;
panVz *= cameraInertia;
if (Math.abs(vZoom) < EPSILON) {
vZoom = 0;
}
if (vZoom !== 0) {
if (self._firstPerson) {
let y;
if (self._walking) {
y = camera.eye[1];
}
if (panToMouse) { // Using mouse input
panToMousePos(mousePos, -vZoom * 2);
} else {
camera.pan([0, 0, vZoom]); // Touchscreen input with no cursor
}
if (self._walking) {
let eye = camera.eye;
eye[1] = y;
camera.eye = eye;
}
} else {
// Do both zoom and ortho scale so that we can switch projections without weird scale jumps
if (self._panToPointer) {
updatePick();
if (pickedSurface && pickResult.worldPos) {
panToWorldPos(pickResult.worldPos, -vZoom);
} else {
camera.zoom(vZoom);
}
} else if (self._panToPivot) {
panToWorldPos(self._pivoter.getPivotPos(), -vZoom); // FIXME: What about when pivotPos undefined?
} else {
camera.zoom(vZoom);
}
camera.ortho.scale = camera.ortho.scale + vZoom;
}
vZoom *= cameraInertia;
}
});
function getZoomRate() {
const aabb = scene.aabb;
const xsize = aabb[3] - aabb[0];
const ysize = aabb[4] - aabb[1];
const zsize = aabb[5] - aabb[2];
let max = (xsize > ysize ? xsize : ysize);
max = (zsize > max ? zsize : max);
return max / 30;
}
document.addEventListener("keydown", function (e) {
if (!self._active) {
return;
}
if (e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") {
ctrlDown = e.ctrlKey || e.keyCode === 17 || e.metaKey; // !important, treat Windows or Mac Command Key as ctrl
altDown = e.altKey || e.keyCode === 18;
shiftDown = e.keyCode === 16;
keyDown[e.keyCode] = true;
}
}, true);
document.addEventListener("keyup", function (e) {
if (!self._active) {
return;
}
if (e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") {
if (e.ctrlKey || e.keyCode === 17) {
ctrlDown = false;
}
if (e.altKey || e.keyCode === 18) {
altDown = false;
}
if (e.keyCode === 16) {
shiftDown = false;
}
keyDown[e.keyCode] = false;
}
});
// Mouse camera rotate, pan and zoom
(function () {
let lastX;
let lastY;
let xDelta = 0;
let yDelta = 0;
let down = false;
let mouseDownLeft;
let mouseDownMiddle;
let mouseDownRight;
canvas.addEventListener("mousedown", function (e) {
if (!(self._active && self._pointerEnabled)) {
return;
}
over = true;
switch (e.which) {
case 1: // Left button
mouseDownLeft = true;
self.scene.canvas.canvas.style.cursor = "move";
down = true;
xDelta = 0;
yDelta = 0;
getCanvasPosFromEvent(e, mousePos);
lastX = mousePos[0];
lastY = mousePos[1];
break;
case 2: // Middle/both buttons
self.scene.canvas.canvas.style.cursor = "move";
mouseDownMiddle = true;
break;
case 3: // Right button
self.scene.canvas.canvas.style.cursor = "move";
mouseDownRight = true;
down = true;
xDelta = 0;
yDelta = 0;
getCanvasPosFromEvent(e, mousePos);
lastX = mousePos[0];
lastY = mousePos[1];
break;
default:
break;
}
});
canvas.addEventListener("mouseup", function (e) {
if (!(self._active && self._pointerEnabled)) {
return;
}
switch (e.which) {
case 1: // Left button
mouseDownLeft = false;
break;
case 2: // Middle/both buttons
mouseDownMiddle = false;
break;
case 3: // Right button
mouseDownRight = false;
break;
default:
break;
}
self.scene.canvas.canvas.style.cursor = "default";
down = false;
xDelta = 0;
yDelta = 0;
});
document.addEventListener("mouseup", function (e) {
if (!(self._active && self._pointerEnabled)) {
return;
}
switch (e.which) {
case 1: // Left button
mouseDownLeft = false;
break;
case 2: // Middle/both buttons
mouseDownMiddle = false;
break;
case 3: // Right button
mouseDownRight = false;
break;
default:
break;
}
down = false;
xDelta = 0;
yDelta = 0;
});
canvas.addEventListener("mouseenter", function () {
if (!(self._active && self._pointerEnabled)) {
return;
}
over = true;
xDelta = 0;
yDelta = 0;
});
canvas.addEventListener("mouseleave", function () {
if (!(self._active && self._pointerEnabled)) {
return;
}
over = false;
xDelta = 0;
yDelta = 0;
});
canvas.addEventListener("mousemove", function (e) {
if (!(self._active && self._pointerEnabled)) {
return;
}
if (!over) {
return;
}
getCanvasPosFromEvent(e, mousePos);
panToMouse = true;
if (!down) {
return;
}
const x = mousePos[0];
const y = mousePos[1];
xDelta += (x - lastX) * mouseOrbitRate;
yDelta += (y - lastY) * mouseOrbitRate;
lastX = x;
lastY = y;
});
scene.on("tick", function () {
if (!(self._active && self._pointerEnabled)) {
return;
}
if (Math.abs(xDelta) === 0 && Math.abs(yDelta) === 0) {
return;
}
const panning = shiftDown || mouseDownRight;
if (panning) {
// Panning
if (shiftDown || (self.panRightClick && mouseDownRight)) {
panVx = xDelta * mousePanRate;
panVy = yDelta * mousePanRate;
}
} else {
if (!self._planView) {
// Orbiting
rotateVy = -xDelta * mouseOrbitRate;
rotateVx = yDelta * mouseOrbitRate;
}
}
xDelta = 0;
yDelta = 0;
});
// Mouse wheel zoom
canvas.addEventListener("wheel", function (e) {
if (!(self._active && self._pointerEnabled)) {
return;
}
if (self._panToPointer) {
needPickSurface = true;
}
const delta = Math.max(-1, Math.min(1, -e.deltaY * 40));
if (delta === 0) {
return;
}
const d = delta / Math.abs(delta);
vZoom = -d * getZoomRate() * mouseZoomRate;
e.preventDefault();
});
// Keyboard zoom
scene.on("tick", function (e) {
if (!(self._active && self._pointerEnabled)) {
return;
}
if (!over) {
return;
}
const elapsed = e.deltaTime;
if (!self.ctrlDown && !self.altDown) {
const wkey = input.keyDown[input.KEY_ADD];
const skey = input.keyDown[input.KEY_SUBTRACT];
if (wkey || skey) {
if (skey) {
vZoom = elapsed * getZoomRate() * keyboardZoomRate;
} else if (wkey) {
vZoom = -elapsed * getZoomRate() * keyboardZoomRate;
}
}
}
});
// Keyboard panning
(function () {
scene.on("tick", function (e) {
if (!(self._active && self._pointerEnabled)) {
return;
}
if (!over) {
return;
}
const elapsed = e.deltaTime;
// if (!self.ctrlDown && !self.altDown) {
let front, back, left, right, up, down;
if (self._keyboardLayout === 'azerty') {
front = input.keyDown[input.KEY_Z];
back = input.keyDown[input.KEY_S];
left = input.keyDown[input.KEY_Q];
right = input.keyDown[input.KEY_D];
up = input.keyDown[input.KEY_W];
down = input.keyDown[input.KEY_X];
} else {
front = input.keyDown[input.KEY_W];
back = input.keyDown[input.KEY_S];
left = input.keyDown[input.KEY_A];
right = input.keyDown[input.KEY_D];
up = input.keyDown[input.KEY_Z];
down = input.keyDown[input.KEY_X];
}
if (front || back || left || right || up || down) {
if (down) {
panVy += elapsed * keyboardPanRate;
} else if (up) {
panVy += -elapsed * keyboardPanRate;
}
if (right) {
panVx += -elapsed * keyboardPanRate;
} else if (left) {
panVx += elapsed * keyboardPanRate;
}
if (back) {
panVz = elapsed * keyboardPanRate;
} else if (front) {
panVz = -elapsed * keyboardPanRate;
}
}
// }
});
})();
})();
// Touch camera rotate, pan and zoom
(function () {
let touchStartTime;
const tapStartPos = new Float32Array(2);
let tapStartTime = -1;
const lastTouches = [];
let numTouches = 0;
const touch0Vec = new Float32Array(2);
const touch1Vec = new Float32Array(2);
const MODE_CHANGE_TIMEOUT = 50;
const MODE_NONE = 0;
const MODE_ROTATE = 1;
const MODE_PAN = 1 << 1;
const MODE_ZOOM = 1 << 2;
let currentMode = MODE_NONE;
let transitionTime = Date.now();
function checkMode(mode) {
const currentTime = Date.now();
if (currentMode === MODE_NONE) {
currentMode = mode;
return true;
}
if (currentMode === mode) {
return currentTime - transitionTime > MODE_CHANGE_TIMEOUT;
}
currentMode = mode;
transitionTime = currentTime;
return false;
}
canvas.addEventListener("touchstart", function (event) {
if (!(self._active && self._pointerEnabled)) {
return;
}
const touches = event.touches;
const changedTouches = event.changedTouches;
touchStartTime = Date.now();
if (touches.length === 1 && changedTouches.length === 1) {
tapStartTime = touchStartTime;
tapStartPos[0] = touches[0].pageX;
tapStartPos[1] = touches[0].pageY;
} else {
tapStartTime = -1;
}
while (lastTouches.length < touches.length) {
lastTouches.push(new Float32Array(2));
}
for (let i = 0, len = touches.length; i < len; ++i) {
lastTouches[i][0] = touches[i].pageX;
lastTouches[i][1] = touches[i].pageY;
}
currentMode = MODE_NONE;
numTouches = touches.length;
event.stopPropagation();
}, {passive: true});
canvas.addEventListener("touchmove", function (event) {
if (!(self._active && self._pointerEnabled)) {
return;
}
const touches = event.touches;
if (numTouches === 1) {
var touch0 = touches[0];
if (checkMode(MODE_ROTATE)) {
const deltaX = touch0.pageX - lastTouches[0][0];
const deltaY = touch0.pageY - lastTouches[0][1];
const rotateX = deltaX * touchRotateRate;
const rotateY = deltaY * touchRotateRate;
rotateVx += rotateY;
rotateVy += -rotateX;
}
} else if (numTouches === 2) {
var touch0 = touches[0];
const touch1 = touches[1];
math.subVec2([touch0.pageX, touch0.pageY], lastTouches[0], touch0Vec);
math.subVec2([touch1.pageX, touch1.pageY], lastTouches[1], touch1Vec);
const panning = math.dotVec2(touch0Vec, touch1Vec) > 0;
if (panning && checkMode(MODE_PAN)) {
math.subVec2([touch0.pageX, touch0.pageY], lastTouches[0], touch0Vec);
panVx += touch0Vec[0] * touchPanRate;
panVy += touch0Vec[1] * touchPanRate;
}
if (!panning && checkMode(MODE_ZOOM)) {
const d1 = math.distVec2([touch0.pageX, touch0.pageY], [touch1.pageX, touch1.pageY]);
const d2 = math.distVec2(lastTouches[0], lastTouches[1]);
vZoom = (d2 - d1) * getZoomRate() * touchZoomRate;
}
}