API Docs for:

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

/**
 An **OBJModel** is a {{#crossLink "Model"}}{{/crossLink}} that loads itself from OBJ and MTL files.

 <a href="../../examples/#importing_obj_conferenceRoom"><img src="../../../assets/images/screenshots/OBJModel.png"></img></a>

 ## Overview

 * Begins loading as soon as you set its {{#crossLink "OBJModel/src:property"}}{{/crossLink}} property to the location of an OBJ file.
 * Once loaded, contains an {{#crossLink "Mesh"}}{{/crossLink}} for each object. The {{#crossLink "Mesh"}}Meshes{{/crossLink}} can then be independently shown, hidden, colored, transformed etc.
 * Set {{#crossLink "OBJModel/src:property"}}{{/crossLink}} to a new file path at any time, to clear the OBJModel and load components from the new file.

 OBJModel inherits these capabilities from its {{#crossLink "Group"}}{{/crossLink}} base class:

 * Allows you to access and manipulate the {{#crossLink "Meshes"}}{{/crossLink}} within it.
 * Can be transformed as a unit within World-space.
 * Can be a child within a parent {{#crossLink "Group"}}{{/crossLink}}.
 * Provides its World-space axis-aligned and object-aligned boundaries.

 ## Examples

 * [Basic example](../../examples/#importing_obj_people)
 * [Models within an object hierarchy](../../examples/#objects_hierarchy_models)

 ## Usage

 Let's load the conference room model (shown in the screenshot above):

 ````javascript
 var confRoom = new xeogl.OBJModel({
     id: "confRoom",
     src: "models/obj/conference/conference.obj"
 });
 ````

 Bind a callback to fire when the model has loaded:

 ````javascript
 confRoom.on("loaded", function() {
     // OBJModel has loaded!
 });
 ````

 That fires immediately if the OBJModel already happens to be loaded. You can also bind a callback to fire if loading fails:

 ````javascript
 confRoom.on("error", function(msg) {
     // Error occurred
 });
 ````

 To switch to a different OBJ file, simply update {{#crossLink "OBJModel/src:property"}}{{/crossLink}}:

 ````javascript
 confRoom.src = "models/obj/female02/female02.obj";
 ````

 ### Fitting to view

 ````javascript
 var cameraFlight = new xeogl.CameraFlightAnimation();
 cameraFlight.flyTo(confRoom);
 ````

 ### Accessing components

 Let's make everything  transparent, except for the conference table and chairs:

 ````javascript
 for (var id in confRoom.meshes) {
    var mesh = confRoom.meshes[id];
    switch (id) {
        case "confRoom#mesh31":
        case "confRoom#mesh29":
        case "confRoom#mesh30":
            break;
        default: // Not a chair mesh
            mesh.material.alpha = 0.5;
            mesh.material.blendMode = "blend"
    }
 }
 ````

 Note the format of the {{#crossLink "Mesh"}}{{/crossLink}} IDs - an OBJModel prefixes its own ID to the IDs of its components:

 ````<OBJModel ID>#<OBJ object/group ID>````

 **Transforms**

 An OBJModel lets us transform its Meshes as a group:

 ```` Javascript
 var model = new xeogl.OBJModel({
     src: "models/obj/conference/conference.obj"
     position: [-35, 0, 0],
     rotation: [0, 45, 0],
     scale: [0.5, 0.5, 0.5]
 });

 model.position = [-20, 0, 0];
 ````

 Let's move the white table top upwards:

 ````javascript
 var tableTop = confRoom.meshes["confRoom#mesh29"];
 tableTop.position = [0, 150, 0];
 ````

 ## Examples

 * [Conference room model](../../examples/#importing_obj_conferenceRoom)
 * [Two character models](../../examples/#importing_obj_people)

 @class OBJModel
 @module xeogl
 @submodule models
 @constructor
 @param [scene] {Scene} Parent {{#crossLink "Scene"}}Scene{{/crossLink}} - creates this OBJModel 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 OBJModel.
 @param [cfg.src] {String} Path to an OBJ file. You can set this to a new file path at any time, which will cause the
 OBJModel to load components from the new file (after first destroying any components loaded from a previous file path).
 @param [cfg.quantizeGeometry=true] {Boolean} When true, quantizes geometry to reduce memory and GPU bus usage.
 @param [cfg.combineGeometry=true] {Boolean} When true, combines geometry vertex buffers to improve rendering performance.
 @param [cfg.ghosted=false] {Boolean} When true, sets all the OBJModel's Meshes initially ghosted.
 @param [cfg.highlighted=false] {Boolean} When true, sets all the OBJModel's Meshes initially highlighted.
 @param [cfg.outline=false] {Boolean} When true, sets all the OBJModel's Meshes initially outlined.
 @param [cfg.edgeThreshold=2] {Number} When ghosting, this is the threshold angle between normals of adjacent triangles, below which their shared wireframe edge is not drawn.
 @param [cfg.transform] {Number|String|Transform} A Local-to-World-space (modelling) {{#crossLink "Transform"}}{{/crossLink}} to attach to this OBJModel.
 Must be within the same {{#crossLink "Scene"}}{{/crossLink}} as this STLModel. Internally, the given
 {{#crossLink "Transform"}}{{/crossLink}} will be inserted above each top-most {{#crossLink "Transform"}}Transform{{/crossLink}}
 that the STLModel attaches to its {{#crossLink "Mesh"}}Meshes{{/crossLink}}.
 @param [cfg.splitMeshes=true] {Boolean} When true, creates a separate {{#crossLink "Mesh"}}{{/crossLink}} for each group of faces that share the same vertex colors. Only works with binary STL.|
 @param [cfg.position=[0,0,0]] {Float32Array} The STLModel's local 3D position.
 @param [cfg.scale=[1,1,1]] {Float32Array} The STLModel's local scale.
 @param [cfg.rotation=[0,0,0]] {Float32Array} The STLModel'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 STLModel's local transform matrix. Overrides the position, scale and rotation parameters.
 @extends Model
 */
{

    xeogl.OBJModel = class xeoglOBJModel extends xeogl.Model {


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


        /**
         Path to a Wavefront OBJ file.

         You can set this to a new file path at any time, which will cause the OBJModel to load components from
         the new file (after first destroying any components loaded from a previous file path).

         Also loads materials from any MTL files referenced in the OBJ.

         Fires a {{#crossLink "OBJModel/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 OBJModel

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

                return;
            }

            this.destroyAll();

            this._src = value;

            xeogl.OBJModel.load(this, this._src);

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

        get src() {
            return this._src;
        }


        /**
         * Loads OBJ and MTL from file(s) into a {{#crossLink "Model"}}{{/crossLink}}.
         *
         * @method load
         * @static
         * @param {Model} model Model to load into.
         * @param {String} src Path to OBJ file.
         * @param {Function} [ok] Completion callback.
         */
        static load(model, src, ok) {

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

            loadOBJ(model, src, function (state) {
                loadMTLs(model, state, function () {

                    createMeshes(model, state);

                    spinner.processes--;

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

                    if (ok) {
                        ok();
                    }
                });
            });
        }

        /**
         * Parses OBJ and MTL text strings into a {{#crossLink "Model"}}{{/crossLink}}.
         *
         * @method parse
         * @static
         * @param {Model} model Model to load into.
         * @param {String} objText OBJ text string.
         * @param {String} [mtlText] MTL text string.
         * @param {String} [basePath] Base path for external resources.
         */
        static parse(model, objText, mtlText, basePath) {
            if (!objText) {
                this.warn("load() param expected: objText");
                return;
            }
            var state = parseOBJ(model, objText, null);
            if (mtlText) {
                parseMTL(model, mtlText, basePath);
            }
            createMeshes(model, state);
            model.src = null;
            model.fire("loaded", true, true);
        }
    };

//--------------------------------------------------------------------------------------------
// Loads OBJ
//
// Parses OBJ into an intermediate state object. The object will contain geometry data
// and material IDs from which meshes can be created later. The object will also
// contain a list of filenames of the MTL files referenced by the OBJ, is any.
//
// Originally based on the THREE.js OBJ and MTL loaders:
//
// https://github.com/mrdoob/three.js/blob/dev/examples/js/loaders/OBJLoader.js
// https://github.com/mrdoob/three.js/blob/dev/examples/js/loaders/MTLLoader.js
//--------------------------------------------------------------------------------------------

    var loadOBJ = function (model, url, ok) {

        loadFile(url, function (text) {
                var state = parseOBJ(model, text, url);
                ok(state);
            },
            function (error) {
                model.error(error);
            });
    };

    var parseOBJ = (function () {

        const regexp = {
            // v float float float
            vertex_pattern: /^v\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/,
            // vn float float float
            normal_pattern: /^vn\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/,
            // vt float float
            uv_pattern: /^vt\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/,
            // f vertex vertex vertex
            face_vertex: /^f\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)(?:\s+(-?\d+))?/,
            // f vertex/uv vertex/uv vertex/uv
            face_vertex_uv: /^f\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+))?/,
            // f vertex/uv/normal vertex/uv/normal vertex/uv/normal
            face_vertex_uv_normal: /^f\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+)\/(-?\d+))?/,
            // f vertex//normal vertex//normal vertex//normal
            face_vertex_normal: /^f\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)(?:\s+(-?\d+)\/\/(-?\d+))?/,
            // o object_name | g group_name
            object_pattern: /^[og]\s*(.+)?/,
            // s boolean
            smoothing_pattern: /^s\s+(\d+|on|off)/,
            // mtllib file_reference
            material_library_pattern: /^mtllib /,
            // usemtl material_name
            material_use_pattern: /^usemtl /
        };

        return function (model, text, url) {

            url = url || ""

            var state = {
                src: url,
                basePath: getBasePath(url),
                objects: [],
                object: {},
                positions: [],
                normals: [],
                uv: [],
                materialLibraries: {}
            };

            startObject(state, "", false);

            // Parts of this parser logic are derived from the THREE.js OBJ loader:
            // https://github.com/mrdoob/three.js/blob/dev/examples/js/loaders/OBJLoader.js

            if (text.indexOf('\r\n') !== -1) {
                // This is faster than String.split with regex that splits on both
                text = text.replace('\r\n', '\n');
            }

            var lines = text.split('\n');
            var line = '', lineFirstChar = '', lineSecondChar = '';
            var lineLength = 0;
            var result = [];

            // Faster to just trim left side of the line. Use if available.
            var trimLeft = ( typeof ''.trimLeft === 'function' );

            for (var i = 0, l = lines.length; i < l; i++) {

                line = lines[i];

                line = trimLeft ? line.trimLeft() : line.trim();

                lineLength = line.length;

                if (lineLength === 0) {
                    continue;
                }

                lineFirstChar = line.charAt(0);

                if (lineFirstChar === '#') {
                    continue;
                }

                if (lineFirstChar === 'v') {

                    lineSecondChar = line.charAt(1);

                    if (lineSecondChar === ' ' && ( result = regexp.vertex_pattern.exec(line) ) !== null) {

                        // 0                  1      2      3
                        // ['v 1.0 2.0 3.0', '1.0', '2.0', '3.0']

                        state.positions.push(
                            parseFloat(result[1]),
                            parseFloat(result[2]),
                            parseFloat(result[3])
                        );

                    } else if (lineSecondChar === 'n' && ( result = regexp.normal_pattern.exec(line) ) !== null) {

                        // 0                   1      2      3
                        // ['vn 1.0 2.0 3.0', '1.0', '2.0', '3.0']

                        state.normals.push(
                            parseFloat(result[1]),
                            parseFloat(result[2]),
                            parseFloat(result[3])
                        );

                    } else if (lineSecondChar === 't' && ( result = regexp.uv_pattern.exec(line) ) !== null) {

                        // 0               1      2
                        // ['vt 0.1 0.2', '0.1', '0.2']

                        state.uv.push(
                            parseFloat(result[1]),
                            parseFloat(result[2])
                        );

                    } else {

                        model.error('Unexpected vertex/normal/uv line: \'' + line + '\'');
                        return;
                    }

                } else if (lineFirstChar === 'f') {

                    if (( result = regexp.face_vertex_uv_normal.exec(line) ) !== null) {

                        // f vertex/uv/normal vertex/uv/normal vertex/uv/normal
                        // 0                        1    2    3    4    5    6    7    8    9   10         11         12
                        // ['f 1/1/1 2/2/2 3/3/3', '1', '1', '1', '2', '2', '2', '3', '3', '3', undefined, undefined, undefined]

                        addFace(state,
                            result[1], result[4], result[7], result[10],
                            result[2], result[5], result[8], result[11],
                            result[3], result[6], result[9], result[12]
                        );

                    } else if (( result = regexp.face_vertex_uv.exec(line) ) !== null) {

                        // f vertex/uv vertex/uv vertex/uv
                        // 0                  1    2    3    4    5    6   7          8
                        // ['f 1/1 2/2 3/3', '1', '1', '2', '2', '3', '3', undefined, undefined]

                        addFace(state,
                            result[1], result[3], result[5], result[7],
                            result[2], result[4], result[6], result[8]
                        );

                    } else if (( result = regexp.face_vertex_normal.exec(line) ) !== null) {

                        // f vertex//normal vertex//normal vertex//normal
                        // 0                     1    2    3    4    5    6   7          8
                        // ['f 1//1 2//2 3//3', '1', '1', '2', '2', '3', '3', undefined, undefined]

                        addFace(state,
                            result[1], result[3], result[5], result[7],
                            undefined, undefined, undefined, undefined,
                            result[2], result[4], result[6], result[8]
                        );

                    } else if (( result = regexp.face_vertex.exec(line) ) !== null) {

                        // f vertex vertex vertex
                        // 0            1    2    3   4
                        // ['f 1 2 3', '1', '2', '3', undefined]

                        addFace(state, result[1], result[2], result[3], result[4]);
                    } else {
                        model.error('Unexpected face line: \'' + line + '\'');
                        return;
                    }

                } else if (lineFirstChar === 'l') {

                    var lineParts = line.substring(1).trim().split(' ');
                    var lineVertices = [], lineUVs = [];

                    if (line.indexOf('/') === -1) {

                        lineVertices = lineParts;

                    } else {
                        for (var li = 0, llen = lineParts.length; li < llen; li++) {
                            var parts = lineParts[li].split('/');
                            if (parts[0] !== '') {
                                lineVertices.push(parts[0]);
                            }
                            if (parts[1] !== '') {
                                lineUVs.push(parts[1]);
                            }
                        }
                    }
                    addLineGeometry(state, lineVertices, lineUVs);

                } else if (( result = regexp.object_pattern.exec(line) ) !== null) {

                    // o object_name
                    // or
                    // g group_name

                    var id = result[0].substr(1).trim();
                    startObject(state, id, true);

                } else if (regexp.material_use_pattern.test(line)) {

                    // material

                    var id = line.substring(7).trim();
                    state.object.material.id = id;

                } else if (regexp.material_library_pattern.test(line)) {

                    // mtl file

                    state.materialLibraries[line.substring(7).trim()] = true;

                } else if (( result = regexp.smoothing_pattern.exec(line) ) !== null) {

                    // smooth shading

                    var value = result[1].trim().toLowerCase();
                    state.object.material.smooth = ( value === '1' || value === 'on' );

                } else {

                    // Handle null terminated files without exception
                    if (line === '\0') {
                        continue;
                    }

                    model.error('Unexpected line: \'' + line + '\'');
                    return;
                }
            }

            return state;
        };

        function getBasePath(src) {
            var n = src.lastIndexOf('/');
            return (n === -1) ? src : src.substring(0, n + 1);
        }

        function startObject(state, id, fromDeclaration) {
            if (state.object && state.object.fromDeclaration === false) {
                state.object.id = id;
                state.object.fromDeclaration = ( fromDeclaration !== false );
                return;
            }
            state.object = {
                id: id || '',
                geometry: {
                    positions: [],
                    normals: [],
                    uv: []
                },
                material: {
                    id: '',
                    smooth: true
                },
                fromDeclaration: ( fromDeclaration !== false )
            };
            state.objects.push(state.object);
        }

        function parseVertexIndex(value, len) {
            var index = parseInt(value, 10);
            return ( index >= 0 ? index - 1 : index + len / 3 ) * 3;
        }

        function parseNormalIndex(value, len) {
            var index = parseInt(value, 10);
            return ( index >= 0 ? index - 1 : index + len / 3 ) * 3;
        }

        function parseUVIndex(value, len) {
            var index = parseInt(value, 10);
            return ( index >= 0 ? index - 1 : index + len / 2 ) * 2;
        }

        function addVertex(state, a, b, c) {
            var src = state.positions;
            var dst = state.object.geometry.positions;
            dst.push(src[a + 0]);
            dst.push(src[a + 1]);
            dst.push(src[a + 2]);
            dst.push(src[b + 0]);
            dst.push(src[b + 1]);
            dst.push(src[b + 2]);
            dst.push(src[c + 0]);
            dst.push(src[c + 1]);
            dst.push(src[c + 2]);
        }

        function addVertexLine(state, a) {
            var src = state.positions;
            var dst = state.object.geometry.positions;
            dst.push(src[a + 0]);
            dst.push(src[a + 1]);
            dst.push(src[a + 2]);
        }

        function addNormal(state, a, b, c) {
            var src = state.normals;
            var dst = state.object.geometry.normals;
            dst.push(src[a + 0]);
            dst.push(src[a + 1]);
            dst.push(src[a + 2]);
            dst.push(src[b + 0]);
            dst.push(src[b + 1]);
            dst.push(src[b + 2]);
            dst.push(src[c + 0]);
            dst.push(src[c + 1]);
            dst.push(src[c + 2]);
        }

        function addUV(state, a, b, c) {
            var src = state.uv;
            var dst = state.object.geometry.uv;
            dst.push(src[a + 0]);
            dst.push(src[a + 1]);
            dst.push(src[b + 0]);
            dst.push(src[b + 1]);
            dst.push(src[c + 0]);
            dst.push(src[c + 1]);
        }

        function addUVLine(state, a) {
            var src = state.uv;
            var dst = state.object.geometry.uv;
            dst.push(src[a + 0]);
            dst.push(src[a + 1]);
        }

        function addFace(state, a, b, c, d, ua, ub, uc, ud, na, nb, nc, nd) {
            var vLen = state.positions.length;
            var ia = parseVertexIndex(a, vLen);
            var ib = parseVertexIndex(b, vLen);
            var ic = parseVertexIndex(c, vLen);
            var id;
            if (d === undefined) {
                addVertex(state, ia, ib, ic);

            } else {
                id = parseVertexIndex(d, vLen);
                addVertex(state, ia, ib, id);
                addVertex(state, ib, ic, id);
            }

            if (ua !== undefined) {

                var uvLen = state.uv.length;

                ia = parseUVIndex(ua, uvLen);
                ib = parseUVIndex(ub, uvLen);
                ic = parseUVIndex(uc, uvLen);

                if (d === undefined) {
                    addUV(state, ia, ib, ic);

                } else {
                    id = parseUVIndex(ud, uvLen);
                    addUV(state, ia, ib, id);
                    addUV(state, ib, ic, id);
                }
            }

            if (na !== undefined) {

                // Normals are many times the same. If so, skip function call and parseInt.

                var nLen = state.normals.length;

                ia = parseNormalIndex(na, nLen);
                ib = na === nb ? ia : parseNormalIndex(nb, nLen);
                ic = na === nc ? ia : parseNormalIndex(nc, nLen);

                if (d === undefined) {
                    addNormal(state, ia, ib, ic);

                } else {

                    id = parseNormalIndex(nd, nLen);
                    addNormal(state, ia, ib, id);
                    addNormal(state, ib, ic, id);
                }
            }
        }

        function addLineGeometry(state, positions, uv) {

            state.object.geometry.type = 'Line';

            var vLen = state.positions.length;
            var uvLen = state.uv.length;

            for (var vi = 0, l = positions.length; vi < l; vi++) {
                addVertexLine(state, parseVertexIndex(positions[vi], vLen));
            }

            for (var uvi = 0, uvl = uv.length; uvi < uvl; uvi++) {
                addUVLine(state, parseUVIndex(uv[uvi], uvLen));
            }
        }
    })();

