Reference Source

src/viewer/scene/marker/Marker.js

import {math} from '../math/math.js';
import {Component} from '../Component.js';
import {worldToRTCPos} from "../math/rtcCoords.js";
import {SceneModelEntity} from "../model/SceneModelEntity.js";

const tempVec4a = math.vec4();
const tempVec4b = math.vec4();


/**
 * @desc Tracks the World, View and Canvas coordinates, and visibility, of a position within a {@link Scene}.
 *
 * ## Position
 *
 * A Marker holds its position in the World, View and Canvas coordinate systems in three properties:
 *
 * * {@link Marker#worldPos} holds the Marker's 3D World-space coordinates. This property can be dynamically updated. The Marker will fire a "worldPos" event whenever this property changes.
 * * {@link Marker#viewPos} holds the Marker's 3D View-space coordinates. This property is read-only, and is automatically updated from {@link Marker#worldPos} and the current {@link Camera} position. The Marker will fire a "viewPos" event whenever this property changes.
 * * {@link Marker#canvasPos} holds the Marker's 2D Canvas-space coordinates. This property is read-only, and is automatically updated from {@link Marker#canvasPos} and the current {@link Camera} position and projection. The Marker will fire a "canvasPos" event whenever this property changes.
 *
 * ## Visibility
 *
 * {@link Marker#visible} indicates if the Marker is currently visible. The Marker will fire a "visible" event whenever {@link Marker#visible} changes.
 *
 * This property will be ````false```` when:
 *
 * * {@link Marker#entity} is set to an {@link Entity}, and {@link Entity#visible} is ````false````,
 * * {@link Marker#occludable} is ````true```` and the Marker is occluded by some {@link Entity} in the 3D view, or
 * * {@link Marker#canvasPos} is outside the boundary of the {@link Canvas}.
 *
 * ## Usage
 *
 * In the example below, we'll create a Marker that's associated with a {@link Mesh} (which a type of {@link Entity}).
 *
 * We'll configure our Marker to
 * become invisible whenever it's occluded by any Entities in the canvas.
 *
 * We'll also demonstrate how to query the Marker's visibility status and position (in the World, View and
 * Canvas coordinate systems), and how to subscribe to change events on those properties.
 *
 * ````javascript
 * import {Viewer, GLTFLoaderPlugin, Marker} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas"
 * });
 *
 * // Create the torus Mesh
 * // Recall that a Mesh is an Entity
 * new Mesh(viewer.scene, {
 *     geometry: new ReadableGeometry(viewer.scene, buildTorusGeometry({
 *         center: [0,0,0],
 *         radius: 1.0,
 *         tube: 0.5,
 *         radialSegments: 32,
 *         tubeSegments: 24,
 *         arc: Math.PI * 2.0
 *     }),
 *     material: new PhongMaterial(viewer.scene, {
 *         diffuseMap: new Texture(viewer.scene, {
 *             src: "textures/diffuse/uvGrid2.jpg"
 *         }),
 *         backfaces: true
 *     })
 * });
 *
 * // Create the Marker, associated with our Mesh Entity
 * const myMarker = new Marker(viewer, {
 *      entity: entity,
 *      worldPos: [10,0,0],
 *      occludable: true
 * });
 *
 * // Get the Marker's current World, View and Canvas coordinates
 * const worldPos   = myMarker.worldPos;     // 3D World-space position
 * const viewPos    = myMarker.viewPos;      // 3D View-space position
 * const canvasPos  = myMarker.canvasPos;    // 2D Canvas-space position
 *
 * const visible = myMarker.visible;
 *
 * // Listen for change of the Marker's 3D World-space position
 * myMarker.on("worldPos", function(worldPos) {
 *    //...
 * });
 *
 * // Listen for change of the Marker's 3D View-space position, which happens
 * // when either worldPos was updated or the Camera was moved
 * myMarker.on("viewPos", function(viewPos) {
 *    //...
 * });
 *
 * // Listen for change of the Marker's 2D Canvas-space position, which happens
 * // when worldPos or viewPos was updated, or Camera's projection was updated
 * myMarker.on("canvasPos", function(canvasPos) {
 *    //...
 * });
 *
 * // Listen for change of Marker visibility. The Marker becomes invisible when it falls outside the canvas,
 * // has an Entity that is also invisible, or when an Entity occludes the Marker's position in the 3D view.
 * myMarker.on("visible", function(visible) { // Marker visibility has changed
 *    if (visible) {
 *        this.log("Marker is visible");
 *    } else {
 *        this.log("Marker is invisible");
 *    }
 * });
 *
 * // Listen for destruction of Marker
 * myMarker.on("destroyed", () => {
 *      //...
 * });
 * ````
 */
