Reference Source

src/plugins/FaceAlignedSectionPlanesPlugin/FaceAlignedSectionPlanesControl.js

import {math} from "../../viewer/scene/math/math.js";

import {buildCylinderGeometry} from "../../viewer/scene/geometry/builders/buildCylinderGeometry.js";
import {buildTorusGeometry} from "../../viewer/scene/geometry/builders/buildTorusGeometry.js";

import {ReadableGeometry} from "../../viewer/scene/geometry/ReadableGeometry.js";
import {PhongMaterial} from "../../viewer/scene/materials/PhongMaterial.js";
import {EmphasisMaterial} from "../../viewer/scene/materials/EmphasisMaterial.js";
import {Node} from "../../viewer/scene/nodes/Node.js";
import {Mesh} from "../../viewer/scene/mesh/Mesh.js";
import {buildSphereGeometry} from "../../viewer/scene/geometry/builders/buildSphereGeometry.js";
import {worldToRTCPos} from "../../viewer/scene/math/rtcCoords.js";

const zeroVec = new Float64Array([0, 0, 1]);
const quat = new Float64Array(4);

/**
 * Controls a {@link SectionPlane} with mouse and touch input.
 */
export class FaceAlignedSectionPlanesControl {

    /** @private */
    constructor(plugin) {

        /**
         * ID of this FaceAlignedSectionPlanesControl.
         *
         * FaceAlignedSectionPlanesControl are mapped by this ID in {@link FaceAlignedSectionPlanesPlugin#controls}.
         *
         * @property id
         * @type {String|Number}
         */
        this.id = null;

        this._viewer = plugin.viewer;
        this._plugin = plugin;
        this._visible = false;
        this._pos = math.vec3(); // Full-precision position of the center of the FaceAlignedSectionPlanesControl
        this._origin = math.vec3();
        this._rtcPos = math.vec3();

        this._baseDir = math.vec3(); // Saves direction of clip plane when we start dragging an arrow or ring.
        this._rootNode = null; // Root of Node graph that represents this control in the 3D scene
        this._displayMeshes = null; // Meshes that are always visible
        this._affordanceMeshes = null; // Meshes displayed momentarily for affordance

        this._ignoreNextSectionPlaneDirUpdate = false;

        this._createNodes();
        this._bindEvents();
    }

    /**
     * Called by FaceAlignedSectionPlanesPlugin to assign this FaceAlignedSectionPlanesControl to a SectionPlane.
     * FaceAlignedSectionPlanesPlugin keeps FaceAlignedSectionPlanesControls in a reuse pool.
     * Call with a null or undefined value to disconnect the FaceAlignedSectionPlanesControl ffrom whatever SectionPlane it was assigned to.
     * @private
     */
    _setSectionPlane(sectionPlane) {
        if (this._sectionPlane) {
            this._sectionPlane.off(this._onSectionPlanePos);
            this._sectionPlane.off(this._onSectionPlaneDir);
            this._onSectionPlanePos = null;
            this._onSectionPlaneDir = null;
            this._sectionPlane = null;
        }
        if (sectionPlane) {
            this.id = sectionPlane.id;
            this._setPos(sectionPlane.pos);
            this._setDir(sectionPlane.dir);
            this._sectionPlane = sectionPlane;
            this._onSectionPlanePos = sectionPlane.on("pos", () => {
                this._setPos(this._sectionPlane.pos);
            });
            this._onSectionPlaneDir = sectionPlane.on("dir", () => {
                if (!this._ignoreNextSectionPlaneDirUpdate) {
                    this._setDir(this._sectionPlane.dir);
                } else {
                    this._ignoreNextSectionPlaneDirUpdate = false;
                }
            });
        }
    }

    /**
     * Gets the {@link SectionPlane} controlled by this FaceAlignedSectionPlanesControl.
     * @returns {SectionPlane} The SectionPlane.
     */
    get sectionPlane() {
        return this._sectionPlane;
    }

    /** @private */
    _setPos(xyz) {

        this._pos.set(xyz);

        worldToRTCPos(this._pos, this._origin, this._rtcPos);

        this._rootNode.origin = this._origin;
        this._rootNode.position = this._rtcPos;
    }

