Explorar o código

Merge pull request #2970 from RaananW/viewr-0.1

Viewer 0.1
Raanan Weber %!s(int64=7) %!d(string=hai) anos
pai
achega
8c2443d105

+ 26 - 13
.vscode/launch.json

@@ -17,22 +17,22 @@
             "preLaunchTask": "run",
             "userDataDir": "${workspaceRoot}/.tempChromeProfileForDebug",
             "runtimeArgs": [
-                "--enable-unsafe-es3-apis" 
+                "--enable-unsafe-es3-apis"
             ]
-        },         
+        },
         {
             "name": "Launch playground (Chrome)",
             "type": "chrome",
             "request": "launch",
             "url": "http://localhost:1338/Playground/index-local.html",
             "webRoot": "${workspaceRoot}/",
-            "sourceMaps": true,            
+            "sourceMaps": true,
             "preLaunchTask": "run",
             "userDataDir": "${workspaceRoot}/.tempChromeProfileForDebug",
             "runtimeArgs": [
-                "--enable-unsafe-es3-apis" 
+                "--enable-unsafe-es3-apis"
             ]
-        },        
+        },
         {
             "name": "Launch playground (Chrome+WebGL 1.0 forced)",
             "type": "chrome",
@@ -43,7 +43,7 @@
             "preLaunchTask": "run",
             "userDataDir": "${workspaceRoot}/.tempChromeProfileForDebug",
             "runtimeArgs": [
-                "--disable-es3-apis" 
+                "--disable-es3-apis"
             ]
         },
         {
@@ -56,7 +56,7 @@
             "preLaunchTask": "run",
             "userDataDir": "${workspaceRoot}/.tempChromeProfileForDebug",
             "runtimeArgs": [
-                "--enable-unsafe-es3-apis" 
+                "--enable-unsafe-es3-apis"
             ]
         },
         {
@@ -69,7 +69,7 @@
             "preLaunchTask": "run",
             "userDataDir": "${workspaceRoot}/.tempChromeProfileForDebug",
             "runtimeArgs": [
-                "--enable-unsafe-es3-apis" 
+                "--enable-unsafe-es3-apis"
             ]
         },
         {
@@ -82,7 +82,7 @@
             "preLaunchTask": "run",
             "userDataDir": "${workspaceRoot}/.tempChromeProfileForDebug",
             "runtimeArgs": [
-                "--enable-unsafe-es3-apis" 
+                "--enable-unsafe-es3-apis"
             ]
         },
         {
@@ -95,7 +95,7 @@
             "preLaunchTask": "run",
             "userDataDir": "${workspaceRoot}/.tempChromeProfileForDebug",
             "runtimeArgs": [
-                "--enable-unsafe-es3-apis" 
+                "--enable-unsafe-es3-apis"
             ]
         },
         {
@@ -108,9 +108,9 @@
             "preLaunchTask": "run",
             "userDataDir": "${workspaceRoot}/.tempChromeProfileForDebug",
             "runtimeArgs": [
-                "--enable-unsafe-es3-apis" 
+                "--enable-unsafe-es3-apis"
             ]
-        },      
+        },
         {
             "name": "Launch Build Validation (Chrome)",
             "type": "chrome",
@@ -121,7 +121,20 @@
             "preLaunchTask": "run",
             "userDataDir": "${workspaceRoot}/.tempChromeProfileForDebug",
             "runtimeArgs": [
-                "--enable-unsafe-es3-apis" 
+                "--enable-unsafe-es3-apis"
+            ]
+        },
+        {
+            "name": "Launch Viewer (Chrome)",
+            "type": "chrome",
+            "request": "launch",
+            "url": "http://localhost:9000/",
+            "webRoot": "${workspaceRoot}/Viewer/dist/",
+            "sourceMaps": true,
+            //"preLaunchTask": "build-viewer", // TODO - test why this fails.
+            "userDataDir": "${workspaceRoot}/.tempChromeProfileForDebug",
+            "runtimeArgs": [
+                "--enable-unsafe-es3-apis"
             ]
         }
     ]

+ 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.
+

BIN=BIN
Viewer/assets/img/fullscreen.png


BIN=BIN
Viewer/assets/img/loading.png


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

