Reference Source

src/viewer/scene/ImagePlane/ImagePlane.js

import {Component} from '../Component.js';
import {Node} from "../nodes/Node.js";
import {Mesh} from "../mesh/Mesh.js";
import {PhongMaterial} from "../materials/PhongMaterial.js";
import {buildPlaneGeometry} from "../geometry/builders/buildPlaneGeometry.js";
import {Texture} from "../materials/Texture.js";
import {buildGridGeometry} from "../geometry/builders/buildGridGeometry.js";
import {ReadableGeometry} from "../geometry/ReadableGeometry.js";
import {math} from "../math/math.js";
import {worldToRTCPos} from "../math/rtcCoords.js";

const tempVec3 = math.vec3();
const tempVec3b = math.vec3();
const tempVec3c = math.vec3();
const zeroVec = math.vec3([0, -1, 0]);
const tempQuat = math.vec4([0, 0, 0, 1]);

/**
 *  @desc A plane-shaped 3D object containing a bitmap image.
 *
 * Use ````ImagePlane```` to embed bitmap images in your scenes.
 *
 * As shown in the examples below, ````ImagePlane```` is useful for creating ground planes from satellite maps and embedding 2D plan
 * view images in cross-section slicing planes.
 *
 * # Example 1: Create a ground plane from a satellite image
 *
 * In our first example, we'll load the Schependomlaan model, then use
 * an ````ImagePlane```` to create a ground plane, which will contain
 * a satellite image sourced from Google Maps.
 *
 * <img src="http://xeokit.io/img/docs/ImagePlane/schependomlaanGoogleSatMapMed.png">
 *
 * [<img src="http://xeokit.io/img/docs/ImagePlane/ImagePlane.png">](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#ImagePlane_groundPlane)
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#ImagePlane_groundPlane)]
 *
 * ````javascript
 * import {Viewer, ImagePlane, XKTLoaderPlugin} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *      canvasId: "myCanvas",
 *      transparent: true
 *  });
 *
 * viewer.camera.eye = [-8.31, 42.21, 54.30];
 * viewer.camera.look = [-0.86, 15.40, 14.90];
 * viewer.camera.up = [0.10, 0.83, -0.54];
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * xktLoader.load({             // Load IFC model
 *      id: "myModel",
 *      src: "./models/xkt/Schependomlaan.xkt",
 *      edges: true,
 *
 *      rotation: [0, 22, 0],   // Rotate, position and scale the model to align it correctly with the ImagePlane
 *      position: [-8, 0, 15],
 *      scale: [1.1, 1.1, 1.1]
 *  });
 *
 * new ImagePlane(viewer.scene, {
 *      src: "./images/schependomlaanSatMap.png",       // Google satellite image; accepted file types are PNG and JPEG
 *      visible: true,                                  // Show the ImagePlane
 *      gridVisible: true,                              // Show the grid - grid is only visible when ImagePlane is also visible
 *      size: 190,                                      // Size of ImagePlane's longest edge
 *      position: [0, -1, 0],                           // World-space position of ImagePlane's center
 *      rotation: [0, 0, 0],                            // Euler angles for X, Y and Z
 *      opacity: 1.0,                                   // Fully opaque
 *      collidable: false,                              // ImagePlane does not contribute to Scene boundary
 *      clippable: true,                                // ImagePlane can be clipped by SectionPlanes
 *      pickable: true                                  // Allow the ground plane to be picked
 * });
 * ````
 *<br>
 *
 * # Example 2: Embed an image in a cross-section plane
 *
 * In our second example, we'll load the Schependomlaan model again, then slice it in half with
 * a {@link SectionPlanesPlugin}, then use an ````ImagePlane```` to embed a 2D plan view image in the slicing plane.
 *
 * <img src="http://xeokit.io/img/docs/ImagePlane/schependomlaanPlanViewMed.png">
 *
 * [<img src="http://xeokit.io/img/docs/ImagePlane/ImagePlane_planView.png">](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#ImagePlane_imageInSectionPlane)
 *
 * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/scenegraph/#ImagePlane_imageInSectionPlane)]
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin, SectionPlanesPlugin, ImagePlane} from "xeokit-sdk.es.js";
 *
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.camera.eye = [-9.11, 20.01, 5.13];
 * viewer.camera.look = [9.07, 0.77, -9.78];
 * viewer.camera.up = [0.47, 0.76, -0.38];
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const sectionPlanes = new SectionPlanesPlugin(viewer, {
 *     overviewVisible: false
 * });
 *
 * model = xktLoader.load({
 *     id: "myModel",
 *     src: "./models/xkt/schependomlaan/schependomlaan.xkt",
 *     metaModelSrc: "./metaModels/schependomlaan/metaModel.json",
 *     edges: true,
 * });
 *
 * const sectionPlane = sectionPlanes.createSectionPlane({
 *     id: "mySectionPlane",
 *     pos: [10.95, 1.95, -10.35],
 *     dir: [0.0, -1.0, 0.0]
 * });
 *
 * const imagePlane = new ImagePlane(viewer.scene, {
 *     src: "./images/schependomlaanPlanView.png",  // Plan view image; accepted file types are PNG and JPEG
 *     visible: true,
 *     gridVisible: true,
 *     size: 23.95,
 *     position: sectionPlane.pos,
 *     dir: sectionPlane.dir,
 *     collidable: false,
 *     opacity: 0.75,
 *     clippable: false,                            // Don't allow ImagePlane to be clipped by the SectionPlane
 *     pickable: false                              // Don't allow ImagePlane to be picked
 *  });
 * ````
 */
