UNPKG

@xeokit/xeokit-sdk

Version:

3D BIM IFC Viewer SDK for AEC engineering applications. Open Source JavaScript Toolkit based on pure WebGL for top performance, real-world coordinates and full double precision

670 lines (567 loc) 22.8 kB
import {math} from '../math/math.js'; import {utils} from '../utils.js'; import {core} from '../core.js'; import {Component} from '../Component.js'; const tempVec3 = math.vec3(); const newLook = math.vec3(); const newEye = math.vec3(); const newUp = math.vec3(); const newLookEyeVec = math.vec3(); /** * @desc Jumps or flies the {@link Scene}'s {@link Camera} to a given target. * * * Located at {@link Viewer#cameraFlight} * * Can fly or jump to its target. * * While flying, can be stopped, or redirected to a different target. * * Can also smoothly transition between ortho and perspective projections. * * * A CameraFlightAnimation's target can be: * * * specific ````eye````, ````look```` and ````up```` positions, * * an axis-aligned World-space bounding box (AABB), or * * an instance or ID of any {@link Component} subtype that provides a World-space AABB. * * A target can also contain a ````projection```` type to transition into. For example, if your {@link Camera#projection} is * currently ````"perspective"```` and you supply {@link CameraFlightAnimation#flyTo} with a ````projection```` property * equal to "ortho", then CameraFlightAnimation will smoothly transition the Camera into an orthographic projection. * * Configure {@link CameraFlightAnimation#fit} and {@link CameraFlightAnimation#fitFOV} to make it stop at the point * where the target occupies a certain amount of the field-of-view. * * ## Flying to an Entity * * Flying to an {@link Entity}: * * ````Javascript * var entity = new Mesh(viewer.scene); * * // Fly to the Entity's World-space AABB * viewer.cameraFlight.flyTo(entity); * ```` * ## Flying to a Position * * Flying the CameraFlightAnimation from the previous example to specified eye, look and up positions: * * ````Javascript * viewer.cameraFlight.flyTo({ * eye: [-5,-5,-5], * look: [0,0,0] * up: [0,1,0], * duration: 1 // Default, seconds * },() => { * // Done * }); * ```` * * ## Flying to an AABB * * Flying the CameraFlightAnimation from the previous two examples explicitly to the {@link Boundary3D"}}Boundary3D's{{/crossLink}} * axis-aligned bounding box: * * ````Javascript * viewer.cameraFlight.flyTo(entity.aabb); * ```` * * ## Transitioning Between Projections * * CameraFlightAnimation also allows us to smoothly transition between Camera projections. We can do that by itself, or * in addition to flying the Camera to a target. * * Let's transition the Camera to orthographic projection: * * [[Run example](/examples/index.html#camera_CameraFlightAnimation_projection)] * * ````Javascript * viewer.cameraFlight.flyTo({ projection: "ortho", () => { * // Done * }); * ```` * * Now let's transition the Camera back to perspective projection: * * ````Javascript * viewer.cameraFlight.flyTo({ projection: "perspective"}, () => { * // Done * }); * ```` * * Fly Camera to a position, while transitioning to orthographic projection: * * ````Javascript * viewer.cameraFlight.flyTo({ * eye: [-100,20,2], * look: [0,0,-40], * up: [0,1,0], * projection: "ortho", () => { * // Done * }); * ```` */ class CameraFlightAnimation extends Component { /** * @private */ get type() { return "CameraFlightAnimation"; } /** @constructor @private */ constructor(owner, cfg = {}) { super(owner, cfg); this._look1 = math.vec3(); this._eye1 = math.vec3(); this._up1 = math.vec3(); this._look2 = math.vec3(); this._eye2 = math.vec3(); this._up2 = math.vec3(); this._orthoScale1 = 1; this._orthoScale2 = 1; this._flying = false; this._flyEyeLookUp = false; this._flyingEye = false; this._flyingLook = false; this._callback = null; this._callbackScope = null; this._time1 = null; this._time2 = null; this.easing = cfg.easing !== false; this.duration = cfg.duration; this.fit = cfg.fit; this.fitFOV = cfg.fitFOV; this.trail = cfg.trail; } /** * Flies the {@link Camera} to a target. * * * When the target is a boundary, the {@link Camera} will fly towards the target and stop when the target fills most of the canvas. * * When the target is an explicit {@link Camera} position, given as ````eye````, ````look```` and ````up````, then CameraFlightAnimation will interpolate the {@link Camera} to that target and stop there. * * @param {Object|Component} [params=Scene] Either a parameters object or a {@link Component} subtype that has * an AABB. Defaults to the {@link Scene}, which causes the {@link Camera} to fit the Scene in view. * @param {Number} [params.arc=0] Factor in range ````[0..1]```` indicating how much the {@link Camera#eye} position * will swing away from its {@link Camera#look} position as it flies to the target. * @param {Number|String|Component} [params.component] ID or instance of a component to fly to. Defaults to the entire {@link Scene}. * @param {Number[]} [params.aabb] World-space axis-aligned bounding box (AABB) target to fly to. * @param {Number[]} [params.eye] Position to fly the eye position to. * @param {Number[]} [params.look] Position to fly the look position to. * @param {Number[]} [params.up] Position to fly the up vector to. * @param {String} [params.projection] Projection type to transition into as we fly. Can be any of the values of {@link Camera.projection}. * @param {Boolean} [params.fit=true] Whether to fit the target to the view volume. Overrides {@link CameraFlightAnimation#fit}. * @param {Number} [params.fitFOV] How much of field-of-view, in degrees, that a target {@link Entity} or its AABB should * fill the canvas on arrival. Overrides {@link CameraFlightAnimation#fitFOV}. * @param {Number} [params.duration] Flight duration in seconds. Overrides {@link CameraFlightAnimation#duration}. * @param {Number} [params.orthoScale] Animate the Camera's orthographic scale to this target value. See {@link Ortho#scale}. * @param {Function} [callback] Callback fired on arrival. * @param {Object} [scope] Optional scope for callback. */ flyTo(params, callback, scope) { params = params || this.scene; if (this._flying) { this.stop(); } this._flying = false; this._flyingEye = false; this._flyingLook = false; this._flyingEyeLookUp = false; this._callback = callback; this._callbackScope = scope; const camera = this.scene.camera; const flyToProjection = (!!params.projection) && (params.projection !== camera.projection); this._eye1[0] = camera.eye[0]; this._eye1[1] = camera.eye[1]; this._eye1[2] = camera.eye[2]; this._look1[0] = camera.look[0]; this._look1[1] = camera.look[1]; this._look1[2] = camera.look[2]; this._up1[0] = camera.up[0]; this._up1[1] = camera.up[1]; this._up1[2] = camera.up[2]; this._orthoScale1 = camera.ortho.scale; this._orthoScale2 = params.orthoScale || this._orthoScale1; let aabb; let eye; let look; let up; let componentId; if (params.aabb) { aabb = params.aabb; } else if (params.length === 6) { aabb = params; } else if ((params.eye && params.look) || params.up) { eye = params.eye; look = params.look; up = params.up; } else if (params.eye) { eye = params.eye; } else if (params.look) { look = params.look; } else { // Argument must be an instance or ID of a Component (subtype) let component = params; if (utils.isNumeric(component) || utils.isString(component)) { componentId = component; component = this.scene.components[componentId]; if (!component) { this.error("Component not found: " + utils.inQuotes(componentId)); if (callback) { if (scope) { callback.call(scope); } else { callback(); } } return; } } if (!flyToProjection) { aabb = component.aabb || this.scene.aabb; } } const poi = params.poi; if (aabb) { if (aabb[3] < aabb[0] || aabb[4] < aabb[1] || aabb[5] < aabb[2]) { // Don't fly to an inverted boundary return; } if (aabb[3] === aabb[0] && aabb[4] === aabb[1] && aabb[5] === aabb[2]) { // Don't fly to an empty boundary return; } aabb = aabb.slice(); const aabbCenter = math.getAABB3Center(aabb); this._look2 = poi || aabbCenter; const eyeLookVec = math.subVec3(this._eye1, this._look1, tempVec3); const eyeLookVecNorm = math.normalizeVec3(eyeLookVec); const diag = poi ? math.getAABB3DiagPoint(aabb, poi) : math.getAABB3Diag(aabb); const fitFOV = params.fitFOV || this._fitFOV; const sca = Math.abs(diag / Math.tan(fitFOV * math.DEGTORAD)); this._orthoScale2 = diag * 1.1; this._eye2[0] = this._look2[0] + (eyeLookVecNorm[0] * sca); this._eye2[1] = this._look2[1] + (eyeLookVecNorm[1] * sca); this._eye2[2] = this._look2[2] + (eyeLookVecNorm[2] * sca); this._up2[0] = this._up1[0]; this._up2[1] = this._up1[1]; this._up2[2] = this._up1[2]; this._flyingEyeLookUp = true; } else if (eye || look || up) { this._flyingEyeLookUp = !!eye && !!look && !!up; this._flyingEye = !!eye && !look; this._flyingLook = !!look && !eye; if (eye) { this._eye2[0] = eye[0]; this._eye2[1] = eye[1]; this._eye2[2] = eye[2]; } if (look) { this._look2[0] = look[0]; this._look2[1] = look[1]; this._look2[2] = look[2]; } if (up) { this._up2[0] = up[0]; this._up2[1] = up[1]; this._up2[2] = up[2]; } } if (flyToProjection) { if (params.projection === "ortho" && camera.projection !== "ortho") { this._projection2 = "ortho"; this._projMatrix1 = camera.projMatrix.slice(); this._projMatrix2 = camera.ortho.matrix.slice(); camera.projection = "customProjection"; } if (params.projection === "perspective" && camera.projection !== "perspective") { this._projection2 = "perspective"; this._projMatrix1 = camera.projMatrix.slice(); this._projMatrix2 = camera.perspective.matrix.slice(); camera.projection = "customProjection"; } } else { this._projection2 = null; } this.fire("started", params, true); this._time1 = Date.now(); this._time2 = this._time1 + (Number.isFinite(params.duration) ? params.duration * 1000 : this._duration); this._flying = true; // False as soon as we stop core.scheduleTask(this._update, this); } /** * Jumps the {@link Scene}'s {@link Camera} to the given target. * * * When the target is a boundary, this CameraFlightAnimation will position the {@link Camera} at where the target fills most of the canvas. * * When the target is an explicit {@link Camera} position, given as ````eye````, ````look```` and ````up```` vectors, then this CameraFlightAnimation will jump the {@link Camera} to that target. * * @param {*|Component} params Either a parameters object or a {@link Component} subtype that has a World-space AABB. * @param {Number} [params.arc=0] Factor in range [0..1] indicating how much the {@link Camera#eye} will swing away from its {@link Camera#look} as it flies to the target. * @param {Number|String|Component} [params.component] ID or instance of a component to fly to. * @param {Number[]} [params.aabb] World-space axis-aligned bounding box (AABB) target to fly to. * @param {Number[]} [params.eye] Position to fly the eye position to. * @param {Number[]} [params.look] Position to fly the look position to. * @param {Number[]} [params.up] Position to fly the up vector to. * @param {String} [params.projection] Projection type to transition into. Can be any of the values of {@link Camera.projection}. * @param {Number} [params.fitFOV] How much of field-of-view, in degrees, that a target {@link Entity} or its AABB should fill the canvas on arrival. Overrides {@link CameraFlightAnimation#fitFOV}. * @param {Boolean} [params.fit] Whether to fit the target to the view volume. Overrides {@link CameraFlightAnimation#fit}. */ jumpTo(params) { this._jumpTo(params); } _jumpTo(params) { if (this._flying) { this.stop(); } const camera = this.scene.camera; var aabb; var componentId; var newEye; var newLook; var newUp; if (params.aabb) { // Boundary3D aabb = params.aabb; } else if (params.length === 6) { // AABB aabb = params; } else if (params.eye || params.look || params.up) { // Camera pose newEye = params.eye; newLook = params.look; newUp = params.up; } else { // Argument must be an instance or ID of a Component (subtype) let component = params; if (utils.isNumeric(component) || utils.isString(component)) { componentId = component; component = this.scene.components[componentId]; if (!component) { this.error("Component not found: " + utils.inQuotes(componentId)); return; } } aabb = component.aabb || this.scene.aabb; } const poi = params.poi; if (aabb) { if (aabb[3] <= aabb[0] || aabb[4] <= aabb[1] || aabb[5] <= aabb[2]) { // Don't fly to an empty boundary return; } var diag = poi ? math.getAABB3DiagPoint(aabb, poi) : math.getAABB3Diag(aabb); newLook = poi || math.getAABB3Center(aabb, newLook); if (this._trail) { math.subVec3(camera.look, newLook, newLookEyeVec); } else { math.subVec3(camera.eye, camera.look, newLookEyeVec); } math.normalizeVec3(newLookEyeVec); let dist; const fit = (params.fit !== undefined) ? params.fit : this._fit; if (fit) { dist = Math.abs((diag) / Math.tan((params.fitFOV || this._fitFOV) * math.DEGTORAD)); } else { dist = math.lenVec3(math.subVec3(camera.eye, camera.look, tempVec3)); } math.mulVec3Scalar(newLookEyeVec, dist); camera.eye = math.addVec3(newLook, newLookEyeVec, tempVec3); camera.look = newLook; this.scene.camera.ortho.scale = diag * 1.1; } else if (newEye || newLook || newUp) { if (newEye) { camera.eye = newEye; } if (newLook) { camera.look = newLook; } if (newUp) { camera.up = newUp; } } if (params.projection) { camera.projection = params.projection; } } _update() { if (!this._flying) { return; } const time = Date.now(); let t = (time - this._time1) / (this._time2 - this._time1); const stopping = (t >= 1); if (t > 1) { t = 1; } const tFlight = this.easing ? CameraFlightAnimation._ease(t, 0, 1, 1) : t; const camera = this.scene.camera; if (this._flyingEye || this._flyingLook) { if (this._flyingEye) { math.subVec3(camera.eye, camera.look, newLookEyeVec); camera.eye = math.lerpVec3(tFlight, 0, 1, this._eye1, this._eye2, newEye); camera.look = math.subVec3(newEye, newLookEyeVec, newLook); } else if (this._flyingLook) { camera.look = math.lerpVec3(tFlight, 0, 1, this._look1, this._look2, newLook); camera.up = math.lerpVec3(tFlight, 0, 1, this._up1, this._up2, newUp); } } else if (this._flyingEyeLookUp) { camera.eye = math.lerpVec3(tFlight, 0, 1, this._eye1, this._eye2, newEye); camera.look = math.lerpVec3(tFlight, 0, 1, this._look1, this._look2, newLook); camera.up = math.lerpVec3(tFlight, 0, 1, this._up1, this._up2, newUp); } if (this._projection2) { const tProj = (this._projection2 === "ortho") ? CameraFlightAnimation._easeOutExpo(t, 0, 1, 1) : CameraFlightAnimation._easeInCubic(t, 0, 1, 1); camera.customProjection.matrix = math.lerpMat4(tProj, 0, 1, this._projMatrix1, this._projMatrix2); } else { camera.ortho.scale = this._orthoScale1 + (t * (this._orthoScale2 - this._orthoScale1)); } if (stopping) { camera.ortho.scale = this._orthoScale2; this.stop(); return; } core.scheduleTask(this._update, this); // Keep flying } static _ease(t, b, c, d) { // Quadratic easing out - decelerating to zero velocity http://gizma.com/easing t /= d; return -c * t * (t - 2) + b; } static _easeInCubic(t, b, c, d) { t /= d; return c * t * t * t + b; } static _easeOutExpo(t, b, c, d) { return c * (-Math.pow(2, -10 * t / d) + 1) + b; } /** * Stops an earlier flyTo, fires arrival callback. */ stop() { if (!this._flying) { return; } this._flying = false; this._time1 = null; this._time2 = null; if (this._projection2) { this.scene.camera.projection = this._projection2; } const callback = this._callback; if (callback) { this._callback = null; if (this._callbackScope) { callback.call(this._callbackScope); } else { callback(); } } this.fire("stopped", true, true); } /** * Cancels an earlier flyTo without calling the arrival callback. */ cancel() { if (!this._flying) { return; } this._flying = false; this._time1 = null; this._time2 = null; if (this._callback) { this._callback = null; } this.fire("canceled", true, true); } /** * Sets the flight duration, in seconds, when calling {@link CameraFlightAnimation#flyTo}. * * Stops any flight currently in progress. * * default value is ````0.5````. * * @param {Number} value New duration value. */ set duration(value) { this._duration = Number.isFinite(value) ? (value * 1000.0) : 500; this.stop(); } /** * Gets the flight duration, in seconds, when calling {@link CameraFlightAnimation#flyTo}. * * default value is ````0.5````. * * @returns {Number} New duration value. */ get duration() { return this._duration / 1000.0; } /** * Sets if, when CameraFlightAnimation is flying to a boundary, it will always adjust the distance between the * {@link Camera#eye} and {@link Camera#look} so as to ensure that the target boundary is always filling the view volume. * * When false, the eye will remain at its current distance from the look position. * * Default value is ````true````. * * @param {Boolean} value Set ````true```` to activate this behaviour. */ set fit(value) { this._fit = value !== false; } /** * Gets if, when CameraFlightAnimation is flying to a boundary, it will always adjust the distance between the * {@link Camera#eye} and {@link Camera#look} so as to ensure that the target boundary is always filling the view volume. * * When false, the eye will remain at its current distance from the look position. * * Default value is ````true````. * * @returns {Boolean} value Set ````true```` to activate this behaviour. */ get fit() { return this._fit; } /** * Sets how much of the perspective field-of-view, in degrees, that a target {@link Entity#aabb} should * fill the canvas when calling {@link CameraFlightAnimation#flyTo} or {@link CameraFlightAnimation#jumpTo}. * * Default value is ````45````. * * @param {Number} value New FOV value. */ set fitFOV(value) { this._fitFOV = value || 45; } /** * Gets how much of the perspective field-of-view, in degrees, that a target {@link Entity#aabb} should * fill the canvas when calling {@link CameraFlightAnimation#flyTo} or {@link CameraFlightAnimation#jumpTo}. * * Default value is ````45````. * * @returns {Number} Current FOV value. */ get fitFOV() { return this._fitFOV; } /** * Sets if this CameraFlightAnimation to point the {@link Camera} * in the direction that it is travelling when flying to a target after calling {@link CameraFlightAnimation#flyTo}. * * Default value is ````true````. * * @param {Boolean} value Set ````true```` to activate trailing behaviour. */ set trail(value) { this._trail = !!value; } /** * Gets if this CameraFlightAnimation points the {@link Camera} * in the direction that it is travelling when flying to a target after calling {@link CameraFlightAnimation#flyTo}. * * Default value is ````true````. * * @returns {Boolean} True if trailing behaviour is active. */ get trail() { return this._trail; } /** * @private */ destroy() { this.stop(); super.destroy(); } } export {CameraFlightAnimation};