@@ -0,0 +1,30 @@
+<style>
+    loading-screen {
+        position: absolute;
+        z-index: 100;
+        opacity: 1;
+        pointer-events: none;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        -webkit-transition: opacity 1.5s ease;
+        -moz-transition: opacity 1.5s ease;
+        transition: opacity 1.5s ease;
+    }
+
+    viewer {
+        position: relative;
+        overflow: hidden;
+        /* Start stage */
+        flex: 1;
+        z-index: 1;
+        justify-content: center;
+        align-items: center;
+
+        width: 100%;
+        height: 100%;
+    }
+</style>
+
+<viewer></viewer>
+<loading-screen></loading-screen>

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

@@ -0,0 +1,39 @@
+<style>
+    .babylonjs-canvas {
+        flex: 1;
+        width: 100%;
+        height: 100%;
+        touch-action: none;
+    }
+
+    nav-bar {
+        position: absolute;
+        height: 160px;
+        width: 100%;
+        bottom: 0;
+        background-color: rgba(0, 0, 0, 0.3);
+        color: white;
+        transition: 1s;
+
+        align-items: center;
+        justify-content: center;
+        display: flex;
+
+        flex-direction: column;
+    }
+
+    /* Big screens have room for the entire navbar */
+
+    @media screen and (min-width: 768px) {
+        nav-bar {
+            flex-direction: row;
+            justify-content: space-between;
+            height: 80px;
+        }
+    }
+</style>
+
+<canvas class="babylonjs-canvas" id="{{canvasId}}">
+</canvas>
+
+<nav-bar></nav-bar>

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

@@ -0,0 +1,26 @@
+<style>
+    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}}">

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