    /** @private */
    _setDir(xyz) {
        this._baseDir.set(xyz);
        this._rootNode.quaternion = math.vec3PairToQuaternion(zeroVec, xyz, quat);
    }

    _setSectionPlaneDir(dir) {
        if (this._sectionPlane) {
            this._ignoreNextSectionPlaneDirUpdate = true;
            this._sectionPlane.dir = dir;
        }
    }

    /**
     * Sets if this FaceAlignedSectionPlanesControl is visible.
     *
     * @type {Boolean}
     */
    setVisible(visible = true) {
        if (this._visible === visible) {
            return;
        }
        this._visible = visible;
        var id;
        for (id in this._displayMeshes) {
            if (this._displayMeshes.hasOwnProperty(id)) {
                this._displayMeshes[id].visible = visible;
            }
        }
        if (!visible) {
            for (id in this._affordanceMeshes) {
                if (this._affordanceMeshes.hasOwnProperty(id)) {
                    this._affordanceMeshes[id].visible = visible;
                }
            }
        }
    }

    /**
     * Gets if this FaceAlignedSectionPlanesControl is visible.
     *
     * @type {Boolean}
     */
    getVisible() {
        return this._visible;
    }

    /**
     * Sets if this FaceAlignedSectionPlanesControl is culled. This is called by FaceAlignedSectionPlanesPlugin to
     * temporarily hide the FaceAlignedSectionPlanesControl while a snapshot is being taken by Viewer#getSnapshot().
     * @param culled
     */
    setCulled(culled) {
        var id;
        for (id in this._displayMeshes) {
            if (this._displayMeshes.hasOwnProperty(id)) {
                this._displayMeshes[id].culled = culled;
            }
        }
        if (!culled) {
            for (id in this._affordanceMeshes) {
                if (this._affordanceMeshes.hasOwnProperty(id)) {
                    this._affordanceMeshes[id].culled = culled;
                }
            }
        }
    }

