Browse Source

Merging from master

Ibraheem Osama 7 năm trước cách đây
mục cha
commit
5c46feb038
71 tập tin đã thay đổi với 113266 bổ sung0 xóa
  1. 113 0
      Tools/Gulp/gulp-babylonModule.js
  2. 62 0
      Tools/Gulp/gulp-dtsModuleSupport.js
  3. 111 0
      Tools/Gulp/gulp-es6ModuleExports.js
  4. 710 0
      Tools/Gulp/gulp-validateTypedoc.js
  5. 166 0
      Tools/Publisher/index.js
  6. 16 0
      Tools/Publisher/package.json
  7. 35 0
      Viewer/README.md
  8. 97 0
      Viewer/assets/deepmerge.min.js
  9. 1 0
      Viewer/assets/es6-promise.min.js
  10. 29 0
      Viewer/assets/handlebars.min.js
  11. BIN
      Viewer/assets/img/close.png
  12. BIN
      Viewer/assets/img/fullscreen.png
  13. BIN
      Viewer/assets/img/help-circle.png
  14. BIN
      Viewer/assets/img/loading.png
  15. 210 0
      Viewer/assets/pep.min.js
  16. 3 0
      Viewer/assets/templates/default/defaultTemplate.html
  17. 26 0
      Viewer/assets/templates/default/defaultViewer.html
  18. 1 0
      Viewer/assets/templates/default/error.html
  19. 1 0
      Viewer/assets/templates/default/help.html
  20. 42 0
      Viewer/assets/templates/default/loadingScreen.html
  21. 112 0
      Viewer/assets/templates/default/navbar.html
  22. 46 0
      Viewer/assets/templates/default/overlay.html
  23. 1 0
      Viewer/assets/templates/default/share.html
  24. 34 0
      Viewer/dist/basicExample.html
  25. 39 0
      Viewer/dist/domExample.html
  26. BIN
      Viewer/dist/environment.dds
  27. 67 0
      Viewer/dist/eventsExample.html
  28. 109021 0
      Viewer/dist/viewer.js
  29. 1 0
      Viewer/dist/viewer.min.js
  30. 43 0
      Viewer/package.json
  31. 186 0
      Viewer/src/configuration/configuration.ts
  32. 0 0
      Viewer/src/configuration/index.ts
  33. 79 0
      Viewer/src/configuration/loader.ts
  34. 121 0
      Viewer/src/configuration/mappers.ts
  35. 122 0
      Viewer/src/configuration/types/default.ts
  36. 16 0
      Viewer/src/configuration/types/index.ts
  37. 34 0
      Viewer/src/configuration/types/minimal.ts
  38. 41 0
      Viewer/src/helper.ts
  39. 30 0
      Viewer/src/index.ts
  40. 15 0
      Viewer/src/initializer.ts
  41. 5 0
      Viewer/src/interfaces.ts
  42. 330 0
      Viewer/src/templateManager.ts
  43. 39 0
      Viewer/src/util/promiseObservable.ts
  44. 516 0
      Viewer/src/viewer/defaultViewer.ts
  45. 172 0
      Viewer/src/viewer/viewer.ts
  46. 57 0
      Viewer/src/viewer/viewerManager.ts
  47. 32 0
      Viewer/tsconfig-gulp.json
  48. 26 0
      Viewer/tsconfig.json
  49. 57 0
      Viewer/webpack.config.js
  50. 51 0
      Viewer/webpack.gulp.config.js
  51. 142 0
      tests/nullEngine/app.js
  52. 5 0
      tests/nullEngine/package.json
  53. BIN
      tests/validation/ReferenceImages/Billboard.png
  54. BIN
      tests/validation/ReferenceImages/GUI.png
  55. BIN
      tests/validation/ReferenceImages/assetContainer.png
  56. BIN
      tests/validation/ReferenceImages/customRTT.png
  57. BIN
      tests/validation/ReferenceImages/gltf1CesiumMan.png
  58. BIN
      tests/validation/ReferenceImages/gltfMaterial.png
  59. BIN
      tests/validation/ReferenceImages/gltfMaterialAlpha.png
  60. BIN
      tests/validation/ReferenceImages/gltfMaterialMetallicRoughness.png
  61. BIN
      tests/validation/ReferenceImages/gltfMaterialSpecularGlossiness.png
  62. BIN
      tests/validation/ReferenceImages/gltfMeshPrimAttribTest.png
  63. BIN
      tests/validation/ReferenceImages/gltfPrimitiveAttribute.png
  64. BIN
      tests/validation/ReferenceImages/gltfTextureSampler.png
  65. BIN
      tests/validation/ReferenceImages/gltfnormals.png
  66. BIN
      tests/validation/ReferenceImages/normals.png
  67. BIN
      tests/validation/ReferenceImages/setParent.png
  68. BIN
      tests/validation/ReferenceImages/upVector.png
  69. 64 0
      tests/validation/integration.js
  70. 93 0
      tests/validation/karma.conf.browserstack.js
  71. 46 0
      tests/validation/karma.conf.js

+ 113 - 0
Tools/Gulp/gulp-babylonModule.js

@@ -0,0 +1,113 @@
+var gutil = require('gulp-util');
+var through = require('through2');
+
+module.exports = function (moduleName, dependencies) {
+    return through.obj(function (file, enc, cb) {
+
+        console.log("Compiling module: " + moduleName);
+
+        var extendsAddition =
+            `var __extends = (this && this.__extends) || (function () {
+var extendStatics = Object.setPrototypeOf ||
+    ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+    function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+return function (d, b) {
+    extendStatics(d, b);
+    function __() { this.constructor = d; }
+    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+};
+})();
+`;
+
+        var decorateAddition =
+            'var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {\n' +
+            'var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\n' +
+            'if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);\n' +
+            'else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\n' +
+            'return c > 3 && r && Object.defineProperty(target, key, r), r;\n' +
+            '};\n';
+
+        let content = file.contents.toString();
+        if (content.indexOf('__extends') === -1 && dependencies.length < 2) {
+            extendsAddition = '';
+        }
+
+        if (content.indexOf('__decorate') === -1) {
+            decorateAddition = '';
+        }
+
+        let dependenciesText = `${extendsAddition}
+${decorateAddition}
+if(typeof require !== 'undefined'){
+    var globalObject = (typeof global !== 'undefined') ? global : ((typeof window !== 'undefined') ? window : this);
+    var BABYLON = globalObject["BABYLON"] || {}; 
+`;
+        if (dependencies) {
+            /*if (dependencies.length > 1) {
+                dependenciesText += 'function nse(ns1, ns2) { Object.keys(ns2).forEach(function(c) {if(!ns1[c]) {ns1[c] = ns2[c]}}) };\n';
+            }*/
+
+            dependencies.forEach(function (d, idx) {
+                dependenciesText += `var BABYLON${idx} = require('babylonjs/${d}');
+`;
+                dependenciesText += `if(BABYLON !== BABYLON${idx}) __extends(BABYLON, BABYLON${idx});
+`;
+            });
+        }
+
+
+
+        let exportRegex = /BABYLON.([0-9A-Za-z-_]*) = .*;\n/g
+
+        var match = exportRegex.exec(content);
+
+        let exportsArray = [];
+        while (match != null) {
+            if (match[1]) {
+                exportsArray.push(match[1])
+            }
+            match = exportRegex.exec(content);
+        }
+
+        let exportsText = '';
+        if (moduleName === "core") {
+            exportsText = `(function() {
+    globalObject["BABYLON"] = globalObject["BABYLON"] || BABYLON;
+    module.exports = BABYLON; 
+})();
+}`
+        }
+        else {
+            exportsText = `(function() {
+var EXPORTS = {};`
+            exportsArray.forEach(e => {
+                if (e.indexOf('.') === -1)
+                    exportsText += `EXPORTS['${e}'] = BABYLON['${e}'];`
+            });
+
+            exportsText += `
+    globalObject["BABYLON"] = globalObject["BABYLON"] || BABYLON;
+    module.exports = EXPORTS;
+    })();
+}`
+        }
+
+        if (file.isNull()) {
+            cb(null, file);
+            return;
+        }
+
+        if (file.isStream()) {
+            //streams not supported, no need for now.
+            return;
+        }
+
+        try {
+            file.contents = new Buffer(dependenciesText.concat(new Buffer(String(file.contents).concat(exportsText))));
+            this.push(file);
+        } catch (err) {
+            this.emit('error', new gutil.PluginError('gulp-add-babylon-module', err, { fileName: file.path }));
+        }
+        cb();
+    });
+}

+ 62 - 0
Tools/Gulp/gulp-dtsModuleSupport.js

@@ -0,0 +1,62 @@
+var gutil = require('gulp-util');
+var through = require('through2');
+
+// inject - if set to true, it will add all declarations as imports.
+module.exports = function (moduleName, inject, declarations) {
+    return through.obj(function (file, enc, cb) {
+
+        let fileContent = file.contents.toString();
+        let importsString = '';
+
+        if (!inject) {
+            declarations[moduleName] = declarations[moduleName] || [];
+            let regexp = /    (abstract class|function|class|interface|type|const|enum|var) ([\w]*)/g;
+
+            var match = regexp.exec(fileContent);
+            while (match != null) {
+                if (match[2]) {
+                    // check it is not SIMD:
+                    let simdMatch = /    interface (\w*\dx\d{1,2}\w*)/.exec(match[0]);
+                    if (!simdMatch && match[2] !== 'earcut' && match[2] !== 'deviation' && match[2] !== 'flatten')
+                        declarations[moduleName].push(match[2]);
+                }
+                match = regexp.exec(fileContent);
+            }
+        } else {
+            let declared = [];
+            Object.keys(declarations).forEach(name => {
+                if (name === moduleName) return;
+                let imports = declarations[name].filter(obj => {
+                    let exists = declared.indexOf(obj) !== -1;
+                    if (!exists) {
+                        declared.push(obj);
+                    }
+                    return !exists;
+                });
+                if (imports.length)
+                    importsString += `import {${imports.join(',')}} from 'babylonjs/${name}';
+`;
+            });
+        }
+
+        if (file.isNull()) {
+            cb(null, file);
+            return;
+        }
+
+        if (file.isStream()) {
+            //streams not supported, no need for now.
+            return;
+        }
+
+        try {
+            file.contents = new Buffer(String(file.contents) + '\n' + importsString);
+            this.push(file);
+
+        } catch (err) {
+            this.emit('error', new gutil.PluginError('gulp-add-module-exports', err, { fileName: file.path }));
+        }
+        cb();
+    });
+};
+

+ 111 - 0
Tools/Gulp/gulp-es6ModuleExports.js

@@ -0,0 +1,111 @@
+var gutil = require('gulp-util');
+var through = require('through2');
+
+module.exports = function (moduleName, dependencies, es6) {
+    return through.obj(function (file, enc, cb) {
+
+        console.log("Compiling es6 module: " + moduleName);
+
+        var extendsAddition =
+            `var __extends = (this && this.__extends) || (function () {
+var extendStatics = Object.setPrototypeOf ||
+    ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+    function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+return function (d, b) {
+    extendStatics(d, b);
+    function __() { this.constructor = d; }
+    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+};
+})();
+`;
+
+        var decorateAddition =
+            'var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {\n' +
+            'var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\n' +
+            'if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);\n' +
+            'else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\n' +
+            'return c > 3 && r && Object.defineProperty(target, key, r), r;\n' +
+            '};\n';
+
+        let content = file.contents.toString();
+        if (content.indexOf('__extends') === -1 && dependencies.length < 2) {
+            extendsAddition = '';
+        }
+
+        if (content.indexOf('__decorate') === -1) {
+            decorateAddition = '';
+        }
+
+        let dependenciesText = `${extendsAddition}
+${decorateAddition}
+`;
+        if (dependencies) {
+            /*if (dependencies.length > 1) {
+                dependenciesText += 'function nse(ns1, ns2) { Object.keys(ns2).forEach(function(c) {if(!ns1[c]) {ns1[c] = ns2[c]}}) };\n';
+            }*/
+
+            dependencies.forEach(function (d, idx) {
+                let name = d === 'core' ? 'BABYLON' : d;
+                dependenciesText += `import * as ${name} from 'babylonjs/${d}/es6';
+`;
+                if (idx > 0) {
+                    dependenciesText += `__extends(BABYLON, ${d});
+`;
+                }
+            });
+        }
+
+
+
+        let exportRegex = /BABYLON.([0-9A-Za-z-_]*) = .*;\n/g
+
+        var match = exportRegex.exec(content);
+
+        let exportsArray = [];
+        while (match != null) {
+            if (match[1]) {
+                exportsArray.push(match[1])
+            }
+            match = exportRegex.exec(content);
+        }
+
+        let exportsText = '';
+        if (moduleName === "core") {
+            exportsText = `(function() {
+    var globalObject = (typeof global !== 'undefined') ? global : ((typeof window !== 'undefined') ? window : this);
+    globalObject["BABYLON"] = BABYLON;
+})();;
+`
+        }
+
+        let exportedItems = '';
+        exportsArray.forEach((e, idx) => {
+            if (e.indexOf('.') === -1) {
+                exportedItems += `${idx ? ',' : ''}${e}`
+                exportsText += `var ${e} = BABYLON.${e};
+`
+            }
+        });
+
+        exportsText += `
+export { ${exportedItems} };`
+
+        if (file.isNull()) {
+            cb(null, file);
+            return;
+        }
+
+        if (file.isStream()) {
+            //streams not supported, no need for now.
+            return;
+        }
+
+        try {
+            file.contents = new Buffer(dependenciesText.concat(new Buffer(String(file.contents).concat(exportsText))));
+            this.push(file);
+        } catch (err) {
+            this.emit('error', new gutil.PluginError('gulp-es6-module-exports', err, { fileName: file.path }));
+        }
+        cb();
+    });
+}

+ 710 - 0
Tools/Gulp/gulp-validateTypedoc.js