@@ -0,0 +1,69 @@
+<style>
+    div.flex-container {
+        display: flex;
+    }
+
+    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;
+        display: flex;
+    }
+
+    div.button {
+        cursor: pointer;
+        height: 30px;
+        margin: 0 10px;
+    }
+
+    div.button img {
+        height: 100%;
+    }
+</style>
+
+<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">
+    <!-- holding the buttons -->
+    {{#each buttons}}
+    <div id="{{id}}" class="button">
+        {{#if text}}
+        <span>{{text}}</span>> {{/if}} {{#if image}}
+        <img src="{{image}}" alt="{{altText}}"> {{/if}}
+    </div>
+    {{/each}}
+</div>

+ 26 - 0
Viewer/dist/basicExample.html

@@ -0,0 +1,26 @@
+<!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 {
+                width: 800px;
+                height: 500px;
+            }
+        </style>
+    </head>
+
+    <body>
+        <babylon model.title="The Bus!" model.subtitle="Remix3D" model.thumbnail="http://d33wubrfki0l68.cloudfront.net/7e08139ddee0ec38005f4232346c7f7386831300/fd934/githubuniverse/remix3d.png"
+            model.url="https://ugcorigin.s-microsoft.com/12/2e77b8e3-0000-0000-7a48-6505db2f0ef9/952/1508427934473.gltf" default-viewer="true"></babylon>
+        <script src="viewer.js"></script>
+    </body>
+
+</html>
+<html>
+
+</html>

+ 34 - 0
Viewer/dist/domExample.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 - DOM usage</title>
+        <style>
+            babylon {
+                width: 800px;
+                height: 500px;
+            }
+        </style>
+    </head>
+
+    <body>
+        <babylon default-viewer="true">
+            <model url="https://ugcorigin.s-microsoft.com/12/2e77b8e3-0000-0000-7a48-6505db2f0ef9/952/1508427934473.gltf" title="The Bus!"
+                subtitle="Remix3D" thumbnail="http://d33wubrfki0l68.cloudfront.net/7e08139ddee0ec38005f4232346c7f7386831300/fd934/githubuniverse/remix3d.png">
+            </model>
+            <camera>
+                <behaviors array="true">
+                    <behavior type="0"></behavior>
+                </behaviors>
+            </camera>
+        </babylon>
+        <script src="viewer.js"></script>
+    </body>
+
+</html>
+<html>
+
+</html>

BIN=BIN
Viewer/dist/environment.dds


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 104716 - 0
Viewer/dist/viewer.js


+ 48 - 0
Viewer/package.json

@@ -0,0 +1,48 @@
+{
+    "name": "babylonjs-viewer",
+    "version": "0.1.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.44",
+        "base64-image-loader": "^1.2.0",
+        "html-loader": "^0.5.1",
+        "json-loader": "^0.5.7",
+        "ts-loader": "^2.3.7",
+        "typescript": "^2.5.3",
+        "uglifyjs-webpack-plugin": "^1.0.0-beta.3",
+        "webpack": "^3.8.1",
+        "webpack-dev-server": "^2.9.2"
+    },
+    "dependencies": {
+        "babylonjs": "^3.1.0-alpha3.6",
+        "babylonjs-loaders": "^3.1.0-alpha3.6",
+        "babylonjs-materials": "^3.1.0-alpha3.6",
+        "babylonjs-post-process": "^3.1.0-alpha3.6",
+        "babylonjs-procedural-textures": "^3.1.0-alpha3.6",
+        "es6-promise": "^4.1.1",
+        "handlebars": "^4.0.11",
+        "lodash": "^4.17.4",
+        "lodash.merge": "^4.6.0",
+        "promise-polyfill": "^6.0.2"
+    }
+}

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 270 - 0
Viewer/src/configuration/configuration.ts


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


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

@@ -0,0 +1,76 @@
+import { mapperManager } from './mappers';
+import { ViewerConfiguration, defaultConfiguration } from './configuration';
+
+import * as merge from 'lodash.merge';
+
+export class ConfigurationLoader {
+
+    private configurationCache: { (url: string): any };
+
+    public loadConfiguration(initConfig: ViewerConfiguration = {}): Promise<ViewerConfiguration> {
+
+        let loadedConfig = merge({}, initConfig);
+
+        if (loadedConfig.defaultViewer) {
+            loadedConfig = merge(loadedConfig, defaultConfiguration);
+        } else {
+            loadedConfig = merge(defaultConfiguration, 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 merge(loadedConfig, parsed);
+            });
+        } else {
+            return Promise.resolve(loadedConfig);
+        }
+    }
+
+    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;

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

@@ -0,0 +1,112 @@
+import { Tools } from 'babylonjs';
+import { ViewerConfiguration } from './configuration';
+
+import { kebabToCamel } from '../helper';
+
+import * as merge from 'lodash.merge';
+
+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) {
+                    currentConfig[camelKey] = attr.nodeValue;
+                } 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 key = kebabToCamel(item.nodeName.toLowerCase());
+                    if (item.attributes.getNamedItem('array') && item.attributes.getNamedItem('array').nodeValue === 'true') {
+                        partConfig[key] = [];
+                    } else {
+                        let configMapped = htmlMapper.map(item);
+                        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]);
+                }
+            }
+            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;

+ 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() });
+}

+ 23 - 0
Viewer/src/index.ts

@@ -0,0 +1,23 @@
+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 'babylonjs-materials';
+
+import { InitTags } from './initializer';
+
+// promise polyfill
+global.Promise = require('es6-promise').Promise;
+
+InitTags();
+
+// public API for initialization
+export { AbstractViewer, InitTags };

+ 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
+}

+ 248 - 0
Viewer/src/templateManager.ts