class Marker extends Component {

    /**
     * @constructor
     * @param {Component} [owner]  Owner component. When destroyed, the owner will destroy this Marker as well.
     * @param {*} [cfg]  Marker configuration
     * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
     * @param {Entity} [cfg.entity] Entity to associate this Marker with. When the Marker has an Entity, then {@link Marker#visible} will always be ````false```` if {@link Entity#visible} is false.
     * @param {Boolean} [cfg.occludable=false] Indicates whether or not this Marker is hidden (ie. {@link Marker#visible} is ````false```` whenever occluded by {@link Entity}s in the {@link Scene}.
     * @param {Number[]} [cfg.worldPos=[0,0,0]] World-space 3D Marker position.
     */
    constructor(owner, cfg) {

        super(owner, cfg);

        this._entity = null;
        this._visible = null;
        this._worldPos = math.vec3();
        this._origin = math.vec3();
        this._rtcPos = math.vec3();
        this._viewPos = math.vec3();
        this._canvasPos = math.vec2();
        this._occludable = false;

        this._onCameraViewMatrix = this.scene.camera.on("matrix", () => {
            this._viewPosDirty = true;
            this._needUpdate();
        });

        this._onCameraProjMatrix = this.scene.camera.on("projMatrix", () => {
            this._canvasPosDirty = true;
            this._needUpdate();
        });

        this._onEntityDestroyed = null;
        this._onEntityModelDestroyed = null;

        this._renderer.addMarker(this);

        this.entity = cfg.entity;
        this.worldPos = cfg.worldPos;
        this.occludable = cfg.occludable;
    }

    _update() { // this._needUpdate() schedules this for next tick
        if (this._viewPosDirty) {
            math.transformPoint3(this.scene.camera.viewMatrix, this._worldPos, this._viewPos);
            this._viewPosDirty = false;
            this._canvasPosDirty = true;
            this.fire("viewPos", this._viewPos);
        }
        if (this._canvasPosDirty) {
            tempVec4a.set(this._viewPos);
            tempVec4a[3] = 1.0;
            math.transformPoint4(this.scene.camera.projMatrix, tempVec4a, tempVec4b);
            const aabb = this.scene.canvas.boundary;
            this._canvasPos[0] = Math.floor((1 + tempVec4b[0] / tempVec4b[3]) * aabb[2] / 2);
            this._canvasPos[1] = Math.floor((1 - tempVec4b[1] / tempVec4b[3]) * aabb[3] / 2);
            this._canvasPosDirty = false;
            this.fire("canvasPos", this._canvasPos);
        }
    }

    _setVisible(visible) { // Called by VisibilityTester and this._entity.on("destroyed"..)
        if (this._visible === visible) {
            //  return;
        }
        this._visible = visible;
        this.fire("visible", this._visible);
    }

    /**
     * Sets the {@link Entity} this Marker is associated with.
     *
     * An Entity is optional. When the Marker has an Entity, then {@link Marker#visible} will always be ````false````
     * if {@link Entity#visible} is false.
     *
     * @type {Entity}
     */
    set entity(entity) {
        if (this._entity) {
            if (this._entity === entity) {
                return;
            }
            if (this._onEntityDestroyed !== null) {
                if (this._entity.model) {
                    this._entity.model.off(this._onEntityDestroyed);
                } else {
                    this._entity.off(this._onEntityDestroyed);
                }
                this._onEntityDestroyed = null;
            }
            if (this._onEntityModelDestroyed !== null) {
                if (this._entity.model) {
                    this._entity.model.off(this._onEntityModelDestroyed);
                }
                this._onEntityModelDestroyed = null;
            }
        }
        this._entity = entity;
        if (this._entity) {
            if (this._entity instanceof SceneModelEntity) {
                this._onEntityModelDestroyed = this._entity.model.on("destroyed", () => { // SceneModelEntity does not fire events, and cannot exist beyond its VBOSceneModel
                    this._entity = null; // Marker now may become visible, if it was synched to invisible Entity
                    this._onEntityModelDestroyed = null;
                });
            } else {
                this._onEntityDestroyed = this._entity.on("destroyed", () => {
                    this._entity = null;
                    this._onEntityDestroyed = null;
                });
            }
        }
        this.fire("entity", this._entity, true /* forget */);
    }