//--------------------------------------------------------------------------------------------
// Loads MTL files listed in parsed state
//--------------------------------------------------------------------------------------------

    function loadMTLs(model, state, ok) {
        var basePath = state.basePath;
        var srcList = Object.keys(state.materialLibraries);
        var numToLoad = srcList.length;
        for (var i = 0, len = numToLoad; i < len; i++) {
            loadMTL(model, basePath, basePath + srcList[i], function () {
                if (--numToLoad === 0) {
                    ok();
                }
            });
        }
    }

//--------------------------------------------------------------------------------------------
// Loads an MTL file
//--------------------------------------------------------------------------------------------

    var loadMTL = function (model, basePath, src, ok) {
        loadFile(src, function (text) {
                parseMTL(model, text, basePath);
                ok();
            },
            function (error) {
                model.error(error);
                ok();
            });
    };

    var parseMTL = (function () {

        var delimiter_pattern = /\s+/;

        return function (model, mtlText, basePath) {

            var lines = mtlText.split('\n');
            var materialCfg = {
                id: "Default"
            };
            var needCreate = false;
            var line;
            var pos;
            var key;
            var value;
            var alpha;

            basePath = basePath || "";

            for (var i = 0; i < lines.length; i++) {

                line = lines[i].trim();

                if (line.length === 0 || line.charAt(0) === '#') { // Blank line or comment ignore
                    continue;
                }

                pos = line.indexOf(' ');

                key = ( pos >= 0 ) ? line.substring(0, pos) : line;
                key = key.toLowerCase();

                value = ( pos >= 0 ) ? line.substring(pos + 1) : '';
                value = value.trim();

                switch (key.toLowerCase()) {

                    case "newmtl": // New material
                        //if (needCreate) {
                        createMaterial(model, materialCfg);
                        //}
                        materialCfg = {
                            id: value
                        };
                        needCreate = true;
                        break;

                    case 'ka':
                        materialCfg.ambient = parseRGB(value);
                        break;

                    case 'kd':
                        materialCfg.diffuse = parseRGB(value);
                        break;

                    case 'ks':
                        materialCfg.specular = parseRGB(value);
                        break;

                    case 'map_kd':
                        if (!materialCfg.diffuseMap) {
                            materialCfg.diffuseMap = createTexture(model, basePath, value, "sRGB");
                        }
                        break;

                    case 'map_ks':
                        if (!materialCfg.specularMap) {
                            materialCfg.specularMap = createTexture(model, basePath, value, "linear");
                        }
                        break;

                    case 'map_bump':
                    case 'bump':
                        if (!materialCfg.normalMap) {
                            materialCfg.normalMap = createTexture(model, basePath, value);
                        }
                        break;

                    case 'ns':
                        materialCfg.shininess = parseFloat(value);
                        break;

                    case 'd':
                        alpha = parseFloat(value);
                        if (alpha < 1) {
                            materialCfg.alpha = alpha;
                            materialCfg.alphaMode = "blend";
                        }
                        break;

                    case 'tr':
                        alpha = parseFloat(value);
                        if (alpha > 0) {
                            materialCfg.alpha = 1 - alpha;
                            materialCfg.alphaMode = "blend";
                        }
                        break;

                    default:
                    // model.error("Unrecognized token: " + key);
                }
            }

            if (needCreate) {
                createMaterial(model, materialCfg);
            }
        };

        function createTexture(model, basePath, value, encoding) {
            var textureCfg = {};
            var items = value.split(/\s+/);
            var pos = items.indexOf('-bm');
            if (pos >= 0) {
                //matParams.bumpScale = parseFloat(items[pos + 1]);
                items.splice(pos, 2);
            }
            pos = items.indexOf('-s');
            if (pos >= 0) {
                textureCfg.scale = [parseFloat(items[pos + 1]), parseFloat(items[pos + 2])];
                items.splice(pos, 4); // we expect 3 parameters here!
            }
            pos = items.indexOf('-o');
            if (pos >= 0) {
                textureCfg.translate = [parseFloat(items[pos + 1]), parseFloat(items[pos + 2])];
                items.splice(pos, 4); // we expect 3 parameters here!
            }
            textureCfg.src = basePath + items.join(' ').trim();
            textureCfg.flipY = true;
            textureCfg.encoding = encoding || "linear";
            //textureCfg.wrapS = self.wrap;
            //textureCfg.wrapT = self.wrap;
            var texture = new xeogl.Texture(model, textureCfg);
            model._addComponent(texture);
            return texture.id;
        }

        function createMaterial(model, materialCfg) {
            model._addComponent(new xeogl.PhongMaterial(model, materialCfg));
        }

        function parseRGB(value) {
            var ss = value.split(delimiter_pattern, 3);
            return [parseFloat(ss[0]), parseFloat(ss[1]), parseFloat(ss[2])];
        }

    })();