@@ -0,0 +1,248 @@
+
+import { Observable } from 'babylonjs';
+import { isUrl, loadFile, camelToKebab } from './helper';
+
+
+export interface TemplateConfiguration {
+    location?: string; // #template-id OR http://example.com/loading.html
+    html?: string; // raw html string
+    id?: string;
+    config?: { [key: string]: string | number | boolean | object };
+    events?: {
+        // pointer events
+        pointerdown?: boolean | Array<string>;
+        pointerup?: boolean | Array<string>;
+        pointermove?: boolean | Array<string>;
+        pointerover?: boolean | Array<string>;
+        pointerout?: boolean | Array<string>;
+        pointerenter?: boolean | Array<string>;
+        pointerleave?: boolean | Array<string>;
+        pointercancel?: boolean | Array<string>;
+        //click, just in case
+        click?: boolean | Array<string>;
+        // drag and drop
+        dragstart?: boolean | Array<string>;
+        drop?: boolean | Array<string>;
+
+        [key: string]: boolean | Array<string>;
+    }
+    children?: { [name: string]: TemplateConfiguration };
+}
+
+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(configuration: TemplateConfiguration, name: string = 'main', parentTemplate?: Template) {
+        //init template
+        let template = new Template(name, configuration);
+        this.templates[name] = template;
+
+        let childrenMap = configuration.children || {};
+        let childrenTemplates = Object.keys(childrenMap).map(name => {
+            return this.initTemplate(configuration.children[name], name, 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;
+    }
+
+    // assumiung only ONE(!) canvas
+    public getCanvas(): HTMLCanvasElement {
+        return this.containerElement.querySelector('canvas');
+    }
+
+    public getTemplate(name: string) {
+        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 'handlebars/dist/handlebars.min.js';
+
+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;
+
+    private fragment: DocumentFragment;
+
+    constructor(public name: string, private configuration: TemplateConfiguration) {
+        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);
+
+        htmlContentPromise.then(htmlTemplate => {
+            if (htmlTemplate) {
+                let compiledTemplate = Handlebars.compile(htmlTemplate);
+                let config = this.configuration.config || {};
+                let rawHtml = compiledTemplate(config);
+                this.fragment = document.createRange().createContextualFragment(rawHtml);
+                this.isLoaded = true;
+                this.onLoaded.notifyObservers(this);
+            }
+        });
+    }
+
+    public appendTo(parent: HTMLElement) {
+        if (this.parent) {
+            console.error('Alread 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) {
+            Object.keys(this.configuration.events).forEach(eventName => {
+                if (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 {
+                        let selectorsArray: Array<string> = <Array<string>>this.configuration.events[eventName];
+                        selectorsArray.forEach(selector => {
+                            let htmlElement = <HTMLElement>this.parent.querySelector(selector);
+                            htmlElement && htmlElement.addEventListener(eventName, functionToFire.bind(this, selector), false)
+                        });
+                    }
+                }
+            });
+        }
+    }
+
+}
+
+export function getTemplateAsHtml(templateConfig: TemplateConfiguration): Promise<string> {
+    if (!templateConfig) {
+        return Promise.resolve(undefined);
+    } else if (templateConfig.html) {
+        return Promise.resolve(templateConfig.html);
+    } else {
+        let location = getTemplateLocation(templateConfig);
+        if (isUrl(location)) {
+            return loadFile(location);
+        } else {
+            location = location.replace('#', '');
+            document.getElementById('#' + location);
+        }
+    }
+}
+
+export function getTemplateLocation(templateConfig): string {
+    if (!templateConfig || typeof templateConfig === 'string') {
+        return templateConfig;
+    } else {
+        return templateConfig.location;
+    }
+}

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

@@ -0,0 +1,333 @@
+import { Template } from './../templateManager';
+import { AbstractViewer } from './viewer';
+import { Observable, ShadowLight, CubeTexture, BouncingBehavior, FramingBehavior, Behavior, Light, Engine, Scene, AutoRotationBehavior, AbstractMesh, Quaternion, StandardMaterial, ShadowOnlyMaterial, ArcRotateCamera, ImageProcessingConfiguration, Color3, Vector3, SceneLoader, Mesh, HemisphericLight } from 'babylonjs';
+import { CameraBehavior } from '../interfaces';
+
+// A small hack for the inspector. to be removed!
+import * as BABYLON from 'babylonjs';
+window['BABYLON'] = BABYLON;
+
+export class DefaultViewer extends AbstractViewer {
+
+    private camera: ArcRotateCamera;
+
+    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');
+
+        let nextHeight: string = '0px';
+
+        viewerElement.parent.addEventListener('pointerover', () => {
+            let currentHeight = navbar.parent.clientHeight + 'px';
+            navbar.parent.style.bottom = nextHeight;
+            nextHeight = '-' + currentHeight;
+            console.log(nextHeight, currentHeight);
+        });
+
+        viewerElement.parent.addEventListener('pointerout', () => {
+            navbar.parent.style.bottom = nextHeight;
+            nextHeight = '0px';
+        });
+
+        // events registration
+        this.registerFullscreenMode();
+
+        return super.onTemplatesLoaded();
+    }
+
+    private registerFullscreenMode() {
+        let isFullscreen = false;
+
+        let navbar = this.templateManager.getTemplate('navBar');
+        let viewerElement = this.templateManager.getTemplate('viewer').parent;
+
+        navbar.onEventTriggered.add((data) => {
+            switch (data.event.type) {
+                case 'pointerdown':
+                    let event: PointerEvent = <PointerEvent>data.event;
+                    if (event.button === 0) {
+                        if (data.selector === '#fullscreen-button') {
+                            //this.engine.switchFullscreen(false);
+                            if (!isFullscreen) {
+                                let requestFullScreen = viewerElement.requestFullscreen || /*viewerElement.parent.msRequestFullscreen || viewerElement.parent.mozRequestFullScreen ||*/ viewerElement.webkitRequestFullscreen;
+                                requestFullScreen.call(viewerElement);
+                            } else {
+                                let exitFullscreen = document.exitFullscreen || document.webkitExitFullscreen
+                                exitFullscreen.call(document);
+                            }
+
+                            isFullscreen = !isFullscreen;
+
+                        }
+
+                    }
+                    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);
+    }
+
+    public onModelLoaded(meshes: Array<AbstractMesh>) {
+
+        // here we could set the navbar's model information:
+        this.setModelMetaData();
+
+        this.hideLoadingScreen();
+
+        // 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();
+    }
+
+    private setModelMetaData() {
+        let navbar = this.templateManager.getTemplate('navBar');
+
+        let metadataContainer = navbar.parent.querySelector('#model-metadata');
+
+        //title
+        if (typeof this.configuration.model === 'object') {
+            if (this.configuration.model.title) {
+                metadataContainer.querySelector('span.model-title').innerHTML = this.configuration.model.title;
+            }
+
+            if (this.configuration.model.subtitle) {
+                metadataContainer.querySelector('span.model-subtitle').innerHTML = this.configuration.model.subtitle;
+            }
+
+            if (this.configuration.model.thumbnail) {
+                (<HTMLDivElement>metadataContainer.querySelector('.thumbnail')).style.backgroundImage = `url('${this.configuration.model.thumbnail}')`;
+            }
+        }
+
+    }
+
+    public initEnvironment(): 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.maxZ - this.scene.activeCamera.minZ) / 2;
+
+                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) {
+                    (<StandardMaterial>box.material).imageProcessingConfiguration = new ImageProcessingConfiguration();
+                }
+
+                this.extendClassWithConfig(box, this.configuration.skybox);
+            }
+        }
+
+        if (this.configuration.ground) {
+            let groundConfig = (typeof this.configuration.ground === 'boolean') ? {} : this.configuration.ground;
+
+            var ground = Mesh.CreateGround('ground', groundConfig.size || 100, groundConfig.size || 100, 8, this.scene);
+            if (this.configuration.ground === true || groundConfig.shadowOnly) {
+                ground.material = new BABYLON.ShadowOnlyMaterial('groundmat', this.scene);
+            } else {
+                ground.material = new StandardMaterial('groundmat', this.scene);
+            }
+            //default configuration
+            if (this.configuration.ground === true) {
+                ground.receiveShadows = true;
+                ground.material.alpha = 0.4;
+            }
+
+
+            this.extendClassWithConfig(ground, groundConfig);
+        }
+
+        return Promise.resolve(this.scene);
+    }
+
+    public showLoadingScreen() {
+        return this.templateManager.getTemplate('loadingScreen').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() {
+        return this.templateManager.getTemplate('loadingScreen').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> = []) {
+        if (!this.configuration.scene.defaultLight && (this.configuration.lights && this.configuration.lights.length)) {
+            // remove old lights
+            this.scene.lights.forEach(l => {
+                l.dispose();
+            });
+
+            this.configuration.lights.forEach((lightConfig, idx) => {
+                lightConfig.name = lightConfig.name || 'light-' + idx;
+                let constructor = Light.GetConstructorFromName(lightConfig.type, lightConfig.name, this.scene);
+                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 BABYLON.ShadowGenerator(512, light)
+                        this.extendClassWithConfig(shadowGenerator, lightConfig.shadowConfig || {});
+                        // add the focues meshes to the shadow list
+                        for (var index = 0; index < focusMeshes.length; index++) {
+                            shadowGenerator.getShadowMap().renderList.push(focusMeshes[index]);
+                        }
+                    }
+                }
+            });
+        }
+    }
+
+    private setupCamera(focusMeshes: Array<AbstractMesh> = []) {
+        if (this.configuration.scene.defaultCamera) {
+            return;
+        }
+
+        let cameraConfig = this.configuration.camera || {};
+
+        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) {
+            cameraConfig.behaviors.forEach((behaviorConfig) => {
+                this.setCameraBehavior(behaviorConfig, focusMeshes);
+            });
+        };
+
+        if (this.configuration.scene.autoRotate) {
+            this.camera.useAutoRotationBehavior = true;
+        }
+    }
+
+    private setCameraBehavior(behaviorConfig: number | {
+        type: number;
+        [propName: string]: any;
+    }, payload: any) {
+
+        let behavior: Behavior<ArcRotateCamera>;
+        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;
+        }
+
+        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) {
+        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];
+                }
+            }
+        });
+    }
+}

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

