API Docs for:

File: /home/lindsay/xeolabs/xeogl-next/xeogl/src/controls/cameraControl.js

/**
 Rotates, pans and zooms the {{#crossLink "Scene"}}{{/crossLink}}'s {{#crossLink "Camera"}}{{/crossLink}} with keyboard, mouse and touch input.

 CameraControl fires these events:

 * "hover" - Hover enters a new object
 * "hoverSurface" - Hover continues over an object surface - fired continuously as mouse moves over an object
 * "hoverLeave"  - Hover has left the last object we were hovering over
 * "hoverOff" - Hover continues over empty space - fired continuously as mouse moves over nothing
 * "picked" - Clicked or tapped object
 * "pickedSurface" -  Clicked or tapped object, with event containing surface intersection details
 * "doublePicked" - Double-clicked or double-tapped object
 * "doublePickedSurface" - Double-clicked or double-tapped object, with event containing surface intersection details
 * "pickedNothing" - Clicked or tapped, but not on any objects
 * "doublePickedNothing" - Double-clicked or double-tapped, but not on any objects

 CameraControl only fires "hover" events when the mouse is up.

 For efficiency, CameraControl only does surface intersection picking when you subscribe to "doublePicked" and
 "doublePickedSurface" events. Therefore, only subscribe to those when you're OK with the overhead incurred by the
 surface intersection tests.

 ## Panning

 ## Rotating

 ## Pivoting

 ## Zooming

 ## Events

 ## Activating and deactivating

 ## Inertia

 ## First person

 ## Zoom to pointer

 TODO: describe only works for first-person
 TODO: make configurable?

 ## Keyboard layout

 # Fly-to


 @class CameraControl
 @module xeogl
 @submodule controls
 @constructor
 @param [owner] {Component} Owner component. When destroyed, the owner will destroy this component as well. Creates this component within the default {{#crossLink "Scene"}}{{/crossLink}} when omitted.
 @param [cfg] {*} Configs
 @param [cfg.id] {String} Optional ID, unique among all components in the parent scene, generated automatically when omitted.
 @param [cfg.meta] {String:Object} Optional map of user-defined metadata to attach to this CameraControl.
 @param [cfg.firstPerson=false] {Boolean} Whether or not this CameraControl is in "first person" mode.
 @param [cfg.walking=false] {Boolean} Whether or not this CameraControl is in "walking" mode.
 @param [cfg.keyboardLayout="qwerty"] {String} Keyboard layout.
 @param [cfg.doublePickFlyTo=true] {Boolean} Whether to fly the camera to each {{#crossLink "Mesh"}}{{/crossLink}} that's double-clicked.
 @param [cfg.active=true] {Boolean} Indicates whether or not this CameraControl is active.
 @param [cfg.pivoting=false] {Boolean} When true, clicking on a {{#crossLink "Mesh"}}{{/crossLink}} and dragging will pivot
 the {{#crossLink "Camera"}}{{/crossLink}} about the picked point on the Mesh's surface.
 @param [cfg.panToPointer=false] {Boolean} When true, mouse wheel when mouse is over a {{#crossLink "Mesh"}}{{/crossLink}} will zoom
 the {{#crossLink "Camera"}}{{/crossLink}} towards the hoveredd point on the Mesh's surface.
 @param [cfg.panToPivot=false] {Boolean} TODO.
 @param [cfg.inertia=0.5] {Number} A factor in range [0..1] indicating how much the camera keeps moving after you finish panning or rotating it.
 @author xeolabs / http://xeolabs.com
 @author DerSchmale / http://www.derschmale.com
 @extends Component
 */

import {core} from "./../core.js";
import {math} from '../math/math.js';
import {Component} from '../component.js';
import {Mesh} from '../objects/mesh.js';
import {AABBGeometry} from '../geometry/aabbGeometry.js';
import {PhongMaterial} from '../materials/phongMaterial.js';
import {CameraFlightAnimation} from '../animation/cameraFlightAnimation.js';
import {componentClasses} from "./../componentClasses.js";

const type = "xeogl.CameraControl";

class CameraControl extends Component {

    /**
     JavaScript class name for this Component.

     For example: "xeogl.AmbientLight", "xeogl.MetallicMaterial" etc.

     @property type
     @type String
     @final
     */
    get type() {
        return type;
    }

