API Docs for: 2.0.0
Show:

File: src/viewer/camera/camera.js

/**
 A **Camera** defines a viewpoint within a {{#crossLink "Viewer"}}Viewer{{/crossLink}}.

 ## Overview

 <ul>
 <li>You can have an unlimited number of Cameras in a {{#crossLink "Viewer"}}{{/crossLink}}.</li>
 <li>At any instant, the Camera we're looking through is the one whose {{#crossLink "Camera/active:property"}}active{{/crossLink}} property is true.</li>
 <li>Cameras can be controlled with controls such as {{#crossLink "CameraControl"}}{{/crossLink}}, {{#crossLink "KeyboardAxisCamera"}}{{/crossLink}},
 {{#crossLink "KeyboardOrbitCamera"}}{{/crossLink}}, {{#crossLink "KeyboardPanCamera"}}{{/crossLink}}, {{#crossLink "KeyboardZoomCamera"}}{{/crossLink}},
 {{#crossLink "MouseOrbitCamera"}}{{/crossLink}}, {{#crossLink "MousePanCamera"}}{{/crossLink}} and {{#crossLink "MouseZoomCamera"}}{{/crossLink}}.</li>
 </ul>

 ## Example

 In this example we define multiple Cameras looking at a {{#crossLink "TeapotObject"}}{{/crossLink}}, then periodically switch between the Cameras.

 <iframe style="width: 600px; height: 400px" src="../../examples/camera_Camera_multiple.html"></iframe>

 ````Javascript
 // Create a Viewer
 var viewer = new BIMSURFER.Viewer({ element: "myDiv" });

 // Create an object
 var box = new BIMSURFER.TeapotObject(viewer);

 // Create some Cameras
 var cameras = [

 new BIMSURFER.Camera(viewer, {
        eye: [5, 5, 5],
        active: false
    }),

 new BIMSURFER.Camera(viewer, {
        eye: [-5, 5, 5],
        active: false
    }),

 new BIMSURFER.Camera(viewer, {
        eye: [5, -5, 5],
        active: false
    }),

 new BIMSURFER.Camera(viewer, {
        eye: [5, 5, -5],
        active: false
    }),

 new BIMSURFER.Camera(viewer, {
        eye: [-5, -5, 5],
        active: false
    })
 ];

 // Periodically switch between the Cameras

 var i = -1;
 var last = -1;

 setInterval(function () {

        if (last > -1) {
            cameras[last].active = false
        }

        i = (i + 1) % (cameras.length - 1);

        cameras[i].active = true;

        last = i;

    }, 1000);
 ````

 @class Camera
 @module BIMSURFER
 @submodule camera
 @constructor
 @param [viewer] {Viewer} Parent {{#crossLink "Viewer"}}{{/crossLink}}.
 @param [cfg] {*} Configs
 @param [cfg.id] {String} Optional ID, unique among all components in the parent viewer, generated automatically when omitted.
 @param [cfg.meta] {String:Object} Optional map of user-defined metadata to attach to this Camera.
 @param [cfg.eye=[0,0,-10]] {Array of Number} Eye position.
 @param [cfg.look=[0,0,0]] {Array of Number} The position of the point-of-interest we're looking at.
 @param [cfg.up=[0,1,0]] {Array of Number} The "up" vector.
 @param [cfg.fovy=60.0] {Number} Field-of-view angle, in degrees, on Y-axis.
 @param [cfg.aspect=1.0] {Number} Aspect ratio.
 @param [cfg.near=0.1] {Number} Position of the near plane on the View-space Z-axis.
 @param [cfg.far=10000] {Number} Position of the far plane on the View-space Z-axis.
 @extends Component
 */