@@ -0,0 +1,134 @@
+import { TemplateManager } from './../templateManager';
+import configurationLoader from './../configuration/loader';
+import { Observable, Engine, Scene, ArcRotateCamera, Vector3, SceneLoader, AbstractMesh, Mesh, HemisphericLight } from 'babylonjs';
+import { ViewerConfiguration } from '../configuration/configuration';
+
+export abstract class AbstractViewer {
+
+    public templateManager: TemplateManager;
+
+    public engine: Engine;
+    public scene: Scene;
+    public baseId: string;
+
+    protected configuration: ViewerConfiguration;
+
+    constructor(public containerElement: HTMLElement, initialConfiguration: ViewerConfiguration = { defaultViewer: true }) {
+        // 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.templateManager = new TemplateManager(containerElement);
+
+        this.prepareContainerElement();
+
+        // extend the configuration
+        configurationLoader.loadConfiguration(initialConfiguration).then((configuration) => {
+            this.configuration = configuration;
+            // initialize the templates
+            this.templateManager.initTemplate(this.configuration.template);
+            // 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();
+        let config = this.configuration.engine || {};
+        // TDO enable further configuration
+        this.engine = new Engine(canvasElement, !!config.antialiasing);
+
+        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 Promise.resolve(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);
+        return Promise.resolve(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('/') + '/';
+
+        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);
+                });
+            });
+        }).then((meshes: Array<AbstractMesh>) => {
+            return this.onModelLoaded(meshes);
+        });
+    }
+
+    protected onModelLoaded(meshes: Array<AbstractMesh>): Promise<Scene> {
+        console.log("model loaded");
+        return Promise.resolve(this.scene);
+    }
+
+    public abstract initEnvironment(): Promise<Scene>;
+
+}