    init(cfg) {

        super.init(cfg);

        const self = this;

        this._boundaryHelper = new Mesh(this, {
            geometry: new AABBGeometry(this),
            material: new PhongMaterial(this, {
                diffuse: [0, 0, 0],
                ambient: [0, 0, 0],
                specular: [0, 0, 0],
                emissive: [1.0, 1.0, 0.6],
                lineWidth: 4
            }),
            visible: false,
            collidable: false
        });

        this._pivoter = new (function () { // Pivots the Camera around an arbitrary World-space position

            // Pivot math by: http://www.derschmale.com/

            const scene = self.scene;
            const camera = scene.camera;
            const canvas = scene.canvas;
            const pivotPoint = new Float32Array(3);
            let cameraOffset;
            let azimuth = 0;
            let polar = 0;
            let radius = 0;
            let pivoting = false; // True while pivoting

            const spot = document.createElement("div");
            spot.innerText = " ";
            spot.style.color = "#ffffff";
            spot.style.position = "absolute";
            spot.style.width = "25px";
            spot.style.height = "25px";
            spot.style.left = "0px";
            spot.style.top = "0px";
            spot.style["border-radius"] = "15px";
            spot.style["border"] = "2px solid #ffffff";
            spot.style["background"] = "black";
            spot.style.visibility = "hidden";
            spot.style["box-shadow"] = "5px 5px 15px 1px #000000";
            spot.style["z-index"] = 0;
            spot.style["pointer-events"] = "none";
            document.body.appendChild(spot);

            (function () {
                const viewPos = math.vec4();
                const projPos = math.vec4();
                const canvasPos = math.vec2();
                let distDirty = true;
                camera.on("viewMatrix", function () {
                    distDirty = true;
                });
                camera.on("projMatrix", function () {
                    distDirty = true;
                });
                scene.on("tick", function () {
                    if (pivoting && distDirty) {
                        math.transformPoint3(camera.viewMatrix, pivotPoint, viewPos);
                        viewPos[3] = 1;
                        math.transformPoint4(camera.projMatrix, viewPos, projPos);
                        const aabb = canvas.boundary;
                        canvasPos[0] = Math.floor((1 + projPos[0] / projPos[3]) * aabb[2] / 2);
                        canvasPos[1] = Math.floor((1 - projPos[1] / projPos[3]) * aabb[3] / 2);
                        const canvasElem = canvas.canvas;
                        const rect = canvasElem.getBoundingClientRect();
                        spot.style.left = (Math.floor(rect.left + canvasPos[0]) - 12) + "px";
                        spot.style.top = (Math.floor(rect.top + canvasPos[1]) - 12) + "px";
                        spot.style.visibility = "visible";
                        distDirty = false;
                    }
                });
            })();

            this.startPivot = function (worldPos) {
                if (worldPos) { // Use last pivotPoint by default
                    pivotPoint.set(worldPos);
                }
                let lookat = math.lookAtMat4v(camera.eye, camera.look, camera.worldUp);
                cameraOffset = math.transformPoint3(lookat, pivotPoint);
                cameraOffset[2] += math.distVec3(camera.eye, pivotPoint);
                lookat = math.inverseMat4(lookat);
                const offset = math.transformVec3(lookat, cameraOffset);
                const diff = math.vec3();
                math.subVec3(camera.eye, pivotPoint, diff);
                math.addVec3(diff, offset);
                if (camera.worldUp[2] === 1) {
                    const t = diff[1];
                    diff[1] = diff[2];
                    diff[2] = t;
                }
                radius = math.lenVec3(diff);
                polar = Math.acos(diff[1] / radius);
                azimuth = Math.atan2(diff[0], diff[2]);
                pivoting = true;
            };

            this.getPivoting = function () {
                return pivoting;
            };

            this.getPivotPos = function () {
                return pivotPoint;
            };

            this.continuePivot = function (yawInc, pitchInc) {
                if (!pivoting) {
                    return;
                }
                if (yawInc === 0 && pitchInc === 0) {
                    return;
                }
                if (camera.worldUp[2] === 1) {
                    dx = -dx;
                }
                var dx = -yawInc;
                const dy = -pitchInc;
                azimuth += -dx * .01;
                polar += dy * .01;
                polar = math.clamp(polar, .001, Math.PI - .001);
                const pos = [
                    radius * Math.sin(polar) * Math.sin(azimuth),
                    radius * Math.cos(polar),
                    radius * Math.sin(polar) * Math.cos(azimuth)
                ];
                if (camera.worldUp[2] === 1) {
                    const t = pos[1];
                    pos[1] = pos[2];
                    pos[2] = t;
                }
                // Preserve the eye->look distance, since in xeogl "look" is the point-of-interest, not the direction vector.
                const eyeLookLen = math.lenVec3(math.subVec3(camera.look, camera.eye, math.vec3()));
                math.addVec3(pos, pivotPoint);
                let lookat = math.lookAtMat4v(pos, pivotPoint, camera.worldUp);
                lookat = math.inverseMat4(lookat);
                const offset = math.transformVec3(lookat, cameraOffset);
                lookat[12] -= offset[0];
                lookat[13] -= offset[1];
                lookat[14] -= offset[2];
                const zAxis = [lookat[8], lookat[9], lookat[10]];
                camera.eye = [lookat[12], lookat[13], lookat[14]];
                math.subVec3(camera.eye, math.mulVec3Scalar(zAxis, eyeLookLen), camera.look);
                camera.up = [lookat[4], lookat[5], lookat[6]];
                spot.style.visibility = "visible";
            };

            this.endPivot = function () {
                spot.style.visibility = "hidden";
                pivoting = false;
            };

        })();

        this._cameraFlight = new CameraFlightAnimation(this, {
            duration: 0.5
        });

        this.firstPerson = cfg.firstPerson;
        this.walking = cfg.walking;
        this.keyboardLayout = cfg.keyboardLayout;
        this.doublePickFlyTo = cfg.doublePickFlyTo;
        this.active = cfg.active;
        this.pivoting = cfg.pivoting;
        this.panToPointer = cfg.panToPointer;
        this.panToPivot = cfg.panToPivot;
        this.inertia = cfg.inertia;

        this._initEvents(); // Set up all the mouse/touch/kb handlers
    }

    /**
     Indicates whether this CameraControl is active or not.

     @property active
     @default true
     @type Boolean
     */
    set active(value) {
        this._active = value !== false;
    }

    get active() {
        return this._active;
    }

    /**
     When true, clicking on a {{#crossLink "Mesh"}}{{/crossLink}} and dragging will pivot
     the {{#crossLink "Camera"}}{{/crossLink}} about the picked point on the Mesh's surface.

     @property pivoting
     @default false
     @type Boolean
     */
    set pivoting(value) {
        this._pivoting = !!value;
    }

    get pivoting() {
        return this._pivoting;
    }

    /**
     When true, mouse wheel when mouse is over a {{#crossLink "Mesh"}}{{/crossLink}} will zoom
     the {{#crossLink "Camera"}}{{/crossLink}} towards the hovered point on the Mesh's surface.

     @property panToPointer
     @default false
     @type Boolean
     */
    set panToPointer(value) {
        this._panToPointer = !!value;
        if (this._panToPointer) {
            this._panToPivot = false;
        }
    }

    get panToPointer() {
        return this._panToPointer;
    }

    /**
     When true, mouse wheel when mouse is over a {{#crossLink "Mesh"}}{{/crossLink}} will zoom
     the {{#crossLink "Camera"}}{{/crossLink}} towards the pivot point.

     @property panToPivot
     @default false
     @type Boolean
     */
    set panToPivot(value) {
        this._panToPivot = !!value;
        if (this._panToPivot) {
            this._panToPointer = false;
        }
    }

    get panToPivot() {
        return this._panToPivot;
    }

    /**
     Indicates whether this CameraControl is in "first person" mode.

     In "first person" mode (disabled by default) the look position rotates about the eye position. Otherwise,
     the eye rotates about the look.

     @property firstPerson
     @default false
     @type Boolean
     */
    set firstPerson(value) {
        this._firstPerson = !!value;
    }

    get firstPerson() {
        return this._firstPerson;
    }

    /**
     Indicates whether this CameraControl is in "walking" mode.

     When set true, this constrains eye movement to the horizontal X-Z plane. When doing a walkthrough,
     this is useful to allow us to look upwards or downwards as we move, while keeping us moving in the
     horizontal plane.

     This only has an effect when also in "first person" mode.

     @property walking
     @default false
     @type Boolean
     */
    set walking(value) {
        this._walking = !!value;
    }

    get walking() {
        return this._walking;
    }

    /**
     * TODO
     *
     *
     * @property doublePickFlyTo
     * @default true
     * @type Boolean
     */
    set doublePickFlyTo(value) {
        this._doublePickFlyTo = value !== false;
    }

    get doublePickFlyTo() {
        return this._doublePickFlyTo;
    }

