Reference Source

src/plugins/SectionPlanesPlugin/Control.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.
 *
 * @private
 */
class Control {

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

        /**
         * ID of this Control.
         *
         * SectionPlaneControls are mapped by this ID in {@link SectionPlanesPlugin#sectionPlaneControls}.
         *
         * @property id
         * @type {String|Number}
         */
        this.id = null;

        this._viewer = plugin.viewer;

        this._visible = false;
        this._pos = math.vec3(); // Full-precision position of the center of the Control
        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 SectionPlanesPlugin to assign this Control to a SectionPlane.
     * SectionPlanesPlugin keeps SectionPlaneControls in a reuse pool.
     * Call with a null or undefined value to disconnect the Control 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 Control.
     * @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 Control 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 Control is visible.
     *
     * @type {Boolean}
     */
    getVisible() {
        return this._visible;
    }

    /**
     * Sets if this Control is culled. This is called by SectionPlanesPlugin to
     * temporarily hide the Control 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 Control.
     * @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],
            isObject: false
        });

        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
            })),

            arrowHeadHandle: new ReadableGeometry(rootNode, buildCylinderGeometry({
                radiusTop: 0.09,
                radiusBottom: 0.09,
                radialSegments: 8,
                heightSegments: 1,
                height: 0.37,
                openEnded: false
            })),

            curve: new ReadableGeometry(rootNode, buildTorusGeometry({
                radius: hoopRadius,
                tube: tubeRadius,
                radialSegments: 64,
                tubeSegments: 14,
                arc: (Math.PI * 2.0) / 4.0
            })),

            curveHandle: new ReadableGeometry(rootNode, buildTorusGeometry({
                radius: hoopRadius,
                tube: handleTubeRadius,
                radialSegments: 64,
                tubeSegments: 14,
                arc: (Math.PI * 2.0) / 4.0
            })),

            hoop: new ReadableGeometry(rootNode, buildTorusGeometry({
                radius: hoopRadius,
                tube: tubeRadius,
                radialSegments: 64,
                tubeSegments: 8,
                arc: (Math.PI * 2.0)
            })),

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

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

        const materials = { // Reusable materials

            pickable: new PhongMaterial(rootNode, { // Invisible material for pickable handles, which define a pickable 3D area
                diffuse: [1, 1, 0],
                alpha: 0, // Invisible
                alphaMode: "blend"
            }),

            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
            }),

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

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

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

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

            highlightBlue: new EmphasisMaterial(rootNode, { // Emphasis for blue rotation affordance hoop
                edges: false,
                fill: true,
                fillColor: [0, 0, 1],
                fillAlpha: 0.2
            }),

            center: new PhongMaterial(rootNode, {
                diffuse: [0.0, 0.0, 0.0],
                emissive: [0, 0, 0],
                ambient: [0.0, 0.0, 0.0],
                specular: [.6, .6, .3],
                shininess: 80
            }),

            highlightBall: new EmphasisMaterial(rootNode, {
                edges: false,
                fill: true,
                fillColor: [0.5, 0.5, 0.5],
                fillAlpha: 0.5,
                vertices: false
            }),

            highlightPlane: new EmphasisMaterial(rootNode, {
                edges: true,
                edgeWidth: 3,
                fill: false,
                fillColor: [0.5, 0.5, .5],
                fillAlpha: 0.5,
                vertices: false
            })
        };

        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,
                ghostMaterial: new EmphasisMaterial(rootNode, {
                    edges: false,
                    filled: true,
                    fillColor: [1, 1, 0],
                    edgeColor: [0, 0, 0],
                    fillAlpha: 0.1,
                    backfaces: true
                }),
                pickable: false,
                collidable: true,
                clippable: false,
                visible: false,
                scale: [2.4, 2.4, 1],
                isObject: false
            }), 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
                }),
                //highlighted: true,
                highlightMaterial: new EmphasisMaterial(rootNode, {
                    edges: false,
                    edgeColor: [0.0, 0.0, 0.0],
                    filled: true,
                    fillColor: [0.8, 0.8, 0.8],
                    fillAlpha: 1.0
                }),
                pickable: false,
                collidable: false,
                clippable: false,
                visible: false,
                scale: [1, 1, .1],
                rotation: [0, 0, 45],
                isObject: false
            }), NO_STATE_INHERIT),

            //----------------------------------------------------------------------------------------------------------
            //
            //----------------------------------------------------------------------------------------------------------

            xCurve: rootNode.addChild(new Mesh(rootNode, { // Red hoop about Y-axis
                geometry: shapes.curve,
                material: materials.red,
                matrix: (function () {
                    const rotate2 = math.rotationMat4v(90 * math.DEGTORAD, [0, 1, 0], math.identityMat4());
                    const rotate1 = math.rotationMat4v(270 * math.DEGTORAD, [1, 0, 0], math.identityMat4());
                    return math.mulMat4(rotate1, rotate2, math.identityMat4());
                })(),
                pickable: false,
                collidable: true,
                clippable: false,
                backfaces: true,
                visible: false,
                isObject: false
            }), NO_STATE_INHERIT),

            xCurveHandle: rootNode.addChild(new Mesh(rootNode, { // Red hoop about Y-axis
                geometry: shapes.curveHandle,
                material: materials.pickable,
                matrix: (function () {
                    const rotate2 = math.rotationMat4v(90 * math.DEGTORAD, [0, 1, 0], math.identityMat4());
                    const rotate1 = math.rotationMat4v(270 * math.DEGTORAD, [1, 0, 0], math.identityMat4());
                    return math.mulMat4(rotate1, rotate2, math.identityMat4());
                })(),
                pickable: true,
                collidable: true,
                clippable: false,
                backfaces: true,
                visible: false,
                isObject: false
            }), NO_STATE_INHERIT),

            xCurveArrow1: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.arrowHead,
                material: materials.red,
                matrix: (function () {
                    const translate = math.translateMat4c(0., -0.07, -0.8, math.identityMat4());
                    const scale = math.scaleMat4v([0.6, 0.6, 0.6], math.identityMat4());
                    const rotate = math.rotationMat4v(0 * math.DEGTORAD, [0, 0, 1], math.identityMat4());
                    return math.mulMat4(math.mulMat4(translate, scale, math.identityMat4()), rotate, math.identityMat4());
                })(),
                pickable: true,
                collidable: true,
                clippable: false,
                visible: false,
                isObject: false
            }), NO_STATE_INHERIT),

            xCurveArrow2: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.arrowHead,
                material: materials.red,
                matrix: (function () {
                    const translate = math.translateMat4c(0.0, -0.8, -0.07, math.identityMat4());
                    const scale = math.scaleMat4v([0.6, 0.6, 0.6], math.identityMat4());
                    const rotate = math.rotationMat4v(90 * math.DEGTORAD, [1, 0, 0], math.identityMat4());
                    return math.mulMat4(math.mulMat4(translate, scale, math.identityMat4()), rotate, math.identityMat4());
                })(),
                pickable: true,
                collidable: true,
                clippable: false,
                visible: false,
                isObject: false
            }), NO_STATE_INHERIT),

            //----------------------------------------------------------------------------------------------------------
            //
            //----------------------------------------------------------------------------------------------------------

            yCurve: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.curve,
                material: materials.green,
                rotation: [-90, 0, 0],
                pickable: false,
                collidable: true,
                clippable: false,
                backfaces: true,
                visible: false,
                isObject: false
            }), NO_STATE_INHERIT),

            yCurveHandle: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.curveHandle,
                material: materials.pickable,
                rotation: [-90, 0, 0],
                pickable: true,
                collidable: true,
                clippable: false,
                backfaces: true,
                visible: false,
                isObject: false
            }), NO_STATE_INHERIT),

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

            yCurveArrow2: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.arrowHead,
                material: materials.green,
                matrix: (function () {
                    const translate = math.translateMat4c(0.8, 0.0, -0.07, math.identityMat4());
                    const scale = math.scaleMat4v([0.6, 0.6, 0.6], math.identityMat4());
                    const rotate = math.rotationMat4v(90 * math.DEGTORAD, [1, 0, 0], math.identityMat4());
                    return math.mulMat4(math.mulMat4(translate, scale, math.identityMat4()), rotate, math.identityMat4());
                })(),
                pickable: true,
                collidable: true,
                clippable: false,
                visible: false,
                isObject: false
            }), NO_STATE_INHERIT),

            //----------------------------------------------------------------------------------------------------------
            //
            //----------------------------------------------------------------------------------------------------------

            zCurve: rootNode.addChild(new Mesh(rootNode, { // Blue hoop about Z-axis
                geometry: shapes.curve,
                material: materials.blue,
                matrix: math.rotationMat4v(180 * math.DEGTORAD, [1, 0, 0], math.identityMat4()),
                pickable: false,
                collidable: true,
                clippable: false,
                visible: false,
                isObject: false
            }), NO_STATE_INHERIT),

            zCurveHandle: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.curveHandle,
                material: materials.pickable,
                matrix: math.rotationMat4v(180 * math.DEGTORAD, [1, 0, 0], math.identityMat4()),
                pickable: true,
                collidable: true,
                clippable: false,
                visible: false,
                isObject: false
            }), NO_STATE_INHERIT),

            zCurveCurveArrow1: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.arrowHead,
                material: materials.blue,
                matrix: (function () {
                    const translate = math.translateMat4c(.8, -0.07, 0, math.identityMat4());
                    const scale = math.scaleMat4v([0.6, 0.6, 0.6], math.identityMat4());
                    return math.mulMat4(translate, scale, math.identityMat4());
                })(),
                pickable: true,
                collidable: true,
                clippable: false,
                visible: false,
                isObject: false
            }), NO_STATE_INHERIT),

            zCurveArrow2: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.arrowHead,
                material: materials.blue,
                matrix: (function () {
                    const translate = math.translateMat4c(.05, -0.8, 0, math.identityMat4());
                    const scale = math.scaleMat4v([0.6, 0.6, 0.6], math.identityMat4());
                    const rotate = math.rotationMat4v(90 * math.DEGTORAD, [0, 0, 1], math.identityMat4());
                    return math.mulMat4(math.mulMat4(translate, scale, math.identityMat4()), rotate, math.identityMat4());
                })(),
                pickable: true,
                collidable: true,
                clippable: false,
                visible: false,
                isObject: false
            }), 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,
                isObject: false
            }), NO_STATE_INHERIT),

            //----------------------------------------------------------------------------------------------------------
            //
            //----------------------------------------------------------------------------------------------------------

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

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

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

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

            //----------------------------------------------------------------------------------------------------------
            //
            //----------------------------------------------------------------------------------------------------------

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

            yAxisArrowHandle: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.arrowHeadHandle,
                material: materials.pickable,
                matrix: (function () {
                    const translate = math.translateMat4c(0, radius + .1, 0, math.identityMat4());
                    const rotate = math.rotationMat4v(180 * math.DEGTORAD, [1, 0, 0], math.identityMat4());
                    return math.mulMat4(rotate, translate, math.identityMat4());
                })(),
                pickable: true,
                collidable: true,
                clippable: false,
                visible: false,
                opacity: 0.2,
                isObject: false
            }), NO_STATE_INHERIT),

            yShaft: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.axis,
                material: materials.green,
                position: [0, -radius / 2, 0],
                pickable: false,
                collidable: true,
                clippable: false,
                visible: false,
                isObject: false
            }), NO_STATE_INHERIT),

            yShaftHandle: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.axisHandle,
                material: materials.pickable,
                position: [0, -radius / 2, 0],
                pickable: true,
                collidable: true,
                clippable: false,
                visible: false,
                isObject: 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,
                isObject: false
            }), NO_STATE_INHERIT),

            zAxisArrowHandle: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.arrowHeadHandle,
                material: materials.pickable,
                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: true,
                collidable: true,
                clippable: false,
                visible: false,
                isObject: 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,
                isObject: false
            }), NO_STATE_INHERIT),

            zAxisHandle: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.axisHandle,
                material: materials.pickable,
                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: true,
                collidable: true,
                visible: false,
                isObject: 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],
                isObject: false
            }), NO_STATE_INHERIT),

            xHoop: rootNode.addChild(new Mesh(rootNode, { // Full
                geometry: shapes.hoop,
                material: materials.red,
                highlighted: true,
                highlightMaterial: materials.highlightRed,
                matrix: (function () {
                    const rotate2 = math.rotationMat4v(90 * math.DEGTORAD, [0, 1, 0], math.identityMat4());
                    const rotate1 = math.rotationMat4v(270 * math.DEGTORAD, [1, 0, 0], math.identityMat4());
                    return math.mulMat4(rotate1, rotate2, math.identityMat4());
                })(),
                pickable: false,
                collidable: true,
                clippable: false,
                visible: false,
                isObject: false
            }), NO_STATE_INHERIT),

            yHoop: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.hoop,
                material: materials.green,
                highlighted: true,
                highlightMaterial: materials.highlightGreen,
                rotation: [-90, 0, 0],
                pickable: false,
                collidable: true,
                clippable: false,
                visible: false,
                isObject: false
            }), NO_STATE_INHERIT),

            zHoop: rootNode.addChild(new Mesh(rootNode, { // Blue hoop about Z-axis
                geometry: shapes.hoop,
                material: materials.blue,
                highlighted: true,
                highlightMaterial: materials.highlightBlue,
                matrix: math.rotationMat4v(180 * math.DEGTORAD, [1, 0, 0], math.identityMat4()),
                pickable: false,
                collidable: true,
                clippable: false,
                backfaces: true,
                visible: false,
                isObject: false
            }), NO_STATE_INHERIT),

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

            yAxisArrow: rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.arrowHeadBig,
                material: materials.green,
                matrix: (function () {
                    const translate = math.translateMat4c(0, radius + .1, 0, math.identityMat4());
                    const rotate = math.rotationMat4v(180 * math.DEGTORAD, [1, 0, 0], math.identityMat4());
                    return math.mulMat4(rotate, translate, math.identityMat4());
                })(),
                pickable: false,
                collidable: true,
                clippable: false,
                visible: false,
                isObject: false
            }), 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,
                isObject: false
            }), NO_STATE_INHERIT)
        };
    }

    _bindEvents() {

        const self = this;

        var grabbed = false;

        const DRAG_ACTIONS = {
            none: -1,
            xTranslate: 0,
            yTranslate: 1,
            zTranslate: 2,
            xRotate: 3,
            yRotate: 4,
            zRotate: 5
        };

        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 xBaseAxis = math.vec3([1, 0, 0]);
        const yBaseAxis = math.vec3([0, 1, 0]);
        const zBaseAxis = math.vec3([0, 0, 1]);

        const canvas = this._viewer.scene.canvas.canvas;
        const camera = this._viewer.camera;
        const scene = this._viewer.scene;

        { // 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", () => {

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

        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 localToWorldVec = (function () {
            const mat = math.mat4();
            return function (localVec, worldVec) {
                math.quaternionToMat4(self._rootNode.quaternion, mat);
                math.transformVec3(mat, localVec, worldVec);
                math.normalizeVec3(worldVec);
                return worldVec;
            };
        })();

        var getTranslationPlane = (function () {
            const planeNormal = math.vec3();
            return function (worldAxis) {
                const absX = Math.abs(worldAxis[0]);
                if (absX > Math.abs(worldAxis[1]) && absX > Math.abs(worldAxis[2])) {
                    math.cross3Vec3(worldAxis, [0, 1, 0], planeNormal);
                } else {
                    math.cross3Vec3(worldAxis, [1, 0, 0], planeNormal);
                }
                math.cross3Vec3(planeNormal, worldAxis, planeNormal);
                math.normalizeVec3(planeNormal);
                return planeNormal;
            }
        })();

        const dragTranslateSectionPlane = (function () {
            const p1 = math.vec3();
            const p2 = math.vec3();
            const worldAxis = math.vec4();
            return function (baseAxis, fromMouse, toMouse) {
                localToWorldVec(baseAxis, worldAxis);
                const planeNormal = getTranslationPlane(worldAxis, fromMouse, toMouse);
                getPointerPlaneIntersect(fromMouse, planeNormal, p1);
                getPointerPlaneIntersect(toMouse, planeNormal, p2);
                math.subVec3(p2, p1);
                const dot = math.dotVec3(p2, worldAxis);
                self._pos[0] += worldAxis[0] * dot;
                self._pos[1] += worldAxis[1] * dot;
                self._pos[2] += worldAxis[2] * dot;
                self._rootNode.position = self._pos;
                if (self._sectionPlane) {
                    self._sectionPlane.pos = self._pos;
                }
            }
        })();

        var dragRotateSectionPlane = (function () {
            const p1 = math.vec4();
            const p2 = math.vec4();
            const c = math.vec4();
            const worldAxis = math.vec4();
            return function (baseAxis, fromMouse, toMouse) {
                localToWorldVec(baseAxis, worldAxis);
                const hasData = getPointerPlaneIntersect(fromMouse, worldAxis, p1) && getPointerPlaneIntersect(toMouse, worldAxis, p2);
                if (!hasData) { // Find intersections with view plane and project down to origin
                    const planeNormal = getTranslationPlane(worldAxis, fromMouse, toMouse);
                    getPointerPlaneIntersect(fromMouse, planeNormal, p1, 1); // Ensure plane moves closer to camera so angles become workable
                    getPointerPlaneIntersect(toMouse, planeNormal, p2, 1);
                    var dot = math.dotVec3(p1, worldAxis);
                    p1[0] -= dot * worldAxis[0];
                    p1[1] -= dot * worldAxis[1];
                    p1[2] -= dot * worldAxis[2];
                    dot = math.dotVec3(p2, worldAxis);
                    p2[0] -= dot * worldAxis[0];
                    p2[1] -= dot * worldAxis[1];
                    p2[2] -= dot * worldAxis[2];
                }
                math.normalizeVec3(p1);
                math.normalizeVec3(p2);
                dot = math.dotVec3(p1, p2);
                dot = math.clamp(dot, -1.0, 1.0); // Rounding errors cause dot to exceed allowed range
                var incDegrees = Math.acos(dot) * math.RADTODEG;
                math.cross3Vec3(p1, p2, c);
                if (math.dotVec3(c, worldAxis) < 0.0) {
                    incDegrees = -incDegrees;
                }
                self._rootNode.rotate(baseAxis, incDegrees);
                rotateSectionPlane();
            }
        })();

        var getPointerPlaneIntersect = (function () {
            const dir = math.vec4([0, 0, 0, 1]);
            const matrix = math.mat4();
            return function (mouse, axis, dest, offset) {
                offset = offset || 0;
                dir[0] = mouse[0] / canvas.width * 2.0 - 1.0;
                dir[1] = -(mouse[1] / canvas.height * 2.0 - 1.0);
                dir[2] = 0.0;
                dir[3] = 1.0;
                math.mulMat4(camera.projMatrix, camera.viewMatrix, matrix); // Unproject norm device coords to view coords
                math.inverseMat4(matrix);
                math.transformVec4(matrix, dir, dir);
                math.mulVec4Scalar(dir, 1.0 / dir[3]); // This is now point A on the ray in world space
                var rayO = camera.eye; // The direction
                math.subVec4(dir, rayO, dir);
                const origin = self._sectionPlane.pos; // Plane origin:
                var d = -math.dotVec3(origin, axis) - offset;
                var dot = math.dotVec3(axis, dir);
                if (Math.abs(dot) > 0.005) {
                    var t = -(math.dotVec3(axis, rayO) + d) / dot;
                    math.mulVec3Scalar(dir, t, dest);
                    math.addVec3(dest, rayO);
                    math.subVec3(dest, origin, dest);
                    return true;
                }
                return false;
            }
        })();

        const rotateSectionPlane = (function () {
            const dir = math.vec3();
            const mat = math.mat4();
            return function () {
                if (self.sectionPlane) {
                    math.quaternionToMat4(rootNode.quaternion, mat);  // << ---
                    math.transformVec3(mat, [0, 0, 1], dir);
                    self._setSectionPlaneDir(dir);
                }
            };
        })();

        {
            var mouseDownLeft;
            var mouseDownMiddle;
            var mouseDownRight;
            var down = false;
            var lastAffordanceMesh;

            this._onCameraControlHover = this._viewer.cameraControl.on("hoverEnter", (hit) => {
                if (!this._visible) {
                    return;
                }
                if (down) {
                    return;
                }
                grabbed = false;
                if (lastAffordanceMesh) {
                    lastAffordanceMesh.visible = false;
                }
                var affordanceMesh;
                const meshId = hit.entity.id;
                switch (meshId) {

                    case this._displayMeshes.xAxisArrowHandle.id:
                        affordanceMesh = this._affordanceMeshes.xAxisArrow;
                        nextDragAction = DRAG_ACTIONS.xTranslate;
                        break;

                    case this._displayMeshes.xAxisHandle.id:
                        affordanceMesh = this._affordanceMeshes.xAxisArrow;
                        nextDragAction = DRAG_ACTIONS.xTranslate;
                        break;

                    case this._displayMeshes.yAxisArrowHandle.id:
                        affordanceMesh = this._affordanceMeshes.yAxisArrow;
                        nextDragAction = DRAG_ACTIONS.yTranslate;
                        break;

                    case this._displayMeshes.yShaftHandle.id:
                        affordanceMesh = this._affordanceMeshes.yAxisArrow;
                        nextDragAction = DRAG_ACTIONS.yTranslate;
                        break;

                    case this._displayMeshes.zAxisArrowHandle.id:
                        affordanceMesh = this._affordanceMeshes.zAxisArrow;
                        nextDragAction = DRAG_ACTIONS.zTranslate;
                        break;

                    case this._displayMeshes.zAxisHandle.id:
                        affordanceMesh = this._affordanceMeshes.zAxisArrow;
                        nextDragAction = DRAG_ACTIONS.zTranslate;
                        break;

                    case this._displayMeshes.xCurveHandle.id:
                        affordanceMesh = this._affordanceMeshes.xHoop;
                        nextDragAction = DRAG_ACTIONS.xRotate;
                        break;

                    case this._displayMeshes.yCurveHandle.id:
                        affordanceMesh = this._affordanceMeshes.yHoop;
                        nextDragAction = DRAG_ACTIONS.yRotate;
                        break;

                    case this._displayMeshes.zCurveHandle.id:
                        affordanceMesh = this._affordanceMeshes.zHoop;
                        nextDragAction = DRAG_ACTIONS.zRotate;
                        break;

                    default:
                        nextDragAction = DRAG_ACTIONS.none;
                        return; // Not clicked an arrow or hoop
                }
                if (affordanceMesh) {
                    affordanceMesh.visible = true;
                }
                lastAffordanceMesh = affordanceMesh;
                grabbed = true;
            });

            this._onCameraControlHoverLeave = this._viewer.cameraControl.on("hoverOutEntity", (hit) => {
                if (!this._visible) {
                    return;
                }
                if (lastAffordanceMesh) {
                    lastAffordanceMesh.visible = false;
                }
                lastAffordanceMesh = null;
                nextDragAction = DRAG_ACTIONS.none;
            });

            canvas.addEventListener("mousedown", this._canvasMouseDownListener = (e) => {
                e.preventDefault();
                if (!this._visible) {
                    return;
                }
                if (!grabbed) {
                    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;
                }
            });

            canvas.addEventListener("mousemove", this._canvasMouseMoveListener = (e) => {
                if (!this._visible) {
                    return;
                }
                if (!down) {
                    return;
                }
                var canvasPos = getClickCoordsWithinElement(e);
                const x = canvasPos[0];
                const y = canvasPos[1];

                switch (dragAction) {
                    case DRAG_ACTIONS.xTranslate:
                        dragTranslateSectionPlane(xBaseAxis, lastCanvasPos, canvasPos);
                        break;
                    case DRAG_ACTIONS.yTranslate:
                        dragTranslateSectionPlane(yBaseAxis, lastCanvasPos, canvasPos);
                        break;
                    case DRAG_ACTIONS.zTranslate:
                        dragTranslateSectionPlane(zBaseAxis, lastCanvasPos, canvasPos);
                        break;
                    case DRAG_ACTIONS.xRotate:
                        dragRotateSectionPlane(xBaseAxis, lastCanvasPos, canvasPos);
                        break;
                    case DRAG_ACTIONS.yRotate:
                        dragRotateSectionPlane(yBaseAxis, lastCanvasPos, canvasPos);
                        break;
                    case DRAG_ACTIONS.zRotate:
                        dragRotateSectionPlane(zBaseAxis, lastCanvasPos, canvasPos);
                        break;
                }

                lastCanvasPos[0] = x;
                lastCanvasPos[1] = y;
            });

            canvas.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;
                grabbed = false;
            });

            canvas.addEventListener("wheel", this._canvasWheelListener = (e) => {
                if (!this._visible) {
                    return;
                }
                var delta = Math.max(-1, Math.min(1, -e.deltaY * 40));
                if (delta === 0) {
                    return;
                }
            });
        }
    }

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

    _unbindEvents() {

        const viewer = this._viewer;
        const scene = viewer.scene;
        const canvas = scene.canvas.canvas;
        const camera = viewer.camera;
        const cameraControl = viewer.cameraControl;

        scene.off(this._onSceneTick);

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

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

        cameraControl.off(this._onCameraControlHover);
        cameraControl.off(this._onCameraControlHoverLeave);
    }

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

export {Control};