class ImagePlane extends Component {

    /**
     * @constructor
     * @param {Component} [owner]  Owner component. When destroyed, the owner will destroy this ````ImagePlane```` as well.
     * @param {*} [cfg]  ````ImagePlane```` configuration
     * @param {String} [cfg.id] Optional ID, unique among all components in the parent {@link Scene}, generated automatically when omitted.
     * @param {Boolean} [cfg.visible=true] Indicates whether or not this ````ImagePlane```` is visible.
     * @param {Boolean} [cfg.gridVisible=true] Indicates whether or not the grid is visible.  Grid is only visible when ````ImagePlane```` is also visible.
     * @param {Number[]} [cfg.position=[0,0,0]] World-space position of the ````ImagePlane````.
     * @param {Number[]} [cfg.size=1] World-space size of the longest edge of the ````ImagePlane````. Note that
     * ````ImagePlane```` sets its aspect ratio to match its image. If we set a value of ````1000````, and the image
     * has size ````400x300````, then the ````ImagePlane```` will then have size ````1000 x 750````.
     * @param {Number[]} [cfg.rotation=[0,0,0]] Local rotation of the ````ImagePlane````, as Euler angles given in degrees, for each of the X, Y and Z axis.
     * @param {Number[]} [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]] Modelling transform matrix for the ````ImagePlane````. Overrides the ````position````, ````size```, ````rotation```` and ````dir```` parameters.
     * @param {Boolean} [cfg.collidable=true] Indicates if the ````ImagePlane```` is initially included in boundary calculations.
     * @param {Boolean} [cfg.clippable=true] Indicates if the ````ImagePlane```` is initially clippable.
     * @param {Boolean} [cfg.pickable=true] Indicates if the ````ImagePlane```` is initially pickable.
     * @param {Number} [cfg.opacity=1.0] ````ImagePlane````'s initial opacity factor, multiplies by the rendered fragment alpha.
     * @param {String} [cfg.src] URL of image. Accepted file types are PNG and JPEG.
     * @param {HTMLImageElement} [cfg.image] An ````HTMLImageElement```` to source the image from. Overrides ````src````.
     */
    constructor(owner, cfg = {}) {

        super(owner, cfg);

        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);

        this._plane = new Mesh(this, {

            geometry: new ReadableGeometry(this, buildPlaneGeometry({
                center: [0, 0, 0],
                xSize: 1,
                zSize: 1,
                xSegments: 10,
                zSegments: 10
            })),

            material: new PhongMaterial(this, {
                diffuse: [0, 0, 0],
                ambient: [0, 0, 0],
                specular: [0, 0, 0],
                diffuseMap: this._texture,
                emissiveMap: this._texture,
                backfaces: true
            }),
            clippable: cfg.clippable
        });

        this._grid = new Mesh(this, {
            geometry: new ReadableGeometry(this, buildGridGeometry({
                size: 1,
                divisions: 10
            })),
            material: new PhongMaterial(this, {
                diffuse: [0.0, 0.0, 0.0],
                ambient: [0.0, 0.0, 0.0],
                emissive: [0.2, 0.8, 0.2]
            }),
            position: [0, 0.001, 0.0],
            clippable: cfg.clippable
        });

        this._node = new Node(this, {
            rotation: [0, 0, 0],
            position: [0, 0, 0],
            scale: [1, 1, 1],
            clippable: false,
            children: [
                this._plane,
                this._grid
            ]
        });

        this._gridVisible = false;

        this.visible = true;
        this.gridVisible = cfg.gridVisible;
        this.position = cfg.position;
        this.rotation = cfg.rotation;
        this.dir = cfg.dir;
        this.size = cfg.size;
        this.collidable = cfg.collidable;
        this.clippable = cfg.clippable;
        this.pickable = cfg.pickable;
        this.opacity = cfg.opacity;

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