    /**
     * Builds the Entities that represent this FaceAlignedSectionPlanesControl.
     * @private
     */
    _createNodes() {

        const NO_STATE_INHERIT = false;
        const scene = this._viewer.scene;
        const radius = 1.0;
        const handleTubeRadius = 0.06;
        const hoopRadius = radius - 0.2;
        const tubeRadius = 0.01;
        const arrowRadius = 0.07;

        this._rootNode = new Node(scene, {
            position: [0, 0, 0],
            scale: [5, 5, 5]
        });

        const rootNode = this._rootNode;

        const shapes = {// Reusable geometries

            arrowHead: new ReadableGeometry(rootNode, buildCylinderGeometry({
                radiusTop: 0.001,
                radiusBottom: arrowRadius,
                radialSegments: 32,
                heightSegments: 1,
                height: 0.2,
                openEnded: false
            })),

            arrowHeadBig: new ReadableGeometry(rootNode, buildCylinderGeometry({
                radiusTop: 0.001,
                radiusBottom: 0.09,
                radialSegments: 32,
                heightSegments: 1,
                height: 0.25,
                openEnded: false
            })),

            axis: new ReadableGeometry(rootNode, buildCylinderGeometry({
                radiusTop: tubeRadius,
                radiusBottom: tubeRadius,
                radialSegments: 20,
                heightSegments: 1,
                height: radius,
                openEnded: false
            }))
        };

        const materials = { // Reusable materials

            red: new PhongMaterial(rootNode, {
                diffuse: [1, 0.0, 0.0],
                emissive: [1, 0.0, 0.0],
                ambient: [0.0, 0.0, 0.0],
                specular: [.6, .6, .3],
                shininess: 80,
                lineWidth: 2
            }),

            green: new PhongMaterial(rootNode, {
                diffuse: [0, 1.0, 0.0],
                emissive: [1, 0.0, 0.0],
                ambient: [0.0, 0.0, 0.0],
                specular: [.6, .6, .3],
                shininess: 80,
                lineWidth: 2
            }),

            blue: new PhongMaterial(rootNode, {
                diffuse: [0, 0.0, 1.0],
                emissive: [1, 0.0, 0.0],
                ambient: [0.0, 0.0, 0.0],
                specular: [.6, .6, .3],
                shininess: 80,
                lineWidth: 2
            }),

            highlightRed: new EmphasisMaterial(rootNode, { // Emphasis for red rotation affordance hoop
                edges: false,
                fill: true,
                fillColor: [1, 0, 0],
                fillAlpha: 0.6
            })
        };

        this._displayMeshes = {

            plane: rootNode.addChild(new Mesh(rootNode, {
                geometry: new ReadableGeometry(rootNode, {
                    primitive: "triangles",
                    positions: [
                        0.5, 0.5, 0.0, 0.5, -0.5, 0.0, // 0
                        -0.5, -0.5, 0.0, -0.5, 0.5, 0.0, // 1
                        0.5, 0.5, -0.0, 0.5, -0.5, -0.0, // 2
                        -0.5, -0.5, -0.0, -0.5, 0.5, -0.0 // 3
                    ],
                    indices: [0, 1, 2, 2, 3, 0]
                }),
                material: new PhongMaterial(rootNode, {
                    emissive: [0, 0.0, 0],
                    diffuse: [0, 0, 0],
                    backfaces: true
                }),
                opacity: 0.6,
                ghosted: true,
                pickable: false,
                collidable: true,
                clippable: false,
                visible: false,
                scale: [2.4, 2.4, 1]
            }), NO_STATE_INHERIT),

            planeFrame: rootNode.addChild(new Mesh(rootNode, { // Visible frame
                geometry: new ReadableGeometry(rootNode, buildTorusGeometry({
                    center: [0, 0, 0],
                    radius: 1.7,
                    tube: tubeRadius * 2,
                    radialSegments: 4,
                    tubeSegments: 4,
                    arc: Math.PI * 2.0
                })),
                material: new PhongMaterial(rootNode, {
                    emissive: [0, 0, 0],
                    diffuse: [0, 0, 0],
                    specular: [0, 0, 0],
                    shininess: 0
                }),
                pickable: false,
                collidable: false,
                clippable: false,
                visible: false,
                scale: [1, 1, .1],
                rotation: [0, 0, 45]
            }), NO_STATE_INHERIT),

            center: rootNode.addChild(new Mesh(rootNode, {
                geometry: new ReadableGeometry(rootNode, buildSphereGeometry({
                    radius: 0.05
                })),
                material: materials.center,
                pickable: false,
                collidable: true,
                clippable: false,
                visible: false
            }), NO_STATE_INHERIT),

            zAxisArrow: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.arrowHead,
                material: materials.blue,
                matrix: (function () {
                    const translate = math.translateMat4c(0, radius + .1, 0, math.identityMat4());
                    const rotate = math.rotationMat4v(-90 * math.DEGTORAD, [0.8, 0, 0], math.identityMat4());
                    return math.mulMat4(rotate, translate, math.identityMat4());
                })(),
                pickable: false,
                collidable: true,
                clippable: false,
                visible: false
            }), NO_STATE_INHERIT),

            zShaft: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.axis,
                material: materials.blue,
                matrix: (function () {
                    const translate = math.translateMat4c(0, radius / 2, 0, math.identityMat4());
                    const rotate = math.rotationMat4v(-90 * math.DEGTORAD, [1, 0, 0], math.identityMat4());
                    return math.mulMat4(rotate, translate, math.identityMat4());
                })(),
                clippable: false,
                pickable: false,
                collidable: true,
                visible: false
            }), NO_STATE_INHERIT)
        };

        this._affordanceMeshes = {

            planeFrame: rootNode.addChild(new Mesh(rootNode, {
                geometry: new ReadableGeometry(rootNode, buildTorusGeometry({
                    center: [0, 0, 0],
                    radius: 2,
                    tube: tubeRadius,
                    radialSegments: 4,
                    tubeSegments: 4,
                    arc: Math.PI * 2.0
                })),
                material: new PhongMaterial(rootNode, {
                    ambient: [1, 1, 1],
                    diffuse: [0, 0, 0],
                    emissive: [1, 1, 0]
                }),
                highlighted: true,
                highlightMaterial: new EmphasisMaterial(rootNode, {
                    edges: false,
                    filled: true,
                    fillColor: [1, 1, 0],
                    fillAlpha: 1.0
                }),
                pickable: false,
                collidable: false,
                clippable: false,
                visible: false,
                scale: [1, 1, 1],
                rotation: [0, 0, 45]
            }), NO_STATE_INHERIT),

            zAxisArrow: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.arrowHeadBig,
                material: materials.blue,
                matrix: (function () {
                    const translate = math.translateMat4c(0, radius + .1, 0, math.identityMat4());
                    const rotate = math.rotationMat4v(-90 * math.DEGTORAD, [0.8, 0, 0], math.identityMat4());
                    return math.mulMat4(rotate, translate, math.identityMat4());
                })(),
                pickable: false,
                collidable: true,
                clippable: false,
                visible: false
            }), NO_STATE_INHERIT)
        };
    }

    _bindEvents() {

        const rootNode = this._rootNode;

        var nextDragAction = null; // As we hover grabbed an arrow or hoop, self is the action we would do if we then dragged it.
        var dragAction = null; // Action we're doing while we drag an arrow or hoop.
        const lastCanvasPos = math.vec2();
        const camera = this._viewer.camera;
        const scene = this._viewer.scene;

        let deltaUpdate = 0;
        let waitForTick = false;

        { // Keep gizmo screen size constant

            const tempVec3a = math.vec3([0, 0, 0]);

            let distDirty = true;
            let lastDist = -1;

            this._onCameraViewMatrix = scene.camera.on("viewMatrix", () => {
                distDirty = true;
            });

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

            this._onSceneTick = scene.on("tick", () => {

                waitForTick = false;
                const dist = Math.abs(math.lenVec3(math.subVec3(scene.camera.eye, this._pos, tempVec3a)));

                if (dist !== lastDist) {
                    if (camera.projection === "perspective") {
                        const worldSize = (Math.tan(camera.perspective.fov * math.DEGTORAD)) * dist;
                        const size = 0.07 * worldSize;
                        rootNode.scale = [size, size, size];
                        lastDist = dist;
                    }
                }

                if (camera.projection === "ortho") {
                    const worldSize = camera.ortho.scale / 10;
                    const size = worldSize;
                    rootNode.scale = [size, size, size];
                    lastDist = dist;
                }

                if (deltaUpdate !== 0) {
                    moveSectionPlane(deltaUpdate);
                    deltaUpdate = 0;
                }
            });
        }

        const getClickCoordsWithinElement = (function () {
            const canvasPos = new Float64Array(2);
            return function (event) {
                if (!event) {
                    event = window.event;
                    canvasPos[0] = event.x;
                    canvasPos[1] = event.y;
                } else {
                    var element = event.target;
                    var totalOffsetLeft = 0;
                    var totalOffsetTop = 0;

                    while (element.offsetParent) {
                        totalOffsetLeft += element.offsetLeft;
                        totalOffsetTop += element.offsetTop;
                        element = element.offsetParent;
                    }
                    canvasPos[0] = event.pageX - totalOffsetLeft;
                    canvasPos[1] = event.pageY - totalOffsetTop;
                }
                return canvasPos;
            };
        })();

        const moveSectionPlane = (delta) => {
            const pos = this._sectionPlane.pos;
            const dir = this._sectionPlane.dir;
            math.addVec3(pos, math.mulVec3Scalar(dir, 0.1 * delta * this._plugin.getDragSensitivity(), math.vec3()));
            this._sectionPlane.pos = pos;
        }

        {
            let mouseDownLeft;
            let mouseDownMiddle;
            let mouseDownRight;
            let down = false;

            this._plugin._controlElement.addEventListener("mousedown", this._canvasMouseDownListener = (e) => {
                e.preventDefault();
                if (!this._visible) {
                    return;
                }
                this._viewer.cameraControl.pointerEnabled = false;
                switch (e.which) {
                    case 1: // Left button
                        mouseDownLeft = true;
                        down = true;
                        var canvasPos = getClickCoordsWithinElement(e);
                        dragAction = nextDragAction;
                        lastCanvasPos[0] = canvasPos[0];
                        lastCanvasPos[1] = canvasPos[1];
                        break;
                    default:
                        break;
                }
            });

            this._plugin._controlElement.addEventListener("mousemove", this._canvasMouseMoveListener = (e) => {
                if (!this._visible) {
                    return;
                }
                if (!down) {
                    return;
                }
                if (waitForTick) {    // Limit changes detection to one per frame
                    return;
                }
                var canvasPos = getClickCoordsWithinElement(e);
                const x = canvasPos[0];
                const y = canvasPos[1];
                moveSectionPlane(y - lastCanvasPos[1]);
                lastCanvasPos[0] = x;
                lastCanvasPos[1] = y;
            });

            this._plugin._controlElement.addEventListener("mouseup", this._canvasMouseUpListener = (e) => {
                if (!this._visible) {
                    return;
                }
                this._viewer.cameraControl.pointerEnabled = true;
                if (!down) {
                    return;
                }
                switch (e.which) {
                    case 1: // Left button
                        mouseDownLeft = false;
                        break;
                    case 2: // Middle/both buttons
                        mouseDownMiddle = false;
                        break;
                    case 3: // Right button
                        mouseDownRight = false;
                        break;
                    default:
                        break;
                }
                down = false;
            });

            this._plugin._controlElement.addEventListener("wheel", this._canvasWheelListener = (e) => {
                if (!this._visible) {
                    return;
                }
                deltaUpdate += Math.max(-1, Math.min(1, -e.deltaY * 40));
            });
        }

        {
            let touchStartY, touchEndY;
            let lastTouchY = null;

            this._plugin._controlElement.addEventListener("touchstart", this._handleTouchStart = (e) => {
                e.stopPropagation();
                e.preventDefault();
                if (!this._visible) {
                    return;
                }
                touchStartY = e.touches[0].clientY;
                lastTouchY = touchStartY;
                deltaUpdate = 0;
            });

            this._plugin._controlElement.addEventListener("touchmove", this._handleTouchMove = (e) => {
                e.stopPropagation();
                e.preventDefault();
                if (!this._visible) {
                    return;
                }
                if (waitForTick) {    // Limit changes detection to one per frame
                    return;
                }
                waitForTick = true;
                touchEndY = e.touches[0].clientY;
                if (lastTouchY !== null) {
                    deltaUpdate += touchEndY - lastTouchY;
                }
                lastTouchY = touchEndY;
            });

            this._plugin._controlElement.addEventListener("touchend", this._handleTouchEnd = (e) => {
                e.stopPropagation();
                e.preventDefault();
                if (!this._visible) {
                    return;
                }
                touchStartY = null;
                touchEndY = null;
                deltaUpdate = 0;
            });
        }
    }

    _destroy() {
        this._unbindEvents();
        this._destroyNodes();
    }

    _unbindEvents() {

        const viewer = this._viewer;
        const scene = viewer.scene;
        const canvas = scene.canvas.canvas;
        const camera = viewer.camera;
        const controlElement = this._plugin._controlElement;

        scene.off(this._onSceneTick);

        canvas.removeEventListener("mousedown", this._canvasMouseDownListener);
        canvas.removeEventListener("mousemove", this._canvasMouseMoveListener);
        canvas.removeEventListener("mouseup", this._canvasMouseUpListener);
        canvas.removeEventListener("wheel", this._canvasWheelListener);

        controlElement.removeEventListener("touchstart", this._handleTouchStart);
        controlElement.removeEventListener("touchmove", this._handleTouchMove);
        controlElement.removeEventListener("touchend", this._handleTouchEnd);

        camera.off(this._onCameraViewMatrix);
        camera.off(this._onCameraProjMatrix);
    }

    _destroyNodes() {
        this._setSectionPlane(null);
        this._rootNode.destroy();
        this._displayMeshes = {};
        this._affordanceMeshes = {};
    }
}