    /**
     * Gets the {@link Entity} this Marker is associated with.
     *
     * @type {Entity}
     */
    get entity() {
        return this._entity;
    }

    /**
     * Sets whether occlusion testing is performed for this Marker.
     *
     * When this is ````true````, then {@link Marker#visible} will be ````false```` whenever the Marker is occluded by an {@link Entity} in the 3D view.
     *
     * The {@link Scene} periodically occlusion-tests all Markers on every 20th "tick" (which represents a rendered frame). We
     * can adjust that frequency via property {@link Scene#ticksPerOcclusionTest}.
     *
     * @type {Boolean}
     */
    set occludable(occludable) {
        occludable = !!occludable;
        if (occludable === this._occludable) {
            return;
        }
        this._occludable = occludable;
        if (this._occludable) {
            this._renderer.markerWorldPosUpdated(this);
        }
    }

    /**
     * Gets whether occlusion testing is performed for this Marker.
     *
     * When this is ````true````, then {@link Marker#visible} will be ````false```` whenever the Marker is occluded by an {@link Entity} in the 3D view.
     *
     * @type {Boolean}
     */
    get occludable() {
        return this._occludable;
    }

    /**
     * Sets the World-space 3D position of this Marker.
     *
     * Fires a "worldPos" event with new World position.
     *
     * @type {Number[]}
     */
    set worldPos(worldPos) {
        this._worldPos.set(worldPos || [0, 0, 0]);
        worldToRTCPos(this._worldPos, this._origin, this._rtcPos);
        if (this._occludable) {
            this._renderer.markerWorldPosUpdated(this);
        }
        this._viewPosDirty = true;
        this.fire("worldPos", this._worldPos);
        this._needUpdate();
    }

    /**
     * Gets the World-space 3D position of this Marker.
     *
     * @type {Number[]}
     */
    get worldPos() {
        return this._worldPos;
    }

    /**
     * Gets the RTC center of this Marker.
     *
     * This is automatically calculated from {@link Marker#worldPos}.
     *
     * @type {Number[]}
     */
    get origin() {
        return this._origin;
    }

    /**
     * Gets the RTC position of this Marker.
     *
     * This is automatically calculated from {@link Marker#worldPos}.
     *
     * @type {Number[]}
     */
    get rtcPos() {
        return this._rtcPos;
    }

    /**
     * View-space 3D coordinates of this Marker.
     *
     * This property is read-only and is automatically calculated from {@link Marker#worldPos} and the current {@link Camera} position.
     *
     * The Marker fires a "viewPos" event whenever this property changes.
     *
     * @type {Number[]}
     * @final
     */
    get viewPos() {
        this._update();
        return this._viewPos;
    }

    /**
     * Canvas-space 2D coordinates of this Marker.
     *
     * This property is read-only and is automatically calculated from {@link Marker#worldPos} and the current {@link Camera} position and projection.
     *
     * The Marker fires a "canvasPos" event whenever this property changes.
     *
     * @type {Number[]}
     * @final
     */
    get canvasPos() {
        this._update();
        return this._canvasPos;
    }

    /**
     * Indicates if this Marker is currently visible.
     *
     * This is read-only and is automatically calculated.
     *
     * The Marker is **invisible** whenever:
     *
     * * {@link Marker#canvasPos} is currently outside the canvas,
     * * {@link Marker#entity} is set to an {@link Entity} that has {@link Entity#visible} ````false````, or
     * * or {@link Marker#occludable} is ````true```` and the Marker is currently occluded by an Entity in the 3D view.
     *
     * The Marker fires a "visible" event whenever this property changes.
     *
     * @type {Boolean}
     * @final
     */
    get visible() {
        return !!this._visible;
    }

    /**
     * Destroys this Marker.
     */
    destroy() {
        this.fire("destroyed", true);
        this.scene.camera.off(this._onCameraViewMatrix);
        this.scene.camera.off(this._onCameraProjMatrix);
        if (this._entity) {
            if (this._onEntityDestroyed !== null) {
                this._entity.model.off(this._onEntityDestroyed);
            }
            if (this._onEntityModelDestroyed !== null) {
                this._entity.model.off(this._onEntityModelDestroyed);
            }
        }
        this._renderer.removeMarker(this);
        super.destroy();
    }
}

export {Marker};