    /**
     * Sets if this ````ImagePlane```` is visible or not.
     *
     * Default value is ````true````.
     *
     * @param {Boolean} visible Set ````true```` to make this ````ImagePlane```` visible.
     */
    set visible(visible) {
        this._plane.visible = visible;
        this._grid.visible = (this._gridVisible && visible);
    }

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

    /**
     * Sets if this ````ImagePlane````'s grid  is visible or not.
     *
     * Default value is ````false````.
     *
     * Grid is only visible when ````ImagePlane```` is also visible.
     *
     * @param {Boolean} visible Set ````true```` to make this ````ImagePlane````'s grid visible.
     */
    set gridVisible(visible) {
        visible = (visible !== false);
        this._gridVisible = visible;
        this._grid.visible = (this._gridVisible && this.visible);
    }

    /**
     * Gets if this ````ImagePlane````'s grid is visible or not.
     *
     * Default value is ````false````.
     *
     * @returns {Boolean} Returns ````true```` if visible.
     */
    get gridVisible() {
        return this._gridVisible;
    }

    /**
     * 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] = image.width;
            this._imageSize[1] = image.height;
            this._updatePlaneSizeFromImage();
            this._src = null;
            this._texture.image = this._image;
        }
    }

    /**
     * Gets the ````HTMLImageElement```` the ````ImagePlane````'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 ````ImagePlane````'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 ````ImagePlane````'s image is sourced from, if set.
     *
     * Returns null if not set.
     *
     * @type {String}
     */
    get src() {
        return this._src;
    }

    /**
     * Sets the World-space position of this ````ImagePlane````.
     *
     * Default value is ````[0, 0, 0]````.
     *
     * @param {Number[]} value New position.
     */
    set position(value) {
        this._pos.set(value || [0, 0, 0]);
        worldToRTCPos(this._pos, this._origin, this._rtcPos);
        this._node.origin = this._origin;
        this._node.position = this._rtcPos;
    }

    /**
     * Gets the World-space position of this ````ImagePlane````.
     *
     * Default value is ````[0, 0, 0]````.
     *
     * @returns {Number[]} Current position.
     */
    get position() {
        return this._pos;
    }

    /**
     * Sets the direction of this ````ImagePlane```` using Euler angles.
     *
     * Default value is ````[0, 0, 0]````.
     *
     * @param {Number[]} value Euler angles for ````X````, ````Y```` and ````Z```` axis rotations.
     */
    set rotation(value) {
        this._node.rotation = value;
    }

    /**
     * Gets the direction of this ````ImagePlane```` as Euler angles.
     *
     * @returns {Number[]} Euler angles for ````X````, ````Y```` and ````Z```` axis rotations.
     */
    get rotation() {
        return this._node.rotation;
    }

    /**
     * Sets the World-space size of the longest edge of the ````ImagePlane````.
     *
     * Note that ````ImagePlane```` sets its aspect ratio to match its image. If we set a value of ````1000````, and
     * the image has size ````400x300````, then the ````ImagePlane```` will then have size ````1000 x 750````.
     *
     * Default value is ````1.0````.
     *
     * @param {Number} size New World-space size of the ````ImagePlane````.
     */
    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 ````ImagePlane````.
     *
     * Returns {Number} World-space size of the ````ImagePlane````.
     */
    get size() {
        return this._size;
    }

    /**
     * Sets the direction of this ````ImagePlane```` as a direction vector.
     *
     * Default value is ````[0, 0, -1]````.
     *
     * @param {Number[]} dir New direction vector.
     */
    set dir(dir) {

        this._dir.set(dir || [0, 0, -1]);

        if (dir) {

            const origin = this.scene.center;
            const negDir = [-this._dir[0], -this._dir[1], -this._dir[2]];

            math.subVec3(origin, this.position, tempVec3);

            const dist = -math.dotVec3(negDir, tempVec3);

            math.normalizeVec3(negDir);
            math.mulVec3Scalar(negDir, dist, tempVec3b);
            math.vec3PairToQuaternion(zeroVec, dir, tempQuat);

            this._node.quaternion = tempQuat;
        }
    }

    /**
     * Gets the direction of this ````ImagePlane```` as a direction vector.
     *
     * @returns {Number[]} value Current direction.
     */
    get dir() {
        return this._dir;
    }

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

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

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

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

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

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

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

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

    /**
     * @destroy
     */
    destroy() {
        super.destroy();
    }

    _updatePlaneSizeFromImage() {
        const size = this._size;
        const width = this._imageSize[0];
        const height = this._imageSize[1];
        if (width > height) {
            const aspect = height / width;
            this._node.scale = [size, 1.0, size * aspect];
        } else {
            const aspect = width / height;
            this._node.scale = [size * aspect, 1.0, size];
        }
    }
}

export {ImagePlane};