@@ -0,0 +1,710 @@
+'use strict';
+
+var fs = require('fs');
+var Vinyl = require('vinyl');
+var path = require('path');
+var through = require('through2');
+var PluginError = require('plugin-error');
+var supportsColor = require('color-support');
+
+// ______________________________________________ LOGS ______________________________________________
+
+var hasColors = supportsColor();
+
+var red =       hasColors ? '\x1b[31m' : '';
+var yellow =    hasColors ? '\x1b[33m' : '';
+var green =     hasColors ? '\x1b[32m' : '';
+var gray =      hasColors ? '\x1b[90m' : '';
+var white =     hasColors ? '\x1b[97m' : '';
+var clear =     hasColors ? '\x1b[0m' : '';
+
+var currentColor = undefined;
+
+function getTimestamp() {
+    var time = new Date();
+    var timeInString = ("0" + time.getHours()).slice(-2) + ":" + 
+        ("0" + time.getMinutes()).slice(-2) + ":" + 
+        ("0" + time.getSeconds()).slice(-2);
+
+    if (currentColor) {
+        return white + '[' + currentColor + timeInString + clear + white + ']';
+    }
+    else {
+        return white + '[' + gray + timeInString + white + ']';
+    }
+}
+
+function log() {
+    currentColor = gray;
+    var time = getTimestamp();
+    process.stdout.write(time + ' ');
+    currentColor = undefined;
+
+    console.log.apply(console, arguments);
+    return this;
+}
+
+function warn() {
+    currentColor = yellow;
+    var time = getTimestamp();
+    process.stdout.write(time + ' ');
+    currentColor = undefined;
+
+    console.warn.apply(console, arguments);
+    return this;
+}
+
+function err() {
+    currentColor = red;
+    var time = getTimestamp();
+    process.stderr.write(time + ' ');
+    currentColor = undefined;
+    
+    console.error.apply(console, arguments);
+    return this;
+}
+
+function success() {
+    currentColor = green;
+    var time = getTimestamp();
+    process.stdout.write(time + ' ');
+    currentColor = undefined;
+
+    console.log.apply(console, arguments);
+    return this;
+}
+
+// ______________________________________________ VALIDATION ____________________________________________
+
+function unixStylePath(filePath) {
+    return filePath.replace(/\\/g, '/');
+}
+
+function Validate(validationBaselineFileName, namespaceName, validateNamingConvention, generateBaseLine) {
+    this.validationBaselineFileName = validationBaselineFileName;
+    this.namespaceName = namespaceName;
+    this.validateNamingConvention = validateNamingConvention;
+    this.generateBaseLine = generateBaseLine;
+
+    this.previousResults = { };
+    this.results = {
+        errors: 0
+    };
+}
+
+Validate.hasTag = function(node, tagName) {
+    tagName = tagName.trim().toLowerCase();
+
+    if (node.comment && node.comment.tags) {
+        for (var i = 0; i < node.comment.tags.length; i++) {
+            if (node.comment.tags[i].tag === tagName) {
+                return true;
+            }
+        }
+    }
+
+    return false;
+}
+
+Validate.position = function(node) {
+    if (!node.sources) {
+        log(node);
+    }
+    return node.sources[0].fileName + ':' + node.sources[0].line;
+}
+
+Validate.upperCase = new RegExp("^[A-Z_]*$");
+Validate.pascalCase = new RegExp("^[A-Z][a-zA-Z0-9_]*$");
+Validate.camelCase = new RegExp("^[a-z][a-zA-Z0-9_]*$");
+Validate.underscoreCamelCase = new RegExp("^_[a-z][a-zA-Z0-9_]*$");
+Validate.underscorePascalCase = new RegExp("^_[A-Z][a-zA-Z0-9_]*$");
+
+Validate.prototype.errorCallback = function (parent, node, nodeKind, category, type, msg, position) {
+    this.results[this.filePath] = this.results[this.filePath] || { errors: 0 };
+    var results = this.results[this.filePath];
+
+    if (node === "toString") {
+        node = "ToString";
+    }
+    
+    // Checks against previous results.
+    var previousResults = this.previousResults[this.filePath];
+    if (previousResults) {
+        var previousRootName = parent ? parent : node;
+        var needCheck = true; 
+
+        if (Array.isArray(previousRootName)) {
+            while (previousRootName.length > 1) {
+                var previousFirst = previousRootName.shift();
+                previousResults = previousResults[previousFirst];
+                if (!previousResults) {
+                    needCheck = false;
+                    break;
+                }
+            }
+            previousRootName = previousRootName.shift();
+        }
+
+        if (needCheck) {
+            var previousNode = previousResults[previousRootName];
+            if (previousNode) {
+                var previousNodeKind = previousNode[nodeKind];
+                if (previousNodeKind) {
+
+                    if (parent) {
+                        previousNode = previousNodeKind[node];
+                    }
+                    else {
+                        previousNode = previousNodeKind;
+                    }
+
+                    if (previousNode) {
+                        var previousCategory = previousNode[category];
+                        if (previousCategory) {
+                            var previousType = previousCategory[type];
+                            if (previousType) {
+                                // Early exit as it was already in the previous build.
+                                return;
+                            }    
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    // Write Error in output JSON.
+    var rootName = parent ? parent : node;
+    var current = results;
+    if (Array.isArray(rootName)) {
+        while (rootName.length > 1) {
+            var first = rootName.shift();
+            current = current[first] = current[first] || { };
+        }
+        rootName = rootName.shift();
+    }
+
+    current = current[rootName] = current[rootName] || { };
+    current = current[nodeKind] = current[nodeKind] || { };    
+    if (parent) {
+        current = current[node] = current[node] || { };
+    }
+    current = current[category] = current[category] || { };
+    current = current[type] = true;
+    
+    results.errors++;
+
+    if (!this.generateBaseLine) {
+        err(msg, position);
+    }
+}
+
+Validate.prototype.init = function (cb) {
+    var self = this;
+    if (!this.generateBaseLine && fs.existsSync(this.validationBaselineFileName)) {
+        fs.readFile(this.validationBaselineFileName, "utf-8", function (err, data) {
+            self.previousResults = JSON.parse(data);
+            cb();
+        });
+    }
+    else {
+        cb();
+    }
+}
+
+Validate.prototype.add = function (filePath, content) {
+    this.filePath = filePath && unixStylePath(filePath);
+
+    if (!Buffer.isBuffer(content)) {
+        content = new Buffer(content);
+    }
+
+    var contentString = content.toString();
+    var json = JSON.parse(contentString);
+
+    this.validateTypedoc(json);
+    this.results.errors += this.results[this.filePath].errors;
+}
+
+Validate.prototype.getResults = function () {
+    return this.results;
+}
+
+Validate.prototype.getContents = function () {
+    return Buffer.from(JSON.stringify(this.results));
+}
+
+/**
+ * Validate a TypeDoc JSON file
+ */
+Validate.prototype.validateTypedoc = function (json) {
+    var namespaces = json.children[0].children;
+    var namespace = null;
+
+    var containerNode;
+    var childNode;
+    var children;
+    var signatures;
+    var signatureNode;
+    var tags;
+    var isPublic;
+
+    // Check for BABYLON namespace
+    for (var child in namespaces) {
+        if (namespaces[child].name === this.namespaceName) {
+            namespace = namespaces[child];
+            break;
+        }
+    }
+
+    // Exit if not BABYLON related.
+    if (!namespace || !namespace.children) {
+        return;
+    }
+
+    // Validate Classes
+    for (var a in namespace.children) {
+        containerNode = namespace.children[a];
+
+        // If comment contains @ignore then skip validation completely
+        if (Validate.hasTag(containerNode, 'ignore')) continue;
+
+        // Account for undefined access modifiers.
+        if (!containerNode.flags.isPublic &&
+            !containerNode.flags.isPrivate &&
+            !containerNode.flags.isProtected) {
+                containerNode.flags.isPublic = true;
+        }
+        isPublic = containerNode.flags.isPublic;
+
+        // Validate naming.
+        this.validateNaming(null, containerNode);
+
+        // Validate Comments.
+        if (isPublic && !this.validateComment(containerNode)) {            
+            this.errorCallback(null,
+                containerNode.name,
+                containerNode.kindString,
+                "Comments",
+                "MissingText",
+                "Missing text for " + containerNode.kindString + " : " + containerNode.name + " (id: " + containerNode.id + ")", Validate.position(containerNode));
+        }
+
+        //if comment contains tag @ignoreChildren, then don't validate children
+        var validateChildren = !Validate.hasTag(containerNode, 'ignoreChildren');
+        children = containerNode.children;
+
+        //Validate Properties
+        if (validateChildren && children) {
+            for (var b in children) {
+                childNode = children[b];
+
+                // Account for undefined access modifiers.
+                if (!childNode.flags.isPublic &&
+                    !childNode.flags.isPrivate &&
+                    !childNode.flags.isProtected) {
+                    childNode.flags.isPublic = true;
+                }
+                isPublic = childNode.flags.isPublic;
+
+                // Validate Naming.
+                this.validateNaming(containerNode, childNode);
+
+                //if comment contains @ignore then skip validation completely
+                if (Validate.hasTag(childNode, 'ignore')) continue;                
+
+                if (isPublic) {
+                    tags = this.validateTags(childNode);
+                    if (tags) {
+                        this.errorCallback(containerNode.name,
+                            childNode.name,
+                            childNode.kindString,
+                            "Tags",
+                            tags,
+                            "Unrecognized tag " + tags + " at " + childNode.name + " (id: " + childNode.id + ") in " + containerNode.name + " (id: " + containerNode.id + ")", Validate.position(childNode));
+                    }
+                }
+
+                if (!this.validateComment(childNode)) {
+                    //Validate Signatures
+                    signatures = childNode.signatures;
+                    if (signatures) {
+                        for (var c in signatures) {
+                            signatureNode = signatures[c];
+
+                            //if node contains @ignore then skip validation completely
+                            if (Validate.hasTag(signatureNode, 'ignore')) continue;
+
+                            if (isPublic) {
+                                if (!this.validateComment(signatureNode)) {
+                                    this.errorCallback(containerNode.name,
+                                        signatureNode.name,
+                                        childNode.kindString,
+                                        "Comments",
+                                        "MissingText",
+                                        "Missing text for " + childNode.kindString + " : " + signatureNode.name + " (id: " + signatureNode.id + ") in " + containerNode.name + " (id: " + containerNode.id + ")", Validate.position(childNode));
+                                }
+
+                                tags = this.validateTags(signatureNode);
+                                if (tags) {
+                                    this.errorCallback(containerNode.name,
+                                        signatureNode.name,
+                                        childNode.kindString,
+                                        "Tags",
+                                        tags,
+                                        "Unrecognized tag " + tags + " at " + signatureNode.name + " (id: " + signatureNode.id + ") in " + containerNode.name + " (id: " + containerNode.id + ")", Validate.position(childNode));
+                                }
+
+                                if (signatureNode.type.name !== "void" && signatureNode.comment && !signatureNode.comment.returns) {
+                                    this.errorCallback(containerNode.name,
+                                        signatureNode.name,
+                                        childNode.kindString,
+                                        "Comments",
+                                        "MissingReturn",
+                                        "No Return Comment at " + signatureNode.name + " (id: " + signatureNode.id + ") in " + containerNode.name + " (id: " + containerNode.id + ")", Validate.position(childNode));
+                                }
+
+                                if (signatureNode.type.name === "void" && signatureNode.comment && signatureNode.comment.returns) {
+                                    this.errorCallback(containerNode.name,
+                                        signatureNode.name,
+                                        childNode.kindString,
+                                        "Comments",
+                                        "UselessReturn",
+                                        "No Return Comment Needed at " + signatureNode.name + " (id: " + signatureNode.id + ") in " + containerNode.name + " (id: " + containerNode.id + ")", Validate.position(childNode));
+                                }
+                            }
+
+                            this.validateParameters(containerNode, childNode, signatureNode, signatureNode.parameters, isPublic);
+                        }
+                    } else {
+                        this.errorCallback(containerNode.name,
+                            childNode.name,
+                            childNode.kindString,
+                            "Comments",
+                            "MissingText",
+                            "Missing text for " + childNode.kindString + " : " + childNode.name + " (id: " + childNode.id + ") in " + containerNode.name + " (id: " + containerNode.id + ")", Validate.position(childNode));
+                    }
+                }
+
+                // this.validateParameters(containerNode, childNode, childNode.parameters, isPublic);
+            }
+        }
+    }
+}
+
+/**
+ * Validate that tags are recognized
+ */
+Validate.prototype.validateTags = function(node) {
+    var tags;
+    var errorTags = [];
+
+    if (node.comment) {
+
+        tags = node.comment.tags;
+        if (tags) {
+            for (var i = 0; i < tags.length; i++) {
+                var tag = tags[i];
+                var validTags = ["constructor", "throw", "type", "deprecated", "example", "examples", "remark", "see", "remarks"]
+                if (validTags.indexOf(tag.tag) === -1) {
+                    errorTags.push(tag.tag);
+                }
+            }
+        }
+
+    }
+
+    return errorTags.join(",");
+}
+
+/**
+ * Validate that a JSON node has the correct TypeDoc comments
+ */
+Validate.prototype.validateComment = function(node) {
+
+    // Return-only methods are allowed to just have a @return tag
+    if ((node.kindString === "Call signature" || node.kindString === "Accessor") && !node.parameters && node.comment && node.comment.returns) {
+        return true;
+    }
+
+    // Return true for private properties (dont validate)
+    if ((node.kindString === "Property" || node.kindString === "Object literal") && (node.flags.isPrivate || node.flags.isProtected)) {
+        return true;
+    }
+
+    // Return true for inherited properties
+    if (node.inheritedFrom) {
+        return true;
+    }
+
+    if (node.comment) {
+
+        if (node.comment.text || node.comment.shortText) {
+            return true;
+        }
+
+        return false;
+    }
+
+    return false;
+}
+
+/**
+ * Validate comments for paramters on a node
+ */
+Validate.prototype.validateParameters = function(containerNode, method, signature, parameters, isPublic) {
+    var parametersNode;
+    for (var parameter in parameters) {
+        parametersNode = parameters[parameter];
+
+        if (isPublic && !this.validateComment(parametersNode)) {
+            // throw containerNode.name + " " + method.kindString + " " + method.name + " " + parametersNode.name + " " + parametersNode.kindString;
+            this.errorCallback([containerNode.name, method.kindString, signature.name],
+                parametersNode.name,
+                parametersNode.kindString,
+                "Comments",
+                "MissingText",
+                "Missing text for parameter " + parametersNode.name + " (id: " + parametersNode.id + ") of " + method.name + " (id: " + method.id + ")", Validate.position(method));
+        }
+
+        if (this.validateNamingConvention && !Validate.camelCase.test(parametersNode.name)) {
+            this.errorCallback([containerNode.name, method.kindString, signature.name],
+                parametersNode.name,
+                parametersNode.kindString,
+                "Naming",
+                "NotCamelCase",
+                "Parameter " + parametersNode.name + " should be Camel Case (id: " + method.id + ")", Validate.position(method));
+        }
+    }
+}
+
+/**
+ * Validate naming conventions of a node
+ */
+Validate.prototype.validateNaming = function(parent, node) {
+    if (!this.validateNamingConvention) {
+        return;
+    }
+
+    if (node.inheritedFrom) {
+        return;
+    }
+
+    // Internals are not subject to the public visibility policy.
+    if (node.name && node.name.length > 0 && node.name[0] === "_") {
+        return;
+    }
+
+    if ((node.flags.isPrivate || node.flags.isProtected) && node.flags.isStatic) {
+        if (!Validate.underscorePascalCase.test(node.name)) {
+            this.errorCallback(parent ? parent.name : null,
+                node.name,
+                node.kindString,
+                "Naming",
+                "NotUnderscorePascalCase",
+                node.name + " should be Underscore Pascal Case (id: " + node.id + ")", Validate.position(node));
+        }
+    }
+    else if (node.flags.isPrivate || node.flags.isProtected) {
+        if (!Validate.underscoreCamelCase.test(node.name)) {
+            this.errorCallback(parent ? parent.name : null,
+                node.name,
+                node.kindString,
+                "Naming",
+                "NotUnderscoreCamelCase",
+                node.name + " should be Underscore Camel Case (id: " + node.id + ")", Validate.position(node));
+        }
+    }
+    else if (node.flags.isStatic) {
+        if (!Validate.pascalCase.test(node.name)) {
+            this.errorCallback(parent ? parent.name : null,
+                node.name,
+                node.kindString,
+                "Naming",
+                "NotPascalCase",
+                node.name + " should be Pascal Case (id: " + node.id + ")", Validate.position(node));
+        }
+    }
+    else if (node.kindString == "Module") {
+        if (!Validate.upperCase.test(node.name)) {
+            this.errorCallback(parent ? parent.name : null,
+                node.name,
+                node.kindString,
+                "Naming",
+                "NotUpperCase",
+                "Module is not Upper Case " + node.name + " (id: " + node.id + ")", Validate.position(node));
+        }
+    }
+    else if (node.kindString == "Interface" ||
+        node.kindString == "Class" ||
+        node.kindString == "Enumeration" ||
+        node.kindString == "Enumeration member" ||
+        node.kindString == "Accessor" ||
+        node.kindString == "Type alias") {
+        if (!Validate.pascalCase.test(node.name)) {
+            this.errorCallback(parent ? parent.name : null,
+                node.name,
+                node.kindString,
+                "Naming",
+                "NotPascalCase",
+                node.name + " should be Pascal Case (id: " + node.id + ")", Validate.position(node));
+        }
+    }
+    else if (node.kindString == "Method" ||
+        node.kindString == "Property" ||
+        node.kindString == "Object literal") {
+
+        // Only warn here as special properties such as FOV may be better capitalized 
+        if (!Validate.camelCase.test(node.name)) {
+            this.errorCallback(parent ? parent.name : null,
+                node.name,
+                node.kindString,
+                "Naming",
+                "NotCamelCase",
+                node.name + " should be Camel Case (id: " + node.id + ")", Validate.position(node));
+        }
+    }
+    else if (node.kindString == "Variable") {
+        this.errorCallback(parent ? parent.name : null,
+            node.name,
+            node.kindString,
+            "Naming",
+            "ShouldNotBeLooseVariable",
+            node.name + " should not be a variable (id: " + node.id + ")", Validate.position(node));
+    }
+    else if (node.kindString === "Function") {
+        if (!Validate.camelCase.test(node.name)) {
+            this.errorCallback(parent ? parent.name : null,
+                node.name,
+                node.kindString,
+                "Naming",
+                "NotCamelCase",
+                node.name + " should be Camel Case (id: " + node.id + ")", Validate.position(node));
+        }
+    }
+    else if (node.kindString == "Constructor") {
+        // Do Nothing Here, this is handled through the class name.
+    }
+    else {
+        this.errorCallback(parent ? parent.name : null,
+            node.name,
+            node.kindString,
+            "Naming",
+            "UnknownNamingConvention",
+            "Unknown naming convention for " + node.kindString + " at " + node.name + " (id: " + node.id + ")", Validate.position(node));
+    }
+}
+
+// ______________________________________________ PLUGIN ____________________________________________
+
+// consts
+const PLUGIN_NAME = 'gulp-validateTypedoc';
+
+// plugin level function (dealing with files)
+function gulpValidateTypedoc(validationBaselineFileName, namespaceName, validateNamingConvention, generateBaseLine) {
+
+    if (!validationBaselineFileName) {
+        throw new PluginError(PLUGIN_NAME, 'Missing validation filename!');
+    }
+    if (typeof validationBaselineFileName !== "string") {
+        throw new PluginError(PLUGIN_NAME, 'Validation filename must be a string!');
+    }
+
+    var validate;
+    var latestFile;
+
+    function bufferContents(file, enc, cb) {
+        // ignore empty files
+        if (file.isNull()) {
+            cb();
+            return;
+        }
+
+        // we don't do streams (yet)
+        if (file.isStream()) {
+            this.emit('error', new Error('gulp-validatTypedoc: Streaming not supported'));
+            cb();
+            return;
+        }
+
+        // set latest file if not already set,
+        // or if the current file was modified more recently.
+        latestFile = file;
+
+        // What will happen once all set.
+        var done = function () {
+            // add file to concat instance
+            validate.add(file.relative, file.contents);
+
+            cb();
+        }
+
+        // Do the validation.
+        if (!validate) {
+            validate = new Validate(validationBaselineFileName, namespaceName, validateNamingConvention, generateBaseLine);
+            validate.init(done);
+        }
+        else {
+            done();
+        }
+    }
+
+    function endStream(cb) {
+        // no files passed in, no file goes out
+        if (!latestFile) {
+            var error = new PluginError(PLUGIN_NAME, 'gulp-validatTypedoc: No Baseline found.');
+            this.emit('error', error);
+            cb();
+            return;
+        }
+
+        var results = validate.getResults();
+        var buffer = Buffer.from(JSON.stringify(results, null, 2))
+
+        if (generateBaseLine) {
+            fs.writeFileSync(validationBaselineFileName, buffer || '');
+        }
+
+        var jsFile = new Vinyl({
+            cwd: process.cwd,
+            base: null,
+            path: validationBaselineFileName,
+            contents: buffer
+        });
+
+        this.push(jsFile);
+
+        var action = generateBaseLine ? "baseline generation" : "validation";
+        var error = function(message) {
+            generateBaseLine ? warn : err;
+            if (generateBaseLine) {
+                warn(message);
+            }
+            else {
+                err(message);
+                var error = new PluginError(PLUGIN_NAME, message);
+                this.emit('error', error);
+            }
+        }
+
+        if (results.errors > 1) {
+            var message = results.errors + " errors have been detected during the " + action + " !";
+            error(message);
+        }
+        else if (results.errors === 1) {
+            var message = "1 error has been detected during the " + action + " !";
+            error(message);
+        }
+        else {
+            var message = "All formatting check passed successfully during the " + action + " !";
+            success(message);
+        }
+
+        cb();
+    }
+
+    return through.obj(bufferContents, endStream);
+};
+
+// exporting the plugin main function
+module.exports = gulpValidateTypedoc;

+ 166 - 0
Tools/Publisher/index.js

@@ -0,0 +1,166 @@
+let prompt = require('prompt');
+let shelljs = require('shelljs');
+let fs = require('fs-extra');
+
+let basePath = '../../dist/preview release';
+
+// This can be changed when we have a new major release.
+let minimumDependency = '>=3.2.0-alpha';
+
+let packages = [
+    {
+        name: 'core',
+        path: '../../'
+    },
+    {
+        name: 'gui',
+        path: basePath + '/gui/'
+    },
+    {
+        name: 'materials',
+        path: basePath + '/materialsLibrary/'
+    },
+    {
+        name: 'postProcess',
+        path: basePath + '/postProcessesLibrary/'
+    },
+    {
+        name: 'loaders',
+        path: basePath + '/loaders/'
+    },
+    {
+        name: 'serializers',
+        path: basePath + '/serializers/'
+    },
+    {
+        name: 'proceduralTextures',
+        path: basePath + '/proceduralTexturesLibrary/'
+    },
+    {
+        name: 'inspector',
+        path: basePath + '/inspector/'
+    },
+    {
+        name: 'viewer',
+        path: basePath + '/viewer/'
+    }
+];
+
+//check if logged in
+console.log("Using npm user:");
+let loginCheck = shelljs.exec('npm whoami');
+console.log("Not that I can check, but - did you run gulp typescript-all?");
+if (loginCheck.code === 0) {
+    prompt.start();
+
+    prompt.get(['version'], function (err, result) {
+        let version = result.version;
+        packages.forEach((package) => {
+            if (package.name === "core") {
+                processCore(package, version);
+            } else {
+                let packageJson = require(package.path + 'package.json');
+                packageJson.version = version;
+                if (packageJson.peerDependencies) packageJson.peerDependencies.babylonjs = minimumDependency;
+                fs.writeFileSync(package.path + 'package.json', JSON.stringify(packageJson, null, 4));
+                console.log('Publishing ' + package.name + " from " + package.path);
+                //publish the respected package
+                shelljs.exec('npm publish \"' + package.path + "\"");
+            }
+
+        });
+        console.log("done, please don't forget to commit the changes")
+    });
+} else {
+    console.log('not logged in.');
+}
+
+function processCore(package, version) {
+    let packageJson = require(package.path + 'package.json');
+
+    // make a temporary directory
+    fs.ensureDirSync(basePath + '/package/');
+
+    let files = [
+        {
+            path: basePath + "/babylon.d.ts",
+            objectName: "babylon.d.ts"
+        },
+        {
+            path: basePath + "/babylon.js",
+            objectName: "babylon.js"
+        },
+        {
+            path: basePath + "/babylon.max.js",
+            objectName: "babylon.max.js"
+        },
+        {
+            path: basePath + "/babylon.worker.js",
+            objectName: "babylon.worker.js"
+        },
+        {
+            path: basePath + "/Oimo.js",
+            objectName: "Oimo.js"
+        },
+        {
+            path: package.path + "readme.md",
+            objectName: "readme.md"
+        }
+    ];
+
+    fs.readdirSync(basePath + '/modules/').forEach(object => {
+        console.log(object);
+        if (fs.statSync(basePath + '/modules/' + object).isDirectory) {
+            files.push({
+                path: basePath + '/modules/' + object,
+                objectName: object,
+                isDir: true
+            });
+        }
+    })
+
+    //copy them to the package path
+    files.forEach(file => {
+        fs.copySync(file.path, basePath + '/package/' + file.objectName);
+    });
+
+    // update package.json
+    packageJson.version = version;
+    console.log("generating file list");
+    let packageFiles = ["package.json"];
+    files.forEach(file => {
+        if (!file.isDir) {
+            packageFiles.push(file.objectName);
+        } else {
+            //todo is it better to read the content and add it? leave it like that ATM
+            packageFiles.push(file.objectName + "/index.js", file.objectName + "/index.d.ts", file.objectName + "/es6.js")
+        }
+    });
+    console.log("updating package.json");
+    packageJson.files = packageFiles;
+    packageJson.main = "babylon.max.js";
+    packageJson.typings = "babylon.d.ts";
+
+    fs.writeFileSync(basePath + '/package/' + 'package.json', JSON.stringify(packageJson, null, 4));
+
+    console.log('Publishing ' + package.name + " from " + basePath + '/package/');
+    //publish the respected package
+    shelljs.exec('npm publish \"' + basePath + '/package/' + "\"");
+
+    // remove package directory
+    //fs.removeSync(basePath + '/package/');
+
+    // now update the main package.json
+    packageJson.files = packageJson.files.map(file => {
+        if (file !== 'package.json' && file !== 'readme.md') {
+            return 'dist/preview release/' + file;
+        } else {
+            return file;
+        }
+    });
+    packageJson.main = "dist/preview release/babylon.max.js";
+    packageJson.typings = "dist/preview release/babylon.d.ts";
+
+    fs.writeFileSync(package.path + 'package.json', JSON.stringify(packageJson, null, 4));
+}
+

+ 16 - 0
Tools/Publisher/package.json

@@ -0,0 +1,16 @@
+{
+  "name": "banylonjs-publisher",
+  "version": "1.0.0",
+  "description": "Publishing babylon's packages automatically",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "Raanan Weber",
+  "license": "ISC",
+  "dependencies": {
+    "fs-extra": "^5.0.0",
+    "prompt": "^1.0.0",
+    "shelljs": "^0.7.8"
+  }
+}

+ 35 - 0
Viewer/README.md

@@ -0,0 +1,35 @@
+# BabylonJS Viewer
+
+This project is a 3d model viewer using babylonjs.
+
+Please note that this is an *initial release*. The API and project structure could (and probably SHOULD) be changed, so please don't rely on this yet in a productive environment.
+
+The viewer is using the latest Babylon from npm (3.1 alpha).
+
+This documentation is also not full. I will slowly add more and more exmplanations.
+
+## Basic usage
+
+See `basicExample.html` in `/dist`.
+
+Basically, all that is needed is an html tag, and the viewer.js, which includes everything needed to render a Scene:
+
+```html
+<babylon model="https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/BoomBox/glTF/BoomBox.gltf" default-viewer="true"></babylon>
+<script src="viewer.js"></script>
+``` 
+
+This will create a (default) viewer and will load the model in this URL using the gltf loader.
+
+The `babylon` tag will be automatically initialized. 
+
+## Configuration
+
+Configuration can be provided using html attributes or a JSON (at the moment). A configuration Mapper can be registered to create new configuration readers. 
+
+Before I finish a full documentation, take a look at `configuration.ts`
+
+## Templating
+
+The default templates are integrated in the viewer.js file. The current templates are located in `/assets/templates/default/` . Those templates can be extended and registered using the configuration file.
+

+ 97 - 0
Viewer/assets/deepmerge.min.js

@@ -0,0 +1,97 @@
+(function (global, factory) {
+	typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+	typeof define === 'function' && define.amd ? define(factory) :
+	(global.deepmerge = factory());
+}(this, (function () { 'use strict';
+
+var isMergeableObject = function isMergeableObject(value) {
+	return isNonNullObject(value)
+		&& !isSpecial(value)
+};
+
+function isNonNullObject(value) {
+	return !!value && typeof value === 'object'
+}
+
+function isSpecial(value) {
+	var stringValue = Object.prototype.toString.call(value);
+
+	return stringValue === '[object RegExp]'
+		|| stringValue === '[object Date]'
+		|| isReactElement(value)
+}
+
+// see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25
+var canUseSymbol = typeof Symbol === 'function' && Symbol.for;
+var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7;
+
+function isReactElement(value) {
+	return value.$$typeof === REACT_ELEMENT_TYPE
+}
+
+function emptyTarget(val) {
+	return Array.isArray(val) ? [] : {}
+}
+
+function cloneUnlessOtherwiseSpecified(value, optionsArgument) {
+	var clone = !optionsArgument || optionsArgument.clone !== false;
+
+	return (clone && isMergeableObject(value))
+		? deepmerge(emptyTarget(value), value, optionsArgument)
+		: value
+}
+
+function defaultArrayMerge(target, source, optionsArgument) {
+	return target.concat(source).map(function(element) {
+		return cloneUnlessOtherwiseSpecified(element, optionsArgument)
+	})
+}
+
+function mergeObject(target, source, optionsArgument) {
+	var destination = {};
+	if (isMergeableObject(target)) {
+		Object.keys(target).forEach(function(key) {
+			destination[key] = cloneUnlessOtherwiseSpecified(target[key], optionsArgument);
+		});
+	}
+	Object.keys(source).forEach(function(key) {
+		if (!isMergeableObject(source[key]) || !target[key]) {
+			destination[key] = cloneUnlessOtherwiseSpecified(source[key], optionsArgument);
+		} else {
+			destination[key] = deepmerge(target[key], source[key], optionsArgument);
+		}
+	});
+	return destination
+}
+
+function deepmerge(target, source, optionsArgument) {
+	var sourceIsArray = Array.isArray(source);
+	var targetIsArray = Array.isArray(target);
+	var options = optionsArgument || { arrayMerge: defaultArrayMerge };
+	var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray;
+
+	if (!sourceAndTargetTypesMatch) {
+		return cloneUnlessOtherwiseSpecified(source, optionsArgument)
+	} else if (sourceIsArray) {
+		var arrayMerge = options.arrayMerge || defaultArrayMerge;
+		return arrayMerge(target, source, optionsArgument)
+	} else {
+		return mergeObject(target, source, optionsArgument)
+	}
+}
+
+deepmerge.all = function deepmergeAll(array, optionsArgument) {
+	if (!Array.isArray(array)) {
+		throw new Error('first argument should be an array')
+	}
+
+	return array.reduce(function(prev, next) {
+		return deepmerge(prev, next, optionsArgument)
+	}, {})
+};
+
+var deepmerge_1 = deepmerge;
+
+return deepmerge_1;
+
+})));

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
Viewer/assets/es6-promise.min.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 29 - 0
Viewer/assets/handlebars.min.js


BIN
Viewer/assets/img/close.png


BIN
Viewer/assets/img/fullscreen.png


BIN
Viewer/assets/img/help-circle.png


BIN
Viewer/assets/img/loading.png


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 210 - 0
Viewer/assets/pep.min.js


+ 3 - 0
Viewer/assets/templates/default/defaultTemplate.html

@@ -0,0 +1,3 @@
+<viewer></viewer>
+<loading-screen></loading-screen>
+<overlay></overlay>

+ 26 - 0
Viewer/assets/templates/default/defaultViewer.html

@@ -0,0 +1,26 @@
+<style>
+    viewer {
+        position: relative;
+        overflow: hidden;
+        /* Start stage */
+        flex: 1;
+        z-index: 1;
+        justify-content: center;
+        align-items: center;
+
+        width: 100%;
+        height: 100%;
+    }
+
+    .babylonjs-canvas {
+        flex: 1;
+        width: 100%;
+        height: 100%;
+        touch-action: none;
+    }
+</style>
+
+<canvas class="babylonjs-canvas" id="{{canvasId}}">
+</canvas>
+
+<nav-bar></nav-bar>

+ 1 - 0
Viewer/assets/templates/default/error.html

@@ -0,0 +1 @@
+Error loading the model

+ 1 - 0
Viewer/assets/templates/default/help.html

@@ -0,0 +1 @@
+HELP

+ 42 - 0
Viewer/assets/templates/default/loadingScreen.html

@@ -0,0 +1,42 @@
+<style>
+    /* Loading Screen element */
+
+    loading-screen {
+        position: absolute;
+        left: 0;
+        z-index: 100;
+        opacity: 1;
+        pointer-events: none;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        -webkit-transition: opacity 2s ease;
+        -moz-transition: opacity 2s ease;
+        transition: opacity 2s ease;
+    }
+
+    img.loading-image {
+        -webkit-animation: spin 2s linear infinite;
+        animation: spin 2s linear infinite;
+    }
+
+    @-webkit-keyframes spin {
+        0% {
+            -webkit-transform: rotate(0deg);
+        }
+        100% {
+            -webkit-transform: rotate(360deg);
+        }
+    }
+
+    @keyframes spin {
+        0% {
+            transform: rotate(0deg);
+        }
+        100% {
+            transform: rotate(360deg);
+        }
+    }
+</style>
+
+<img class="loading-image" src="{{loadingImage}}">

+ 112 - 0
Viewer/assets/templates/default/navbar.html

@@ -0,0 +1,112 @@
+<style>
+    nav-bar {
+        position: absolute;
+        height: 160px;
+        width: 100%;
+        bottom: 0;
+        background-color: rgba(0, 0, 0, 0.3);
+        color: white;
+        transition: 1s;
+        align-items: flex-start;
+        justify-content: space-around;
+        display: flex;
+
+        flex-direction: column;
+    }
+
+    /* Big screens have room for the entire navbar */
+
+    @media screen and (min-width: 768px) {
+        nav-bar {
+            align-items: center;
+            flex-direction: row;
+            justify-content: space-between;
+            height: 80px;
+        }
+    }
+
+    div.flex-container {
+        display: flex;
+        width: 100%;
+    }
+
+    div.thumbnail {
+        position: relative;
+        overflow: hidden;
+        display: block;
+        width: 40px;
+        height: 40px;
+        background-size: cover;
+        background-position: center;
+        border-radius: 20px;
+        margin: 0 10px;
+    }
+
+    div.title-container {
+        flex-direction: column;
+        display: flex;
+        justify-content: space-between;
+    }
+
+    span.model-title {
+        font-size: 125%;
+    }
+
+    span.model-subtitle {
+        font-size: 90%;
+    }
+
+    div.button-container {
+        align-items: center;
+        justify-content: flex-end;
+    }
+
+    div.button {
+        cursor: pointer;
+        height: 30px;
+        margin: 0 10px;
+    }
+
+    div.button img {
+        height: 100%;
+    }
+</style>
+
+{{#if disableOnFullscreen}}
+<style>
+    viewer:fullscreen nav-bar {
+        display: none;
+    }
+
+    viewer:-moz-full-screen nav-bar {
+        display: none;
+    }
+
+    viewer:-webkit-full-screen nav-bar {
+        display: none;
+    }
+</style>
+{{/if}}
+
+<div class="flex-container" id="model-metadata">
+    <!-- holding the description -->
+    <div class="thumbnail">
+        <!-- holding the thumbnail 
+        <img src="{{thumbnail}}" alt="{{title}}">-->
+    </div>
+    <div class="title-container">
+
+        <span class="model-title">{{#if title}}{{title}}{{/if}}</span>
+        <span class="model-subtitle"> {{#if subtitle}}{{subtitle}} {{/if}}</span>
+    </div>
+</div>
+<div class="button-container flex-container">
+    <!-- holding the buttons -->
+    {{#eachInMap buttons}}
+    <div id="{{id}}" class="button">
+        {{#if text}}
+        <span>{{text}}</span>> {{/if}} {{#if image}}
+        <img src="{{image}}" alt="{{altText}}"> {{/if}}
+    </div>
+    {{/eachInMap}}
+</div>

+ 46 - 0
Viewer/assets/templates/default/overlay.html

@@ -0,0 +1,46 @@
+<style>
+    overlay {
+        position: absolute;
+        z-index: 99;
+        opacity: 0;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        -webkit-transition: opacity 1s ease;
+        -moz-transition: opacity 1s ease;
+        transition: opacity 1s ease;
+    }
+
+    .overlay-item {
+        width: 100%;
+        height: 100%;
+        display: none;
+        align-items: center;
+        justify-content: center;
+        background-color: rgba(121, 121, 121, 0.3);
+    }
+
+    error.overlay-item {
+        background-color: rgba(121, 121, 121, 1);
+    }
+
+    div#close-button {
+        position: absolute;
+        top: 10px;
+        right: 10px;
+        width: 30px;
+        height: 30px;
+        cursor: pointer;
+    }
+
+    div#close-button img {
+        width: 100%;
+    }
+</style>
+
+<div id="close-button">
+    <img src="{{closeImage}}" alt="{{closeText}}">
+</div>
+<help class="overlay-item"></help>
+<error class="overlay-item"></error>
+<share class="overlay-item"></share>

+ 1 - 0
Viewer/assets/templates/default/share.html

@@ -0,0 +1 @@
+SHARE

+ 34 - 0
Viewer/dist/basicExample.html

@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html lang="en">
+
+    <head>
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta http-equiv="X-UA-Compatible" content="ie=edge">
+        <title>BabylonJS Viewer - Basic usage</title>
+        <style>
+            babylon {
+                max-width: 800px;
+                max-height: 500px;
+                width: 100%;
+                height: 600px;
+            }
+        </style>
+    </head>
+
+    <body>
+        <babylon model.title="Damaged Helmet" model.subtitle="BabylonJS" model.thumbnail="https://www.babylonjs.com/img/favicon/apple-icon-144x144.png"
+            model.url="https://www.babylonjs.com/Assets/DamagedHelmet/glTF/DamagedHelmet.gltf" camera.behaviors.auto-rotate="0" templates.nav-bar.params.disable-on-fullscreen="true"></babylon>
+        <script src="viewer.js"></script>
+        <script>
+            // The following lines are redundant. 
+            // They are only here to show how you could achive the tag initialization on your own.
+
+            // a simple way of disabling auto init 
+            BabylonViewer.disableInit = true;
+            // Initializing the viewer on specific HTML tags.
+            BabylonViewer.InitTags('babylon');
+        </script>
+    </body>
+
+</html>

+ 39 - 0
Viewer/dist/domExample.html

@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html lang="en">
+
+    <head>
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta http-equiv="X-UA-Compatible" content="ie=edge">
+        <title>BabylonJS Viewer - DOM usage</title>
+        <style>
+            babylon {
+                width: 800px;
+                height: 500px;
+            }
+        </style>
+    </head>
+
+    <body>
+        <babylon extends="minimal" scene.default-camera="false">
+            <model url="https://playground.babylonjs.com/scenes/BoomBox.glb" title="GLB Model" subtitle="BabylonJS">
+            </model>
+            <camera>
+                <behaviors>
+                    <auto-rotate type="0"></auto-rotate>
+                </behaviors>
+            </camera>
+            <lights>
+                <light1 type="1" shadow-enabled="true" position.y="0.5" direction.y="-1" intensity="4.5">
+                    <shadow-config use-blur-exponential-shadow-map="true" use-kernel-blur="true" blur-kernel="64" blur-scale="4">
+                    </shadow-config>
+                </light1>
+            </lights>
+        </babylon>
+        <script src="viewer.js"></script>
+    </body>
+
+</html>
+<html>
+
+</html>

BIN
Viewer/dist/environment.dds


+ 67 - 0
Viewer/dist/eventsExample.html

@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html lang="en">
+
+    <head>
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta http-equiv="X-UA-Compatible" content="ie=edge">
+        <title>BabylonJS Viewer - Basic usage</title>
+        <style>
+            babylon {
+                max-width: 800px;
+                max-height: 500px;
+                width: 100%;
+                height: 600px;
+            }
+        </style>
+    </head>
+
+    <body>
+        <babylon id="babylon-viewer" model.title="Amazing Rabbit" model.subtitle="BabylonJS" model.thumbnail="https://www.babylonjs.com/img/favicon/apple-icon-144x144.png"
+            model.url="https://playground.babylonjs.com/scenes/Rabbit.babylon" observers.on-scene-init="globalSceneInitCallback"></babylon>
+        <script src="viewer.js"></script>
+        <script>
+            //get by id ONLY after viewer init
+            var willNotWork = BabylonViewer.viewerManager.getViewerById('babylon-viewer');
+            console.log('viewer not yet initialized');
+
+            // Pomise-based API:
+            BabylonViewer.viewerManager.getViewerPromiseById('babylon-viewer').then(function (viewer) {
+                // this will resolve only after the viewer with this specific ID is initialized
+                console.log('Using promises: ', 'viewer - ' + viewer.getBaseId());
+
+                viewerObservables(viewer);
+            });
+
+            // call back variant:
+            BabylonViewer.viewerManager.onViewerAdded = function (viewer) {
+                console.log('Using viewerManager.onViewerAdded: ', 'viewer - ' + viewer.getBaseId());
+            }
+
+            // using observers:
+            BabylonViewer.viewerManager.onViewerAddedObservable.add(function (viewer) {
+                console.log('Using viewerManager.onViewerAddedObservable: ', 'viewer - ' + viewer.getBaseId());
+            });
+
+            function viewerObservables(viewer) {
+                viewer.onEngineInitObservable.add(function (engine) {
+                    console.log('Engine initialized');
+                });
+
+                viewer.onSceneInitObservable.add(function (scene) {
+                    console.log('Scene initialized');
+                });
+
+                viewer.onModelLoadedObservable.add(function (meshes) {
+                    console.log('Model loaded');
+                });
+            }
+
+            function globalSceneInitCallback(scene) {
+                console.log('scene-init function defined in the configuration');
+            }
+
+        </script>
+    </body>
+
+</html>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 109021 - 0
Viewer/dist/viewer.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
Viewer/dist/viewer.min.js


+ 43 - 0
Viewer/package.json

@@ -0,0 +1,43 @@
+{
+    "name": "babylonjs-viewer",
+    "version": "0.2.0",
+    "description": "A viewer using BabylonJS to display 3D elements natively",
+    "scripts": {
+        "start:server": "webpack-dev-server",
+        "build": "webpack",
+        "test": "echo \"Error: no test specified\" && exit 1"
+    },
+    "repository": {
+        "type": "git",
+        "url": "git+https://github.com/BabylonJS/Babylon.js.git"
+    },
+    "keywords": [
+        "3d",
+        "webgl",
+        "viewer"
+    ],
+    "author": "Raanan Weber",
+    "license": "Apache2",
+    "bugs": {
+        "url": "https://github.com/BabylonJS/Babylon.js/issues"
+    },
+    "homepage": "https://github.com/BabylonJS/Babylon.js#readme",
+    "devDependencies": {
+        "@types/node": "^8.0.53",
+        "base64-image-loader": "^1.2.0",
+        "html-loader": "^0.5.1",
+        "json-loader": "^0.5.7",
+        "ts-loader": "^2.3.7",
+        "typescript": "^2.6.2",
+        "uglifyjs-webpack-plugin": "^1.1.1",
+        "webpack": "^3.8.1",
+        "webpack-dev-server": "^2.9.5"
+    },
+    "dependencies": {
+        "babylonjs": "^3.1.0-beta6",
+        "babylonjs-loaders": "^3.1.0-beta6",
+        "deepmerge": "^2.0.1",
+        "es6-promise": "^4.1.1",
+        "handlebars": "^4.0.11"
+    }
+}

+ 186 - 0
Viewer/src/configuration/configuration.ts

@@ -0,0 +1,186 @@
+import { ITemplateConfiguration } from './../templateManager';
+
+export interface ViewerConfiguration {
+
+    // configuration version
+    version?: string;
+    extends?: string; // is this configuration extending an existing configuration?
+
+    pageUrl?: string; // will be used for sharing and other fun stuff. This is the page showing the model (not the model's url!)
+
+    configuration?: string | {
+        url: string;
+        mapper?: string; // json (default), html, yaml, xml, etc'. if not provided, file extension will be used.
+    };
+
+    // Deprecated
+    /*// native (!!!) javascript events. Mainly used in the JSON-format.
+    // those events will be triggered by the container element (the <babylon> tag);
+    events?: {
+        load: boolean | string;
+        init: boolean | string;
+        meshselected: boolean | string;
+        pointerdown: boolean | string;
+        pointerup: boolean | string;
+        pointermove: boolean | string;
+        // load: 'onViewerLoaded' // will trigger the event prefix-onViewerLoaded instead of prefix-onLoad (and ONLY this event).
+    } | boolean; //events: true - fire all events*/
+    //eventPrefix?: string;
+
+    // names of functions in the window context.
+    observers?: {
+        onEngineInit?: string;
+        onSceneInit?: string;
+        onModelLoaded?: string;
+    }
+
+    canvasElement?: string; // if there is a need to override the standard implementation - ID of HTMLCanvasElement
+
+    model?: {
+        url?: string;
+        loader?: string; // obj, gltf?
+        position?: { x: number, y: number, z: number };
+        rotation?: { x: number, y: number, z: number, w: number };
+        scaling?: { x: number, y: number, z: number };
+        parentObjectIndex?: number; // the index of the parent object of the model in the loaded meshes array.
+
+        title: string;
+        subtitle?: string;
+        thumbnail?: string; // URL or data-url
+
+        [propName: string]: any; // further configuration, like title and creator
+    } | string;
+
+    scene?: {
+        debug?: boolean;
+        autoRotate?: boolean;
+        rotationSpeed?: number;
+        defaultCamera?: boolean;
+        defaultLight?: boolean;
+        clearColor?: { r: number, g: number, b: number, a: number };
+        imageProcessingConfiguration?: IImageProcessingConfiguration;
+    },
+    // at the moment, support only a single camera.
+    camera?: {
+        position?: { x: number, y: number, z: number };
+        rotation?: { x: number, y: number, z: number, w: number };
+        fov?: number;
+        fovMode?: number;
+        minZ?: number;
+        maxZ?: number;
+        inertia?: number;
+        behaviors?: {
+            [name: string]: number | {
+                type: number;
+                [propName: string]: any;
+            };
+        };
+
+        [propName: string]: any;
+    },
+    skybox?: {
+        cubeTexture: {
+            noMipMap?: boolean;
+            gammaSpace?: boolean;
+            url: string | Array<string>;
+        };
+        pbr?: boolean;
+        scale?: number;
+        blur?: number;
+        material?: {
+            imageProcessingConfiguration?: IImageProcessingConfiguration;
+        };
+        infiniteDIstance?: boolean;
+
+    };
+
+    ground?: boolean | {
+        size?: number;
+        receiveShadows?: boolean;
+        shadowOnly?: boolean;
+        mirror?: boolean;
+        material?: {
+            [propName: string]: any;
+        }
+    };
+    lights?: {
+        [name: string]: {
+            type: number;
+            name?: string;
+            disabled?: boolean;
+            position?: { x: number, y: number, z: number };
+            target?: { x: number, y: number, z: number };
+            direction?: { x: number, y: number, z: number };
+            diffuse?: { r: number, g: number, b: number };
+            specular?: { r: number, g: number, b: number };
+            intensity?: number;
+            radius?: number;
+            shadownEnabled?: boolean; // only on specific lights!
+            shadowConfig?: {
+                useBlurExponentialShadowMap?: boolean;
+                useKernelBlur?: boolean;
+                blurKernel?: number;
+                blurScale?: number;
+                [propName: string]: any;
+            }
+            [propName: string]: any;
+
+            // no behaviors for light at the moment, but allowing configuration for future reference.
+            behaviors?: {
+                [name: string]: number | {
+                    type: number;
+                    [propName: string]: any;
+                };
+            };
+        }
+    },
+    // engine configuration. optional!
+    engine?: {
+        antialiasing?: boolean;
+    },
+    //templateStructure?: ITemplateStructure,
+    templates?: {
+        main: ITemplateConfiguration,
+        [key: string]: ITemplateConfiguration
+    };
+    // nodes?
+}
+
+export interface IImageProcessingConfiguration {
+    colorGradingEnabled?: boolean;
+    colorCurvesEnabled?: boolean;
+    colorCurves?: {
+        globalHue?: number;
+        globalDensity?: number;
+        globalSaturation?: number;
+        globalExposure?: number;
+        highlightsHue?: number;
+        highlightsDensity?: number;
+        highlightsSaturation?: number;
+        highlightsExposure?: number;
+        midtonesHue?: number;
+        midtonesDensity?: number;
+        midtonesSaturation?: number;
+        midtonesExposure?: number;
+        shadowsHue?: number;
+        shadowsDensity?: number;
+        shadowsSaturation?: number;
+        shadowsExposure?: number;
+    };
+    colorGradingWithGreenDepth?: boolean;
+    colorGradingBGR?: boolean;
+    exposure?: number;
+    toneMappingEnabled?: boolean;
+    contrast?: number;
+    vignetteEnabled?: boolean;
+    vignetteStretch?: number;
+    vignetteCentreX?: number;
+    vignetteCentreY?: number;
+    vignetteWeight?: number;
+    vignetteColor?: { r: number, g: number, b: number, a?: number };
+    vignetteCameraFov?: number;
+    vignetteBlendMode?: number;
+    vignetteM?: boolean;
+    applyByPostProcess?: boolean;
+
+}

+ 0 - 0
Viewer/src/configuration/index.ts


+ 79 - 0
Viewer/src/configuration/loader.ts

@@ -0,0 +1,79 @@
+import { mapperManager } from './mappers';
+import { ViewerConfiguration } from './configuration';
+import { getConfigurationType } from './types';
+
+import * as deepmerge from '../../assets/deepmerge.min.js';
+
+export class ConfigurationLoader {
+
+    private configurationCache: { (url: string): any };
+
+    public loadConfiguration(initConfig: ViewerConfiguration = {}): Promise<ViewerConfiguration> {
+
+        let loadedConfig = deepmerge({}, initConfig);
+
+        let extendedConfiguration = getConfigurationType(loadedConfig && loadedConfig.extends);
+
+        loadedConfig = deepmerge(extendedConfiguration, loadedConfig);
+
+        if (loadedConfig.configuration) {
+
+            let mapperType = "json";
+            let url = loadedConfig.configuration;
+
+            // if configuration is an object
+            if (loadedConfig.configuration.url) {
+                url = loadedConfig.configuration.url;
+                mapperType = loadedConfig.configuration.mapper;
+                if (!mapperType) {
+                    // load mapper type from filename / url
+                    mapperType = loadedConfig.configuration.url.split('.').pop();
+                }
+            }
+
+            let mapper = mapperManager.getMapper(mapperType);
+            return this.loadFile(url).then((data: any) => {
+                let parsed = mapper.map(data);
+                return deepmerge(loadedConfig, parsed);
+            });
+        } else {
+            return Promise.resolve(loadedConfig);
+        }
+    }
+
+    public getConfigurationType(type: string) {
+
+    }
+
+    private loadFile(url: string): Promise<any> {
+        let cacheReference = this.configurationCache;
+        if (cacheReference[url]) {
+            return Promise.resolve(cacheReference[url]);
+        }
+
+        return new Promise(function (resolve, reject) {
+            var xhr = new XMLHttpRequest();
+            xhr.open('GET', url);
+            xhr.send();
+            xhr.onreadystatechange = function () {
+                var DONE = 4;
+                var OK = 200;
+                if (xhr.readyState === DONE) {
+                    if (xhr.status === OK) {
+                        cacheReference[url] = xhr.responseText;
+                        resolve(xhr.responseText); // 'This is the returned text.'
+                    }
+                } else {
+                    console.log('Error: ' + xhr.status, url);
+                    reject('Error: ' + xhr.status); // An error occurred during the request.
+                }
+            }
+        });
+    }
+
+
+}
+
+export let configurationLoader = new ConfigurationLoader();
+
+export default configurationLoader;

+ 121 - 0
Viewer/src/configuration/mappers.ts

@@ -0,0 +1,121 @@
+import { Tools } from 'babylonjs';
+import { ViewerConfiguration } from './configuration';
+
+import { kebabToCamel } from '../helper';
+
+export interface IMapper {
+    map(rawSource: any): ViewerConfiguration;
+}
+
+class HTMLMapper implements IMapper {
+
+    map(element: HTMLElement): ViewerConfiguration {
+
+        let config = {};
+        for (let attrIdx = 0; attrIdx < element.attributes.length; ++attrIdx) {
+            let attr = element.attributes.item(attrIdx);
+            // map "object.property" to the right configuration place.
+            let split = attr.nodeName.split('.');
+            split.reduce((currentConfig, key, idx) => {
+                //convert html-style to json-style
+                let camelKey = kebabToCamel(key);
+                if (idx === split.length - 1) {
+                    let val: any = attr.nodeValue; // firefox warns nodeValue is deprecated, but I found no sign of it anywhere.
+                    if (val === "true") {
+                        val = true;
+                    } else if (val === "false") {
+                        val = false;
+                    } else {
+                        let number = parseFloat(val);
+                        if (!isNaN(number)) {
+                            val = number;
+                        }
+                    }
+                    currentConfig[camelKey] = val;
+                } else {
+                    currentConfig[camelKey] = currentConfig[camelKey] || {};
+                }
+                return currentConfig[camelKey];
+            }, config);
+        }
+
+        return config;
+    }
+}
+
+class JSONMapper implements IMapper {
+    map(rawSource: any) {
+        return JSON.parse(rawSource);
+    }
+}
+
+// TODO - Dom configuration mapper.
+class DOMMapper implements IMapper {
+
+    map(baseElement: HTMLElement): ViewerConfiguration {
+        let htmlMapper = new HTMLMapper();
+        let config = htmlMapper.map(baseElement);
+
+        let traverseChildren = function (element: HTMLElement, partConfig) {
+            let children = element.children;
+            if (children.length) {
+                for (let i = 0; i < children.length; ++i) {
+                    let item = <HTMLElement>children.item(i);
+                    let configMapped = htmlMapper.map(item);
+                    let key = kebabToCamel(item.nodeName.toLowerCase());
+                    if (item.attributes.getNamedItem('array') && item.attributes.getNamedItem('array').nodeValue === 'true') {
+                        partConfig[key] = [];
+                    } else {
+                        if (element.attributes.getNamedItem('array') && element.attributes.getNamedItem('array').nodeValue === 'true') {
+                            partConfig.push(configMapped)
+                        } else if (partConfig[key]) {
+                            //exists already! problem... probably an array
+                            element.setAttribute('array', 'true');
+                            let oldItem = partConfig[key];
+                            partConfig = [oldItem, configMapped]
+                        } else {
+                            partConfig[key] = configMapped;
+                        }
+                    }
+                    traverseChildren(item, partConfig[key] || configMapped);
+                }
+            }
+            return partConfig;
+        }
+
+        traverseChildren(baseElement, config);
+
+
+        return config;
+    }
+
+}
+
+export class MapperManager {
+
+    private mappers: { [key: string]: IMapper };
+    public static DefaultMapper = 'json';
+
+    constructor() {
+        this.mappers = {
+            "html": new HTMLMapper(),
+            "json": new JSONMapper(),
+            "dom": new DOMMapper()
+        }
+    }
+
+    public getMapper(type: string) {
+        if (!this.mappers[type]) {
+            Tools.Error("No mapper defined for " + type);
+        }
+        return this.mappers[type] || this.mappers[MapperManager.DefaultMapper];
+    }
+
+    public registerMapper(type: string, mapper: IMapper) {
+        this.mappers[type] = mapper;
+    }
+
+}
+
+export let mapperManager = new MapperManager();
+export default mapperManager;

+ 122 - 0
Viewer/src/configuration/types/default.ts

@@ -0,0 +1,122 @@
+import { ViewerConfiguration } from './../configuration';
+
+export let defaultConfiguration: ViewerConfiguration = {
+    version: "0.1",
+    //eventPrefix: 'babylonviewer-',
+    //events: true,
+    templates: {
+        main: {
+            html: require("../../../assets/templates/default/defaultTemplate.html")
+        },
+        loadingScreen: {
+            html: require("../../../assets/templates/default/loadingScreen.html"),
+            params: {
+                backgroundColor: "#000000",
+                loadingImage: require('../../../assets/img/loading.png')
+            }
+        },
+        viewer: {
+            html: require("../../../assets/templates/default/defaultViewer.html"),
+        },
+        navBar: {
+            html: require("../../../assets/templates/default/navbar.html"),
+            params: {
+                buttons: {
+                    /*"help-button": {
+                        altText: "Help",
+                        image: require('../../../assets/img/help-circle.png')
+                    },*/
+                    "fullscreen-button": {
+                        altText: "Fullscreen",
+                        image: require('../../../assets/img/fullscreen.png')
+                    }
+                },
+                visibilityTimeout: 2000
+            },
+            events: {
+                pointerdown: { 'fullscreen-button': true/*, '#help-button': true*/ }
+            }
+        },
+        overlay: {
+            html: require("../../../assets/templates/default/overlay.html"),
+            params: {
+                closeImage: require('../../../assets/img/close.png'),
+                closeText: 'Close'
+            }
+        },
+        help: {
+            html: require("../../../assets/templates/default/help.html")
+        },
+        share: {
+            html: require("../../../assets/templates/default/share.html")
+        },
+        error: {
+            html: require("../../../assets/templates/default/error.html")
+        }
+
+    },
+    camera: {
+        behaviors: {
+            autoRotate: 0,
+            framing: {
+                type: 2,
+                zoomOnBoundingInfo: true,
+                zoomStopsAnimation: false
+            }
+        }
+    },
+    /*lights: [
+        {
+            type: 1,
+            shadowEnabled: true,
+            direction: { x: -0.2, y: -1, z: 0 },
+            position: { x: 0.017, y: 50, z: 0 },
+            intensity: 4.5,
+            shadowConfig: {
+                useBlurExponentialShadowMap: true,
+                useKernelBlur: true,
+                blurKernel: 64,
+                blurScale: 4
+            }
+        }
+    ],*/
+    skybox: {
+        cubeTexture: {
+            url: 'https://playground.babylonjs.com/textures/environment.dds',
+            gammaSpace: false
+        },
+        pbr: true,
+        blur: 0.7,
+        infiniteDIstance: false,
+        material: {
+            imageProcessingConfiguration: {
+                colorCurves: {
+                    globalDensity: 89,
+                    globalHue: 58.88,
+                    globalSaturation: 94
+                },
+                colorCurvesEnabled: true,
+                exposure: 1.5,
+                contrast: 1.66,
+                toneMappingEnabled: true,
+                vignetteEnabled: true,
+                vignetteWeight: 5,
+                vignetteColor: { r: 0.8, g: 0.6, b: 0.4 },
+                vignetteM: true
+            }
+        }
+    },
+    ground: true,
+    engine: {
+        antialiasing: true
+    },
+    scene: {
+        imageProcessingConfiguration: {
+            exposure: 1.4,
+            contrast: 1.66,
+            toneMappingEnabled: true
+        }
+        //autoRotate: true,
+        //rotationSpeed: 0.1
+    }
+}

+ 16 - 0
Viewer/src/configuration/types/index.ts

@@ -0,0 +1,16 @@
+import { minimalConfiguration } from './minimal';
+import { defaultConfiguration } from './default';
+
+let getConfigurationType = function (type: string) {
+    switch (type) {
+        case 'default':
+            return defaultConfiguration;
+        case 'minimal':
+            return minimalConfiguration;
+        default:
+            return defaultConfiguration;
+    }
+
+}
+
+export { getConfigurationType, defaultConfiguration, minimalConfiguration }

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 34 - 0
Viewer/src/configuration/types/minimal.ts


+ 41 - 0
Viewer/src/helper.ts

@@ -0,0 +1,41 @@
+export function isUrl(urlToCheck: string): boolean {
+    if (urlToCheck.indexOf('http') === 0 || urlToCheck.indexOf('/') === 0 || urlToCheck.indexOf('./') === 0 || urlToCheck.indexOf('../') === 0) {
+        return true;
+    }
+    return false;
+}
+
+export function loadFile(url: string): Promise<any> {
+    /*let cacheReference = this.configurationCache;
+    if (cacheReference[url]) {
+        return Promise.resolve(cacheReference[url]);
+    }*/
+
+    return new Promise(function (resolve, reject) {
+        var xhr = new XMLHttpRequest();
+        xhr.open('GET', url);
+        xhr.send();
+        xhr.onreadystatechange = function () {
+            var DONE = 4;
+            var OK = 200;
+            if (xhr.readyState === DONE) {
+                if (xhr.status === OK) {
+                    //cacheReference[url] = xhr.responseText;
+                    resolve(xhr.responseText); // 'This is the returned text.'
+                }
+            } else {
+                console.log('Error: ' + xhr.status, url);
+                reject('Error: ' + xhr.status); // An error occurred during the request.
+            }
+        }
+    });
+}
+
+export function kebabToCamel(s) {
+    return s.replace(/(\-\w)/g, function (m) { return m[1].toUpperCase(); });
+}
+
+//https://gist.github.com/youssman/745578062609e8acac9f
+export function camelToKebab(str) {
+    return !str ? null : str.replace(/([A-Z])/g, function (g) { return '-' + g[0].toLowerCase() });
+}

+ 30 - 0
Viewer/src/index.ts

@@ -0,0 +1,30 @@
+import { mapperManager } from './configuration/mappers';
+import { viewerManager } from './viewer/viewerManager';
+import { DefaultViewer } from './viewer/defaultViewer';
+import { AbstractViewer } from './viewer/viewer';
+
+/**
+ * BabylonJS Viewer
+ * 
+ * An HTML-Based viewer for 3D models, based on BabylonJS and its extensions.
+ */
+
+
+// load babylon and needed modules.
+import 'babylonjs';
+import 'babylonjs-loaders';
+import '../assets/pep.min';
+
+import { InitTags } from './initializer';
+
+// promise polyfill, if needed!
+global.Promise = typeof Promise === 'undefined' ? require('es6-promise').Promise : Promise;
+
+export let disableInit: boolean = false;
+document.addEventListener("DOMContentLoaded", function (event) {
+    if (disableInit) return;
+    InitTags();
+});
+
+// public API for initialization
+export { InitTags, DefaultViewer, AbstractViewer, viewerManager, mapperManager };

+ 15 - 0
Viewer/src/initializer.ts

@@ -0,0 +1,15 @@
+import { DefaultViewer } from './viewer/defaultViewer';
+import { mapperManager } from './configuration/mappers';
+
+export function InitTags(selector: string = 'babylon') {
+    let elements = document.querySelectorAll(selector);
+    for (let i = 0; i < elements.length; ++i) {
+        let element: HTMLElement = <HTMLElement>elements.item(i);
+
+        // get the html configuration
+        let configMapper = mapperManager.getMapper('dom');
+        let config = configMapper.map(element);
+
+        let viewer = new DefaultViewer(element, config);
+    }
+}

+ 5 - 0
Viewer/src/interfaces.ts

@@ -0,0 +1,5 @@
+export const enum CameraBehavior {
+    AUTOROTATION,
+    BOUNCING,
+    FRAMING
+}

+ 330 - 0
Viewer/src/templateManager.ts

@@ -0,0 +1,330 @@
+
+import { Observable } from 'babylonjs';
+import { isUrl, loadFile, camelToKebab, kebabToCamel } from './helper';
+
+export interface ITemplateConfiguration {
+    location?: string; // #template-id OR http://example.com/loading.html
+    html?: string; // raw html string
+    id?: string;
+    params?: { [key: string]: string | number | boolean | object };
+    events?: {
+        // pointer events
+        pointerdown?: boolean | { [id: string]: boolean; };
+        pointerup?: boolean | { [id: string]: boolean; };
+        pointermove?: boolean | { [id: string]: boolean; };
+        pointerover?: boolean | { [id: string]: boolean; };
+        pointerout?: boolean | { [id: string]: boolean; };
+        pointerenter?: boolean | { [id: string]: boolean; };
+        pointerleave?: boolean | { [id: string]: boolean; };
+        pointercancel?: boolean | { [id: string]: boolean; };
+        //click, just in case
+        click?: boolean | { [id: string]: boolean; };
+        // drag and drop
+        dragstart?: boolean | { [id: string]: boolean; };
+        drop?: boolean | { [id: string]: boolean; };
+
+        [key: string]: boolean | { [id: string]: boolean; } | undefined;
+    }
+}
+
+export interface EventCallback {
+    event: Event;
+    template: Template;
+    selector: string;
+    payload?: any;
+}
+
+export class TemplateManager {
+
+    public onInit: Observable<Template>;
+    public onLoaded: Observable<Template>;
+    public onStateChange: Observable<Template>;
+    public onAllLoaded: Observable<TemplateManager>;
+
+    private templates: { [name: string]: Template };
+
+    constructor(public containerElement: HTMLElement) {
+        this.templates = {};
+
+        this.onInit = new Observable<Template>();
+        this.onLoaded = new Observable<Template>();
+        this.onStateChange = new Observable<Template>();
+        this.onAllLoaded = new Observable<TemplateManager>();
+    }
+
+    public initTemplate(templates: { [key: string]: ITemplateConfiguration }) {
+
+        let internalInit = (dependencyMap, name: string, parentTemplate?: Template) => {
+            //init template
+            let template = this.templates[name];
+
+            let childrenTemplates = Object.keys(dependencyMap).map(childName => {
+                return internalInit(dependencyMap[childName], childName, template);
+            });
+
+            // register the observers
+            //template.onLoaded.add(() => {
+            let addToParent = () => {
+                let containingElement = parentTemplate && parentTemplate.parent.querySelector(camelToKebab(name)) || this.containerElement;
+                template.appendTo(containingElement);
+                this.checkLoadedState();
+            }
+
+            if (parentTemplate && !parentTemplate.parent) {
+                parentTemplate.onAppended.add(() => {
+                    addToParent();
+                });
+            } else {
+                addToParent();
+            }
+            //});
+
+            return template;
+        }
+
+        //build the html tree
+        this.buildHTMLTree(templates).then(htmlTree => {
+            internalInit(htmlTree, 'main');
+        });
+    }
+
+    /**
+     * 
+     * This function will create a simple map with child-dependencies of the template html tree.
+     * It will compile each template, check if its children exist in the configuration and will add them if they do.
+     * It is expected that the main template will be called main!
+     * 
+     * @private
+     * @param {{ [key: string]: ITemplateConfiguration }} templates 
+     * @memberof TemplateManager
+     */
+    private buildHTMLTree(templates: { [key: string]: ITemplateConfiguration }): Promise<object> {
+        let promises = Object.keys(templates).map(name => {
+            let template = new Template(name, templates[name]);
+            this.templates[name] = template;
+            return template.initPromise;
+        });
+
+        return Promise.all(promises).then(() => {
+            let templateStructure = {};
+            // now iterate through all templates and check for children:
+            let buildTree = (parentObject, name) => {
+                let childNodes = this.templates[name].getChildElements().filter(n => !!this.templates[n]);
+                childNodes.forEach(element => {
+                    parentObject[element] = {};
+                    buildTree(parentObject[element], element);
+                });
+            }
+
+            buildTree(templateStructure, "main");
+            return templateStructure;
+        });
+    }
+
+    // assumiung only ONE(!) canvas
+    public getCanvas(): HTMLCanvasElement | null {
+        return this.containerElement.querySelector('canvas');
+    }
+
+    public getTemplate(name: string): Template | undefined {
+        return this.templates[name];
+    }
+
+    private checkLoadedState() {
+        let done = Object.keys(this.templates).every((key) => {
+            return this.templates[key].isLoaded && !!this.templates[key].parent;
+        });
+
+        if (done) {
+            this.onAllLoaded.notifyObservers(this);
+        }
+    }
+
+}
+
+
+import * as Handlebars from '../assets/handlebars.min.js';
+// register a new helper. modified https://stackoverflow.com/questions/9838925/is-there-any-method-to-iterate-a-map-with-handlebars-js
+Handlebars.registerHelper('eachInMap', function (map, block) {
+    var out = '';
+    Object.keys(map).map(function (prop) {
+        let data = map[prop];
+        if (typeof data === 'object') {
+            data.id = data.id || prop;
+            out += block.fn(data);
+        } else {
+            out += block.fn({ id: prop, value: data });
+        }
+    });
+    return out;
+});
+
+export class Template {
+
+    public onInit: Observable<Template>;
+    public onLoaded: Observable<Template>;
+    public onAppended: Observable<Template>;
+    public onStateChange: Observable<Template>;
+    public onEventTriggered: Observable<EventCallback>;
+
+    public isLoaded: boolean;
+
+    public parent: HTMLElement;
+
+    public initPromise: Promise<Template>;
+
+    private fragment: DocumentFragment;
+
+    constructor(public name: string, private _configuration: ITemplateConfiguration) {
+        this.onInit = new Observable<Template>();
+        this.onLoaded = new Observable<Template>();
+        this.onAppended = new Observable<Template>();
+        this.onStateChange = new Observable<Template>();
+        this.onEventTriggered = new Observable<EventCallback>();
+
+        this.isLoaded = false;
+        /*
+        if (configuration.id) {
+            this.parent.id = configuration.id;
+        }
+        */
+        this.onInit.notifyObservers(this);
+
+        let htmlContentPromise = getTemplateAsHtml(_configuration);
+
+        this.initPromise = htmlContentPromise.then(htmlTemplate => {
+            if (htmlTemplate) {
+                let compiledTemplate = Handlebars.compile(htmlTemplate);
+                let config = this._configuration.params || {};
+                let rawHtml = compiledTemplate(config);
+                this.fragment = document.createRange().createContextualFragment(rawHtml);
+                this.isLoaded = true;
+                this.onLoaded.notifyObservers(this);
+            }
+            return this;
+        });
+    }
+
+    public get configuration(): ITemplateConfiguration {
+        return this._configuration;
+    }
+
+    public getChildElements(): Array<string> {
+        let childrenArray: string[] = [];
+        //Edge and IE don't support frage,ent.children
+        let children = this.fragment.children;
+        if (!children) {
+            // casting to HTMLCollection, as both NodeListOf and HTMLCollection have 'item()' and 'length'.
+            children = <HTMLCollection>this.fragment.querySelectorAll('*');
+        }
+        for (let i = 0; i < children.length; ++i) {
+            childrenArray.push(kebabToCamel(children.item(i).nodeName.toLowerCase()));
+        }
+        return childrenArray;
+    }
+
+    public appendTo(parent: HTMLElement) {
+        if (this.parent) {
+            console.error('Already appanded to ', this.parent);
+        } else {
+            this.parent = parent;
+
+            if (this._configuration.id) {
+                this.parent.id = this._configuration.id;
+            }
+            this.parent.appendChild(this.fragment);
+            // appended only one frame after.
+            setTimeout(() => {
+                this.registerEvents();
+                this.onAppended.notifyObservers(this);
+            });
+        }
+
+    }
+
+    public show(visibilityFunction?: (template: Template) => Promise<Template>): Promise<Template> {
+        if (visibilityFunction) {
+            return visibilityFunction(this).then(() => {
+                this.onStateChange.notifyObservers(this);
+                return this;
+            });
+        } else {
+            // flex? box? should this be configurable easier than the visibilityFunction?
+            this.parent.style.display = 'flex';
+            this.onStateChange.notifyObservers(this);
+            return Promise.resolve(this);
+        }
+    }
+
+    public hide(visibilityFunction?: (template: Template) => Promise<Template>): Promise<Template> {
+        if (visibilityFunction) {
+            return visibilityFunction(this).then(() => {
+                this.onStateChange.notifyObservers(this);
+                return this;
+            });
+        } else {
+            this.parent.style.display = 'none';
+            this.onStateChange.notifyObservers(this);
+            return Promise.resolve(this);
+        }
+    }
+
+    // TODO - Should events be removed as well? when are templates disposed?
+    private registerEvents() {
+        if (this._configuration.events) {
+            for (let eventName in this._configuration.events) {
+                if (this._configuration.events && this._configuration.events[eventName]) {
+                    let functionToFire = (selector, event) => {
+                        this.onEventTriggered.notifyObservers({ event: event, template: this, selector: selector });
+                    }
+
+                    // if boolean, set the parent as the event listener
+                    if (typeof this._configuration.events[eventName] === 'boolean') {
+                        this.parent.addEventListener(eventName, functionToFire.bind(this, '#' + this.parent.id), false);
+                    } else if (typeof this._configuration.events[eventName] === 'object') {
+                        let selectorsArray: Array<string> = Object.keys(this._configuration.events[eventName] || {});
+                        // strict null checl is working incorrectly, must override:
+                        let event = this._configuration.events[eventName] || {};
+                        selectorsArray.filter(selector => event[selector]).forEach(selector => {
+                            if (selector.indexOf('#') !== 0) {
+                                selector = '#' + selector;
+                            }
+                            let htmlElement = <HTMLElement>this.parent.querySelector(selector);
+                            htmlElement && htmlElement.addEventListener(eventName, functionToFire.bind(this, selector), false)
+                        });
+                    }
+                }
+            }
+        }
+    }
+
+}
+
+export function getTemplateAsHtml(templateConfig: ITemplateConfiguration): Promise<string> {
+    if (!templateConfig) {
+        return Promise.reject('No templateConfig provided');
+    } else if (templateConfig.html) {
+        return Promise.resolve(templateConfig.html);
+    } else {
+        let location = getTemplateLocation(templateConfig);
+        if (isUrl(location)) {
+            return loadFile(location);
+        } else {
+            location = location.replace('#', '');
+            let element = document.getElementById(location);
+            if (element) {
+                return Promise.resolve(element.innerHTML);
+            } else {
+                return Promise.reject('Template ID not found');
+            }
+        }
+    }
+}
+
+export function getTemplateLocation(templateConfig): string {
+    if (!templateConfig || typeof templateConfig === 'string') {
+        return templateConfig;
+    } else {
+        return templateConfig.location;
+    }
+}

+ 39 - 0
Viewer/src/util/promiseObservable.ts

@@ -0,0 +1,39 @@
+import { Observable } from 'babylonjs';
+
+export class PromiseObservable<T> extends Observable<T> {
+
+    public notifyWithPromise(eventData: T, mask: number = -1, target?: any, currentTarget?: any): Promise<any> {
+
+        let p = Promise.resolve();
+
+        if (!this._observers.length) {
+            return p;
+        }
+
+        let state = this['_eventState'];
+        state.mask = mask;
+        state.target = target;
+        state.currentTarget = currentTarget;
+        state.skipNextObservers = false;
+
+        this._observers.forEach(obs => {
+            if (state.skipNextObservers) {
+                return;
+            }
+            if (obs.mask & mask) {
+                if (obs.scope) {
+                    // TODO - I can add the variable from the last function here. Requires changing callback sig
+                    p = p.then(() => {
+                        return obs.callback.apply(obs.scope, [eventData, state]);
+                    });
+                } else {
+                    p = p.then(() => {
+                        return obs.callback(eventData, state);
+                    });
+                }
+            }
+        });
+
+        return p;
+    }
+}

+ 516 - 0
Viewer/src/viewer/defaultViewer.ts

@@ -0,0 +1,516 @@
+
+
+import { ViewerConfiguration } from './../configuration/configuration';
+import { Template } from './../templateManager';
+import { AbstractViewer } from './viewer';
+import { MirrorTexture, Plane, ShadowGenerator, Texture, BackgroundMaterial, Observable, ShadowLight, CubeTexture, BouncingBehavior, FramingBehavior, Behavior, Light, Engine, Scene, AutoRotationBehavior, AbstractMesh, Quaternion, StandardMaterial, ArcRotateCamera, ImageProcessingConfiguration, Color3, Vector3, SceneLoader, Mesh, HemisphericLight } from 'babylonjs';
+import { CameraBehavior } from '../interfaces';
+
+export class DefaultViewer extends AbstractViewer {
+
+    public camera: ArcRotateCamera;
+
+    constructor(public containerElement: HTMLElement, initialConfiguration: ViewerConfiguration = { extends: 'default' }) {
+        super(containerElement, initialConfiguration);
+        this.onModelLoadedObservable.add(this.onModelLoaded);
+    }
+
+    public initScene(): Promise<Scene> {
+        return super.initScene().then(() => {
+            this.extendClassWithConfig(this.scene, this.configuration.scene);
+            return this.scene;
+        })
+    }
+
+    protected onTemplatesLoaded() {
+
+        this.showLoadingScreen();
+
+        // navbar
+        let viewerElement = this.templateManager.getTemplate('viewer');
+        let navbar = this.templateManager.getTemplate('navBar');
+        if (viewerElement && navbar) {
+            let navbarHeight = navbar.parent.clientHeight + 'px';
+
+            let navbarShown: boolean = true;
+            let timeoutCancel /*: number*/;
+
+            let triggerNavbar = function (show: boolean = false, evt: PointerEvent) {
+                // only left-click on no-button.
+                if (!navbar || evt.button > 0) return;
+                // clear timeout
+                timeoutCancel && clearTimeout(timeoutCancel);
+                // if state is the same, do nothing
+                if (show === navbarShown) return;
+                //showing? simply show it!
+                if (show) {
+                    navbar.parent.style.bottom = show ? '0px' : '-' + navbarHeight;
+                    navbarShown = show;
+                } else {
+                    let visibilityTimeout = 2000;
+                    if (navbar.configuration.params && navbar.configuration.params.visibilityTimeout !== undefined) {
+                        visibilityTimeout = <number>navbar.configuration.params.visibilityTimeout;
+                    }
+                    // not showing? set timeout until it is removed.
+                    timeoutCancel = setTimeout(function () {
+                        if (navbar) {
+                            navbar.parent.style.bottom = '-' + navbarHeight;
+                        }
+                        navbarShown = show;
+                    }, visibilityTimeout);
+                }
+            }
+
+
+
+            viewerElement.parent.addEventListener('pointerout', triggerNavbar.bind(this, false));
+            viewerElement.parent.addEventListener('pointerdown', triggerNavbar.bind(this, true));
+            viewerElement.parent.addEventListener('pointerup', triggerNavbar.bind(this, false));
+            navbar.parent.addEventListener('pointerover', triggerNavbar.bind(this, true))
+            // triggerNavbar(false);
+
+            // events registration
+            this.registerNavbarButtons();
+        }
+
+        // close overlay button
+        let closeButton = document.getElementById('close-button');
+        if (closeButton) {
+            closeButton.addEventListener('pointerdown', () => {
+                this.hideOverlayScreen();
+            })
+        }
+
+        return super.onTemplatesLoaded();
+    }
+
+    private registerNavbarButtons() {
+        let isFullscreen = false;
+
+        let navbar = this.templateManager.getTemplate('navBar');
+        let viewerTemplate = this.templateManager.getTemplate('viewer');
+        if (!navbar || !viewerTemplate) return;
+
+        let viewerElement = viewerTemplate.parent;
+
+
+        navbar.onEventTriggered.add((data) => {
+            switch (data.event.type) {
+                case 'pointerdown':
+                    let event: PointerEvent = <PointerEvent>data.event;
+                    if (event.button === 0) {
+                        switch (data.selector) {
+                            case '#fullscreen-button':
+                                if (!isFullscreen) {
+                                    let requestFullScreen = viewerElement.requestFullscreen || viewerElement.webkitRequestFullscreen || (<any>viewerElement).msRequestFullscreen || (<any>viewerElement).mozRequestFullScreen;
+                                    requestFullScreen.call(viewerElement);
+                                } else {
+                                    let exitFullscreen = document.exitFullscreen || document.webkitExitFullscreen || (<any>document).msExitFullscreen || (<any>document).mozCancelFullScreen
+                                    exitFullscreen.call(document);
+                                }
+
+                                isFullscreen = !isFullscreen;
+                                break;
+                            case '#help-button':
+                                this.showOverlayScreen('help');
+                                break;
+                        }
+                    }
+                    break;
+            }
+        });
+    }
+
+    protected prepareContainerElement() {
+        this.containerElement.style.position = 'relative';
+        this.containerElement.style.display = 'flex';
+    }
+
+    public loadModel(model: any = this.configuration.model): Promise<Scene> {
+        this.showLoadingScreen();
+        return super.loadModel(model, true).catch((error) => {
+            console.log(error);
+            this.hideLoadingScreen();
+            this.showOverlayScreen('error');
+            return this.scene;
+        });
+    }
+
+    private onModelLoaded = (meshes: Array<AbstractMesh>) => {
+
+        // here we could set the navbar's model information:
+        this.setModelMetaData();
+
+        // with a short timeout, making sure everything is there already.
+        setTimeout(() => {
+            this.hideLoadingScreen();
+        }, 500);
+
+
+        // recreate the camera
+        this.scene.createDefaultCameraOrLight(true, true, true);
+        this.camera = <ArcRotateCamera>this.scene.activeCamera;
+
+        meshes[0].rotation.y += Math.PI;
+
+        this.setupCamera(meshes);
+        this.setupLights(meshes);
+
+        return this.initEnvironment(meshes);
+    }
+
+    private setModelMetaData() {
+        let navbar = this.templateManager.getTemplate('navBar');
+        if (!navbar) return;
+
+        let metadataContainer = navbar.parent.querySelector('#model-metadata');
+
+        //title
+        if (metadataContainer && typeof this.configuration.model === 'object') {
+            if (this.configuration.model.title) {
+                let element = metadataContainer.querySelector('span.model-title');
+                if (element) {
+                    element.innerHTML = this.configuration.model.title;
+                }
+            }
+
+            if (this.configuration.model.subtitle) {
+                let element = metadataContainer.querySelector('span.model-subtitle');
+                if (element) {
+                    element.innerHTML = this.configuration.model.subtitle;
+                }
+            }
+
+            if (this.configuration.model.thumbnail) {
+                (<HTMLDivElement>metadataContainer.querySelector('.thumbnail')).style.backgroundImage = `url('${this.configuration.model.thumbnail}')`;
+            }
+        }
+
+    }
+
+    public initEnvironment(focusMeshes: Array<AbstractMesh> = []): Promise<Scene> {
+        if (this.configuration.skybox) {
+            // Define a general environment textue
+            let texture;
+            // this is obligatory, but still - making sure it is there.
+            if (this.configuration.skybox.cubeTexture) {
+                if (typeof this.configuration.skybox.cubeTexture.url === 'string') {
+                    texture = CubeTexture.CreateFromPrefilteredData(this.configuration.skybox.cubeTexture.url, this.scene);
+                } else {
+                    texture = CubeTexture.CreateFromImages(this.configuration.skybox.cubeTexture.url, this.scene, this.configuration.skybox.cubeTexture.noMipMap);
+                }
+            }
+            if (texture) {
+                this.extendClassWithConfig(texture, this.configuration.skybox.cubeTexture);
+
+                let scale = this.configuration.skybox.scale || this.scene.activeCamera && (this.scene.activeCamera.maxZ - this.scene.activeCamera.minZ) / 2 || 1;
+
+                let box = this.scene.createDefaultSkybox(texture, this.configuration.skybox.pbr, scale, this.configuration.skybox.blur);
+
+                // before extending, set the material's imageprocessing configuration object, if needed:
+                if (this.configuration.skybox.material && this.configuration.skybox.material.imageProcessingConfiguration && box) {
+                    (<StandardMaterial>box.material).imageProcessingConfiguration = new ImageProcessingConfiguration();
+                }
+
+                this.extendClassWithConfig(box, this.configuration.skybox);
+
+                box && focusMeshes.push(box);
+            }
+        }
+
+        if (this.configuration.ground) {
+            let groundConfig = (typeof this.configuration.ground === 'boolean') ? {} : this.configuration.ground;
+
+            let groundSize = groundConfig.size || (this.configuration.skybox && this.configuration.skybox.scale) || 3000;
+
+            let ground = Mesh.CreatePlane("BackgroundPlane", groundSize, this.scene);
+            let backgroundMaterial = new BackgroundMaterial('groundmat', this.scene);
+            ground.rotation.x = Math.PI / 2; // Face up by default.
+            ground.receiveShadows = groundConfig.receiveShadows || false;
+
+            // default values
+            backgroundMaterial.alpha = 0.9;
+            backgroundMaterial.alphaMode = Engine.ALPHA_PREMULTIPLIED_PORTERDUFF;
+            backgroundMaterial.shadowLevel = 0.5;
+            backgroundMaterial.primaryLevel = 1;
+            backgroundMaterial.primaryColor = new Color3(0.2, 0.2, 0.3).toLinearSpace().scale(3);
+            backgroundMaterial.secondaryLevel = 0;
+            backgroundMaterial.tertiaryLevel = 0;
+            backgroundMaterial.useRGBColor = false;
+            backgroundMaterial.enableNoise = true;
+
+            // if config provided, extend the default values
+            if (groundConfig.material) {
+                this.extendClassWithConfig(ground, ground.material);
+            }
+
+            ground.material = backgroundMaterial;
+            if (this.configuration.ground === true || groundConfig.shadowOnly) {
+                // shadow only:
+                ground.receiveShadows = true;
+                const diffuseTexture = new Texture("https://assets.babylonjs.com/environments/backgroundGround.png", this.scene);
+                diffuseTexture.gammaSpace = false;
+                diffuseTexture.hasAlpha = true;
+                backgroundMaterial.diffuseTexture = diffuseTexture;
+            } else if (groundConfig.mirror) {
+                var mirror = new MirrorTexture("mirror", 512, this.scene);
+                mirror.mirrorPlane = new Plane(0, -1, 0, 0);
+                mirror.renderList = mirror.renderList || [];
+                focusMeshes.length && focusMeshes.forEach(m => {
+                    m && mirror.renderList && mirror.renderList.push(m);
+                });
+
+                backgroundMaterial.reflectionTexture = mirror;
+            } else {
+                if (groundConfig.material) {
+                    if (groundConfig.material.diffuseTexture) {
+                        const diffuseTexture = new Texture(groundConfig.material.diffuseTexture, this.scene);
+                        backgroundMaterial.diffuseTexture = diffuseTexture;
+                    }
+                }
+                // ground.material = new StandardMaterial('groundmat', this.scene);
+            }
+            //default configuration
+            if (this.configuration.ground === true) {
+                ground.receiveShadows = true;
+                if (ground.material)
+                    ground.material.alpha = 0.4;
+            }
+
+
+
+
+            this.extendClassWithConfig(ground, groundConfig);
+        }
+
+        return Promise.resolve(this.scene);
+    }
+
+    public showOverlayScreen(subScreen: string) {
+        let template = this.templateManager.getTemplate('overlay');
+        if (!template) return Promise.reject('Overlay template not found');
+
+        return template.show((template => {
+
+            var canvasRect = this.containerElement.getBoundingClientRect();
+            var canvasPositioning = window.getComputedStyle(this.containerElement).position;
+
+            template.parent.style.display = 'flex';
+            template.parent.style.width = canvasRect.width + "px";
+            template.parent.style.height = canvasRect.height + "px";
+            template.parent.style.opacity = "1";
+
+            let subTemplate = this.templateManager.getTemplate(subScreen);
+            if (!subTemplate) {
+                return Promise.reject(subScreen + ' template not found');
+            }
+            return subTemplate.show((template => {
+                template.parent.style.display = 'flex';
+                return Promise.resolve(template);
+            }));
+        }));
+    }
+
+    public hideOverlayScreen() {
+        let template = this.templateManager.getTemplate('overlay');
+        if (!template) return Promise.reject('Overlay template not found');
+
+        return template.hide((template => {
+            template.parent.style.opacity = "0";
+            let onTransitionEnd = () => {
+                template.parent.removeEventListener("transitionend", onTransitionEnd);
+                template.parent.style.display = 'none';
+            }
+            template.parent.addEventListener("transitionend", onTransitionEnd);
+
+            let overlays = template.parent.querySelectorAll('.overlay');
+            if (overlays) {
+                for (let i = 0; i < overlays.length; ++i) {
+                    let htmlElement = <HTMLElement>overlays.item(i);
+                    htmlElement.style.display = 'none';
+                }
+            }
+
+            /*return this.templateManager.getTemplate(subScreen).show((template => {
+                template.parent.style.display = 'none';
+                return Promise.resolve(template);
+            }));*/
+            return Promise.resolve(template);
+        }));
+    }
+
+    public showLoadingScreen() {
+        let template = this.templateManager.getTemplate('loadingScreen');
+        if (!template) return Promise.reject('oading Screen template not found');
+
+        return template.show((template => {
+
+            var canvasRect = this.containerElement.getBoundingClientRect();
+            var canvasPositioning = window.getComputedStyle(this.containerElement).position;
+
+            template.parent.style.display = 'flex';
+            template.parent.style.width = canvasRect.width + "px";
+            template.parent.style.height = canvasRect.height + "px";
+            template.parent.style.opacity = "1";
+            // from the configuration!!!
+            template.parent.style.backgroundColor = "black";
+            return Promise.resolve(template);
+        }));
+    }
+
+    public hideLoadingScreen() {
+        let template = this.templateManager.getTemplate('loadingScreen');
+        if (!template) return Promise.reject('oading Screen template not found');
+
+        return template.hide((template => {
+            template.parent.style.opacity = "0";
+            let onTransitionEnd = () => {
+                template.parent.removeEventListener("transitionend", onTransitionEnd);
+                template.parent.style.display = 'none';
+            }
+            template.parent.addEventListener("transitionend", onTransitionEnd);
+            return Promise.resolve(template);
+        }));
+    }
+
+    private setupLights(focusMeshes: Array<AbstractMesh> = []) {
+
+        let sceneConfig = this.configuration.scene || { defaultLight: true };
+
+        if (!sceneConfig.defaultLight && (this.configuration.lights && Object.keys(this.configuration.lights).length)) {
+            // remove old lights
+            this.scene.lights.forEach(l => {
+                l.dispose();
+            });
+
+            Object.keys(this.configuration.lights).forEach((name, idx) => {
+                let lightConfig = this.configuration.lights && this.configuration.lights[name] || { name: name, type: 0 };
+                lightConfig.name = name;
+                let constructor = Light.GetConstructorFromName(lightConfig.type, lightConfig.name, this.scene);
+                if (!constructor) return;
+                let light = constructor();
+
+                //enabled
+                if (light.isEnabled() !== !lightConfig.disabled) {
+                    light.setEnabled(!lightConfig.disabled);
+                }
+
+                this.extendClassWithConfig(light, lightConfig);
+
+                //position. Some lights don't support shadows
+                if (light instanceof ShadowLight) {
+                    if (lightConfig.shadowEnabled) {
+                        var shadowGenerator = new ShadowGenerator(512, light);
+                        this.extendClassWithConfig(shadowGenerator, lightConfig.shadowConfig || {});
+                        // add the focues meshes to the shadow list
+                        let shadownMap = shadowGenerator.getShadowMap();
+                        if (!shadownMap) return;
+                        let renderList = shadownMap.renderList;
+                        for (var index = 0; index < focusMeshes.length; index++) {
+                            renderList && renderList.push(focusMeshes[index]);
+                        }
+                    }
+                }
+            });
+        }
+    }
+
+    private setupCamera(focusMeshes: Array<AbstractMesh> = []) {
+
+        let cameraConfig = this.configuration.camera || {};
+        let sceneConfig = this.configuration.scene || { autoRotate: false, defaultCamera: true };
+
+        if (!this.configuration.camera && sceneConfig.defaultCamera) {
+            if (sceneConfig.autoRotate) {
+                this.camera.useAutoRotationBehavior = true;
+            }
+            return;
+        }
+
+        if (cameraConfig.position) {
+            this.camera.position.copyFromFloats(cameraConfig.position.x || 0, cameraConfig.position.y || 0, cameraConfig.position.z || 0);
+        }
+
+        if (cameraConfig.rotation) {
+            this.camera.rotationQuaternion = new Quaternion(cameraConfig.rotation.x || 0, cameraConfig.rotation.y || 0, cameraConfig.rotation.z || 0, cameraConfig.rotation.w || 0)
+        }
+
+        this.camera.minZ = cameraConfig.minZ || this.camera.minZ;
+        this.camera.maxZ = cameraConfig.maxZ || this.camera.maxZ;
+
+        if (cameraConfig.behaviors) {
+            for (let name in cameraConfig.behaviors) {
+                this.setCameraBehavior(cameraConfig.behaviors[name], focusMeshes);
+            }
+        };
+
+        if (sceneConfig.autoRotate) {
+            this.camera.useAutoRotationBehavior = true;
+        }
+    }
+
+    private setCameraBehavior(behaviorConfig: number | {
+        type: number;
+        [propName: string]: any;
+    }, payload: any) {
+
+        let behavior: Behavior<ArcRotateCamera> | null;
+        let type = (typeof behaviorConfig !== "object") ? behaviorConfig : behaviorConfig.type;
+
+        let config: { [propName: string]: any } = (typeof behaviorConfig === "object") ? behaviorConfig : {};
+
+        // constructing behavior
+        switch (type) {
+            case CameraBehavior.AUTOROTATION:
+                behavior = new AutoRotationBehavior();
+                break;
+            case CameraBehavior.BOUNCING:
+                behavior = new BouncingBehavior();
+                break;
+            case CameraBehavior.FRAMING:
+                behavior = new FramingBehavior();
+                break;
+            default:
+                behavior = null;
+                break;
+        }
+
+        if (behavior) {
+            if (typeof behaviorConfig === "object") {
+                this.extendClassWithConfig(behavior, behaviorConfig);
+            }
+            this.camera.addBehavior(behavior);
+        }
+
+        // post attach configuration. Some functionalities require the attached camera.
+        switch (type) {
+            case CameraBehavior.AUTOROTATION:
+                break;
+            case CameraBehavior.BOUNCING:
+                break;
+            case CameraBehavior.FRAMING:
+                if (config.zoomOnBoundingInfo) {
+                    //payload is an array of meshes
+                    let meshes = <Array<AbstractMesh>>payload;
+                    let bounding = meshes[0].getHierarchyBoundingVectors();
+                    (<FramingBehavior>behavior).zoomOnBoundingInfo(bounding.min, bounding.max);
+                }
+                break;
+        }
+    }
+
+    private extendClassWithConfig(object: any, config: any) {
+        if (!config) return;
+        Object.keys(config).forEach(key => {
+            if (key in object && typeof object[key] !== 'function') {
+                if (typeof object[key] === 'function') return;
+                // if it is an object, iterate internally until reaching basic types
+                if (typeof object[key] === 'object') {
+                    this.extendClassWithConfig(object[key], config[key]);
+                } else {
+                    object[key] = config[key];
+                }
+            }
+        });
+    }
+}

+ 172 - 0
Viewer/src/viewer/viewer.ts

@@ -0,0 +1,172 @@
+import { viewerManager } from './viewerManager';
+import { TemplateManager } from './../templateManager';
+import configurationLoader from './../configuration/loader';
+import { Observable, Engine, Scene, ArcRotateCamera, Vector3, SceneLoader, AbstractMesh, Mesh, HemisphericLight, Database } from 'babylonjs';
+import { ViewerConfiguration } from '../configuration/configuration';
+import { PromiseObservable } from '../util/promiseObservable';
+
+export abstract class AbstractViewer {
+
+    public templateManager: TemplateManager;
+
+    public engine: Engine;
+    public scene: Scene;
+    public baseId: string;
+
+    protected configuration: ViewerConfiguration;
+
+    // observables
+    public onSceneInitObservable: PromiseObservable<Scene>;
+    public onEngineInitObservable: PromiseObservable<Engine>;
+    public onModelLoadedObservable: PromiseObservable<AbstractMesh[]>;
+
+    constructor(public containerElement: HTMLElement, initialConfiguration: ViewerConfiguration = {}) {
+        // if exists, use the container id. otherwise, generate a random string.
+        if (containerElement.id) {
+            this.baseId = containerElement.id;
+        } else {
+            this.baseId = containerElement.id = 'bjs' + Math.random().toString(32).substr(2, 8);
+        }
+
+        this.onSceneInitObservable = new PromiseObservable();
+        this.onEngineInitObservable = new PromiseObservable();
+        this.onModelLoadedObservable = new PromiseObservable();
+
+        // add this viewer to the viewer manager
+        viewerManager.addViewer(this);
+
+        // create a new template manager. TODO - singleton?
+        this.templateManager = new TemplateManager(containerElement);
+
+        this.prepareContainerElement();
+
+        // extend the configuration
+        configurationLoader.loadConfiguration(initialConfiguration).then((configuration) => {
+            this.configuration = configuration;
+
+            // adding preconfigured functions
+            if (this.configuration.observers) {
+                if (this.configuration.observers.onEngineInit) {
+                    this.onEngineInitObservable.add(window[this.configuration.observers.onEngineInit]);
+                }
+                if (this.configuration.observers.onSceneInit) {
+                    this.onSceneInitObservable.add(window[this.configuration.observers.onSceneInit]);
+                }
+                if (this.configuration.observers.onModelLoaded) {
+                    this.onModelLoadedObservable.add(window[this.configuration.observers.onModelLoaded]);
+                }
+            }
+
+            // initialize the templates
+            let templateConfiguration = this.configuration.templates || {};
+            this.templateManager.initTemplate(templateConfiguration);
+            // when done, execute onTemplatesLoaded()
+            this.templateManager.onAllLoaded.add(() => {
+                this.onTemplatesLoaded();
+            });
+        });
+
+    }
+
+    public getBaseId(): string {
+        return this.baseId;
+    }
+
+    protected abstract prepareContainerElement();
+
+    /**
+     * This function will execute when the HTML templates finished initializing.
+     * It should initialize the engine and continue execution.
+     * 
+     * @protected
+     * @returns {Promise<AbstractViewer>} The viewer object will be returned after the object was loaded.
+     * @memberof AbstractViewer
+     */
+    protected onTemplatesLoaded(): Promise<AbstractViewer> {
+        return this.initEngine().then(() => {
+            return this.loadModel();
+        }).then(() => {
+            return this;
+        });
+    }
+
+    /**
+     * Initialize the engine. Retruns a promise in case async calls are needed.
+     * 
+     * @protected
+     * @returns {Promise<Engine>} 
+     * @memberof Viewer
+     */
+    protected initEngine(): Promise<Engine> {
+        let canvasElement = this.templateManager.getCanvas();
+        if (!canvasElement) {
+            return Promise.reject('Canvas element not found!');
+        }
+        let config = this.configuration.engine || {};
+        // TDO enable further configuration
+        this.engine = new Engine(canvasElement, !!config.antialiasing);
+
+        // Disable manifest checking
+        Database.IDBStorageEnabled = false;
+
+        window.addEventListener('resize', () => {
+            this.engine.resize();
+        });
+
+        this.engine.runRenderLoop(() => {
+            this.scene && this.scene.render();
+        });
+
+        var scale = Math.max(0.5, 1 / (window.devicePixelRatio || 2));
+        this.engine.setHardwareScalingLevel(scale);
+
+        return this.onEngineInitObservable.notifyWithPromise(this.engine).then(() => {
+            return this.engine;
+        });
+    }
+
+    protected initScene(): Promise<Scene> {
+
+        // if the scen exists, dispose it.
+        if (this.scene) {
+            this.scene.dispose();
+        }
+
+        // create a new scene
+        this.scene = new Scene(this.engine);
+        // make sure there is a default camera and light.
+        this.scene.createDefaultCameraOrLight(true, true, true);
+        if (this.configuration.scene && this.configuration.scene.debug) {
+            this.scene.debugLayer.show();
+        }
+        return this.onSceneInitObservable.notifyWithPromise(this.scene).then(() => {
+            return this.scene;
+        });
+    }
+
+    public loadModel(model: any = this.configuration.model, clearScene: boolean = true): Promise<Scene> {
+        let modelUrl = (typeof model === 'string') ? model : model.url;
+        let parts = modelUrl.split('/');
+        let filename = parts.pop();
+        let base = parts.join('/') + '/';
+        let plugin = (typeof model === 'string') ? undefined : model.loader;
+
+        return Promise.resolve().then(() => {
+            if (!this.scene || clearScene) return this.initScene();
+            else return this.scene;
+        }).then(() => {
+            return new Promise<Array<AbstractMesh>>((resolve, reject) => {
+                SceneLoader.ImportMesh(undefined, base, filename, this.scene, (meshes) => {
+                    resolve(meshes);
+                }, undefined, (e, m, exception) => {
+                    console.log(m, exception);
+                    reject(m);
+                }, plugin);
+            });
+        }).then((meshes: Array<AbstractMesh>) => {
+            return this.onModelLoadedObservable.notifyWithPromise(meshes).then(() => {
+                return this.scene;
+            });
+        });
+    }
+}

+ 57 - 0
Viewer/src/viewer/viewerManager.ts

@@ -0,0 +1,57 @@
+/// <reference path="../../../dist/preview release/babylon.d.ts"/>
+
+import { Observable } from 'babylonjs';
+import { AbstractViewer } from './viewer';
+
+class ViewerManager {
+
+    private viewers: { [key: string]: AbstractViewer };
+
+    public onViewerAdded: (viewer: AbstractViewer) => void;
+    public onViewerAddedObservable: Observable<AbstractViewer>;
+
+    constructor() {
+        this.viewers = {};
+        this.onViewerAddedObservable = new Observable();
+    }
+
+    public addViewer(viewer: AbstractViewer) {
+        this.viewers[viewer.getBaseId()] = viewer;
+        this._onViewerAdded(viewer);
+    }
+
+    public getViewerById(id: string): AbstractViewer {
+        return this.viewers[id];
+    }
+
+    public getViewerByHTMLElement(element: HTMLElement) {
+        for (let id in this.viewers) {
+            if (this.viewers[id].containerElement === element) {
+                return this.getViewerById(id);
+            }
+        }
+    }
+
+    public getViewerPromiseById(id: string): Promise<AbstractViewer> {
+        return new Promise((resolve, reject) => {
+            let localViewer = this.getViewerById(id)
+            if (localViewer) {
+                return resolve(localViewer);
+            }
+            let viewerFunction = (viewer: AbstractViewer) => {
+                if (viewer.getBaseId() === id) {
+                    resolve(viewer);
+                    this.onViewerAddedObservable.removeCallback(viewerFunction);
+                }
+            }
+            this.onViewerAddedObservable.add(viewerFunction);
+        });
+    }
+
+    private _onViewerAdded(viewer: AbstractViewer) {
+        this.onViewerAdded && this.onViewerAdded(viewer);
+        this.onViewerAddedObservable.notifyObservers(viewer);
+    }
+}
+
+export let viewerManager = new ViewerManager();

+ 32 - 0
Viewer/tsconfig-gulp.json

@@ -0,0 +1,32 @@
+{
+    "compilerOptions": {
+        "target": "es5",
+        "module": "commonjs",
+        "noResolve": false,
+        "noImplicitAny": false, //mainly due to usage of external libs without typings.
+        "strictNullChecks": true,
+        "removeComments": true,
+        "preserveConstEnums": true,
+        "sourceMap": false,
+        "experimentalDecorators": true,
+        "isolatedModules": false,
+        "declaration": false,
+        "lib": [
+            "dom",
+            "es2015.promise",
+            "es5"
+        ],
+        "types": [
+            "node"
+        ],
+        "baseUrl": ".",
+        "paths": {
+            "babylonjs": [
+                "../dist/preview release/babylon.max.js"
+            ],
+            "babylonjs-loaders": [
+                "../dist/preview release/loaders/babylonjs.loaders.js"
+            ]
+        }
+    }
+}

+ 26 - 0
Viewer/tsconfig.json

@@ -0,0 +1,26 @@
+{
+    "compilerOptions": {
+        "target": "es5",
+        "module": "commonjs",
+        "noResolve": true,
+        "noImplicitAny": false, //mainly due to usage of external libs without typings.
+        "strictNullChecks": true,
+        "removeComments": true,
+        "preserveConstEnums": true,
+        "sourceMap": true,
+        "experimentalDecorators": true,
+        "isolatedModules": false,
+        "lib": [
+            "dom",
+            "es2015.promise",
+            "es5"
+        ],
+        //"declaration": true,
+        "outDir": "./temp/",
+        "types": [
+            "node",
+            "babylonjs",
+            "babylonjs-loaders"
+        ]
+    }
+}

+ 57 - 0
Viewer/webpack.config.js

@@ -0,0 +1,57 @@
+const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
+const path = require('path');
+const webpack = require('webpack');
+
+module.exports = {
+    entry: {
+        'viewer': './src/index.ts',
+        'viewer.min': './src/index.ts',
+    },
+    output: {
+        path: path.resolve(__dirname, 'dist'),
+        filename: '[name].js',
+        libraryTarget: 'umd',
+        library: 'BabylonViewer',
+        umdNamedDefine: true,
+        devtoolModuleFilenameTemplate: '[absolute-resource-path]'
+    },
+    resolve: {
+        extensions: ['.ts', '.tsx', '.js']
+    },
+    devtool: 'source-map',
+    plugins: [
+        new webpack.WatchIgnorePlugin([
+            /\.d\.ts$/
+        ]),
+        new UglifyJSPlugin({
+            parallel: true,
+            test: /\.min\.js$/i,
+        })
+    ],
+    module: {
+        loaders: [{
+            test: /\.tsx?$/,
+            loader: 'ts-loader',
+            exclude: /node_modules/
+        },
+        {
+            test: /\.(html)$/,
+            use: {
+                loader: 'html-loader',
+                options: {
+                    minimize: true
+                }
+            }
+        },
+        {
+            test: /\.(jpe?g|png|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
+            use: 'base64-image-loader?limit=1000&name=[name].[ext]'
+        }]
+    },
+    devServer: {
+        contentBase: path.join(__dirname, "dist"),
+        compress: true,
+        //open: true,
+        port: 9000
+    }
+}

+ 51 - 0
Viewer/webpack.gulp.config.js

@@ -0,0 +1,51 @@
+module.exports = {
+    //context: __dirname,
+    entry: [
+        __dirname + '/src/index.ts'
+    ]
+    ,
+    output: {
+        libraryTarget: 'var',
+        library: 'BabylonViewer',
+        umdNamedDefine: true
+    },
+    externals: {
+        cannon: true,
+        vertx: true
+    },
+    resolve: {
+        extensions: ['.ts', '.js'],
+        alias: {
+            "babylonjs": __dirname + '/../dist/preview release/babylon.max.js',
+            "babylonjs-materials": __dirname + '/../dist/preview release/materialsLibrary/babylonjs.materials.js',
+            "babylonjs-loaders": __dirname + '/../dist/preview release/loaders/babylonjs.loaders.js',
+            "es6-promise": __dirname + '/assets/es6-promise.min.js',
+            "deepmerge": __dirname + '/assets/deepmerge.min.js',
+        }
+    },
+    module: {
+        loaders: [{
+            test: /\.tsx?$/,
+            use: {
+                loader: 'ts-loader',
+                options: {
+                    configFile: 'tsconfig-gulp.json'
+                }
+            },
+            exclude: /node_modules/
+        },
+        {
+            test: /\.(html)$/,
+            use: {
+                loader: 'html-loader',
+                options: {
+                    minimize: true
+                }
+            }
+        },
+        {
+            test: /\.(jpe?g|png|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
+            use: 'base64-image-loader?limit=1000&name=[name].[ext]'
+        }]
+    }
+}

+ 142 - 0
tests/nullEngine/app.js

@@ -0,0 +1,142 @@
+var BABYLON = require("../../dist/preview release/babylon.max");
+var LOADERS = require("../../dist/preview release/loaders/babylonjs.loaders");
+global.XMLHttpRequest = require('xhr2').XMLHttpRequest;
+
+var engine = new BABYLON.NullEngine();
+
+// //Adding a light
+// var light = new BABYLON.PointLight("Omni", new BABYLON.Vector3(20, 20, 100), scene);
+
+// //Adding an Arc Rotate Camera
+// var camera = new BABYLON.ArcRotateCamera("Camera", 0, 0.8, 100, BABYLON.Vector3.Zero(), scene);
+
+// // The first parameter can be used to specify which mesh to import. Here we import all meshes
+// BABYLON.SceneLoader.ImportMesh("", "https://playground.babylonjs.com/scenes/", "skull.babylon", scene, function (newMeshes) {
+//     // Set the target of the camera to the first imported mesh
+//     camera.target = newMeshes[0];
+
+//     console.log("Meshes loaded from babylon file: " + newMeshes.length);
+//     for (var index = 0; index < newMeshes.length; index++) {
+//         console.log(newMeshes[index].toString());
+//     }
+
+//     BABYLON.SceneLoader.ImportMesh("", "https://www.babylonjs.com/Assets/DamagedHelmet/glTF/", "DamagedHelmet.gltf", scene, function (meshes) {
+//         console.log("Meshes loaded from gltf file: " + meshes.length);
+//         for (var index = 0; index < meshes.length; index++) {
+//             console.log(meshes[index].toString());
+//         }
+//     });
+
+//     console.log("render started")
+//     engine.runRenderLoop(function() {
+//         scene.render();
+//     })
+// });
+    
+// Setup environment
+// var camera = new BABYLON.ArcRotateCamera("Camera", 0, 0.8, 90, BABYLON.Vector3.Zero(), scene);
+// camera.lowerBetaLimit = 0.1;
+// camera.upperBetaLimit = (Math.PI / 2) * 0.9;
+// camera.lowerRadiusLimit = 30;
+// camera.upperRadiusLimit = 150;
+
+// // light1
+// var light = new BABYLON.DirectionalLight("dir01", new BABYLON.Vector3(-1, -2, -1), scene);
+// light.position = new BABYLON.Vector3(20, 40, 20);
+// light.intensity = 0.5;
+
+// var lightSphere = BABYLON.Mesh.CreateSphere("sphere", 10, 2, scene);
+// lightSphere.position = light.position;
+// lightSphere.material = new BABYLON.StandardMaterial("light", scene);
+// lightSphere.material.emissiveColor = new BABYLON.Color3(1, 1, 0);
+
+// // light2
+// var light2 = new BABYLON.SpotLight("spot02", new BABYLON.Vector3(30, 40, 20),
+//                                             new BABYLON.Vector3(-1, -2, -1), 1.1, 16, scene);
+// light2.intensity = 0.5;
+
+// var lightSphere2 = BABYLON.Mesh.CreateSphere("sphere", 10, 2, scene);
+// lightSphere2.position = light2.position;
+// lightSphere2.material = new BABYLON.StandardMaterial("light", scene);
+// lightSphere2.material.emissiveColor = new BABYLON.Color3(1, 1, 0);
+
+// // Ground
+// var ground = BABYLON.Mesh.CreateGround("ground1", 6, 6, 2, scene);
+// var groundMaterial = new BABYLON.StandardMaterial("ground", scene);
+// groundMaterial.diffuseTexture = new BABYLON.Texture("textures/ground.jpg", scene);
+// groundMaterial.diffuseTexture.uScale = 6;
+// groundMaterial.diffuseTexture.vScale = 6;
+// groundMaterial.specularColor = new BABYLON.Color3(0, 0, 0);
+// ground.position.y = -2.05;
+// ground.material = groundMaterial;
+
+// // Torus
+// var torus = BABYLON.Mesh.CreateTorus("torus", 4, 2, 30, scene, false);
+
+// // Box
+// var box = BABYLON.Mesh.CreateBox("box", 3);
+// box.parent = torus;	
+
+// // Shadows
+// var shadowGenerator = new BABYLON.ShadowGenerator(1024, light);
+// shadowGenerator.addShadowCaster(torus);
+// shadowGenerator.useExponentialShadowMap = true;
+
+// var shadowGenerator2 = new BABYLON.ShadowGenerator(1024, light2);
+// shadowGenerator2.addShadowCaster(torus);
+// shadowGenerator2.usePoissonSampling = true;
+
+// ground.receiveShadows = true;
+
+// // Animations
+// var alpha = 0;
+// scene.registerBeforeRender(function () {
+//     torus.rotation.x += 0.01;
+//     torus.rotation.z += 0.02;
+
+//     torus.position = new BABYLON.Vector3(Math.cos(alpha) * 30, 10, Math.sin(alpha) * 30);
+//     alpha += 0.01;
+
+// });
+// 	//Adding a light
+// 	var light = new BABYLON.PointLight("Omni", new BABYLON.Vector3(20, 20, 100), scene);
+    
+//         //Adding an Arc Rotate Camera
+//         var camera = new BABYLON.ArcRotateCamera("Camera", -0.5, 2.2, 100, BABYLON.Vector3.Zero(), scene);
+    
+//         // The first parameter can be used to specify which mesh to import. Here we import all meshes
+//         BABYLON.SceneLoader.ImportMesh("", "https://www.babylonjs-playground.com/scenes/", "skull.babylon", scene, function (newMeshes) {
+//             // Set the target of the camera to the first imported mesh
+//             camera.target = newMeshes[0];
+    
+//             newMeshes[0].material = new BABYLON.StandardMaterial("skull", scene);
+//             newMeshes[0].material.emissiveColor = new BABYLON.Color3(0.2, 0.2, 0.2);
+//         });
+    
+//         // Create the "God Rays" effect (volumetric light scattering)
+//         var godrays = new BABYLON.VolumetricLightScatteringPostProcess('godrays', 1.0, camera, null, 100, BABYLON.Texture.BILINEAR_SAMPLINGMODE, engine, false);
+    
+//         // By default it uses a billboard to render the sun, just apply the desired texture
+//         // position and scale
+//         godrays.mesh.material.diffuseTexture = new BABYLON.Texture('https://www.babylonjs-playground.com/textures/sun.png', scene, true, false, BABYLON.Texture.BILINEAR_SAMPLINGMODE);
+//         godrays.mesh.material.diffuseTexture.hasAlpha = true;
+//         godrays.mesh.position = new BABYLON.Vector3(-150, 150, 150);
+//         godrays.mesh.scaling = new BABYLON.Vector3(350, 350, 350);
+    
+//         light.position = godrays.mesh.position;
+
+// engine.runRenderLoop(function() {
+//     scene.render();
+// })
+
+BABYLON.SceneLoader.Load("https://playground.babylonjs.com/scenes/", "skull.babylon", engine, (scene) => {
+    console.log('scene loaded!');
+    for (var index = 0; index < scene.meshes.length; index++) {
+        console.log(scene.meshes[index].name);
+    } 
+    engine.dispose();   
+   // engine.runRenderLoop(function() {
+     //   scene.render();
+    //});
+  
+  }, progress => {}, (scene, err) => console.error('error:', err));

+ 5 - 0
tests/nullEngine/package.json

@@ -0,0 +1,5 @@
+{
+  "devDependencies": {
+    "xhr2": "^0.1.4"
+  }
+}

BIN
tests/validation/ReferenceImages/Billboard.png


BIN
tests/validation/ReferenceImages/GUI.png


BIN
tests/validation/ReferenceImages/assetContainer.png


BIN
tests/validation/ReferenceImages/customRTT.png


BIN
tests/validation/ReferenceImages/gltf1CesiumMan.png


BIN
tests/validation/ReferenceImages/gltfMaterial.png


BIN
tests/validation/ReferenceImages/gltfMaterialAlpha.png


BIN
tests/validation/ReferenceImages/gltfMaterialMetallicRoughness.png


BIN
tests/validation/ReferenceImages/gltfMaterialSpecularGlossiness.png


BIN
tests/validation/ReferenceImages/gltfMeshPrimAttribTest.png


BIN
tests/validation/ReferenceImages/gltfPrimitiveAttribute.png


BIN
tests/validation/ReferenceImages/gltfTextureSampler.png


BIN
tests/validation/ReferenceImages/gltfnormals.png


BIN
tests/validation/ReferenceImages/normals.png


BIN
tests/validation/ReferenceImages/setParent.png


BIN
tests/validation/ReferenceImages/upVector.png


+ 64 - 0
tests/validation/integration.js

@@ -0,0 +1,64 @@
+window.__karma__.loaded = function () { };
+
+// Loading tests
+var xhr = new XMLHttpRequest();
+
+xhr.open("GET", "/tests/validation/config.json", true);
+
+xhr.addEventListener("load", function () {
+    if (xhr.status === 200) {
+
+        config = JSON.parse(xhr.responseText);
+
+        describe("Validation Tests", function () {
+            before(function (done) {
+                this.timeout(180000);
+                require = null;
+                BABYLONDEVTOOLS.Loader
+                    .require('/tests/validation/validation.js')
+                    .useDist()
+                    .load(function () {
+                        var info = engine.getGlInfo();
+                        console.log("Webgl Version: " + info.version);
+                        console.log("Webgl Vendor: " + info.vendor);
+                        console.log("Webgl Renderer: " + info.renderer);
+                        done();
+                    });
+            });
+
+            // Run tests
+            for (let index = 0; index < config.tests.length; index++) {
+                var test = config.tests[index];
+                if (test.onlyVisual || test.excludeFromAutomaticTesting) {
+                    continue;
+                }
+
+                it(test.title, function (done) {
+                    this.timeout(240000);
+
+                    try {
+                        runTest(index, function(result, screenshot) {
+                            try {
+                                expect(result).to.be.true; 
+                                done();
+                            }
+                            catch (e) {
+                                if (screenshot) {
+                                    console.error(screenshot);
+                                }
+                                done(e);
+                            }
+                        });
+                    }
+                    catch (e) {
+                        done(e);
+                    }
+                });
+            };
+        });
+
+        window.__karma__.start();
+    }
+}, false);
+
+xhr.send();

+ 93 - 0
tests/validation/karma.conf.browserstack.js

@@ -0,0 +1,93 @@
+module.exports = function (config) {
+    'use strict';
+    config.set({
+
+        basePath: '../../',
+        captureTimeout: 3e5,
+        browserNoActivityTimeout: 3e5,
+        browserDisconnectTimeout: 3e5,
+        browserDisconnectTolerance: 3,
+        concurrency: 1,
+
+        urlRoot: '/karma',
+
+        frameworks: ['mocha', 'chai', 'sinon'],
+
+        files: [
+            './Tools/DevLoader/BabylonLoader.js',
+            './tests/validation/index.css',
+            './tests/validation/integration.js',
+            './favicon.ico',
+            { pattern: 'dist/**/*', watched: false, included: false, served: true },
+            { pattern: 'assets/**/*', watched: false, included: false, served: true },
+            { pattern: 'tests/**/*', watched: false, included: false, served: true },
+            { pattern: 'Playground/scenes/**/*', watched: false, included: false, served: true },
+            { pattern: 'Playground/textures/**/*', watched: false, included: false, served: true },
+            { pattern: 'Playground/sounds/**/*', watched: false, included: false, served: true },
+            { pattern: 'Tools/DevLoader/**/*', watched: false, included: false, served: true },            
+            { pattern: 'Tools/Gulp/config.json', watched: false, included: false, served: true },
+        ],
+        proxies: {
+            '/': '/base/'
+        },
+
+        port: 1338,
+        colors: true,
+        autoWatch: false,
+        singleRun: false,
+
+        // level of logging
+        // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+        logLevel: config.LOG_INFO,
+
+        browserStack: {
+            project: 'Babylon JS Validation Tests',
+            video: false,
+            debug : 'true',
+            timeout: 1200,
+            build: process.env.TRAVIS_BUILD_NUMBER,
+            username: process.env.BROWSER_STACK_USERNAME,
+            accessKey: process.env.BROWSER_STACK_ACCESS_KEY
+        },
+        customLaunchers: {
+            bs_chrome_win: {
+                base: 'BrowserStack',
+                browser: 'Chrome',
+                browser_version: '63.0',
+                os: 'Windows',
+                os_version: '10'
+            },
+            bs_edge_win: {
+                base: 'BrowserStack',
+                browser: 'Edge',
+                browser_version: '16.0',
+                os: 'Windows',
+                os_version: '10'
+            },
+            bs_firefox_win: {
+                base: 'BrowserStack',
+                browser: 'Firefox',
+                browser_version: '57.0',
+                os: 'Windows',
+                os_version: '10'
+            },
+            bs_chrome_android: {
+                base: 'BrowserStack',
+                os: 'Android',
+                os_version : '8.0',
+                device : 'Google Pixel',
+                real_mobile : 'true'
+            },
+            bs_safari_ios: {
+                base: 'BrowserStack',
+                os: 'ios',
+                os_version : '10.3',
+                device : 'iPhone 7',
+                real_mobile : 'true'
+            }
+        },
+        browsers: ['bs_firefox_win', 'bs_chrome_android'],
+        reporters: ['dots', 'BrowserStack'],
+        singleRun: true
+    });
+};

+ 46 - 0
tests/validation/karma.conf.js

@@ -0,0 +1,46 @@
+module.exports = function (config) {
+    'use strict';
+    config.set({
+
+        basePath: '../../',
+        captureTimeout: 3e5,
+        browserNoActivityTimeout: 3e5,
+        browserDisconnectTimeout: 3e5,
+        browserDisconnectTolerance: 3,
+        concurrency: 1,
+
+        urlRoot: '/karma',
+
+        frameworks: ['mocha', 'chai', 'sinon'],
+
+        files: [
+            './Tools/DevLoader/BabylonLoader.js',
+            './tests/validation/index.css',
+            './tests/validation/integration.js',
+            './favicon.ico',
+            { pattern: 'dist/**/*', watched: false, included: false, served: true },
+            { pattern: 'assets/**/*', watched: false, included: false, served: true },
+            { pattern: 'tests/**/*', watched: false, included: false, served: true },
+            { pattern: 'Playground/scenes/**/*', watched: false, included: false, served: true },
+            { pattern: 'Playground/textures/**/*', watched: false, included: false, served: true },
+            { pattern: 'Playground/sounds/**/*', watched: false, included: false, served: true },
+            { pattern: 'Tools/DevLoader/**/*', watched: false, included: false, served: true },            
+            { pattern: 'Tools/Gulp/config.json', watched: false, included: false, served: true },
+        ],
+        proxies: {
+            '/': '/base/'
+        },
+        
+        port: 1338,
+        colors: true,
+        autoWatch: false,
+        singleRun: false,
+
+        // level of logging
+        // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+        logLevel: config.LOG_INFO,
+
+        browsers: ['Chrome']
+
+    });
+};