Reference Source

src/viewer/scene/marker/SpriteMarker.js

import {Mesh} from "../mesh/Mesh.js";
import {ReadableGeometry} from "../geometry/ReadableGeometry.js";
import {PhongMaterial, Texture} from "../materials/index.js";
import {math} from "../math/math.js";
import {Marker} from "./Marker.js";

/**
 * A {@link Marker} with a billboarded and textured quad attached to it.
 *
 * * Extends {@link Marker}
 * * Keeps the quad oriented towards the viewpoint
 * * Auto-fits the quad to the texture
 * * Has a world-space position
 * * Can be configured to hide the quad whenever the position is occluded by some other object
 *
 * ## Usage
 *
 * [[Run this example](/examples/index.html#markers_SpriteMarker)]
 *
 * ```` javascript
 * import {Viewer, SpriteMarker } from "./https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/xeokit-sdk.es.min.js";
 *
 * const viewer = new Viewer({
 *        canvasId: "myCanvas",
 *      transparent: true
 *  });
 *
 * viewer.scene.camera.eye = [0, 0, 25];
 * viewer.scene.camera.look = [0, 0, 0];
 * viewer.scene.camera.up = [0, 1, 0];
 *
 * new SpriteMarker(viewer.scene, {
 *      worldPos: [-10, 0, 0],
 *      src: "../assets/textures/diffuse/uvGrid2_512x1024.jpg",
 *      size: 5,
 *      occludable: false
 *  });
 *
 * new SpriteMarker(viewer.scene, {
 *      worldPos: [+10, 0, 0],
 *      src: "../assets/textures/diffuse/uvGrid2_1024x512.jpg",
 *      size: 4,
 *      occludable: false
 *  });
 *````
 */
class SpriteMarker extends Marker {

    /**
     * @constructor
     * @param {Component} owner Owner component. When destroyed, the owner will destroy this SpriteMarker as well.
     * @param {*} [cfg] Configs
     * @param {String} [cfg.id] Optional ID for this SpriteMarker, unique among all components in the parent scene, generated automatically when omitted.
     * @param {Entity} [cfg.entity] Entity to associate this Marker with. When the SpriteMarker 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.
     * @param {String} [cfg.src=null] Path to image file to load into this SpriteMarker. See the {@link SpriteMarker#src} property for more info.
     * @param {HTMLImageElement} [cfg.image=null] HTML Image object to load into this SpriteMarker. See the {@link SpriteMarker#image} property for more info.
     * @param {Boolean} [cfg.flipY=false] Flips this SpriteMarker's texture image along its vertical axis when true.
     * @param  {String} [cfg.encoding="linear"] Texture encoding format.  See the {@link Texture#encoding} property for more info.
     */
    constructor(owner, cfg = {}) {

        super(owner, {
            entity: cfg.entity,
            occludable: cfg.occludable,
            worldPos: cfg.worldPos
        });

        this._occluded = false;
        this._visible = true;
        this._src = null;
        this._image = null;
        this._pos = math.vec3();
        this._origin = math.vec3();
        this._rtcPos = math.vec3();
        this._dir = math.vec3();
        this._size = 1.0;
        this._imageSize = math.vec2();

        this._texture = new Texture(this, {
            src: cfg.src
        });

        this._geometry = new ReadableGeometry(this, {
            primitive: "triangles",
            positions: [3, 3, 0, -3, 3, 0, -3, -3, 0, 3, -3, 0],
            normals: [-1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0],
            uv: [1, -1, 0, -1, 0, 0, 1, 0],
            indices: [0, 1, 2, 0, 2, 3] // Ensure these will be front-faces
        });

        this._mesh = new Mesh(this, {
            geometry: this._geometry,
            material: new PhongMaterial(this, {
                ambient: [0.9, 0.3, 0.9],
                shininess: 30,
                diffuseMap: this._texture,
                backfaces: true
            }),
            scale: [1, 1, 1], // Note: by design, scale does not work with billboard
            position: cfg.worldPos,
            rotation: [90, 0, 0],
            billboard: "spherical",
            occluder: false // Don't occlude SpriteMarkers or Annotations
        });

        this.visible = true;
        this.collidable = cfg.collidable;
        this.clippable = cfg.clippable;
        this.pickable = cfg.pickable;
        this.opacity = cfg.opacity;
        this.size = cfg.size;

        if (cfg.image) {
            this.image = cfg.image;
        } else {
            this.src = cfg.src;
        }
    }

    _setVisible(visible) { // Called by VisibilityTester and this._entity.on("destroyed"..)
        this._occluded = (!visible);
        this._mesh.visible = this._visible && (!this._occluded);
        super._setVisible(visible);
    }

    /**
     * Sets if this ````SpriteMarker```` is visible or not.
     *
     * Default value is ````true````.
     *
     * @param {Boolean} visible Set ````true```` to make this ````SpriteMarker```` visible.
     */
    set visible(visible) {
        this._visible = (visible === null || visible === undefined) ? true : visible;
        this._mesh.visible = this._visible && (!this._occluded);
    }