+ 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.
+        "removeComments": true,
+        "preserveConstEnums": true,
+        "sourceMap": true,
+        "experimentalDecorators": true,
+        "isolatedModules": false,
+        "lib": [
+            "dom",
+            "es2015.promise",
+            "es5"
+        ],
+        //"declaration": true,
+        "outDir": "./temp/",
+        "types": [
+            "node",
+            "babylonjs",
+            "babylonjs-loaders",
+            "babylonjs-materials"
+        ]
+    }
+}

+ 63 - 0
Viewer/webpack.config.js

@@ -0,0 +1,63 @@
+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: 'Viewer3D',
+        umdNamedDefine: true,
+        devtoolModuleFilenameTemplate: '[absolute-resource-path]'
+    },
+    resolve: {
+        extensions: ['.ts', '.tsx', '.js']
+    },
+    devtool: 'source-map',
+    plugins: [
+        new webpack.WatchIgnorePlugin([
+            /\.d\.ts$/
+        ]),
+        /*new UglifyJSPlugin({
+
+            uglifyOptions: {
+                compress: {
+                    warnings: false,
+                },
+                output: {
+                    comments: false
+                }
+            },
+            sourceMap: true,
+            include: /\.min/,
+        })*/
+    ],
+    module: {
+        loaders: [{
+            test: /\.tsx?$/,
+            loader: 'ts-loader',
+            exclude: /node_modules/
+        },
+        {
+            test: /\.(html)$/,
+            use: {
+                loader: 'html-loader'
+            }
+        },
+        {
+            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
+    }
+}