API Docs for:

File: /home/lindsay/xeolabs/xeogl-next/xeogl/examples/js/models/sceneJSModel.js

/**
 A **SceneJSModel** is a {{#crossLink "Model"}}{{/crossLink}} that
 imports content from the JSON-based <a href="http://scenejs.org">SceneJS</a> scene definition format.

 <a href="../../examples/#importing_scenejs_tronTank"><img src="http://i.giphy.com/l3vR50pFTpEbJTztS.gif"></img></a>

 ## Overview

 * A SceneJSModel is a container of {{#crossLink "Component"}}Components{{/crossLink}} that loads itself from a
 SceneJS scene definition, given as either a JSON file or a JavaScript object (POJO).
 * It begins loading as soon as you either set its {{#crossLink "SceneJSModel/src:property"}}{{/crossLink}}
 property to the location of a valid SceneJS JSON file, or set its {{#crossLink "SceneJSModel/data:property"}}{{/crossLink}} property to a
 valid POJO.
 * You can set these properties to new values at any time, which causes
 the SceneJSModel to clear itself and load fresh components.
 * Can be configured to do a best-effort conversion of SceneJS Phong materials into xeogl's PBR {{#crossLink "PBRMetalness"}}{{/crossLink}} or {{#crossLink "SpecularMaterials"}}{{/crossLink}}.

 It inherits these capabilities from its {{#crossLink "Model"}}{{/crossLink}} base class:

 * Allows you to access and manipulate the components within it.
 * Can be transformed within World-space by attaching it to a {{#crossLink "Transform"}}{{/crossLink}}.
 * Provides its World-space boundary as a {{#crossLink "Boundary3D"}}{{/crossLink}}.

 <img src="../../../assets/images/SceneJSModel.png"></img>

 ## SceneJS Support

 SceneJSModel was developed to import the [Tron Tank model](../../examples/#importing_scenejs_tronTank). As such,
 it only imports a limited subset of the SceneJS scene definition API. <b>Use with caution</b> and be prepared to
 fix and contribute missing functionality!

 SceneJS nodes supported so far:

 * ````"node"````
 * ````"rotate"````
 * ````"translate"````
 * ````"scale"````
 * ````"material"````
 * ````"texture"````
 * ````"fresnel"````
 * ````"flags"````
 * ````"geometry"````
 * ````"layer"````
 * ````"stage"````

 Unsupported API features include:

 * Lights
 * Cameras
 * Shared node cores
 * SceneJS plugins

 ## Examples

 * [Importing POJO defining geometry with diffuse, specular and normal maps](../../examples/#importing_scenejs_pojo_textures)
 * [Importing POJO defining transparent geometry](../../examples/#importing_scenejs_pojo_transparency)
 * [Importing JSON file defining geometry with diffuse, specular and normal maps](../../examples/#importing_scenejs_json_textures)
 * [Importing JSON file defining the SceneJS Tron Tank](../../examples/#importing_scenejs_tronTank)

 ## Usage

 #### Loading a POJO scene definition

 The simplest way to import SceneJS content is by setting a POJO on the SceneJSModel's {{#crossLink "SceneJSModel/data:property"}}data{{/crossLink}}
 property:

 ````javascript
 var pojoModel = new xeogl.SceneJSModel({
        id: "myModel",

        // Our POJO scene definition
        data: {
            type: "node",
            nodes: [
                {
                    type: "rotate",
                    id: "myRotate",
                    nodes: [
                        {
                            type: "geometry",
                            id: "boxEntity",
                            primitive: "triangles",
                            positions: [
                                2, 2, 2, -2, 2, 2, -2, -2, 2, 2, -2, 2, 2, 2, 2, 2, -2,
                                2, 2, -2, -2, 2, 2, -2, 2, 2, 2, 2, 2, -2, -2, 2, -2,
                                -2, 2, 2, -2, 2, 2, -2, 2, -2, -2, -2, -2, -2, -2, 2,
                                -2, -2, -2, 2, -2, -2, 2, -2, 2, -2, -2, 2, 2, -2, -2,
                                -2, -2, -2, -2, 2, -2, 2, 2, -2
                            ],
                            normals: [
                                0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1,
                                0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, -1, 0,
                                0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1, 0, 0,
                               -1, 0, 0, -1, 0, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1
                            ],
                            uv: [
                                5, 5, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 5, 5,
                                5, 0, 5, 5, 0, 5, 0, 0, 5, 5, 0, 5, 0, 0, 5, 0,
                                0, 0, 5, 0, 5, 5, 0, 5, 0, 0, 5, 0, 5, 5, 0, 5
                            ],
                            indices: [
                                0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11,
                                12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21,
                                22, 20, 22, 23
                            ]
                        }
                    ]
                }
            ]
        }
     });

 // Set camera position
 var camera = pojoModel.scene.camera;
 camera.eye = [0, 0, -25];
 camera.look = [0, 0, 0];
 camera.up = [0, 1, 0];
 ````

 #### Finding components

 Our SceneJSModel has now created various xeogl components
 within itself, which we can find by their IDs. In this particular example, our POJO has a SceneJS ````"rotate"```` node
 with ID ````"myRotate"````. Our SceneJSModel parsed that into a {{#crossLink "Rotate"}}{{/crossLink}} component with
 ID ````"myModel.myRotate"````.

 To see what components our SceneJSModel created, we can drop this expression into the browser's JavaScript
 debug console (we're using Chrome here):

 ````
 pojoModel.types;
 ````

 The result is the value of the SceneJSModel's {{#crossLink "Model/types:property"}}types{{/crossLink}} map, which
 contains its xeogl components, mapped to their types:

 <img src="../../../assets/images/screenshots/SceneJSModel_console_tankModel.types.png"></img>

 Here we've expanded the {{#crossLink "Rotate"}}{{/crossLink}} components, and we can see
 our {{#crossLink "Rotate"}}{{/crossLink}}. **Note that its ID is prefixed with the ID of the SceneJSModel.**

 Let's get that {{#crossLink "Rotate"}}{{/crossLink}} from our SceneJSModel's
 {{#crossLink "Model/components:property"}}{{/crossLink}} map and set it spinning:

 ```` JavaScript
 var rotate = pojoModel.components["myModel.myRotate"];

 pojoModel.scene.on("tick", function() {
    rotate.angle += 0.2;
 });
 ````

 #### Loading a JSON scene definition

 As shown in the example below, we can also import a SceneJS scene definition from a JSON file (eg. <a href="../../examples/models/scenejs/tronTank.json">tronTank.json</a>).
 Note that we need to wait for the SceneJSModel's {{#crossLink "SceneJSModel/loaded:event"}}{{/crossLink}} event before we
 can access its components. In this example we're also showing how a SceneJSModel can be attached to a modeling {{#crossLink "Transform"}}{{/crossLink}}
 hierarchy to transform it within World space (see {{#crossLink "Model"}}{{/crossLink}}).

 ````javascript
 // Import SceneJS JSON model
 var tankModel = new xeogl.SceneJSModel({
        id: "tankModel",

        // Path to our JSON scene definition file
        src: "models/scenejs/tronTank.json",

        // We can also bolt on a hierarchy of modeling transforms,
        // to transform the entire SceneJSModel in World space
        transform: new xeogl.Rotate({
            xyz: [0, 1, 0],
            angle: 0,
            parent: new xeogl.Translate({
                xyz: [0, 0, 0]
            })
        })
     });

 // Once our SceneJSModel has loaded, we can access its components
 tankModel.on("loaded", function() {

        tankModel.components["tankModel.gunDir"].angle = gunDir;

        // Set camera position
        var camera = tankModel.scene.camera;
        camera.eye = [0, 0, -70];
        camera.look = [0, 0, 0];
        camera.up = [0, 1, 0];
    });
 ````

 #### Converting materials to PBR


 ````javascript
 var pbrSpecularTankModel = new xeogl.SceneJSModel({
    src: "models/scenejs/tronTank.json",
    materialWorkflow: "SpecularMaterial"
 });
 ````

 ````javascript
 var pbrMetalnessTankModel = new xeogl.SceneJSModel({
    src: "models/scenejs/tronTank.json",
    materialWorkflow: "MetallicMaterial"
 });
 ````

 @class SceneJSModel
 @module xeogl
 @submodule models
 @constructor
 @param [scene] {Scene} Parent {{#crossLink "Scene"}}Scene{{/crossLink}} - creates this SceneJSModel in the default
 {{#crossLink "Scene"}}Scene{{/crossLink}} when omitted.
 @param [cfg] {*} Configs
 @param [cfg.id] {String} Optional ID, unique among all components in the parent {{#crossLink "Scene"}}Scene{{/crossLink}},
 generated automatically when omitted.
 @param [cfg.entityType] {String} Optional entity classification when using within a semantic data model. See the {{#crossLink "Object"}}{{/crossLink}} documentation for usage.
 @param [cfg.meta] {String:Object} Optional map of user-defined metadata to attach to this SceneJSModel.
 @param [cfg.materialWorkflow] {String} Selects material workflow - "classic" | "pbrMatalness" | "pbrSpecular"
 @param [cfg.src] {String} Path to a SceneJS JSON scene description file.
 @param [cfg.data] {String} Path to a SceneJS JSON scene description file.
 @param [cfg.position=[0,0,0]] {Float32Array} The Model's local 3D position.
 @param [cfg.scale=[1,1,1]] {Float32Array} The SceneJSModel's local scale.
 @param [cfg.rotation=[0,0,0]] {Float32Array} The SceneJSModel's local rotation, as Euler angles given in degrees.
 @param [cfg.matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1] {Float32Array} The SceneJSModel's local transform matrix. Overrides the position, scale and rotation parameters.
 @extends Geometry
 */
{

    xeogl.SceneJSModel = class xeoglSceneJSModel extends xeogl.Model {

        init(cfg) {
            super.init(cfg);
            this._src = null;
            this.materialWorkflow = cfg.materialWorkflow;
            this.src = cfg.src;
            this.data = cfg.data;
        }

        /**
         Selects which xeogl material type to create from each SceneJS Phong material.

         Causes the SceneJSModel to attempt a best-effort conversion.

         Update this at any time to reconvert the materials.

         Fires a {{#crossLink "SceneJSModel/materialWorkFlow:event"}}{{/crossLink}} event on change.

         @property materialWorkflow
         @type {*}
         */
        set materialWorkflow(value) {

            value = value || "PhongMaterial";

            if (value !== "MetallicMaterial" && value !== "SpecularMaterial" && value !== "PhongMaterial") {
                this.error("Unsupported value for 'materialWorkflow' - defaulting to 'PhongMaterial'");
                value = "PhongMaterial";
            }

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

            this._materialWorkflow = value;

            //this.destroyAll();
            //
            //this._src = null;
            //
            //this._parse(this._materialWorkflow, null, null, null);

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

        get materialWorkFlow() {
            return this._materialWorkflow;
        }

        /**
         Path to the SceneJS JSON scene description file.

         Update this at any time to clear and re-import content.

         Fires a {{#crossLink "SceneJSModel/src:event"}}{{/crossLink}} event on change.

         @property src
         @type String
         */
        set src(value) {

            if (!value) {
                return;
            }

            if (!xeogl._isString(value)) {
                this.error("Value for 'src' should be a string");
                return;
            }

            if (value === this._src) { // Already loaded this SceneJSModel

                /**
                 Fired whenever this SceneJSModel has finished loading the SceneJS JSON file
                 specified by {{#crossLink "SceneJSModel/src:property"}}{{/crossLink}}.
                 @event loaded
                 */
                this.fire("loaded", true, true);

                return;
            }

            this.destroyAll();

            this._data = null;

            this._src = value;

            // Increment processes represented by loading spinner
            // Spinner appears as soon as count is non-zero

            var spinner = this.scene.canvas.spinner;
            spinner.processes++;

            var self = this;

            load(this._src, function (node) {

                    var group = self;

                    self._parse(node, group, null, null);

                    // Decrement processes represented by loading spinner
                    // Spinner disappears if the count is now zero
                    spinner.processes--;

                    xeogl.scheduleTask(function () {
                        self.fire("loaded", true);
                    });
                },

                function (msg) {

                    spinner.processes--;

                    self.error("Failed to load JSON file: " + msg);

                    self.fire("failed", msg);
                });

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

        get src() {
            return this._src;
        }

        /**
         A SceneJS POJO scene definition.

         Update this at any time to clear and re-import content.

         Fires a {{#crossLink "SceneJSModel/data:event"}}{{/crossLink}} event on change.

         @property data
         @type {*}
         */
        set data(value) {

            if (!value) {
                return;
            }

            this.destroyAll();

            this._src = null;

            this._data = value;

            var group = this;

            this._parse(this._data, group, null, null);

            var self = this;

            xeogl.scheduleTask(function () {
                self.fire("loaded", true);
            });

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

        get data() {
            return this._data;
        }

        //---------------------------------------------------------------------------------------------------------------
        // A simple recursive descent parser that loads SceneJS JSON into a xeogl.Model.
        // This is just the bare essentials to prove the concept - just transforms, diffuse material and geometry.
        //---------------------------------------------------------------------------------------------------------------

        _parse(node,
               group,
               material,
               diffuseMap,
               specularMap,
               emissiveMap,
               normalMap,
               alphaMap,
               diffuseFresnel,
               specularFresnel,
               emissiveFresnel,
               normalFresnel,
               alphaFresnel,
               transparent,
               backfaces,
               layer) {

            switch (node.type) {

                case "material":

                    var scenejsBaseColor = node.baseColor;
                    var scenejsSpecularColor = node.specularColor;
                    var scenejsSpecular = node.specular;
                    var scenejsEmit = node.emit;
                    var diffuse = scenejsBaseColor ? [scenejsBaseColor.r, scenejsBaseColor.g, scenejsBaseColor.b] : null;
                    var specular = (scenejsSpecular && scenejsSpecularColor) ? [scenejsSpecular * scenejsSpecularColor.r, scenejsSpecular * scenejsSpecularColor.g, scenejsSpecular * scenejsSpecularColor.b] : null;
                    var emissive = (scenejsEmit && diffuse) ? [scenejsEmit * diffuse[0], scenejsEmit * diffuse[1], scenejsEmit * diffuse[2]] : null;

                    switch (this._materialWorkflow) {
                        case "MetallicMaterial":
                            material = {
                                id: this._createID(node),
                                type: "xeogl.MetallicMaterial",
                                baseColor: diffuse,
                                metallic: 1.0,
                                roughness: 0.4,
                                emissive: emissive,
                                alpha: node.alpha,
                                alphaMode: "blend"
                            };
                            break;

                        case "SpecularMaterial":
                            material = {
                                id: this._createID(node),
                                type: "xeogl.SpecularMaterial",
                                diffuse: diffuse,
                                specular: specular,
                                glossiness: 0.5,
                                emissive: emissive,
                                alpha: node.alpha,
                                alphaMode: "blend"
                            };
                            break;

                        default:
                            material = {
                                id: this._createID(node),
                                type: "xeogl.PhongMaterial",
                                ambient: [.2, .2, .2],
                                diffuse: diffuse,
                                specular: specular,
                                // shininess: node.shine,
                                emissive: emissive,
                                alpha: node.alpha,
                                alphaMode: "blend"
                            };
                    }

                    break;

                case "translate":

                    group = group.addChild(new xeogl.Group(this.scene, {
                        id: this._createID(node),
                        position: [node.x, node.y, node.z]
                    }));

                    this._addComponent(group);

                    break;

                case "scale":

                    group = group.addChild(new xeogl.Group(this.scene, {
                        id: this._createID(node),
                        scale: [node.x, node.y, node.z]
                    }));

                    this._addComponent(group);

                    break;

                case "rotate":

                    var newGroup = new xeogl.Group(this.scene, {
                        id: this._createID(node)
                    });

                    newGroup.rotate([node.x, node.y, node.z], node.angle);

                    group = group.addChild(newGroup);

                    this._addComponent(group);

                    // var localMatrix = xeogl.math.rotationMat4c(node.angle * xeogl.math.DEGTORAD, node.x, node.y, node.z);
                    // if (matrix) {
                    //     matrix = xeogl.math.mulMat4(matrix, localMatrix, xeogl.math.mat4());
                    // } else {
                    //     matrix = localMatrix;
                    // }

                    break;

                case "texture":

                    var texture = new xeogl.Texture(this.scene, {
                        id: this._createID(node),
                        src: node.src,
                        wrapS: node.wrapS,
                        wrapT: node.wrapT,
                        scale: node.scale ? [node.scale.x || 1, node.scale.y || 1] : undefined,
                        translate: node.translate ? [node.translate.x || 0, node.translate.y || 1] : undefined,
                        rotate: node.rotate,
                        minFilter: node.minFilter,
                        maxFilter: node.maxFilter,
                        encoding: "sRGB"
                    });

                    this._addComponent(texture);

                    switch (node.applyTo) {

                        case "baseColor":
                        case "color":
                            diffuseMap = texture;
                            break;

                        case "specular":
                            specularMap = texture;
                            break;

                        case "emit":
                            emissiveMap = texture;
                            break;

                        case "alpha":
                            alphaMap = texture;
                            break;

                        case "normals":
                            normalMap = texture;
                            break;

                        case "shine":
                            this.warn("Unsupported SceneJS feature - texture applyTo:'shine'");
                            break;
                    }

                    break;

                case "fresnel":

                    var fresnel = new xeogl.Fresnel(this.scene, {
                        id: this._createID(node)
                        // TODO
                    });

                    this._addComponent(fresnel);

                    switch (node.applyTo) {

                        case "baseColor":
                        case "color":
                            diffuseFresnel = fresnel;
                            break;

                        case "specular":
                            specularFresnel = fresnel;
                            break;

                        case "emit":
                            emissiveFresnel = fresnel;
                            break;

                        case "alpha":
                            alphaFresnel = fresnel;
                            break;
                    }

                    break;

                case "flags":

                    transparent = node.transparent;
                    backfaces = node.backfaces;

                    // TODO: pickable and clippable

                    break;

                case "layer":

                    layer = node.priority;

                    break;


                case "geometry":

                    var geometry = new xeogl.Geometry(this.scene, {
                        primitive: node.primitive,
                        positions: node.positions,
                        normals: node.normals,
                        uv: node.uv,
                        indices: node.indices
                    });

                    this._addComponent(geometry);

                    if (material) {

                        // Set properties on material component

                        material.diffuseMap = diffuseMap;
                        material.specularMap = specularMap;
                        material.emissiveMap = emissiveMap;
                        material.alphaMap = alphaMap;
                        material.normalMap = normalMap;

                        material.diffuseFresnel = diffuseFresnel;
                        material.specularFresnel = specularFresnel;
                        material.emissiveFresnel = emissiveFresnel;
                        material.alphaFresnel = alphaFresnel;

                        material.alphaMode = transparent ? "blend" : "opaque";

                        material.backfaces = !!backfaces;
                    }

                    var material2 = this.scene.components[material.id];
                    if (!material2) {
                        material2 = new xeogl[material.type.substring(6)](this.scene, material);
                    }

                    var mesh = new xeogl.Mesh(this.scene, {
                        id: this._createID(node),
                        geometry: geometry,
                        material: material2,
                        layer: layer
                    });

                    if (group) {
                        group.addChild(mesh);
                    } else {
                        this.addChild(mesh);
                    }

                    this._addComponent(mesh);

                    break;
            }

            var nodes = node.nodes;

            if (nodes) {
                for (var i = 0, len = nodes.length; i < len; i++) {
                    this._parse(
                        nodes[i],
                        group,
                        material,
                        diffuseMap,
                        specularMap,
                        emissiveMap,
                        normalMap,
                        alphaMap,
                        diffuseFresnel,
                        specularFresnel,
                        emissiveFresnel,
                        normalFresnel,
                        alphaFresnel,
                        transparent,
                        backfaces,
                        layer);
                }
            }
        }

        _createID(node, type) {
            return (node.id !== null && node.id !== undefined) ? ("" + this.id + "." + (type ? type + "." : "") + node.id) : null;
        }
    };

    function load(url, ok, error) {
        var xhr = new XMLHttpRequest();
        xhr.overrideMimeType("application/json");
        xhr.addEventListener('load', function (event) {
            if (event.target.responseText) {
                try {
                    ok(JSON.parse(event.target.responseText));
                } catch (e) {
                    error('Invalid file [' + url + ']: ' + e);
                }
            } else {
                error('Invalid file [' + url + ']');
            }
        }, false);
        xhr.addEventListener('error', function () {
            error('Couldn\'t load URL [' + url + ']');
        }, false);
        xhr.open('GET', url, true);
        xhr.send(null);
    }

}