(function () {

    "use strict";

    /**
     * Defines a viewpoint within a {@link Viewer}.
     */
    BIMSURFER.Camera = BIMSURFER.Component.extend({

        /**
         JavaScript class name for this Component.

         @property className
         @type String
         @final
         */
        className: "BIMSURFER.Camera",

        /**
         Indicates that only one instance of a Camera may be active within
         its {{#crossLink "Viewer"}}{{/crossLink}} at a time. When a Camera is activated, that has
         a true value for this flag, then any other active Camera will be deactivated first.

         @property exclusive
         @type Boolean
         @final
         */
        exclusive: true,

        
        _init: function (cfg) {

            // The ViewerJS nodes that this Camera controls
            this._lookatNode = this.viewer.scene.getNode('theLookat');
            this._cameraNode = this.viewer.scene.getNode('theCamera');

            // Schedule update of view and projection transforms for next tick
            this._lookatNodeDirty = true;
            this._cameraNodeDirty = true;

            // Camera not at rest now
            this._rested = false;

            this._tickSub = null;


            // TODO: compute/orthogolalize 'up'

            this.eye = cfg.eye;
            this.look = cfg.look;
            this.up = cfg.up;
            this.aspect = cfg.aspect;
            this.fovy = cfg.fovy;
            this.near = cfg.near;
            this.far = cfg.far;
            this.screenPan = cfg.screenPan;

            this.active = cfg.active !== false;
        },

        /**
         * Rotate 'eye' about 'look', around the 'up' vector
         *
         * @param {Number} angle Angle of rotation in degrees
         */
        rotateEyeY: function (angle) {

            // Get 'look' -> 'eye' vector
            var eye2 = BIMSURFER.math.subVec3(this._eye, this._look, []);

            // Rotate 'eye' vector about 'up' vector
            var mat = BIMSURFER.math.rotationMat4v(angle * 0.0174532925, this._up);
            eye2 = BIMSURFER.math.transformPoint3(mat, eye2, []);

            // Set eye position as 'look' plus 'eye' vector
            this._eye = BIMSURFER.math.addVec3(eye2, this._look, []);

            this._lookatNodeDirty = true;
        },

        /**
         * Rotate 'eye' about 'look' around the X-axis
         *
         * @param {Number} angle Angle of rotation in degrees
         */
        rotateEyeX: function (angle) {

            // Get 'look' -> 'eye' vector
            var eye2 = BIMSURFER.math.subVec3(this._eye, this._look, []);

            // Get orthogonal vector from 'eye' and 'up'
            var left = BIMSURFER.math.cross3Vec3(BIMSURFER.math.normalizeVec3(eye2, []), BIMSURFER.math.normalizeVec3(this._up, []));

            // Rotate 'eye' vector about orthogonal vector
            var mat = BIMSURFER.math.rotationMat4v(angle * 0.0174532925, left);
            eye2 = BIMSURFER.math.transformPoint3(mat, eye2, []);

            // Set eye position as 'look' plus 'eye' vector
            this._eye = BIMSURFER.math.addVec3(eye2, this._look, []);

            // Rotate 'up' vector about orthogonal vector
            this._up = BIMSURFER.math.transformPoint3(mat, this._up, []);

            this._lookatNodeDirty = true;
        },

        /**
         * Rotate 'look' about 'eye', around the 'up' vector
         *
         * <p>Applies constraints added with {@link #addConstraint}.</p>
         *
         * @param {Number} angle Angle of rotation in degrees
         */
        rotateLookY: function (angle) {

            // Get 'look' -> 'eye' vector
            var look2 = BIMSURFER.math.subVec3(this._look, this._eye, []);

            // Rotate 'look' vector about 'up' vector
            var mat = BIMSURFER.math.rotationMat4v(angle * 0.0174532925, this._up);
            look2 = BIMSURFER.math.transformPoint3(mat, look2, []);

            // Set look position as 'look' plus 'eye' vector
            this._look = BIMSURFER.math.addVec3(look2, this._eye, []);

            this._lookatNodeDirty = true;
        },

        /**
         * Rotate 'eye' about 'look' around the X-axis
         *
         * @param {Number} angle Angle of rotation in degrees
         */
        rotateLookX: function (angle) {

            // Get 'look' -> 'eye' vector
            var look2 = BIMSURFER.math.subVec3(this._look, this._eye, []);

            // Get orthogonal vector from 'eye' and 'up'
            var left = BIMSURFER.math.cross3Vec3(BIMSURFER.math.normalizeVec3(look2, []), BIMSURFER.math.normalizeVec3(this._up, []));

            // Rotate 'look' vector about orthogonal vector
            var mat = BIMSURFER.math.rotationMat4v(angle * 0.0174532925, left);
            look2 = BIMSURFER.math.transformPoint3(mat, look2, []);

            // Set eye position as 'look' plus 'eye' vector
            this._look = BIMSURFER.math.addVec3(look2, this._eye, []);

            // Rotate 'up' vector about orthogonal vector
            this._up = BIMSURFER.math.transformPoint3(mat, this._up, []);

            this._lookatNodeDirty = true;
        },

        /**
         * Pans the camera along X and Y axis.
         * @param pan The pan vector
         */
        pan: function (pan) {

            // Get 'look' -> 'eye' vector
            var eye2 = BIMSURFER.math.subVec3(this._eye, this._look, []);

            // Building this pan vector
            var vec = [0, 0, 0];
            var v;

            if (pan[0] !== 0) {

                // Pan along orthogonal vector to 'look' and 'up'

                var left = BIMSURFER.math.cross3Vec3(BIMSURFER.math.normalizeVec3(eye2, []), BIMSURFER.math.normalizeVec3(this._up, []));

                v = BIMSURFER.math.mulVec3Scalar(left, pan[0]);

                vec[0] += v[0];
                vec[1] += v[1];
                vec[2] += v[2];
            }

            if (pan[1] !== 0) {

                // Pan along 'up' vector

                v = BIMSURFER.math.mulVec3Scalar(BIMSURFER.math.normalizeVec3(this._up, []), pan[1]);

                vec[0] += v[0];
                vec[1] += v[1];
                vec[2] += v[2];
            }

            if (pan[3] !== 0) {

                // Pan along 'eye'- -> 'look' vector

                v = BIMSURFER.math.mulVec3Scalar(BIMSURFER.math.normalizeVec3(eye2, []), pan[2]);

                vec[0] += v[0];
                vec[1] += v[1];
                vec[2] += v[2];
            }

            this._eye = BIMSURFER.math.addVec3(this._eye, vec, []);
            this._look = BIMSURFER.math.addVec3(this._look, vec, []);

            this._lookatNodeDirty = true;
        },

        /**
         * Increments/decrements zoom factor, ie. distance between eye and look.
         * @param delta
         */
        zoom: function (delta) {

            var vec = BIMSURFER.math.subVec3(this._eye, this._look, []); // Get vector from eye to look
            var lenLook = Math.abs(BIMSURFER.math.lenVec3(vec, []));    // Get len of that vector
            var newLenLook = Math.abs(lenLook + delta);         // Get new len after zoom

            var dir = BIMSURFER.math.normalizeVec3(vec, []);  // Get normalised vector
            this._eye = BIMSURFER.math.addVec3(this._look, BIMSURFER.math.mulVec3Scalar(dir, newLenLook), []);

            this._lookatNodeDirty = true;
        },

        _props: {

            /**
             * Flag which indicates whether this Camera is active or not.
             *
             * Fires an {{#crossLink "Camera/active:event"}}{{/crossLink}} event on change.
             *
             * @property active
             * @type Boolean
             */
            active: {

                set: function (value) {

                    if (this._active === value) {
                        return;
                    }

                    if (value) {

                        this._lookatNodeDirty = true;
                        this._cameraNodeDirty = true;

                        var self = this;

                        this._tickSub = this.viewer.on("tick",
                            function () {

                                if (self._lookatNodeDirty) {

                                    // View transform update scheduled for viewer graph

                                    self._lookatNode.setEye(BIMSURFER.math.vec3ArrayToObj(self._eye));
                                    self._lookatNode.setLook(BIMSURFER.math.vec3ArrayToObj(self._look));
                                    self._lookatNode.setUp(BIMSURFER.math.vec3ArrayToObj(self._up));

                                    // Camera not at rest now
                                    self._rested = false;

                                    // Viewer camera position now up to date
                                    self._lookatNodeDirty = false;

                                } else {

                                    // Else camera position now at rest

                                    if (!self._rested) {
                                        self._rested = true;
                                    }
                                }

                                if (self._cameraNodeDirty) {

                                    // Projection update scheduled for viewer graph

                                    // Update the viewer graph

                                    self._cameraNode.set({
                                        optics: {
                                            type: "perspective",
                                            fovy: self.fovy,
                                            near: self.near,
                                            far: self.far
                                        }
                                    });

                                    // Viewer projection now up to date
                                    self._cameraNodeDirty = false;
                                }
                            });

                    } else {

                        this.viewer.off(this._tickSub);
                    }

                    /**
                     * Fired whenever this Camera's {{#crossLink "Camera/active:property"}}{{/crossLink}} property changes.
                     * @event active
                     * @param value The property's new value
                     */
                    this.fire('active', this._active = value);
                },

                get: function () {
                    return this._active;
                }
            },

            aspect: {

                set: function (value) {
                    this._aspect = value || 1.0;
                    this._cameraNodeDirty = true;
                },

                get: function () {
                    return this._aspect;
                }
            },

            /**
             * Position of the eye.
             * Fires an {{#crossLink "Camera/eye:event"}}{{/crossLink}} event on change.
             * @property eye
             * @default [0,0,-10]
             * @type Array(Number)
             */
            eye: {

                set: function (value) {
                    if (!this._eye) {
                        this._eye = [0, 0, 0];
                    }
                    this._eye[0] = value ? value[0] : 0;
                    this._eye[1] = value ? value[1] : 1;
                    this._eye[2] = value ? value[2] : -10;

                    this._lookatNodeDirty = true;
                },

                get: function () {
                    return this._eye;
                }
            },

            /**
             * Position of the point-of-interest.
             * Fires a {{#crossLink "Camera/look:event"}}{{/crossLink}} event on change.
             * @property look
             * @default [0,0,0]
             * @type Array(Number)
             */
            look: {

                set: function (value) {
                    if (!this._look) {
                        this._look = [0, 0, 0];
                    }
                    this._look[0] = value ? value[0] : 0;
                    this._look[1] = value ? value[1] : 0;
                    this._look[2] = value ? value[2] : 0;

                    this._lookatNodeDirty = true;
                },

                get: function () {
                    return this._look;
                }
            },

            /**
             * Direction of the "up" vector.
             * Fires an {{#crossLink "Camera/up:event"}}{{/crossLink}} event on change.
             * @property up
             * @default [0,1,0]
             * @type Array(Number)
             */
            up: {

                set: function (value) {
                    if (!this._up) {
                        this._up = [0, 0, 0];
                    }
                    this._up[0] = value ? value[0] : 0;
                    this._up[1] = value ? value[1] : 1;
                    this._up[2] = value ? value[2] : 0;

                    this._lookatNodeDirty = true;
                },

                get: function () {
                    return this._up;
                }
            },

            /**
             * Field-of-view angle on Y-axis.
             * Fires an {{#crossLink "Camera/fovy:event"}}{{/crossLink}} event on change.
             * @property up
             * @default 60
             * @type Number
             */
            fovy: {

                set: function (value) {
                    this._fovy = value || 60;
                    this._cameraNodeDirty = true;

                    /**
                     * Fired whenever this Camera's {{#crossLink "Camera/fovy:property"}}{{/crossLink}} property changes.
                     * @event fovy
                     * @param value The property's new value
                     */
                    this.fire('fovy', this._fovy);
                },

                get: function () {
                    return this._fovy;
                }
            },

            /**
             * Distance to near clip plane in normalized device coordinates [0..1].
             * Fires an {{#crossLink "Camera/near:event"}}{{/crossLink}} event on change.
             * @property near
             * @default 0.1
             * @type Number
             */
            near: {

                set: function (value) {
                    this._near = value || 0.1;
                    this._cameraNodeDirty = true;

                    /**
                     * Fired whenever this Camera's {{#crossLink "Camera/near:property"}}{{/crossLink}} property changes.
                     * @event near
                     * @param value The property's new value
                     */
                    this.fire('near', this._near);
                },

                get: function () {
                    return this._near;
                }
            },

            /**
             * Distance to far clip plane in normalized device coordinates [0..1].
             * Fires an {{#crossLink "Camera/far:event"}}{{/crossLink}} event on change.
             * @property far
             * @default 10000
             * @type Number
             */
            far: {

                set: function (value) {
                    this._far = value || 10000;
                    this._cameraNodeDirty = true;
                },

                get: function () {

                    /**
                     * Fired whenever this Camera's {{#crossLink "Camera/far:property"}}{{/crossLink}} property changes.
                     * @event far
                     * @param value The property's new value
                     */
                    return this._far;
                }
            },


            screenPan: {

                set: function (value) {
                    this._screenPan = value || [0, 0];
                    this._cameraNodeDirty = true;
                },

                get: function () {
                    return this._screenPan;
                }
            }
        },

        _destroy: function () {
            this.active = false;
        }

    });

})();