API Docs for:

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

/**
 A **XML3DModel** is a {{#crossLink "Model"}}{{/crossLink}} loaded from a <a href="https://en.wikipedia.org/wiki/3DXML" target = "_other">3DXML</a> file.

 @class XML3DModel
 @module xeogl
 @submodule models
 @constructor
 @param [scene] {Scene} Parent {{#crossLink "Scene"}}Scene{{/crossLink}} - creates this XML3DModel 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.meta] {String:Object} Optional map of user-defined metadata to attach to this XML3D.
 @param [cfg.src] {String} Path to a 3DXML file. You can set this to a new file path at any time.
 @param [cfg.displayEffect] {String} Display effect to render with: "shadedWithEdges" | "shaded" | "hiddenLinesRemoved" | "hiddenLinesVisible" | "wireframe".
 @extends Model
 */
(function () {

    "use strict";

    xeogl.XML3DModel = xeogl.Model.extend({

        type: "xeogl.XML3DModel",

        _init: function (cfg) {
            this._super(cfg);

            /**
             * Supported 3DXML schema versions
             * @property supportedSchemas
             * @type {string[]}
             */
            this.supportedSchemas = ["4.2"];

            this._defaultMaterial = new xeogl.MetallicMaterial(this, {
                baseColor: [1, 1, 1],
                metallic: 0.6,
                roughness: 0.6
            });

            // Material shared by all Meshes that have "lines" Geometry
            // Overrides whatever material 3DXML would apply.
            this._wireframeMaterial = new xeogl.LambertMaterial(this, {
                color: [0, 0, 0],
                lineWidth: 2
            });

            // EmphasisMaterial used to create solid, hidden-line effect when rendering solid wireframe
            this._wireframeGhostMaterial = new xeogl.EmphasisMaterial(this, {
                fill: true,
                fillColor: [1, 1, 1],
                fillAlpha: .8,
                //edges: false,
                edges: true,
                vertices: false
            });

            this._src = null;
            this._options = cfg;

            /**
             * Default viewpoint, containing eye, look and up vectors.
             * Only defined if found in the 3DXML file.
             * @property viewpoint
             * @type {Float32Array}
             */
            this.viewpoint = null;

            this.src = cfg.src;
            this.displayEffect = cfg.displayEffect;
        },

        _props: {

            /**
             Path to a 3DXML file.

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

             Fires a {{#crossLink "XML3D/loaded:event"}}{{/crossLink}} event when the 3DXML has loaded.

             @property src
             @type String
             */
            src: {
                set: function (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 XML3D

                        /**
                         Fired whenever this XML3D has finished loading components from the 3DXML file
                         specified by {{#crossLink "XML3D/src:property"}}{{/crossLink}}.
                         @event loaded
                         */
                        this.fire("loaded", true, true);
                        return;
                    }
                    this.destroyAll();
                    this._src = value;
                    xeogl.XML3DModel.load(this, this._src, this._options); // Don't need completion callbacks (model fires "loaded" or "error")
                },
                get: function () {
                    return this._src;
                }
            },

            /**
             Display effect to render with: "shadedWithEdges" | "shaded" | "hiddenLinesRemoved" | "hiddenLinesVisible" | "wireframe".

             @property displayEffect
             @default "shadedWithEdges"
             @type String
             */
            displayEffect: {
                set: function (displayEffect) {
                    displayEffect = displayEffect || "shadedWithEdges";
                    if (this._displayEffect === displayEffect) {
                        return;
                    }
                    this._displayEffect = displayEffect;
                    var meshes = this.types["xeogl.Mesh"];
                    if (meshes) {
                        switch (this._displayEffect) {
                            case "shadedWithEdges":
                                this._wireframeMaterial.lineWidth = 2;
                                for (var id in meshes) {
                                    var mesh = meshes[id];
                                    if (mesh.geometry.primitive !== "lines") {
                                        mesh.visible = true;
                                        mesh.ghosted = false;
                                        mesh.ghostMaterial.fillAlpha = 0.5;
                                        mesh.opacity = 1;
                                    } else {
                                        mesh.visible = true; // Show lines
                                    }
                                }
                                break;
                            case "shaded":
                                this._wireframeMaterial.lineWidth = 2;
                                for (var id in meshes) {
                                    var mesh = meshes[id];
                                    if (mesh.geometry.primitive !== "lines") {
                                        mesh.visible = true;
                                        mesh.ghosted = false;
                                        mesh.ghostMaterial.fillAlpha = 0.5;
                                        mesh.opacity = 1;
                                    } else {
                                        mesh.visible = false;  // Hide lines
                                    }
                                }
                                break;
                            case "hiddenLinesRemoved":
                                this._wireframeMaterial.lineWidth = 2;
                                for (var id in meshes) {
                                    var mesh = meshes[id];
                                    if (mesh.geometry.primitive !== "lines") {
                                        mesh.visible = true;
                                        mesh.ghosted = true;
                                        mesh.ghostMaterial.fillAlpha = 1.0;
                                        mesh.opacity = 1.0;
                                    } else {
                                        mesh.visible = true;  // Show lines
                                    }
                                }
                                break;
                            case "hiddenLinesVisible":
                                this._wireframeMaterial.lineWidth = 1;
                                for (var id in meshes) {
                                    var mesh = meshes[id];
                                    if (mesh.geometry.primitive !== "lines") {
                                        mesh.visible = true;
                                        mesh.ghosted = false;
                                        mesh.ghostMaterial.fillAlpha = 0.5;
                                        mesh.opacity = 0.5;
                                    } else {
                                        mesh.visible = true;  // Show lines
                                    }
                                }
                                break;
                            case "wireframe":
                                this._wireframeMaterial.lineWidth = 1;
                                for (var id in meshes) {
                                    var mesh = meshes[id];
                                    if (mesh.geometry.primitive !== "lines") {
                                        mesh.visible = false;
                                        mesh.ghosted = false;
                                        mesh.ghostMaterial.fillAlpha = 0.5;
                                        mesh.opacity = 0.5;
                                    } else {
                                        mesh.visible = true;  // Show lines
                                    }
                                }
                                break;
                        }
                    }
                },
                get: function () {
                    return this._displayEffect;
                }
            }
        },

        _destroy: function () {
            this.destroyAll();
        }
    });

    /**
     * Loads 3DXML from a URL into a {{#crossLink "Model"}}{{/crossLink}}.
     *
     * @method load
     * @static
     * @param {Model} model Model to load into.
     * @param {String} src Path to 3DXML file.
     * @param {Object} options Loading options.
     * @param {Function} [ok] Completion callback.
     * @param {Function} [error] Error callback.
     */
    xeogl.XML3DModel.load = function (model, src, options, ok, error) {

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

        load3DXML(model, src, options, function () {
                spinner.processes--;
                xeogl.scheduleTask(function () {
                    //console.log("3DXML loaded.");
                    model.fire("loaded", true, true);
                });
                if (ok) {
                    ok();
                }
            },
            function (msg) {
                spinner.processes--;
                model.error(msg);
                if (error) {
                    error(msg);
                }
                /**
                 Fired whenever this XML3D fails to load the 3DXML file
                 specified by {{#crossLink "XML3D/src:property"}}{{/crossLink}}.
                 @event error
                 @param msg {String} Description of the error
                 */
                model.fire("error", msg);
            },
            function (err) {
                console.log("Error, Will Robinson: " + err);
            });
    };

    var load3DXML = (function () {
        return function (model, src, options, ok, error) {
            loadZIP(src, function (zip) { // OK
                    parse3DXML(zip, options, model, ok, error);
                },
                error);
        };
    })();

    var parse3DXML = (function () {

        return function (zip, options, model, ok) {
            var ctx = {
                zip: zip,
                edgeThreshold: options.edgeThreshold || 20,
                materialWorkflow: options.materialWorkflow,
                scene: model.scene,
                model: model,
                info: {
                    references: {}
                },
                materials: {}
            };
            model.scene.loading++; // Disables (re)compilation


            // Now parse 3DXML

            parseDocument(ctx, function () {
                model.scene.loading--; // Re-enables (re)compilation
                //console.log("3DXML parsed.");
                ok();
            });
        };

        function parseDocument(ctx, ok) {
            ctx.zip.getFile("Manifest.xml", function (xmlDoc, json) {
                var node = json;
                var children = node.children;
                for (var i = 0, len = children.length; i < len; i++) {
                    var child = children[i];
                    switch (child.type) {
                        case "Manifest":
                            parseManifest(ctx, child, ok);
                            break;
                    }
                }
            });
        }

        function parseManifest(ctx, manifest, ok) {
            var children = manifest.children;
            for (var i = 0, len = children.length; i < len; i++) {
                var child = children[i];
                switch (child.type) {
                    case "Root":
                        var rootFileSrc = child.children[0];
                        ctx.zip.getFile(rootFileSrc, function (xmlDoc, json) {
                            parseRoot(ctx, json, ok);
                        });
                        break;
                }
            }
        }

        function parseRoot(ctx, node, ok) {
            var children = node.children;
            for (var i = 0, len = children.length; i < len; i++) {
                var child = children[i];
                switch (child.type) {
                    case "Model_3dxml":
                        parseModel(ctx, child, ok);
                        break;
                }
            }
        }

        function parseModel(ctx, node, ok) {
            var children = node.children;
            for (var i = 0, len = children.length; i < len; i++) {
                var child = children[i];
                switch (child.type) {
                    case "Header":
                        parseHeader(ctx, child);
                        break;
                    case "ProductStructure":
                        parseProductStructure(ctx, child, ok);
                        break;
                    case "DefaultView":
                        parseDefaultView(ctx, child);
                        break;
                }
            }
        }

        function parseHeader(ctx, node) {
            var children = node.children;
            var metaData = {};
            for (var i = 0, len = children.length; i < len; i++) {
                var child = children[i];
                switch (child.type) {
                    case "SchemaVersion":
                        metaData.schemaVersion = child.children[0];
                        if (!isSchemaVersionSupported(ctx, metaData.schemaVersion)) {
                            ctx.model.error("Schema version not supported: " + metaData.schemaVersion + " - supported versions are: " + ctx.model.supportedSchemas.join(","));
                        } else {
                            //ctx.model.log("Parsing schema version: " + metaData.schemaVersion);
                        }
                        break;
                    case "Title":
                        metaData.title = child.children[0];
                        break;
                    case "Author":
                        metaData.author = child.children[0];
                        break;
                    case "Created":
                        metaData.created = child.children[0];
                        break;
                }
            }
            ctx.model.meta = metaData;
        }

        function isSchemaVersionSupported(ctx, schemaVersion) {
            var supportedSchemas = ctx.model.supportedSchemas;
            for (var i = 0, len = supportedSchemas.length; i < len; i++) {
                if (schemaVersion === supportedSchemas[i]) {
                    return true;
                }
            }
            return false;
        }

        function parseProductStructure(ctx, productStructureNode, ok) {

            parseReferenceReps(ctx, productStructureNode, function (referenceReps) {

                //----------------------------------------------------------------------------------
                // Parse out an intermediate scene DAG representation, that we can then
                // recursive descend through to build a xeogl Object hierarchy.
                //----------------------------------------------------------------------------------

                var children = productStructureNode.children;

                var reference3Ds = {};
                var instanceReps = {};
                var instance3Ds = {};

                var rootNode;
                var nodes = {};

                // Map all the elements

                for (var i = 0, len = children.length; i < len; i++) {
                    var child = children[i];
                    switch (child.type) {

                        case "Reference3D":
                            reference3Ds[child.id] = {
                                type: "Reference3D",
                                id: child.id,
                                name: child.name,
                                instance3Ds: {},
                                instanceReps: {}
                            };
                            break;

                        case "InstanceRep":
                            var isAggregatedBy;
                            var isInstanceOf;
                            var relativeMatrix;
                            for (var j = 0, lenj = child.children.length; j < lenj; j++) {
                                var child2 = child.children[j];
                                switch (child2.type) {
                                    case "IsAggregatedBy":
                                        isAggregatedBy = child2.children[0];
                                        break;
                                    case "IsInstanceOf":
                                        isInstanceOf = child2.children[0];
                                        break;
                                }
                            }
                            instanceReps[child.id] = {
                                type: "InstanceRep",
                                id: child.id,
                                isAggregatedBy: isAggregatedBy,
                                isInstanceOf: isInstanceOf,
                                referenceReps: {}
                            };
                            break;

                        case "Instance3D":
                            var isAggregatedBy;
                            var isInstanceOf;
                            var relativeMatrix;
                            for (var j = 0, lenj = child.children.length; j < lenj; j++) {
                                var child2 = child.children[j];
                                switch (child2.type) {
                                    case "IsAggregatedBy":
                                        isAggregatedBy = child2.children[0];
                                        break;
                                    case "IsInstanceOf":
                                        isInstanceOf = child2.children[0];
                                        break;
                                    case "RelativeMatrix":
                                        relativeMatrix = child2.children[0];
                                        break;
                                }
                            }
                            instance3Ds[child.id] = {
                                type: "Instance3D",
                                id: child.id,
                                isAggregatedBy: isAggregatedBy,
                                isInstanceOf: isInstanceOf,
                                relativeMatrix: relativeMatrix,
                                reference3Ds: {}
                            };
                            break;
                    }
                }

                // Connect Reference3Ds to the Instance3Ds they aggregate

                for (var id in instance3Ds) {
                    var instance3D = instance3Ds[id];
                    var reference3D = reference3Ds[instance3D.isAggregatedBy];
                    if (reference3D) {
                        reference3D.instance3Ds[instance3D.id] = instance3D;
                    } else {
                        alert("foo")
                    }
                }

                // Connect Instance3Ds to the Reference3Ds they instantiate

                for (var id in instance3Ds) {
                    var instance3D = instance3Ds[id];
                    var reference3D = reference3Ds[instance3D.isInstanceOf];
                    instance3D.reference3Ds[reference3D.id] = reference3D;
                    reference3D.instance3D = instance3D;
                }

                // Connect InstanceReps to the ReferenceReps they instantiate

                for (var id in instanceReps) {
                    var instanceRep = instanceReps[id];
                    var referenceRep = referenceReps[instanceRep.isInstanceOf];
                    if (referenceRep) {
                        instanceRep.referenceReps[referenceRep.id] = referenceRep;
                    }
                }

                // Connect Reference3Ds to the InstanceReps they aggregate

                for (var id in instanceReps) {
                    var instanceRep = instanceReps[id];
                    var reference3D = reference3Ds[instanceRep.isAggregatedBy];
                    if (reference3D) {
                        reference3D.instanceReps[instanceRep.id] = instanceRep;
                    }
                }

                // console.log("*****************************************************************************");
                // console.log("reference3Ds:\n\n");
                // console.log(JSON.stringify(reference3Ds, null, "\t"));
                // console.log("*****************************************************************************");

                function parseReference3D(ctx, reference3D, group) {
                    //ctx.model.log("parseReference3D( " + reference3D.id + " )");
                    for (var id in reference3D.instance3Ds) {
                        parseInstance3D(ctx, reference3D.instance3Ds[id], group);
                    }
                    for (var id in reference3D.instanceReps) {
                        parseInstanceRep(ctx, reference3D.instanceReps[id], group);
                    }
                }

                function parseInstance3D(ctx, instance3D, group) {
                    //ctx.model.log("parseInstance3D( " + instance3D.id + " )");

                    if (instance3D.relativeMatrix) {
                        var matrix = parseFloatArray(instance3D.relativeMatrix, 12);
                        var translate = [matrix[9], matrix[10], matrix[11]];
                        var mat3 = matrix.slice(0, 9); // Rotation matrix
                        var mat4 = xeogl.math.mat3ToMat4(mat3, xeogl.math.identityMat4()); // Convert rotation matrix to 4x4
                        var childGroup = new xeogl.Group(ctx.model.scene, {
                            position: translate
                        });
                        if (group) {
                            group.addChild(childGroup);
                        } else {
                            ctx.model.addChild(childGroup);
                        }
                        group = childGroup;
                        childGroup = new xeogl.Group(ctx.model.scene, {
                            matrix: mat4
                        });
                        group.addChild(childGroup);
                        group = childGroup;
                    } else {
                        var childGroup = new xeogl.Group(ctx.model.scene, {});
                        if (group) {
                            group.addChild(childGroup);
                        } else {
                            ctx.model.addChild(childGroup);
                        }
                        group = childGroup;
                    }
                    for (var id in instance3D.reference3Ds) {
                        parseReference3D(ctx, instance3D.reference3Ds[id], group);
                    }
                }

                function parseInstanceRep(ctx, instanceRep, group) {
                    //ctx.model.log("parseInstanceRep( " + instanceRep.id + " )");
                    if (instanceRep.referenceReps) {
                        for (var id in instanceRep.referenceReps) {
                            var referenceRep = instanceRep.referenceReps[id];
                            for (var id2 in referenceRep) {
                                if (id2 === "id") {
                                    continue; // HACK
                                }
                                var meshCfg = referenceRep[id2];
                                var lines = meshCfg.geometry.primitive === "lines";
                                var material = lines ? ctx.model._wireframeMaterial : (meshCfg.materialId ? ctx.materials[meshCfg.materialId] : null);
                                var colorize = meshCfg.color;
                                var mesh = new xeogl.Mesh(ctx.model.scene, {
                                    geometry: meshCfg.geometry,
                                    material: material || ctx.model._defaultMaterial,
                                    colorize: colorize,
                                    ghostMaterial: !lines ? ctx.model._wireframeGhostMaterial : null,
                                    backfaces: false,
                                    quantized: true
                                });
                                ctx.model._addComponent(mesh);
                                if (group) {
                                    group.addChild(mesh);
                                } else {
                                    ctx.model.addChild(mesh);
                                }
                                mesh.colorize = colorize; // HACK: Mesh has inherited model's colorize state, so we need to restore it (we'd better not modify colorize on the model).
                            }
                        }
                    }
                }

                // Find the root Reference3D

                for (var id in reference3Ds) {
                    var reference3D = reference3Ds[id];
                    if (!reference3D.instance3D) {
                        parseReference3D(ctx, reference3D, null); // HACK: Assuming that root has id == "1"
                        ok();
                        return;
                    }
                }

                alert("No root Reference3D element found in this model - can't load.");

                ok();
            });
        }

        function parseIntArray(str) {
            var parts = str.split(" ");
            var result = new Int32Array(parts.length);
            for (var i = 0; i < parts.length; i++) {
                result[i] = parseInt(parts[i]);
            }
            return result;
        }

        function parseReferenceReps(ctx, node, ok) {
            var referenceReps = {};
            var children = node.children;
            var numToLoad = 0;
            for (var i = 0, len = children.length; i < len; i++) {
                var child = children[i];
                if (child.type === "ReferenceRep") {
                    numToLoad++;
                }
            }
            for (var i = 0, len = children.length; i < len; i++) {
                var child = children[i];
                switch (child.type) {
                    case "ReferenceRep":
                        if (child.associatedFile) {
                            var src = stripURN(child.associatedFile);
                            (function () {
                                var childId = child.id;
                                ctx.zip.getFile(src, function (xmlDoc, json) {

                                        var materialIds = xmlDoc.getElementsByTagName("MaterialId");

                                        loadCATMaterialRefDocuments(ctx, materialIds, function () {

                                            // ctx.model.log("reference loaded: " + src);
                                            var referenceRep = {
                                                id: childId
                                            };
                                            parse3DRepDocument(ctx, json, referenceRep);
                                            referenceReps[childId] = referenceRep;
                                            if (--numToLoad === 0) {
                                                console.log("All ReferenceReps loaded.");
                                                ok(referenceReps);
                                            }
                                        });
                                    },
                                    function (error) {
                                        // TODO:
                                    });
                            })();
                        }
                        break;
                }
            }
        }


        function parseDefaultView(ctx, node) {
            // ctx.model.log("parseDefaultView");
            var children = node.children;
            for (var i = 0, len = children.length; i < len; i++) {
                var child = children[i];
                switch (child.type) {
                    case "Viewpoint":
                        var children2 = child.children;
                        ctx.model.viewpoint = {};
                        for (var i2 = 0, len2 = children2.length; i2 < len2; i2++) {
                            var child2 = children2[i];
                            switch (child2.type) {
                                case "Position":
                                    ctx.model.viewpoint.eye = parseFloatArray(child2.children[0], 3);
                                    break;
                                case "Sight":
                                    ctx.model.viewpoint.look = parseFloatArray(child2.children[0], 3);
                                    break;
                                case "Up":
                                    ctx.model.viewpoint.up = parseFloatArray(child2.children[0], 3);
                                    break;
                            }
                        }
                        break;
                    case "DefaultViewProperty":
                        break;
                }
            }
        }

        function parse3DRepDocument(ctx, node, result) {
            // ctx.model.log("parse3DRepDocument");
            var children = node.children;
            for (var i = 0, len = children.length; i < len; i++) {
                var child = children[i];
                switch (child.type) {
                    case "XMLRepresentation":
                        parseXMLRepresentation(ctx, child, result);
                        break;
                }
            }
        }

        function parseXMLRepresentation(ctx, node, result) {
            // ctx.model.log("parseXMLRepresentation");
            var children = node.children;
            for (var i = 0, len = children.length; i < len; i++) {
                var child = children[i];
                switch (child.type) {
                    case "Root":
                        parse3DRepRoot(ctx, child, result);
                        break;
                }
            }
        }

        function parse3DRepRoot(ctx, node, result) {
            // ctx.model.log("parse3DRepRoot");
            switch (node["xsi:type"]) {
                case "BagRepType":
                    break;
                case "PolygonalRepType":
                    break;
            }
            var children = node.children;
            for (var i = 0, len = children.length; i < len; i++) {
                var child = children[i];
                switch (child.type) {
                    case "Rep":
                        parse3DRepRep(ctx, child, result);
                        break;
                }
            }
        }

        function parse3DRepRep(ctx, node, result) {
            // ctx.model.log("parse3DRep");
            switch (node["xsi:type"]) {
                case "BagRepType":
                    break;
                case "PolygonalRepType":
                    break;
            }
            var meshesResult = {
                edgeThreshold: ctx.edgeThreshold || 20
            };
            var children = node.children;
            for (var i = 0, len = children.length; i < len; i++) {
                var child = children[i];
                switch (child.type) {
                    case "Rep":
                        parse3DRepRep(ctx, child, result);
                        break;
                    case "Edges":
                        meshesResult.primitive = "lines";
                        parseEdges(ctx, child, meshesResult);
                        break;
                    case "Faces":
                        meshesResult.primitive = "triangles";
                        parseFaces(ctx, child, meshesResult);
                        break;
                    case "VertexBuffer":
                        parseVertexBuffer(ctx, child, meshesResult);
                        break;
                    case "SurfaceAttributes":
                        parseSurfaceAttributes(ctx, child, meshesResult);
                        break;
                }
            }
            if (meshesResult.positions) {
                var geometry = new xeogl.Geometry(ctx.model.scene, meshesResult);
                ctx.model._addComponent(geometry);
                result[geometry.id] = {
                    geometry: geometry,
                    color: meshesResult.color || [1.0, 1.0, 1.0, 1.0],
                    materialId: meshesResult.materialId
                };
            }
        }

        function parseEdges(ctx, node, result) {
            // ctx.model.log("parseEdges");
            result.positions = [];
            result.indices = [];
            var children = node.children;
            for (var i = 0, len = children.length; i < len; i++) {
                var child = children[i];
                switch (child.type) {
                    case "Polyline":
                        parsePolyline(ctx, child, result);
                        break;
                }
            }
        }

        function parsePolyline(ctx, node, result) {
            //ctx.model.log("parsePolyline");
            var vertices = node.vertices;
            if (vertices) {
                var positions = parseFloatArray(vertices, 3);
                if (positions.length > 0) {
                    var positionsOffset = result.positions.length / 3;
                    for (var i = 0, len = positions.length; i < len; i++) {
                        result.positions.push(positions[i]);
                    }
                    for (var i = 0, len = (positions.length / 3) - 1; i < len; i++) {
                        result.indices.push(positionsOffset + i);
                        result.indices.push(positionsOffset + i + 1);
                    }
                }
            }
        }

        function parseFaces(ctx, node, result) {
            // ctx.model.log("parseFaces");
            var children = node.children;
            for (var i = 0, len = children.length; i < len; i++) {
                var child = children[i];
                switch (child.type) {
                    case "Face":
                        parseFace(ctx, child, result);
                        break;
                }
            }
        }

        function parseFace(ctx, node, result) {
            // ctx.model.log("parseFace");

            var strips = node.strips;
            if (strips) {

                // Triangle strips

                var arrays = parseIntArrays(strips);
                if (arrays.length > 0) {
                    result.primitive = "triangles";
                    var indices = [];
                    for (var i = 0, len = arrays.length; i < len; i++) {
                        var array = convertTriangleStrip(arrays[i]);
                        for (var j = 0, lenj = array.length; j < lenj; j++) {
                            indices.push(array[j]);
                        }
                    }
                    result.indices = indices; // TODO
                }
            } else {

                // Triangle meshes

                var triangles = node.triangles;
                if (triangles) {
                    result.primitive = "triangles";
                    result.indices = parseIntArray(triangles);
                }
            }

            // Material

            var children = node.children;
            for (var i = 0, len = children.length; i < len; i++) {
                var child = children[i];
                switch (child.type) {
                    case "SurfaceAttributes":
                        parseSurfaceAttributes(ctx, child, result);
                        break;
                }
            }
        }

        function convertTriangleStrip(indices) {
            var ccw = false;
            var indices2 = [];
            for (var i = 0, len = indices.length; i < len - 2; i++) {
                if (ccw) {
                    if (i & 1) { //
                        indices2.push(indices[i]);
                        indices2.push(indices[i + 1]);
                        indices2.push(indices[i + 2]);
                    } else {
                        indices2.push(indices[i]);
                        indices2.push(indices[i + 2]);
                        indices2.push(indices[i + 1]);
                    }
                } else {
                    if (i & 1) { //
                        indices2.push(indices[i]);
                        indices2.push(indices[i + 2]);
                        indices2.push(indices[i + 1]);
                    } else {
                        indices2.push(indices[i]);
                        indices2.push(indices[i + 1]);
                        indices2.push(indices[i + 2]);
                    }
                }
            }
            return indices2;
        }

        function parseVertexBuffer(ctx, node, result) {
            var children = node.children;
            for (var i = 0, len = children.length; i < len; i++) {
                var child = children[i];
                switch (child.type) {
                    case "Positions":
                        result.positions = parseFloatArray(child.children[0], 3);
                        break;
                    case "Normals":
                        result.normals = parseFloatArray(child.children[0], 3);
                        break;
                    case "TextureCoordinates": // TODO: Support dimension and channel?
                        result.uv = parseFloatArray(child.children[0], 2);
                        break;
                }
            }
        }

        function parseIntArrays(str) {
            var coordStrings = str.split(",");
            var array = [];
            for (var i = 0, len = coordStrings.length; i < len; i++) {
                var coordStr = coordStrings[i].trim();
                if (coordStr.length > 0) {
                    var elemStrings = coordStr.split(" ");
                    var arr = new Int16Array(elemStrings.length);
                    var arrIdx = 0;
                    for (var j = 0, lenj = elemStrings.length; j < lenj; j++) {
                        if (elemStrings[j] !== "") {
                            arr[arrIdx++] = parseInt(elemStrings[j]);
                        }
                    }
                    array.push(arr);
                }
            }
            return array;
        }

        function parseFloatArray(str, numElems) {
            str = str.split(",");
            var arr = new Float32Array(str.length * numElems);
            var arrIdx = 0;
            for (var i = 0, len = str.length; i < len; i++) {
                var value = str[i];
                value = value.split(" ");
                for (var j = 0, lenj = value.length; j < lenj; j++) {
                    if (value[j] !== "") {
                        arr[arrIdx++] = parseFloat(value[j]);
                    }
                }
            }
            return arr;
        }

        function parseIntArray(str) {
            str = str.split(" ");
            var arr = new Int32Array(str.length);
            var arrIdx = 0;
            for (var i = 0, len = str.length; i < len; i++) {
                var value = str[i];
                arr[i] = parseInt(value);
            }
            return arr;
        }

        function parseSurfaceAttributes(ctx, node, result) {
            result.color = [1, 1, 1, 1];
            var children = node.children;
            for (var i = 0, len = children.length; i < len; i++) {
                var child = children[i];
                switch (child.type) {
                    case "Color":
                        result.color[0] = child.red;
                        result.color[1] = child.green;
                        result.color[2] = child.blue;
                        result.color[3] = child.alpha;
                        break;
                    case "MaterialApplication":
                        var children2 = child.children;
                        for (var j = 0, lenj = children2.length; j < lenj; j++) {
                            var child2 = children2[j];
                            switch (child2.type) {
                                case "MaterialId":
                                    var materialId = getIDFromURI(child2.id);
                                    var material = ctx.materials[materialId];
                                    if (!material) {
                                        ctx.model.error("material  not found: " + materialId);
                                    }
                                    result.materialId = materialId;
                                    break;
                            }
                        }
                        break;
                }
            }
        }
    })();

    //----------------------------------------------------------------------------------------------------
    // Materials
    //----------------------------------------------------------------------------------------------------

    function loadCATMaterialRefDocuments(ctx, materialIds, ok) {
        var loaded = {};

        function load(i, done) {
            if (i >= materialIds.length) {
                ok();
                return;
            }
            var materialId = materialIds[i];
            var src = materialId.id;
            var colonIdx = src.lastIndexOf(":");
            if (colonIdx > 0) {
                src = src.substring(colonIdx + 1);
            }
            var hashIdx = src.lastIndexOf("#");
            if (hashIdx > 0) {
                src = src.substring(0, hashIdx);
            }
            if (!loaded[src]) {
                loadCATMaterialRefDocument(ctx, src, function () {
                    loaded[src] = true;
                    load(i + 1, done);
                });
            } else {
                load(i + 1, done);
            }
        }

        load(0, ok);
    }

    function loadCATMaterialRefDocument(ctx, src, ok) { // Loads CATMaterialRef.3dxml
        ctx.zip.getFile(src, function (xmlDoc, json) {
            parseCATMaterialRefDocument(ctx, json, ok);
        });
    }

    function parseCATMaterialRefDocument(ctx, node, ok) { // Parse CATMaterialRef.3dxml
        // ctx.model.log("parseCATMaterialRefDocument");
        var children = node.children;
        var child;
        for (var i = 0, len = children.length; i < len; i++) {
            child = children[i];
            if (child.type === "Model_3dxml") {
                parseModel_3dxml(ctx, child, ok);
            }
        }
    }

    function parseModel_3dxml(ctx, node, ok) { // Parse CATMaterialRef.3dxml
        // ctx.model.log("parseModel_3dxml");
        var children = node.children;
        var child;
        for (var i = 0, len = children.length; i < len; i++) {
            child = children[i];
            if (child.type === "CATMaterialRef") {
                parseCATMaterialRef(ctx, child, ok);
            }
        }
    }

    function parseCATMaterialRef(ctx, node, ok) {

        // ctx.model.log("parseCATMaterialRef");

        var domainToReferenceMap = {};
        var materials = {};

        var result = {};
        var children = node.children;
        var child;
        var numToLoad = 0;

        for (var j = 0, lenj = children.length; j < lenj; j++) {
            var child2 = children[j];
            switch (child2.type) {
                case "MaterialDomainInstance":
                    var isAggregatedBy;
                    var isInstanceOf;
                    for (var k = 0, lenk = child2.children.length; k < lenk; k++) {
                        var child3 = child2.children[k];
                        switch (child3.type) {
                            case "IsAggregatedBy":
                                isAggregatedBy = child3.children[0];
                                break;
                            case "IsInstanceOf":
                                isInstanceOf = child3.children[0];
                                break;
                        }
                    }
                    domainToReferenceMap[isInstanceOf] = isAggregatedBy;
                    break;
            }
        }

        for (var j = 0, lenj = children.length; j < lenj; j++) {
            var child2 = children[j];
            switch (child2.type) {
                case "MaterialDomain":
                    numToLoad++;
                    break;
            }
        }

        // Now load them

        for (var j = 0, lenj = children.length; j < lenj; j++) {
            var child2 = children[j];
            switch (child2.type) {
                case "MaterialDomain":
                    if (child2.associatedFile) {
                        (function () {
                            var childId = child2.id;
                            var src = stripURN(child2.associatedFile);
                            ctx.zip.getFile(src, function (xmlDoc, json) {
                                    // ctx.model.log("Material def loaded: " + src);
                                    ctx.materials[domainToReferenceMap[childId]] = parseMaterialDefDocument(ctx, json);

                                    if (--numToLoad === 0) {
                                        //       console.log("All ReferenceReps loaded.");
                                        ok();
                                    }
                                },
                                function (error) {
                                    // TODO:
                                });
                        })();
                    }
                    break;
            }
        }
    }

    function parseMaterialDefDocument(ctx, node) {
        // ctx.model.log("parseMaterialDefDocumentOsm");
        var children = node.children;
        for (var i = 0, len = children.length; i < len; i++) {
            var child = children[i];
            switch (child.type) {
                case "Osm":
                    return parseMaterialDefDocumentOsm(ctx, child);
                    break;
            }
        }
    }

    function parseMaterialDefDocumentOsm(ctx, node) {
        // ctx.model.log("parseMaterialDefDocument");
        var children = node.children;
        for (var i = 0, len = children.length; i < len; i++) {
            var child = children[i];
            // ctx.model.log("parseMaterialDefDocument: child.type == " + child.type);
            switch (child.type) {
                case "RenderingRootFeature":
                    //..
                    break;
                case "Feature":

                    if (child.Alias === "RenderingFeature") {
                        // Parse the coefficients, then parse the colors, scaling those by their coefficients.

                        var coeffs = {};
                        var materialCfg = {};
                        var children2 = child.children;
                        var j;
                        var lenj;
                        var child2;
                        for (j = 0, lenj = children2.length; j < lenj; j++) {
                            child2 = children2[j];
                            switch (child2.Name) {
                                case "AmbientCoef":
                                    coeffs.ambient = parseFloat(child2.Value);
                                    break;
                                case "DiffuseCoef":
                                    coeffs.diffuse = parseFloat(child2.Value);
                                    break;
                                case "EmissiveCoef":
                                    coeffs.emissive = parseFloat(child2.Value);
                                    break;
                                case "SpecularExponent":
                                    coeffs.specular = parseFloat(child2.Value);
                                    break;
                            }
                        }
                        for (j = 0, lenj = children2.length; j < lenj; j++) {
                            child2 = children2[j];
                            switch (child2.Name) {
                                case "AmbientColor":
                                    materialCfg.ambient = parseRGB(child2.Value, coeffs.ambient);
                                    break;
                                case "DiffuseColor":
                                    materialCfg.diffuse = parseRGB(child2.Value, coeffs.diffuse);
                                    break;
                                case "EmissiveColor":
                                    materialCfg.emissive = parseRGB(child2.Value, coeffs.emissive);
                                    break;
                                case "SpecularColor":
                                    materialCfg.specular = parseRGB(child2.Value, coeffs.specular);
                                    break;
                                case "Transparency":
                                    var alpha = 1.0 - parseFloat(child2.Value); // GOTCHA: Degree of transparency, not degree of opacity
                                    if (alpha < 1.0) {
                                        materialCfg.alpha = alpha;
                                        materialCfg.alphaMode = "blend";
                                    }
                                    break;
                            }
                        }

                        var material;

                        switch (ctx.materialWorkflow) {
                            case "MetallicMaterial":
                                material = new xeogl.MetallicMaterial(ctx.model.scene, {
                                    baseColor: materialCfg.diffuse,
                                    metallic: 0.7,
                                    roughness: 0.5,
                                    emissive: materialCfg.emissive,
                                    alpha: materialCfg.alpha,
                                    alphaMode: materialCfg.alphaMode
                                });
                                break;

                            case "SpecularMaterial":
                                material = new xeogl.SpecularMaterial(ctx.model.scene, {
                                    diffuse: materialCfg.diffuse,
                                    specular: materialCfg.specular,
                                    glossiness: 0.5,
                                    emissive: materialCfg.emissive,
                                    alpha: materialCfg.alpha,
                                    alphaMode: materialCfg.alphaMode
                                });
                                break;

                            default:
                                material = new xeogl.PhongMaterial(ctx.model.scene, {
                                    reflectivity: 0.5,
                                    ambient: materialCfg.ambient,
                                    diffuse: materialCfg.diffuse,
                                    specular: materialCfg.specular,
                                    // shininess: node.shine,
                                    emissive: materialCfg.emissive,
                                    alphaMode: materialCfg.alphaMode,
                                    alpha: node.alpha
                                });
                        }

                        ctx.model._addComponent(material);
                        return material;
                    }

                    break;
            }
        }
    }

    function parseRGB(str, coeff) {
        coeff = (coeff !== undefined) ? coeff : 0.5;
        var openBracketIndex = str.indexOf("[");
        var closeBracketIndex = str.indexOf("]");
        str = str.substring(openBracketIndex + 1, closeBracketIndex - openBracketIndex);
        str = str.split(",");
        var arr = new Float32Array(str.length);
        var arrIdx = 0;
        for (var i = 0, len = str.length; i < len; i++) {
            var value = str[i];
            value = value.split(" ");
            for (var j = 0, lenj = value.length; j < lenj; j++) {
                if (value[j] !== "") {
                    arr[arrIdx++] = parseFloat(value[j]) * coeff;
                }
            }
        }
        return arr;
    }


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

    /**
     * Wraps zip.js to provide an in-memory ZIP archive representing the 3DXML file bundle.
     *
     * Allows us to pluck each file from it as XML and JSON.
     *
     * @constructor
     */
    var ZIP = function () {

        var reader;
        var files = {};

        /**
         Loads this ZIP

         @param src
         @param ok
         @param error
         */
        this.load = function (src, ok, error) {
            var self = this;
            zip.createReader(new zip.HttpReader(src), function (reader) {
                reader.getEntries(function (entries) {
                    if (entries.length > 0) {
                        for (var i = 0, len = entries.length; i < len; i++) {
                            var entry = entries[i];
                            files[entry.filename] = entry;
                        }
                    }
                    ok();
                });
            }, error);
        };

        /**
         Gets a file as XML and JSON from this ZIP
         @param src
         @param ok
         @param error
         */
        this.getFile = function (src, ok, error) {
            var entry = files[src];
            if (!entry) {
                var errMsg = "ZIP entry not found: " + src;
                console.error(errMsg);
                if (error) {
                    error(errMsg);
                }
                return;
            }
            entry.getData(new zip.TextWriter(), function (text) {

                // Parse to XML
                var parser = new DOMParser();
                var xmlDoc = parser.parseFromString(text, "text/xml");

                // Parse to JSON
                var json = xmlToJSON(xmlDoc, {});

                ok(xmlDoc, json);
            });
        };

        function xmlToJSON(node, attributeRenamer) {
            if (node.nodeType === node.TEXT_NODE) {
                var v = node.nodeValue;
                if (v.match(/^\s+$/) === null) {
                    return v;
                }
            } else if (node.nodeType === node.ELEMENT_NODE ||
                node.nodeType === node.DOCUMENT_NODE) {
                var json = {type: node.nodeName, children: []};
                if (node.nodeType === node.ELEMENT_NODE) {
                    for (var j = 0; j < node.attributes.length; j++) {
                        var attribute = node.attributes[j];
                        var nm = attributeRenamer[attribute.nodeName] || attribute.nodeName;
                        json[nm] = attribute.nodeValue;
                    }
                }
                for (var i = 0; i < node.childNodes.length; i++) {
                    var item = node.childNodes[i];
                    var j = xmlToJSON(item, attributeRenamer);
                    if (j) json.children.push(j);
                }
                return json;
            }
        }

        /**
         Disposes of this ZIP
         */
        this.destroy = function () {
            reader.close(function () {
                // onclose callback
            });
        };
    };

    function loadZIP(src, ok, err) {
        var zip = new ZIP();
        zip.load(src, function () {
            ok(zip);
        }, function (errMsg) {
            err("Error loading ZIP archive: " + errMsg);
        })
    }

    // function loadXML(url, ok, err) {
    //     var request = new XMLHttpRequest();
    //     request.overrideMimeType("text/xml");
    //     request.open('GET', url, true);
    //     request.addEventListener('load', function (event) {
    //         //var response = event.target.response;
    //         if (this.status === 200) {
    //             if (ok) {
    //                 var parser = new DOMParser();
    //                 var xmlDoc = parser.parseFromString(request.responseText, "text/xml");
    //                 ok(xmlDoc, this);
    //             }
    //         } 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) {
    //                 var parser = new DOMParser();
    //                 var xmlDoc = parser.parseFromString(request.responseText, "text/xml");
    //                 ok(xmlDoc, this);
    //             }
    //         } else {
    //             if (err) {
    //                 err(event);
    //             }
    //         }
    //     }, false);
    //
    //     request.addEventListener('error', function (event) {
    //         if (err) {
    //             err(event);
    //         }
    //     }, false);
    //     request.send(null);
    // }
    //
    // function xmlToJSON(node, attributeRenamer) {
    //     if (node.nodeType === node.TEXT_NODE) {
    //         var v = node.nodeValue;
    //         if (v.match(/^\s+$/) === null) {
    //             return v;
    //         }
    //     } else if (node.nodeType === node.ELEMENT_NODE ||
    //         node.nodeType === node.DOCUMENT_NODE) {
    //         var json = {type: node.nodeName, children: []};
    //         if (node.nodeType === node.ELEMENT_NODE) {
    //             for (var j = 0; j < node.attributes.length; j++) {
    //                 var attribute = node.attributes[j];
    //                 var nm = attributeRenamer[attribute.nodeName] || attribute.nodeName;
    //                 json[nm] = attribute.nodeValue;
    //             }
    //         }
    //         for (var i = 0; i < node.childNodes.length; i++) {
    //             var item = node.childNodes[i];
    //             var j = xmlToJSON(item, attributeRenamer);
    //             if (j) json.children.push(j);
    //         }
    //         return json;
    //     }
    // }

    function stripURN(str) {
        var subStr = "urn:3DXML:";
        return (str.indexOf(subStr) === 0) ? str.substring(subStr.length) : str;
    }


    function getIDFromURI(str) {
        var hashIdx = str.lastIndexOf("#");
        return hashIdx != -1 ? str.substring(hashIdx + 1) : str;
    }

})();