    /**
     * Gets if this ````SpriteMarker```` is visible or not.
     *
     * Default value is ````true````.
     *
     * @returns {Boolean} Returns ````true```` if visible.
     */
    get visible() {
        return this._visible;
    }

    /**
     * Sets an ````HTMLImageElement```` to source the image from.
     *
     * Sets {@link Texture#src} null.
     *
     * @type {HTMLImageElement}
     */
    set image(image) {
        this._image = image;
        if (this._image) {
            this._imageSize[0] = this._image.width;
            this._imageSize[1] = this._image.height;
            this._updatePlaneSizeFromImage();
            this._src = null;
            this._texture.image = this._image;
        }
    }

    /**
     * Gets the ````HTMLImageElement```` the ````SpriteMarker````'s image is sourced from, if set.
     *
     * Returns null if not set.
     *
     * @type {HTMLImageElement}
     */
    get image() {
        return this._image;
    }

    /**
     * Sets an image file path that the ````SpriteMarker````'s image is sourced from.
     *
     * Accepted file types are PNG and JPEG.
     *
     * Sets {@link Texture#image} null.
     *
     * @type {String}
     */
    set src(src) {
        this._src = src;
        if (this._src) {
            this._image = null;
            const image = new Image();
            image.onload = () => {
                this._texture.image = image;
                this._imageSize[0] = image.width;
                this._imageSize[1] = image.height;
                this._updatePlaneSizeFromImage();
            };
            image.src = this._src;
        }
    }

    /**
     * Gets the image file path that the ````SpriteMarker````'s image is sourced from, if set.
     *
     * Returns null if not set.
     *
     * @type {String}
     */
    get src() {
        return this._src;
    }

    /**
     * Sets the World-space size of the longest edge of the ````SpriteMarker````.
     *
     * Note that ````SpriteMarker```` sets its aspect ratio to match its image. If we set a value of ````1000````, and
     * the image has size ````400x300````, then the ````SpriteMarker```` will then have size ````1000 x 750````.
     *
     * Default value is ````1.0````.
     *
     * @param {Number} size New World-space size of the ````SpriteMarker````.
     */
    set size(size) {
        this._size = (size === undefined || size === null) ? 1.0 : size;
        if (this._image) {
            this._updatePlaneSizeFromImage()
        }
    }

    /**
     * Gets the World-space size of the longest edge of the ````SpriteMarker````.
     *
     * Returns {Number} World-space size of the ````SpriteMarker````.
     */
    get size() {
        return this._size;
    }

    /**
     * Sets if this ````SpriteMarker```` is included in boundary calculations.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    set collidable(value) {
        this._mesh.collidable = (value !== false);
    }

    /**
     * Gets if this ````SpriteMarker```` is included in boundary calculations.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    get collidable() {
        return this._mesh.collidable;
    }

    /**
     * Sets if this ````SpriteMarker```` is clippable.
     *
     * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    set clippable(value) {
        this._mesh.clippable = (value !== false);
    }

    /**
     * Gets if this ````SpriteMarker````  is clippable.
     *
     * Clipping is done by the {@link SectionPlane}s in {@link Scene#sectionPlanes}.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    get clippable() {
        return this._mesh.clippable;
    }

    /**
     * Sets if this ````SpriteMarker```` is pickable.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    set pickable(value) {
        this._mesh.pickable = (value !== false);
    }

    /**
     * Gets if this ````SpriteMarker````  is pickable.
     *
     * Default is ````true````.
     *
     * @type {Boolean}
     */
    get pickable() {
        return this._mesh.pickable;
    }

    /**
     * Sets the opacity factor for this ````SpriteMarker````.
     *
     * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas.
     *
     * @type {Number}
     */
    set opacity(opacity) {
        this._mesh.opacity = opacity;
    }

    /**
     * Gets this ````SpriteMarker````'s opacity factor.
     *
     * This is a factor in range ````[0..1]```` which multiplies by the rendered fragment alphas.
     *
     * @type {Number}
     */
    get opacity() {
        return this._mesh.opacity;
    }

    _updatePlaneSizeFromImage() {
        const halfSize = this._size * 0.5;
        const width = this._imageSize[0];
        const height = this._imageSize[1];
        const aspect = height / width;
        if (width > height) {
            this._geometry.positions = [
                halfSize, halfSize * aspect, 0,
                -halfSize, halfSize * aspect, 0,
                -halfSize, -halfSize * aspect, 0,
                halfSize, -halfSize * aspect, 0
            ];
        } else {
            this._geometry.positions = [
                halfSize / aspect, halfSize, 0,
                -halfSize / aspect, halfSize, 0,
                -halfSize / aspect, -halfSize, 0,
                halfSize / aspect, -halfSize, 0
            ];
        }
    }
}

export {SpriteMarker};