src/viewer/scene/CameraControl/lib/controllers/PivotController.js
import { math } from "../../../math/math.js";
import { PhongMaterial } from "../../../materials/PhongMaterial.js";
import { Mesh } from "../../../mesh/Mesh.js";
import { VBOGeometry } from "../../../geometry/VBOGeometry.js";
import { buildSphereGeometry } from "../../../geometry/builders/buildSphereGeometry.js";
import { worldToRTCPos } from "../../../math/rtcCoords.js";
const tempVec3a = math.vec3();
const tempVec3b = math.vec3();
const tempVec3c = math.vec3();
const tempVec4a = math.vec4();
const tempVec4b = math.vec4();
const tempVec4c = math.vec4();
/** @private */
class PivotController {
/**
* @private
*/
constructor(scene, configs) {
// Pivot math by: http://www.derschmale.com/
this._scene = scene;
this._configs = configs;
this._pivotWorldPos = math.vec3();
this._cameraOffset = math.vec3();
this._azimuth = 0;
this._polar = 0;
this._radius = 0;
this._pivotPosSet = false; // Initially false, true as soon as _pivotWorldPos has been set to some value
this._pivoting = false; // True while pivoting
this._shown = false;
this._pivotSphereEnabled = false;
this._pivotSphere = null;
this._pivotSphereSize = 1;
this._pivotSphereGeometry = null;
this._pivotSphereMaterial = null;
this._rtcCenter = math.vec3();
this._rtcPos = math.vec3();
this._pivotViewPos = math.vec4();
this._pivotProjPos = math.vec4();
this._pivotCanvasPos = math.vec2();
this._cameraDirty = true;
this._onViewMatrix = this._scene.camera.on("viewMatrix", () => {
this._cameraDirty = true;
});
this._onProjMatrix = this._scene.camera.on("projMatrix", () => {
this._cameraDirty = true;
});
this._onTick = this._scene.on("tick", () => {
this.updatePivotElement();
this.updatePivotSphere();
});
}
createPivotSphere() {
const currentPos = this.getPivotPos();
const cameraPos = math.vec3();
math.decomposeMat4(math.inverseMat4(this._scene.viewer.camera.viewMatrix, math.mat4()), cameraPos, math.vec4(), math.vec3());
const length = math.distVec3(cameraPos, currentPos);
let radius = (Math.tan(Math.PI / 500) * length) * this._pivotSphereSize;
if (this._scene.camera.projection == "ortho") {
radius /= (this._scene.camera.ortho.scale / 2);
}
worldToRTCPos(currentPos, this._rtcCenter, this._rtcPos);
this._pivotSphereGeometry = new VBOGeometry(
this._scene,
buildSphereGeometry({ radius })
);
this._pivotSphere = new Mesh(this._scene, {
geometry: this._pivotSphereGeometry,
material: this._pivotSphereMaterial,
pickable: false,
position: this._rtcPos,
rtcCenter: this._rtcCenter
});
};
destroyPivotSphere() {
if (this._pivotSphere) {
this._pivotSphere.destroy();
this._pivotSphere = null;
}
if (this._pivotSphereGeometry) {
this._pivotSphereGeometry.destroy();
this._pivotSphereGeometry = null;
}
}
updatePivotElement() {
const camera = this._scene.camera;
const canvas = this._scene.canvas;
if (this._pivoting && this._cameraDirty) {
math.transformPoint3(camera.viewMatrix, this.getPivotPos(), this._pivotViewPos);
this._pivotViewPos[3] = 1;
math.transformPoint4(camera.projMatrix, this._pivotViewPos, this._pivotProjPos);
const canvasAABB = canvas.boundary;
const canvasWidth = canvasAABB[2];
const canvasHeight = canvasAABB[3];
this._pivotCanvasPos[0] = Math.floor((1 + this._pivotProjPos[0] / this._pivotProjPos[3]) * canvasWidth / 2);
this._pivotCanvasPos[1] = Math.floor((1 - this._pivotProjPos[1] / this._pivotProjPos[3]) * canvasHeight / 2);
// data-textures: avoid to do continuous DOM layout calculations
let canvasBoundingRect = canvas._lastBoundingClientRect;
if (!canvasBoundingRect || canvas._canvasSizeChanged)
{
const canvasElem = canvas.canvas;
canvasBoundingRect = canvas._lastBoundingClientRect = canvasElem.getBoundingClientRect ();
}
if (this._pivotElement) {
this._pivotElement.style.left = (Math.floor(canvasBoundingRect.left + this._pivotCanvasPos[0]) - (this._pivotElement.clientWidth / 2) + window.scrollX) + "px";
this._pivotElement.style.top = (Math.floor(canvasBoundingRect.top + this._pivotCanvasPos[1]) - (this._pivotElement.clientHeight / 2) + window.scrollY) + "px";
}
this._cameraDirty = false;
}
}
updatePivotSphere() {
if (this._pivoting && this._pivotSphere) {
worldToRTCPos(this.getPivotPos(), this._rtcCenter, this._rtcPos);
if(!math.compareVec3(this._rtcPos, this._pivotSphere.position)) {
this.destroyPivotSphere();
this.createPivotSphere();
}
}
}
/**
* Sets the HTML DOM element that will represent the pivot position.
*
* @param pivotElement
*/
setPivotElement(pivotElement) {
this._pivotElement = pivotElement;
}
/**
* Sets a sphere as the representation of the pivot position.
*
* @param {Object} [cfg] Sphere configuration.
* @param {String} [cfg.size=1] Optional size factor of the sphere. Defaults to 1.
* @param {String} [cfg.color=Array] Optional maretial color. Defaults to a red.
*/
enablePivotSphere(cfg = {}) {
this.destroyPivotSphere();
this._pivotSphereEnabled = true;
if (cfg.size) {
this._pivotSphereSize = cfg.size;
}
const color = cfg.color || [1, 0, 0];
this._pivotSphereMaterial = new PhongMaterial(this._scene, {
emissive: color,
ambient: color,
specular: [0,0,0],
diffuse: [0,0,0],
});
}
/**
* Remove the sphere as the representation of the pivot position.
*
*/
disablePivotSphere() {
this.destroyPivotSphere();
this._pivotSphereEnabled = false;
}
/**
* Begins pivoting.
*/
startPivot() {
if (this._cameraLookingDownwards()) {
this._pivoting = false;
return false;
}
const camera = this._scene.camera;
let lookat = math.lookAtMat4v(camera.eye, camera.look, camera.worldUp);
math.transformPoint3(lookat, this.getPivotPos(), this._cameraOffset);
const pivotPos = this.getPivotPos();
this._cameraOffset[2] += math.distVec3(camera.eye, pivotPos);
lookat = math.inverseMat4(lookat);
const offset = math.transformVec3(lookat, this._cameraOffset);
const diff = math.vec3();
math.subVec3(camera.eye, pivotPos, diff);
math.addVec3(diff, offset);
if (camera.zUp) {
const t = diff[1];
diff[1] = diff[2];
diff[2] = t;
}
this._radius = math.lenVec3(diff);
this._polar = Math.acos(diff[1] / this._radius);
this._azimuth = Math.atan2(diff[0], diff[2]);
this._pivoting = true;
}
_cameraLookingDownwards() { // Returns true if angle between camera viewing direction and World-space "up" axis is too small
const camera = this._scene.camera;
const forwardAxis = math.normalizeVec3(math.subVec3(camera.look, camera.eye, tempVec3a));
const rightAxis = math.cross3Vec3(forwardAxis, camera.worldUp, tempVec3b);
let rightAxisLen = math.sqLenVec3(rightAxis);
return (rightAxisLen <= 0.0001);
}
/**
* Returns true if we are currently pivoting.
*
* @returns {Boolean}
*/
getPivoting() {
return this._pivoting;
}
/**
* Sets a 3D World-space position to pivot about.
*
* @param {Number[]} worldPos The new World-space pivot position.
*/
setPivotPos(worldPos) {
this._pivotWorldPos.set(worldPos);
this._pivotPosSet = true;
}
/**
* Sets the pivot position to the 3D projection of the given 2D canvas coordinates on a sphere centered
* at the viewpoint. The radius of the sphere is configured via {@link CameraControl#smartPivot}.
*
*
* @param canvasPos
*/
setCanvasPivotPos(canvasPos) {
const camera = this._scene.camera;
const pivotShereRadius = Math.abs(math.distVec3(this._scene.center, camera.eye));
const transposedProjectMat = camera.project.transposedMatrix;
const Pt3 = transposedProjectMat.subarray(8, 12);
const Pt4 = transposedProjectMat.subarray(12);
const D = [0, 0, -1.0, 1];
const screenZ = math.dotVec4(D, Pt3) / math.dotVec4(D, Pt4);
const worldPos = tempVec4a;
camera.project.unproject(canvasPos, screenZ, tempVec4b, tempVec4c, worldPos);
const eyeWorldPosVec = math.normalizeVec3(math.subVec3(worldPos, camera.eye, tempVec3a));
const posOnSphere = math.addVec3(camera.eye, math.mulVec3Scalar(eyeWorldPosVec, pivotShereRadius, tempVec3b), tempVec3c);
this.setPivotPos(posOnSphere);
}
/**
* Gets the current position we're pivoting about.
* @returns {Number[]} The current World-space pivot position.
*/
getPivotPos() {
return (this._pivotPosSet) ? this._pivotWorldPos : this._scene.camera.look; // Avoid pivoting about [0,0,0] by default
}
/**
* Continues to pivot.
*
* @param {Number} yawInc Yaw rotation increment.
* @param {Number} pitchInc Pitch rotation increment.
*/
continuePivot(yawInc, pitchInc) {
if (!this._pivoting) {
return;
}
if (yawInc === 0 && pitchInc === 0) {
return;
}
const camera = this._scene.camera;
var dx = -yawInc;
const dy = -pitchInc;
if (camera.worldUp[2] === 1) {
dx = -dx;
}
this._azimuth += -dx * .01;
this._polar += dy * .01;
this._polar = math.clamp(this._polar, .001, Math.PI - .001);
const pos = [
this._radius * Math.sin(this._polar) * Math.sin(this._azimuth),
this._radius * Math.cos(this._polar),
this._radius * Math.sin(this._polar) * Math.cos(this._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()));
const pivotPos = this.getPivotPos();
math.addVec3(pos, pivotPos);
let lookat = math.lookAtMat4v(pos, pivotPos, camera.worldUp);
lookat = math.inverseMat4(lookat);
const offset = math.transformVec3(lookat, this._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]];
this.showPivot();
}
/**
* Shows the pivot position.
*
* Only works if we set an HTML DOM element to represent the pivot position.
*/
showPivot() {
if (this._shown) {
return;
}
if (this._pivotElement) {
this.updatePivotElement();
this._pivotElement.style.visibility = "visible";
}
if (this._pivotSphereEnabled) {
this.destroyPivotSphere();
this.createPivotSphere();
}
this._shown = true;
}
/**
* Hides the pivot position.
*
* Only works if we set an HTML DOM element to represent the pivot position.
*/
hidePivot() {
if (!this._shown) {
return;
}
if (this._pivotElement) {
this._pivotElement.style.visibility = "hidden";
}
if (this._pivotSphereEnabled) {
this.destroyPivotSphere();
}
this._shown = false;
}
/**
* Finishes pivoting.
*/
endPivot() {
this._pivoting = false;
}
destroy() {
this.destroyPivotSphere();
this._scene.camera.off(this._onViewMatrix);
this._scene.camera.off(this._onProjMatrix);
this._scene.off(this._onTick);
}
}
export {PivotController};