Explorar el Código

Basic Viewer app (webpack project)

Raanan Weber hace 7 años
padre
commit
9a9a32f306

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 68 - 0
Viewer/src/configuration/configuration.ts


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


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

@@ -0,0 +1,75 @@
+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);
+            return Promise.resolve(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(initConfig);
+        }
+    }
+
+    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;

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

@@ -0,0 +1,72 @@
+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) {
+                    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 {
+
+}
+
+export class MapperManager {
+
+    private mappers: { [key: string]: IMapper };
+    public static DefaultMapper = 'json';
+
+    constructor() {
+        this.mappers = {
+            "html": new HTMLMapper(),
+            "json": new JSONMapper()
+        }
+    }
+
+    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('html');
+        let config = configMapper.map(element);
+
+        let viewer = new DefaultViewer(element, config);
+    }
+}

+ 196 - 0
Viewer/src/templateManager.ts

@@ -0,0 +1,196 @@
+
+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 };
+    children?: { [name: string]: TemplateConfiguration };
+}
+
+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', parent?: 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 = parent && parent.parent.querySelector(camelToKebab(name)) || this.containerElement;
+                template.appendTo(containingElement);
+                this.checkLoadedState();
+            }
+
+            if (parent && !parent.parent) {
+                parent.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 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.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.onAppended.notifyObservers(this);
+            });
+        }
+
+    }
+
+    public show(visibilityFunction?: (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) => 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);
+        }
+    }
+
+}
+
+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;
+    }
+}

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

@@ -0,0 +1,106 @@
+import { AbstractViewer } from './viewer';
+import { Observable, Engine, Scene, ArcRotateCamera, Vector3, SceneLoader, Mesh, HemisphericLight } from 'babylonjs';
+
+// A small hack for the inspector. to be removed!
+import * as BABYLON from 'babylonjs';
+window['BABYLON'] = BABYLON;
+
+export class DefaultViewer extends AbstractViewer {
+
+    protected initScene() {
+        return super.initScene();
+
+    }
+
+    protected onTemplatesLoaded() {
+
+        this.showLoadingScreen();
+
+        // navbar
+        let viewerElement = this.templateManager.getTemplate('viewer');
+        let navbar = this.templateManager.getTemplate('navBar');
+
+        viewerElement.parent.addEventListener('pointerover', () => {
+            navbar.parent.style.bottom = '0px';
+        });
+
+        viewerElement.parent.addEventListener('pointerout', () => {
+            navbar.parent.style.bottom = '-80px';
+        });
+
+        return super.onTemplatesLoaded();
+    }
+
+    protected prepareContainerElement() {
+        this.containerElement.style.display = 'flex';
+    }
+
+    protected initCameras(): Promise<Scene> {
+        var camera = new BABYLON.ArcRotateCamera("camera", 4.712, 1.571, 0.05, BABYLON.Vector3.Zero(), this.scene);
+        camera.attachControl(this.engine.getRenderingCanvas(), true);
+        camera.wheelPrecision = 100.0;
+        camera.minZ = 0.01;
+        camera.maxZ = 1000;
+        camera.useFramingBehavior = true;
+
+        return Promise.resolve(this.scene);
+    }
+
+    protected initLights(): Promise<Scene> {
+        var light = new HemisphericLight("light1", new Vector3(0, 1, 0), this.scene);
+        return Promise.resolve(this.scene);
+    }
+
+    public loadModel(model: any = this.configuration.model): Promise<Scene> {
+        this.showLoadingScreen();
+
+        //TODO should the scene be cleared?
+
+        let modelUrl = (typeof model === 'string') ? model : model.url;
+        let parts = modelUrl.split('/');
+        let filename = parts.pop();
+        let base = parts.join('/') + '/';
+
+        return new Promise((resolve, reject) => {
+            SceneLoader.Append(base, filename, this.scene, (scene) => {
+                console.log("scene loaded");
+                //this.scene.debugLayer.show();
+
+                // TODO do it better, no casting!
+                let camera: ArcRotateCamera = <ArcRotateCamera>this.scene.activeCamera;
+
+                camera.setTarget(scene.meshes[0]);
+
+                this.engine.runRenderLoop(() => {
+                    this.scene.render();
+                });
+
+                this.hideLoadingScreen();
+                this.showViewer().then(() => {
+                    resolve(this.scene);
+                });
+
+            }, undefined, (e, m, exception) => {
+                console.log(m, exception);
+                reject(m);
+            });
+        })
+
+
+    }
+
+    public showLoadingScreen() {
+        return this.templateManager.getTemplate('loadingScreen').show();
+    }
+
+    public hideLoadingScreen() {
+        return this.templateManager.getTemplate('loadingScreen').hide();
+    }
+
+    public showViewer() {
+        return this.templateManager.getTemplate('viewer').show().then(() => {
+            this.engine.resize();
+        });
+    }
+
+}

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

@@ -0,0 +1,92 @@
+import { TemplateManager } from './../templateManager';
+import configurationLoader from './../configuration/loader';
+import { Observable, Engine, Scene, ArcRotateCamera, Vector3, SceneLoader, 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();
+
+        configurationLoader.loadConfiguration(initialConfiguration).then((configuration) => {
+            this.configuration = configuration;
+            this.templateManager.initTemplate(this.configuration.template);
+            this.templateManager.onAllLoaded.add(() => {
+                this.onTemplatesLoaded();
+            })
+        });
+
+    }
+
+    public getBaseId(): string {
+        return this.baseId;
+    }
+
+    protected prepareContainerElement() {
+        // nothing to see here, go home!
+    }
+
+    protected onTemplatesLoaded(): Promise<AbstractViewer> {
+        return this.initEngine().then(() => {
+            return this.initScene();
+        }).then(() => {
+            return this.initCameras();
+        }).then(() => {
+            return this.initLights();
+        }).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();
+        });
+
+        return Promise.resolve(this.engine);
+    }
+
+    protected initScene(): Promise<Scene> {
+        this.scene = new Scene(this.engine);
+        return Promise.resolve(this.scene);
+    }
+
+    protected abstract initCameras(): Promise<Scene>;
+
+    protected abstract initLights(): Promise<Scene>;
+
+    public abstract loadModel(model?: string): Promise<Scene>;
+
+}