//--------------------------------------------------------------------------------------------
// Creates meshes from parsed state
//--------------------------------------------------------------------------------------------

    var createMeshes = (function () {

        return function (model, state) {

            for (var j = 0, k = state.objects.length; j < k; j++) {

                var object = state.objects[j];
                var geometry = object.geometry;
                var isLine = ( geometry.type === 'Line' );

                if (geometry.positions.length === 0) {
                    // Skip o/g line declarations that did not follow with any faces
                    continue;
                }

                var geometryCfg = {
                    primitive: "triangles"
                };

                geometryCfg.positions = geometry.positions;

                if (geometry.normals.length > 0) {
                    geometryCfg.normals = geometry.normals;
                } else {
                    geometryCfg.autoVertexNormals = true;
                }

                if (geometry.uv.length > 0) {
                    geometryCfg.uv = geometry.uv;
                }

                var indices = new Array(geometryCfg.positions.length / 3); // Triangle soup
                for (var idx = 0; idx < indices.length; idx++) {
                    indices[idx] = idx;
                }
                geometryCfg.indices = indices;

                var xeoGeometry = new xeogl.Geometry(model, geometryCfg);
                model._addComponent(xeoGeometry);

                var materialId = object.material.id;
                var material;
                if (materialId && materialId !== "") {
                    material = model.scene.components[materialId];
                    if (!material) {
                        model.error("Material not found: " + materialId);
                    }
                } else {
                    material = new xeogl.PhongMaterial(model, {
                        //emissive: [0.6, 0.6, 0.0],
                        diffuse: [0.6, 0.6, 0.6],
                        backfaces: true
                    });
                    model._addComponent(material);
                }

                // material.emissive = [Math.random(), Math.random(), Math.random()];

                var mesh = new xeogl.Mesh(model, {
                    id: model.id + "#" + object.id,
                    geometry: xeoGeometry,
                    material: material,
                    pickable: true
                });

                model.addChild(mesh);
                model._addComponent(mesh);
            }
        };
    })();

    function loadFile(url, ok, err) {
        var request = new XMLHttpRequest();
        request.open('GET', url, true);
        request.addEventListener('load', function (event) {
            var response = event.target.response;
            if (this.status === 200) {
                if (ok) {
                    ok(response);
                }
            } else if (this.status === 0) {
                // Some browsers return HTTP Status 0 when using non-http protocol
                // e.g. 'file://' or 'data://'. Handle as success.
                console.warn('loadFile: HTTP Status 0 received.');
                if (ok) {
                    ok(response);
                }
            } else {
                if (err) {
                    err(event);
                }
            }
        }, false);

        request.addEventListener('error', function (event) {
            if (err) {
                err(event);
            }
        }, false);
        request.send(null);
    }
}