    /**
     Factor in range [0..1] indicating how much the camera keeps moving after you finish
     panning or rotating it.

     A value of 0.0 causes it to immediately stop, 0.5 causes its movement to decay 50% on each tick,
     while 1.0 causes no decay, allowing it continue moving, by the current rate of pan or rotation.

     You may choose an inertia of zero when you want be able to precisely position or rotate the camera,
     without interference from inertia. ero inertia can also mean that less frames are rendered while
     you are positioning the camera.

     @property inertia
     @default 0.5
     @type Number
     */
    set inertia(value) {
        this._inertia = value === undefined ? 0.5 : value;
    }

    get inertia() {
        return this._inertia;
    }

    /**
     * TODO
     *
     * @property keyboardLayout
     * @default "qwerty"
     * @type String
     */
    set keyboardLayout(value) {
        this._keyboardLayout = value || "qwerty";
    }

    get keyboardLayout() {
        return this._keyboardLayout;
    }

    _initEvents() {

        const self = this;
        const scene = this.scene;
        const input = scene.input;
        const camera = scene.camera;
        const canvas = this.scene.canvas.canvas;
        let over = false;
        const mouseHoverDelay = 500;
        const mouseOrbitRate = 0.4;
        const mousePanRate = 0.4;
        const mouseZoomRate = 0.8;
        const mouseWheelPanRate = 0.4;
        const keyboardOrbitRate = .02;
        const keyboardPanRate = .02;
        const keyboardZoomRate = .02;
        const touchRotateRate = 0.3;
        const touchPanRate = 0.2;
        const touchZoomRate = 0.05;

        canvas.oncontextmenu = function (e) {
            e.preventDefault();
        };

        const getCanvasPosFromEvent = function (event, canvasPos) {
            if (!event) {
                event = window.event;
                canvasPos[0] = event.x;
                canvasPos[1] = event.y;
            } else {
                let element = event.target;
                let totalOffsetLeft = 0;
                let 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 pickCursorPos = [0, 0];
        let needPickMesh = false;
        let needPickSurface = false;
        let lastPickedMeshId;
        let hit;
        let picked = false;
        let pickedSurface = false;

        function updatePick() {
            if (!needPickMesh && !needPickSurface) {
                return;
            }
            picked = false;
            pickedSurface = false;
            if (needPickSurface || self.hasSubs("hoverSurface")) {
                hit = scene.pick({
                    pickSurface: true,
                    canvasPos: pickCursorPos
                });
            } else { // needPickMesh == true
                hit = scene.pick({
                    canvasPos: pickCursorPos
                });
            }
            if (hit) {
                picked = true;
                const pickedMeshId = hit.mesh.id;
                if (lastPickedMeshId !== pickedMeshId) {
                    if (lastPickedMeshId !== undefined) {

                        /**
                         * Fired whenever the pointer no longer hovers over an {{#crossLink "Mesh"}}{{/crossLink}}.
                         * @event hoverOut
                         * @param mesh The Mesh
                         */
                        self.fire("hoverOut", {
                            mesh: scene.meshes[lastPickedMeshId]
                        });
                    }

                    /**
                     * Fired when the pointer is over a new {{#crossLink "Mesh"}}{{/crossLink}}.
                     * @event hoverEnter
                     * @param hit A pick hit result containing the ID of the Mesh - see {{#crossLink "Scene/pick:method"}}Scene#pick(){{/crossLink}}.
                     */
                    self.fire("hoverEnter", hit);
                    lastPickedMeshId = pickedMeshId;
                }
                /**
                 * Fired continuously while the pointer is moving while hovering over an {{#crossLink "Mesh"}}{{/crossLink}}.
                 * @event hover
                 * @param hit A pick hit result containing the ID of the Mesh - see {{#crossLink "Scene/pick:method"}}Scene#pick(){{/crossLink}}.
                 */
                self.fire("hover", hit);
                if (hit.worldPos) {
                    pickedSurface = true;

                    /**
                     * Fired while the pointer hovers over the surface of an {{#crossLink "Mesh"}}{{/crossLink}}.
                     *
                     * This event provides 3D information about the point on the surface that the pointer is
                     * hovering over.
                     *
                     * @event hoverSurface
                     * @param hit A surface pick hit result, containing the ID of the Mesh and 3D info on the
                     * surface position - see {{#crossLink "Scene/pick:method"}}Scene#pick(){{/crossLink}}.
                     */
                    self.fire("hoverSurface", hit);
                }
            } else {
                if (lastPickedMeshId !== undefined) {
                    /**
                     * Fired whenever the pointer no longer hovers over an {{#crossLink "Mesh"}}{{/crossLink}}.
                     * @event hoverOut
                     * @param mesh The Mesh
                     */
                    self.fire("hoverOut", {
                        mesh: scene.meshes[lastPickedMeshId]
                    });
                    lastPickedMeshId = undefined;
                }
                /**
                 * Fired continuously while the pointer is moving but not hovering over anything.
                 *
                 * @event hoverOff
                 */
                self.fire("hoverOff", {
                    canvasPos: pickCursorPos
                });
            }
            needPickMesh = false;
            needPickSurface = false;
        }

        scene.on("tick", updatePick);

        //------------------------------------------------------------------------------------
        // Mouse, touch and keyboard camera control
        //------------------------------------------------------------------------------------

        (function () {

            let rotateVx = 0;
            let rotateVy = 0;
            let panVx = 0;
            let panVy = 0;
            let panVz = 0;
            let vZoom = 0;
            const mousePos = math.vec2();
            let panToMouse = false;

            let ctrlDown = false;
            let altDown = false;
            let shiftDown = false;
            const keyDown = {};

            const EPSILON = 0.001;

            const getEyeLookDist = (function () {
                const vec = new Float32Array(3);
                return function () {
                    return math.lenVec3(math.subVec3(camera.look, camera.eye, vec));
                };
            })();

            const getInverseProjectMat = (function () {
                let projMatDirty = true;
                camera.on("projMatrix", function () {
                    projMatDirty = true;
                });
                const inverseProjectMat = math.mat4();
                return function () {
                    if (projMatDirty) {
                        math.inverseMat4(camera.projMatrix, inverseProjectMat);
                    }
                    return inverseProjectMat;
                }
            })();

            const getTransposedProjectMat = (function () {
                let projMatDirty = true;
                camera.on("projMatrix", function () {
                    projMatDirty = true;
                });
                const transposedProjectMat = math.mat4();
                return function () {
                    if (projMatDirty) {
                        math.transposeMat4(camera.projMatrix, transposedProjectMat);
                    }
                    return transposedProjectMat;
                }
            })();

            const getInverseViewMat = (function () {
                let viewMatDirty = true;
                camera.on("viewMatrix", function () {
                    viewMatDirty = true;
                });
                const inverseViewMat = math.mat4();
                return function () {
                    if (viewMatDirty) {
                        math.inverseMat4(camera.viewMatrix, inverseViewMat);
                    }
                    return inverseViewMat;
                }
            })();

            const getSceneDiagSize = (function () {
                let sceneSizeDirty = true;
                let diag = 1; // Just in case
                scene.on("boundary", function () {
                    sceneSizeDirty = true;
                });
                return function () {
                    if (sceneSizeDirty) {
                        diag = math.getAABB3Diag(scene.aabb);
                    }
                    return diag;
                };
            })();

            const panToMousePos = (function () {

                const cp = math.vec4();
                const viewPos = math.vec4();
                const worldPos = math.vec4();
                const eyeCursorVec = math.vec3();

                const unproject = function (inverseProjMat, inverseViewMat, mousePos, z, viewPos, worldPos) {
                    const canvas = scene.canvas.canvas;
                    const halfCanvasWidth = canvas.offsetWidth / 2.0;
                    const halfCanvasHeight = canvas.offsetHeight / 2.0;
                    cp[0] = (mousePos[0] - halfCanvasWidth) / halfCanvasWidth;
                    cp[1] = (mousePos[1] - halfCanvasHeight) / halfCanvasHeight;
                    cp[2] = z;
                    cp[3] = 1.0;
                    math.mulMat4v4(inverseProjMat, cp, viewPos);
                    math.mulVec3Scalar(viewPos, 1.0 / viewPos[3]); // Normalize homogeneous coord
                    viewPos[3] = 1.0;
                    viewPos[1] *= -1; // TODO: Why is this reversed?
                    math.mulMat4v4(inverseViewMat, viewPos, worldPos);
                };

                return function (mousePos, factor) {

                    const lastHoverDistance = 0;
                    const inverseProjMat = getInverseProjectMat();
                    const inverseViewMat = getInverseViewMat();

                    // Get last two columns of projection matrix
                    const transposedProjectMat = getTransposedProjectMat();
                    const Pt3 = transposedProjectMat.subarray(8, 12);
                    const Pt4 = transposedProjectMat.subarray(12);
                    const D = [0, 0, -(lastHoverDistance || getSceneDiagSize()), 1];
                    const Z = math.dotVec4(D, Pt3) / math.dotVec4(D, Pt4);

                    unproject(inverseProjMat, inverseViewMat, mousePos, Z, viewPos, worldPos);

                    math.subVec3(worldPos, camera.eye, eyeCursorVec);
                    math.normalizeVec3(eyeCursorVec);

                    const px = eyeCursorVec[0] * factor;
                    const py = eyeCursorVec[1] * factor;
                    const pz = eyeCursorVec[2] * factor;

                    const eye = camera.eye;
                    const look = camera.look;

                    camera.eye = [eye[0] + px, eye[1] + py, eye[2] + pz];
                    camera.look = [look[0] + px, look[1] + py, look[2] + pz];
                };
            })();

            const panToWorldPos = (function () {
                const eyeCursorVec = math.vec3();
                return function (worldPos, factor) {
                    math.subVec3(worldPos, camera.eye, eyeCursorVec);
                    math.normalizeVec3(eyeCursorVec);
                    const px = eyeCursorVec[0] * factor;
                    const py = eyeCursorVec[1] * factor;
                    const pz = eyeCursorVec[2] * factor;
                    const eye = camera.eye;
                    const look = camera.look;
                    camera.eye = [eye[0] + px, eye[1] + py, eye[2] + pz];
                    camera.look = [look[0] + px, look[1] + py, look[2] + pz];
                };
            })();

            scene.on("tick", function () {

                const cameraInertia = self._inertia;

                if (Math.abs(rotateVx) < EPSILON) {
                    rotateVx = 0;
                }

                if (Math.abs(rotateVy) < EPSILON) {
                    rotateVy = 0;
                }

                if (rotateVy !== 0 || rotateVx !== 0) {

                    if (self._pivoter.getPivoting()) {
                        self._pivoter.continuePivot(rotateVy, rotateVx);

                    } else {

                        if (rotateVx !== 0) {

                            if (self._firstPerson) {
                                camera.pitch(-rotateVx);

                            } else {
                                camera.orbitPitch(rotateVx);
                            }
                        }

                        if (rotateVy !== 0) {

                            if (self._firstPerson) {
                                camera.yaw(rotateVy);

                            } else {
                                camera.orbitYaw(rotateVy);
                            }
                        }
                    }

                    rotateVx *= cameraInertia;
                    rotateVy *= cameraInertia;
                }

                if (Math.abs(panVx) < EPSILON) {
                    panVx = 0;
                }

                if (Math.abs(panVy) < EPSILON) {
                    panVy = 0;
                }

                if (Math.abs(panVz) < EPSILON) {
                    panVz = 0;
                }

                if (panVx !== 0 || panVy !== 0 || panVz !== 0) {
                    const f = getEyeLookDist() / 80;
                    if (self._walking) {
                        var y = camera.eye[1];
                        camera.pan([panVx * f, panVy * f, panVz * f]);
                        var eye = camera.eye;
                        eye[1] = y;
                        camera.eye = eye;
                    } else {
                        camera.pan([panVx * f, panVy * f, panVz * f]);
                    }
                }

                panVx *= cameraInertia;
                panVy *= cameraInertia;
                panVz *= cameraInertia;

                if (Math.abs(vZoom) < EPSILON) {
                    vZoom = 0;
                }

                if (vZoom !== 0) {
                    if (self._firstPerson) {
                        var y;
                        if (self._walking) {
                            y = camera.eye[1];
                        }
                        if (panToMouse) { // Using mouse input
                            panToMousePos(mousePos, -vZoom * 2);
                        } else {
                            camera.pan([0, 0, vZoom]); // Touchscreen input with no cursor
                        }
                        if (self._walking) {
                            var eye = camera.eye;
                            eye[1] = y;
                            camera.eye = eye;
                        }
                    } else {
                        // Do both zoom and ortho scale so that we can switch projections without weird scale jumps
                        if (self._panToPointer) {
                            updatePick();
                            if (pickedSurface) {
                                panToWorldPos(hit.worldPos, -vZoom);
                            } else {
                                camera.zoom(vZoom);
                            }
                        } else if (self._panToPivot) {
                            panToWorldPos(self._pivoter.getPivotPos(), -vZoom); // FIXME: What about when pivotPos undefined?
                        } else {
                            camera.zoom(vZoom);
                        }
                        camera.ortho.scale = camera.ortho.scale + vZoom;
                    }
                    vZoom *= cameraInertia;
                }
            });

            function getZoomRate() {
                const aabb = scene.aabb;
                const xsize = aabb[3] - aabb[0];
                const ysize = aabb[4] - aabb[1];
                const zsize = aabb[5] - aabb[2];
                let max = (xsize > ysize ? xsize : ysize);
                max = (zsize > max ? zsize : max);
                return max / 30;
            }

            document.addEventListener("keyDown", function (e) {
                if (!self._active) {
                    return;
                }
                if (e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") {
                    ctrlDown = e.ctrlKey || e.keyCode === 17 || e.metaKey; // !important, treat Windows or Mac Command Key as ctrl
                    altDown = e.altKey || e.keyCode === 18;
                    shiftDown = e.keyCode === 16;
                    keyDown[e.keyCode] = true;
                }
            }, true);

            document.addEventListener("keyup", function (e) {
                if (!self._active) {
                    return;
                }
                if (e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") {
                    if (e.ctrlKey || e.keyCode === 17) {
                        ctrlDown = false;
                    }
                    if (e.altKey || e.keyCode === 18) {
                        altDown = false;
                    }
                    if (e.keyCode === 16) {
                        shiftDown = false;
                    }
                    keyDown[e.keyCode] = false;
                }
            });

            // Mouse camera rotate, pan and zoom

            (function () {

                let lastX;
                let lastY;
                let xDelta = 0;
                let yDelta = 0;
                let down = false;

                let mouseDownLeft;
                let mouseDownMiddle;
                let mouseDownRight;

                canvas.addEventListener("mousedown", function (e) {
                    if (!self._active) {
                        return;
                    }
                    over = true;
                    switch (e.which) {
                        case 1: // Left button
                            mouseDownLeft = true;
                            down = true;
                            xDelta = 0;
                            yDelta = 0;
                            getCanvasPosFromEvent(e, mousePos);
                            lastX = mousePos[0];
                            lastY = mousePos[1];
                            break;
                        case 2: // Middle/both buttons
                            mouseDownMiddle = true;
                            break;
                        case 3: // Right button
                            mouseDownRight = true;
                            down = true;
                            xDelta = 0;
                            yDelta = 0;
                            getCanvasPosFromEvent(e, mousePos);
                            lastX = mousePos[0];
                            lastY = mousePos[1];
                            break;
                            break;
                        default:
                            break;
                    }
                });

                canvas.addEventListener("mouseup", function (e) {
                    if (!self._active) {
                        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;
                    xDelta = 0;
                    yDelta = 0;
                });

                document.addEventListener("mouseup", function (e) {
                    if (!self._active) {
                        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;
                    xDelta = 0;
                    yDelta = 0;
                });

                canvas.addEventListener("mouseenter", function () {
                    if (!self._active) {
                        return;
                    }
                    over = true;
                    xDelta = 0;
                    yDelta = 0;
                });

                canvas.addEventListener("mouseleave", function () {
                    if (!self._active) {
                        return;
                    }
                    over = false;
                    xDelta = 0;
                    yDelta = 0;
                });

                canvas.addEventListener("mousemove", function (e) {
                    if (!self._active) {
                        return;
                    }
                    if (!over) {
                        return;
                    }
                    getCanvasPosFromEvent(e, mousePos);
                    panToMouse = true;
                    if (!down) {
                        return;
                    }
                    const x = mousePos[0];
                    const y = mousePos[1];
                    xDelta += (x - lastX) * mouseOrbitRate;
                    yDelta += (y - lastY) * mouseOrbitRate;
                    lastX = x;
                    lastY = y;
                });

                scene.on("tick", function () {
                    if (!self._active) {
                        return;
                    }
                    if (Math.abs(xDelta) === 0 && Math.abs(yDelta) === 0) {
                        return;
                    }

                    const panning = shiftDown || mouseDownRight;

                    if (panning) {

                        // Panning

                        panVx = xDelta * mousePanRate;
                        panVy = yDelta * mousePanRate;

                    } else {

                        // Orbiting

                        rotateVy = -xDelta * mouseOrbitRate;
                        rotateVx = yDelta * mouseOrbitRate;
                    }

                    xDelta = 0;
                    yDelta = 0;
                });

                // Mouse wheel zoom

                canvas.addEventListener("wheel", function (e) {
                    if (!self._active) {
                        return;
                    }
                    if (self._panToPointer) {
                        needPickSurface = true;
                    }
                    const delta = Math.max(-1, Math.min(1, -e.deltaY * 40));
                    if (delta === 0) {
                        return;
                    }
                    const d = delta / Math.abs(delta);
                    vZoom = -d * getZoomRate() * mouseZoomRate;
                    e.preventDefault();
                });

                // Keyboard zoom

                scene.on("tick", function (e) {
                    if (!self._active) {
                        return;
                    }
                    if (!over) {
                        return;
                    }
                    const elapsed = e.deltaTime;
                    if (!self.ctrlDown && !self.altDown) {
                        const wkey = input.keyDown[input.KEY_ADD];
                        const skey = input.keyDown[input.KEY_SUBTRACT];
                        if (wkey || skey) {
                            if (skey) {
                                vZoom = elapsed * getZoomRate() * keyboardZoomRate;
                            } else if (wkey) {
                                vZoom = -elapsed * getZoomRate() * keyboardZoomRate;
                            }
                        }
                    }
                });

                // Keyboard panning

                (function () {

                    scene.on("tick", function (e) {
                        if (!self._active) {
                            return;
                        }
                        if (!over) {
                            return;
                        }

                        const elapsed = e.deltaTime;

                        // if (!self.ctrlDown && !self.altDown) {
                        let front, back, left, right, up, down;
                        if (self._keyboardLayout == 'azerty') {
                            front = input.keyDown[input.KEY_Z];
                            back = input.keyDown[input.KEY_S];
                            left = input.keyDown[input.KEY_Q];
                            right = input.keyDown[input.KEY_D];
                            up = input.keyDown[input.KEY_W];
                            down = input.keyDown[input.KEY_X];
                        } else {
                            front = input.keyDown[input.KEY_W];
                            back = input.keyDown[input.KEY_S];
                            left = input.keyDown[input.KEY_A];
                            right = input.keyDown[input.KEY_D];
                            up = input.keyDown[input.KEY_Z];
                            down = input.keyDown[input.KEY_X];
                        }
                        if (front || back || left || right || up || down) {
                            if (down) {
                                panVy += elapsed * keyboardPanRate;
                            } else if (up) {
                                panVy -= -elapsed * keyboardPanRate;
                            }
                            if (right) {
                                panVx += -elapsed * keyboardPanRate;
                            } else if (left) {
                                panVx = elapsed * keyboardPanRate;
                            }
                            if (back) {
                                panVz = elapsed * keyboardPanRate;
                            } else if (front) {
                                panVz = -elapsed * keyboardPanRate;
                            }
                        }
                        //          }
                    });
                })();
            })();

            // Touch camera rotate, pan and zoom

            (function () {

                let touchStartTime;
                const tapStartPos = new Float32Array(2);
                let tapStartTime = -1;

                const lastTouches = [];
                let numTouches = 0;

                const touch0Vec = new Float32Array(2);
                const touch1Vec = new Float32Array(2);

                const MODE_CHANGE_TIMEOUT = 50;
                const MODE_NONE = 0;
                const MODE_ROTATE = 1;
                const MODE_PAN = 1 << 1;
                const MODE_ZOOM = 1 << 2;
                let currentMode = MODE_NONE;
                let transitionTime = Date.now();

                function checkMode(mode) {
                    const currentTime = Date.now();
                    if (currentMode === MODE_NONE) {
                        currentMode = mode;
                        return true;
                    }
                    if (currentMode === mode) {
                        return currentTime - transitionTime > MODE_CHANGE_TIMEOUT;
                    }
                    currentMode = mode;
                    transitionTime = currentTime;
                    return false;
                }

                canvas.addEventListener("touchstart", function (event) {
                    if (!self._active) {
                        return;
                    }
                    const touches = event.touches;
                    const changedTouches = event.changedTouches;

                    touchStartTime = Date.now();

                    if (touches.length === 1 && changedTouches.length === 1) {
                        tapStartTime = touchStartTime;
                        tapStartPos[0] = touches[0].pageX;
                        tapStartPos[1] = touches[0].pageY;
                    } else {
                        tapStartTime = -1;
                    }

                    while (lastTouches.length < touches.length) {
                        lastTouches.push(new Float32Array(2));
                    }

                    for (let i = 0, len = touches.length; i < len; ++i) {
                        lastTouches[i][0] = touches[i].pageX;
                        lastTouches[i][1] = touches[i].pageY;
                    }

                    currentMode = MODE_NONE;
                    numTouches = touches.length;

                    event.stopPropagation();
                }, {passive: true});

                canvas.addEventListener("touchmove", function (event) {
                    if (!self._active) {
                        return;
                    }
                    const touches = event.touches;

                    if (numTouches === 1) {

                        var touch0 = touches[0];

                        if (checkMode(MODE_ROTATE)) {
                            const deltaX = touch0.pageX - lastTouches[0][0];
                            const deltaY = touch0.pageY - lastTouches[0][1];
                            const rotateX = deltaX * touchRotateRate;
                            const rotateY = deltaY * touchRotateRate;
                            rotateVx = rotateY;
                            rotateVy = -rotateX;
                        }

                    } else if (numTouches === 2) {

                        var touch0 = touches[0];
                        const touch1 = touches[1];

                        math.subVec2([touch0.pageX, touch0.pageY], lastTouches[0], touch0Vec);
                        math.subVec2([touch1.pageX, touch1.pageY], lastTouches[1], touch1Vec);

                        const panning = math.dotVec2(touch0Vec, touch1Vec) > 0;

                        if (panning && checkMode(MODE_PAN)) {
                            math.subVec2([touch0.pageX, touch0.pageY], lastTouches[0], touch0Vec);
                            panVx = touch0Vec[0] * touchPanRate;
                            panVy = touch0Vec[1] * touchPanRate;
                        }

                        if (!panning && checkMode(MODE_ZOOM)) {
                            const d1 = math.distVec2([touch0.pageX, touch0.pageY], [touch1.pageX, touch1.pageY]);
                            const d2 = math.distVec2(lastTouches[0], lastTouches[1]);
                            vZoom = (d2 - d1) * getZoomRate() * touchZoomRate;
                        }
                    }

                    for (let i = 0; i < numTouches; ++i) {
                        lastTouches[i][0] = touches[i].pageX;
                        lastTouches[i][1] = touches[i].pageY;
                    }

                    event.stopPropagation();
                }, {passive: true});

            })();

            // Keyboard rotation

            (function () {

                scene.on("tick", function (e) {
                    if (!self._active) {
                        return;
                    }
                    if (!over) {
                        return;
                    }
                    const elapsed = e.deltaTime;
                    const left = input.keyDown[input.KEY_LEFT_ARROW];
                    const right = input.keyDown[input.KEY_RIGHT_ARROW];
                    const up = input.keyDown[input.KEY_UP_ARROW];
                    const down = input.keyDown[input.KEY_DOWN_ARROW];
                    if (left || right || up || down) {
                        if (right) {
                            rotateVy += -elapsed * keyboardOrbitRate;

                        } else if (left) {
                            rotateVy += elapsed * keyboardOrbitRate;
                        }
                        if (down) {
                            rotateVx += elapsed * keyboardOrbitRate;

                        } else if (up) {
                            rotateVx += -elapsed * keyboardOrbitRate;
                        }
                    }
                });
            })();

            // First-person rotation about vertical axis with A and E keys for AZERTY layout

            (function () {

                scene.on("tick", function (e) {
                    if (!self._active) {
                        return;
                    }
                    if (!over) {
                        return;
                    }
                    const elapsed = e.deltaTime;
                    let rotateLeft;
                    let rotateRight;
                    if (self._keyboardLayout == 'azerty') {
                        rotateLeft = input.keyDown[input.KEY_A];
                        rotateRight = input.keyDown[input.KEY_E];
                    } else {
                        rotateLeft = input.keyDown[input.KEY_Q];
                        rotateRight = input.keyDown[input.KEY_E];
                    }
                    if (rotateRight || rotateLeft) {
                        if (rotateLeft) {
                            rotateVy += elapsed * keyboardOrbitRate;
                        } else if (rotateRight) {
                            rotateVy += -elapsed * keyboardOrbitRate;
                        }
                    }
                });

            })();
        })();

        //------------------------------------------------------------------------------------
        // Mouse and touch picking
        //------------------------------------------------------------------------------------

        (function () {

            // Mouse picking

            (function () {

                canvas.addEventListener("mousemove", function (e) {

                    if (!self._active) {
                        return;
                    }

                    getCanvasPosFromEvent(e, pickCursorPos);

                    if (self.hasSubs("hover") || self.hasSubs("hoverOut") || self.hasSubs("hoverOff") || self.hasSubs("hoverSurface")) {
                        needPickMesh = true;
                    }
                });

                let downX;
                let downY;
                let downCursorX;
                let downCursorY;

                canvas.addEventListener('mousedown', function (e) {
                    if (!self._active) {
                        return;
                    }
                    downX = e.clientX;
                    downY = e.clientY;
                    downCursorX = pickCursorPos[0];
                    downCursorY = pickCursorPos[1];

                    needPickSurface = self._pivoting;
                    updatePick();
                    if (self._pivoting) {
                        if (hit) {
                            self._pivoter.startPivot(hit.worldPos);
                        } else {
                            self._pivoter.startPivot(); // Continue to use last pivot point
                        }
                    }
                });

                canvas.addEventListener('mouseup', (function (e) {

                    let clicks = 0;
                    let timeout;

                    return function (e) {

                        if (!self._active) {
                            return;
                        }

                        self._pivoter.endPivot();

                        if (Math.abs(e.clientX - downX) > 3 || Math.abs(e.clientY - downY) > 3) {
                            return;
                        }

                        if (!self._doublePickFlyTo && !self.hasSubs("doublePicked") && !self.hasSubs("doublePickedSurface") && !self.hasSubs("doublePickedNothing")) {

                            //  Avoid the single/double click differentiation timeout

                            needPickSurface = !!self.hasSubs("pickedSurface");

                            updatePick();

                            if (hit) {

                                /**
                                 * Fired whenever the pointer has picked (ie. clicked or tapped) an {{#crossLink "Mesh"}}{{/crossLink}}.
                                 *
                                 * @event picked
                                 * @param hit A surface pick hit result containing the ID of the Mesh - see {{#crossLink "Scene/pick:method"}}Scene#pick(){{/crossLink}}.
                                 */
                                self.fire("picked", hit);
                                if (pickedSurface) {

                                    /**
                                     * Fired when the pointer has picked (ie. clicked or tapped) the surface of an {{#crossLink "Mesh"}}{{/crossLink}}.
                                     *
                                     * This event provides 3D information about the point on the surface that the pointer has picked.
                                     *
                                     * @event pickedSurface
                                     * @param hit A surface pick hit result, containing the ID of the Mesh and 3D info on the
                                     * surface possition - see {{#crossLink "Scene/pick:method"}}Scene#pick(){{/crossLink}}.
                                     */
                                    self.fire("pickedSurface", hit);
                                }
                            } else {

                                /**
                                 * Fired when the pointer attempted a pick (ie. clicked or tapped), but has hit nothing.
                                 *
                                 * @event pickedNothing
                                 */
                                self.fire("pickedNothing");
                            }

                            return;
                        }

                        clicks++;

                        if (clicks == 1) {
                            timeout = setTimeout(function () {

                                needPickMesh = self._doublePickFlyTo;
                                needPickSurface = needPickMesh || !!self.hasSubs("pickedSurface");
                                pickCursorPos[0] = downCursorX;
                                pickCursorPos[1] = downCursorY;

                                updatePick();

                                if (hit) {
                                    self.fire("picked", hit);
                                    if (pickedSurface) {
                                        self.fire("pickedSurface", hit);
                                    }
                                } else {
                                    self.fire("pickedNothing");
                                }

                                clicks = 0;
                            }, 250);  // FIXME: Too short for track pads

                        } else {

                            clearTimeout(timeout);

                            needPickMesh = self._doublePickFlyTo;
                            needPickSurface = needPickMesh && !!self.hasSubs("doublePickedSurface");

                            updatePick();

                            if (hit) {
                                /**
                                 * Fired whenever the pointer has double-picked (ie. double-clicked or double-tapped) an {{#crossLink "Mesh"}}{{/crossLink}}.
                                 *
                                 * @event picked
                                 * @param hit A surface pick hit result containing the ID of the Mesh - see {{#crossLink "Scene/pick:method"}}Scene#pick(){{/crossLink}}.
                                 */
                                self.fire("doublePicked", hit);
                                if (pickedSurface) {
                                    /**
                                     * Fired when the pointer has double-picked (ie. double-clicked or double-tapped) the surface of an {{#crossLink "Mesh"}}{{/crossLink}}.
                                     *
                                     * This event provides 3D information about the point on the surface that the pointer has picked.
                                     *
                                     * @event doublePickedSurface
                                     * @param hit A surface pick hit result, containing the ID of the Mesh and 3D info on the
                                     * surface possition - see {{#crossLink "Scene/pick:method"}}Scene#pick(){{/crossLink}}.
                                     */
                                    self.fire("doublePickedSurface", hit);
                                }
                                if (self._doublePickFlyTo) {
                                    self._flyTo(hit);
                                }
                            } else {

                                /**
                                 * Fired when the pointer attempted a double-pick (ie. double-clicked or double-tapped), but has hit nothing.
                                 *
                                 * @event doublePickedNothing
                                 */
                                self.fire("doublePickedNothing");
                                if (self._doublePickFlyTo) {
                                    self._flyTo();
                                }
                            }
                            clicks = 0;
                        }
                    };
                })(), false);

            })();

            // Touch picking

            (function () {

                const TAP_INTERVAL = 150;
                const DBL_TAP_INTERVAL = 325;
                const TAP_DISTANCE_THRESHOLD = 4;

                let touchStartTime;
                const activeTouches = [];
                const tapStartPos = new Float32Array(2);
                let tapStartTime = -1;
                let lastTapTime = -1;

                canvas.addEventListener("touchstart", function (event) {

                    if (!self._active) {
                        return;
                    }

                    const touches = event.touches;
                    const changedTouches = event.changedTouches;

                    touchStartTime = Date.now();

                    if (touches.length === 1 && changedTouches.length === 1) {
                        tapStartTime = touchStartTime;
                        tapStartPos[0] = touches[0].pageX;
                        tapStartPos[1] = touches[0].pageY;
                    } else {
                        tapStartTime = -1;
                    }

                    while (activeTouches.length < touches.length) {
                        activeTouches.push(new Float32Array(2))
                    }

                    for (let i = 0, len = touches.length; i < len; ++i) {
                        activeTouches[i][0] = touches[i].pageX;
                        activeTouches[i][1] = touches[i].pageY;
                    }

                    activeTouches.length = touches.length;

                    event.stopPropagation();
                }, {passive: true});

                //canvas.addEventListener("touchmove", function (event) {
                //    event.preventDefault();
                //    event.stopPropagation();
                //});

                canvas.addEventListener("touchend", function (event) {

                    if (!self._active) {
                        return;
                    }

                    const currentTime = Date.now();
                    const touches = event.touches;
                    const changedTouches = event.changedTouches;

                    // process tap

                    if (touches.length === 0 && changedTouches.length === 1) {

                        if (tapStartTime > -1 && currentTime - tapStartTime < TAP_INTERVAL) {

                            if (lastTapTime > -1 && tapStartTime - lastTapTime < DBL_TAP_INTERVAL) {

                                // Double-tap

                                pickCursorPos[0] = Math.round(changedTouches[0].clientX);
                                pickCursorPos[1] = Math.round(changedTouches[0].clientY);
                                needPickMesh = true;
                                needPickSurface = !!self.hasSubs("pickedSurface");

                                updatePick();

                                if (hit) {
                                    self.fire("doublePicked", hit);
                                    if (pickedSurface) {
                                        self.fire("doublePickedSurface", hit);
                                    }
                                    if (self._doublePickFlyTo) {
                                        self._flyTo(hit);
                                    }
                                } else {
                                    self.fire("doublePickedNothing");
                                    if (self._doublePickFlyTo) {
                                        self._flyTo();
                                    }
                                }

                                lastTapTime = -1;

                            } else if (math.distVec2(activeTouches[0], tapStartPos) < TAP_DISTANCE_THRESHOLD) {

                                // Single-tap

                                pickCursorPos[0] = Math.round(changedTouches[0].clientX);
                                pickCursorPos[1] = Math.round(changedTouches[0].clientY);
                                needPickMesh = true;
                                needPickSurface = !!self.hasSubs("pickedSurface");

                                updatePick();

                                if (hit) {
                                    self.fire("picked", hit);
                                    if (pickedSurface) {
                                        self.fire("pickedSurface", hit);
                                    }
                                } else {
                                    self.fire("pickedNothing");
                                }

                                lastTapTime = currentTime;
                            }

                            tapStartTime = -1
                        }
                    }

                    activeTouches.length = touches.length;

                    for (let i = 0, len = touches.length; i < len; ++i) {
                        activeTouches[i][0] = touches[i].pageX;
                        activeTouches[i][1] = touches[i].pageY;
                    }

                    event.stopPropagation();
                }, {passive: true});
            })();
        })();

        //------------------------------------------------------------------------------------
        // Keyboard camera axis views
        //------------------------------------------------------------------------------------

        (function () {

            const KEY_NUM_1 = 49;
            const KEY_NUM_2 = 50;
            const KEY_NUM_3 = 51;
            const KEY_NUM_4 = 52;
            const KEY_NUM_5 = 53;
            const KEY_NUM_6 = 54;

            const center = math.vec3();
            const tempVec3a = math.vec3();
            const tempVec3b = math.vec3();
            const tempVec3c = math.vec3();

            const cameraTarget = {
                eye: new Float32Array(3),
                look: new Float32Array(3),
                up: new Float32Array(3)
            };

            document.addEventListener("keydown", function (e) {

                if (!self._active) {
                    return;
                }

                if (!over) {
                    return;
                }

                const keyCode = e.keyCode;

                if (keyCode !== KEY_NUM_1 &&
                    keyCode !== KEY_NUM_2 &&
                    keyCode !== KEY_NUM_3 &&
                    keyCode !== KEY_NUM_4 &&
                    keyCode !== KEY_NUM_5 &&
                    keyCode !== KEY_NUM_6) {
                    return;
                }

                const aabb = scene.aabb;
                const diag = math.getAABB3Diag(aabb);
                center[0] = aabb[0] + aabb[3] / 2.0;
                center[1] = aabb[1] + aabb[4] / 2.0;
                center[2] = aabb[2] + aabb[5] / 2.0;
                const dist = Math.abs((diag) / Math.tan(self._cameraFlight.fitFOV / 2));

                switch (keyCode) {

                    case KEY_NUM_1: // Right

                        cameraTarget.eye.set(math.mulVec3Scalar(camera.worldRight, dist, tempVec3a));
                        cameraTarget.look.set(center);
                        cameraTarget.up.set(camera.worldUp);

                        break;

                    case KEY_NUM_2: // Back

                        cameraTarget.eye.set(math.mulVec3Scalar(camera.worldForward, dist, tempVec3a));
                        cameraTarget.look.set(center);
                        cameraTarget.up.set(camera.worldUp);

                        break;

                    case KEY_NUM_3: // Left

                        cameraTarget.eye.set(math.mulVec3Scalar(camera.worldRight, -dist, tempVec3a));
                        cameraTarget.look.set(center);
                        cameraTarget.up.set(camera.worldUp);

                        break;

                    case KEY_NUM_4: // Front

                        cameraTarget.eye.set(math.mulVec3Scalar(camera.worldForward, -dist, tempVec3a));
                        cameraTarget.look.set(center);
                        cameraTarget.up.set(camera.worldUp);

                        break;

                    case KEY_NUM_5: // Top

                        cameraTarget.eye.set(math.mulVec3Scalar(camera.worldUp, dist, tempVec3a));
                        cameraTarget.look.set(center);
                        cameraTarget.up.set(math.normalizeVec3(math.mulVec3Scalar(camera.worldForward, 1, tempVec3b), tempVec3c));

                        break;

                    case KEY_NUM_6: // Bottom

                        cameraTarget.eye.set(math.mulVec3Scalar(camera.worldUp, -dist, tempVec3a));
                        cameraTarget.look.set(center);
                        cameraTarget.up.set(math.normalizeVec3(math.mulVec3Scalar(camera.worldForward, -1, tempVec3b)));

                        break;

                    default:
                        return;
                }

                if (self._cameraFlight.duration > 0) {
                    self._cameraFlight.flyTo(cameraTarget);
                } else {
                    self._cameraFlight.jumpTo(cameraTarget);
                }
            });

        })();
    }

    _flyTo(hit) {

        let pos;

        if (hit && hit.worldPos) {
            pos = hit.worldPos
        }

        const aabb = hit ? hit.mesh.aabb : this.scene.aabb;

        this._boundaryHelper.geometry.targetAABB = aabb;
        //    this._boundaryHelper.visible = true;

        if (pos) {

            // Fly to look at point, don't change eye->look dist

            const camera = this.scene.camera;
            const diff = math.subVec3(camera.eye, camera.look, []);

            this._cameraFlight.flyTo({
                    // look: pos,
                    // eye: xeogl.math.addVec3(pos, diff, []),
                    // up: camera.up,
                    aabb: aabb
                },
                this._hideBoundary, this);

            // TODO: Option to back off to fit AABB in view

        } else {

            // Fly to fit target boundary in view

            this._cameraFlight.flyTo({
                    aabb: aabb
                },
                this._hideBoundary, this);
        }
    }

    _hideBoundary() {
        //    this._boundaryHelper.visible = false;
    }

    destroy() {
        this.active = false;
        super.destroy();
    }
}

componentClasses[type] = CameraControl;

export {CameraControl};