Преглед изворни кода

configuration update flow
Integrated entry and exit animations

Raanan Weber пре 7 година
родитељ
комит
8a57c8a052

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

@@ -46,6 +46,7 @@ export interface ViewerConfiguration {
     lights?: { [name: string]: boolean | ILightConfiguration },
     // engine configuration. optional!
     engine?: {
+        renderInBackground?: boolean;
         antialiasing?: boolean;
         disableResize?: boolean;
         engineOptions?: EngineOptions;
@@ -108,6 +109,38 @@ export interface ViewerConfiguration {
     }
 }
 
+/**
+ * Defines an animation to be applied to a model (translation, scale or rotation).
+ */
+export interface IModelAnimationConfiguration {
+    /**
+     * Time of animation, in seconds
+     */
+    time?: number;
+
+    /**
+     * Scale to apply
+     */
+    scaling?: {
+        x: number;
+        y: number;
+        z: number;
+    };
+
+    /**
+     * Easing function to apply
+     * See SPECTRE.EasingFunction
+     */
+    easingFunction?: number;
+
+    /**
+     * An Easing mode to apply to the easing function
+     * See BABYLON.EasingFunction
+     */
+    easingMode?: number;
+}
+
+
 export interface IDefaultRenderingPipelineConfiguration {
     sharpenEnabled?: boolean;
     bloomEnabled?: boolean;
@@ -132,6 +165,7 @@ export interface IModelConfiguration {
     id?: string;
     url?: string;
     root?: string; //optional
+    file?: string; // is a file being loaded? root and url ignored
     loader?: string; // obj, gltf?
     position?: { x: number, y: number, z: number };
     rotation?: { x: number, y: number, z: number, w?: number };
@@ -155,6 +189,9 @@ export interface IModelConfiguration {
         playOnce?: boolean;
     }
 
+    entryAnimation?: IModelAnimationConfiguration;
+    exitAnimation?: IModelAnimationConfiguration;
+
     material?: {
         directEnabled?: boolean;
         directIntensity?: number;

+ 2 - 2
Viewer/src/configuration/types/default.ts

@@ -25,10 +25,10 @@ export let defaultConfiguration: ViewerConfiguration = {
             html: require("../../../assets/templates/default/navbar.html"),
             params: {
                 buttons: {
-                    "help-button": {
+                    /*"help-button": {
                         altText: "Help",
                         image: require('../../../assets/img/help-circle.png')
-                    },
+                    },*/
                     "fullscreen-button": {
                         altText: "Fullscreen",
                         image: require('../../../assets/img/fullscreen.png')

+ 133 - 113
Viewer/src/configuration/types/extended.ts

@@ -5,30 +5,30 @@ export let extendedConfiguration: ViewerConfiguration = {
     version: "3.2.0",
     extends: "default",
     camera: {
-        "exposure": 3.034578,
-        "fov": 0.7853981633974483,
-        "contrast": 1.6,
-        "toneMappingEnabled": true,
-        "upperBetaLimit": 1.3962634015954636 + Math.PI / 2,
-        "lowerBetaLimit": -1.4835298641951802 + Math.PI / 2,
-        "behaviors": {
-            "framing": {
+        exposure: 3.034578,
+        fov: 0.7853981633974483,
+        contrast: 1.6,
+        toneMappingEnabled: true,
+        upperBetaLimit: 1.3962634015954636 + Math.PI / 2,
+        lowerBetaLimit: -1.4835298641951802 + Math.PI / 2,
+        behaviors: {
+            framing: {
                 type: 2,
-                "mode": 0,
-                "positionScale": 0.5,
-                "defaultElevation": 0.2617993877991494,
-                "elevationReturnWaitTime": 3000,
-                "elevationReturnTime": 2000,
-                "framingTime": 500,
-                "zoomStopsAnimation": false,
-                "radiusScale": 0.866
+                mode: 0,
+                positionScale: 0.5,
+                defaultElevation: 0.2617993877991494,
+                elevationReturnWaitTime: 3000,
+                elevationReturnTime: 2000,
+                framingTime: 500,
+                zoomStopsAnimation: false,
+                radiusScale: 0.866
             },
-            "autoRotate": {
+            autoRotate: {
                 type: 0,
-                "idleRotationWaitTime": 4000,
-                "idleRotationSpeed": 0.17453292519943295,
-                "idleRotationSpinupTime": 2500,
-                "zoomStopsAnimation": false
+                idleRotationWaitTime: 4000,
+                idleRotationSpeed: 0.17453292519943295,
+                idleRotationSpinupTime: 2500,
+                zoomStopsAnimation: false
             },
             bouncing: {
                 type: 1,
@@ -38,11 +38,11 @@ export let extendedConfiguration: ViewerConfiguration = {
         },
         upperRadiusLimit: 5,
         lowerRadiusLimit: 0.5,
-        "frameOnModelLoad": true,
-        "framingElevation": 0.2617993877991494,
-        "framingRotation": 1.5707963267948966,
+        frameOnModelLoad: true,
+        framingElevation: 0.2617993877991494,
+        framingRotation: 1.5707963267948966,
         radius: 2,
-        alpha: -1.5708,
+        alpha: 1.5708,
         beta: Math.PI * 0.5 - 0.2618,
         wheelPrecision: 300,
         minZ: 0.1,
@@ -54,34 +54,34 @@ export let extendedConfiguration: ViewerConfiguration = {
     lights: {
         light0: {
             type: 0,
-            "frustumEdgeFalloff": 0,
-            "intensity": 7,
-            "intensityMode": 0,
-            "radius": 0.6,
-            "range": 0.6,
-            "spotAngle": 60,
-            "diffuse": {
-                "r": 1,
-                "g": 1,
-                "b": 1
+            frustumEdgeFalloff: 0,
+            intensity: 7,
+            intensityMode: 0,
+            radius: 0.6,
+            range: 0.6,
+            spotAngle: 60,
+            diffuse: {
+                r: 1,
+                g: 1,
+                b: 1
             },
-            "position": {
-                "x": -2,
-                "y": 2.5,
-                "z": 2
+            position: {
+                x: -2,
+                y: 2.5,
+                z: 2
             },
-            "target": {
-                "x": 0,
-                "y": 0,
-                "z": 0
+            target: {
+                x: 0,
+                y: 0,
+                z: 0
             },
-            "enabled": true,
-            "shadowEnabled": true,
-            "shadowBufferSize": 512,
-            "shadowMinZ": 1,
-            "shadowMaxZ": 10,
-            "shadowFieldOfView": 60,
-            "shadowFrustumSize": 2,
+            enabled: true,
+            shadowEnabled: true,
+            shadowBufferSize: 512,
+            shadowMinZ: 1,
+            shadowMaxZ: 10,
+            shadowFieldOfView: 60,
+            shadowFrustumSize: 2,
             shadowConfig: {
                 useBlurCloseExponentialShadowMap: true,
                 useKernelBlur: true,
@@ -92,70 +92,70 @@ export let extendedConfiguration: ViewerConfiguration = {
             }
         },
         light1: {
-            "type": 0,
-            "frustumEdgeFalloff": 0,
-            "intensity": 7,
-            "intensityMode": 0,
-            "radius": 0.4,
-            "range": 0.4,
-            "spotAngle": 57,
-            "diffuse": {
-                "r": 1,
-                "g": 1,
-                "b": 1
+            type: 0,
+            frustumEdgeFalloff: 0,
+            intensity: 7,
+            intensityMode: 0,
+            radius: 0.4,
+            range: 0.4,
+            spotAngle: 57,
+            diffuse: {
+                r: 1,
+                g: 1,
+                b: 1
             },
-            "position": {
-                "x": 4,
-                "y": 3,
-                "z": -0.5
+            position: {
+                x: 4,
+                y: 3,
+                z: -0.5
             },
-            "target": {
-                "x": 0,
-                "y": 0,
-                "z": 0
+            target: {
+                x: 0,
+                y: 0,
+                z: 0
             },
-            "enabled": true,
-            "shadowEnabled": false,
-            "shadowBufferSize": 512,
-            "shadowMinZ": 0.2,
-            "shadowMaxZ": 10,
-            "shadowFieldOfView": 28,
-            "shadowFrustumSize": 2
+            enabled: true,
+            shadowEnabled: false,
+            shadowBufferSize: 512,
+            shadowMinZ: 0.2,
+            shadowMaxZ: 10,
+            shadowFieldOfView: 28,
+            shadowFrustumSize: 2
         },
         light2: {
-            "type": 0,
-            "frustumEdgeFalloff": 0,
-            "intensity": 1,
-            "intensityMode": 0,
-            "radius": 0.5,
-            "range": 0.5,
-            "spotAngle": 42.85,
-            "diffuse": {
-                "r": 0.8,
-                "g": 0.8,
-                "b": 0.8
+            type: 0,
+            frustumEdgeFalloff: 0,
+            intensity: 1,
+            intensityMode: 0,
+            radius: 0.5,
+            range: 0.5,
+            spotAngle: 42.85,
+            diffuse: {
+                r: 0.8,
+                g: 0.8,
+                b: 0.8
             },
-            "position": {
-                "x": -1,
-                "y": 3,
-                "z": -3
+            position: {
+                x: -1,
+                y: 3,
+                z: -3
             },
-            "target": {
-                "x": 0,
-                "y": 0,
-                "z": 0
+            target: {
+                x: 0,
+                y: 0,
+                z: 0
             },
-            "enabled": true,
-            "shadowEnabled": false,
-            "shadowBufferSize": 512,
-            "shadowMinZ": 0.2,
-            "shadowMaxZ": 10,
-            "shadowFieldOfView": 45,
-            "shadowFrustumSize": 2
+            enabled: true,
+            shadowEnabled: false,
+            shadowBufferSize: 512,
+            shadowMinZ: 0.2,
+            shadowMaxZ: 10,
+            shadowFieldOfView: 45,
+            shadowFrustumSize: 2
         }
     },
     ground: {
-        "shadowLevel": 0.9,
+        shadowLevel: 0.9,
         texture: "Ground_2.0-1024.png",
         material: {
             primaryColorHighlightLevel: 0.035,
@@ -167,8 +167,8 @@ export let extendedConfiguration: ViewerConfiguration = {
                 gammaSpace: true
             }
         },
-        "opacity": 1,
-        "mirror": false,
+        opacity: 1,
+        mirror: false,
         receiveShadows: true,
         size: 5
     },
@@ -177,7 +177,7 @@ export let extendedConfiguration: ViewerConfiguration = {
         cubeTexture: {
             url: "Skybox_2.0-256.dds"
         },
-        "material": {
+        material: {
             primaryColorHighlightLevel: 0.03,
             primaryColorShadowLevel: 0.03,
             enableNoise: true,
@@ -188,7 +188,7 @@ export let extendedConfiguration: ViewerConfiguration = {
         }
     },
     engine: {
-
+        renderInBackground: true
     },
     scene: {
         flags: {
@@ -249,9 +249,9 @@ export let extendedConfiguration: ViewerConfiguration = {
             }
         },
         mainColor: {
-            "r": 0.8823529411764706,
-            "g": 0.8823529411764706,
-            "b": 0.8823529411764706
+            r: 0.8823529411764706,
+            g: 0.8823529411764706,
+            b: 0.8823529411764706
         }
     },
     loaderPlugins: {
@@ -273,6 +273,26 @@ export let extendedConfiguration: ViewerConfiguration = {
             emissiveIntensity: 1.04,
             environmentIntensity: 0.6
         },
+        entryAnimation: {
+            scaling: {
+                x: 0,
+                y: 0,
+                z: 0
+            },
+            time: 0.5,
+            easingFunction: 4,
+            easingMode: 1
+        },
+        exitAnimation: {
+            scaling: {
+                x: 0,
+                y: 0,
+                z: 0
+            },
+            time: 0.5,
+            easingFunction: 4,
+            easingMode: 1
+        },
         normalize: true,
         castShadow: true,
         receiveShadows: true
@@ -280,9 +300,9 @@ export let extendedConfiguration: ViewerConfiguration = {
     lab: {
         assetsRootURL: '/assets/environment/',
         environmentMap: {
-            "texture": "EnvMap_2.0-256.env",
-            "rotationY": 3,
-            "tintLevel": 0.4
+            texture: "EnvMap_2.0-256.env",
+            rotationY: 3,
+            tintLevel: 0.4
         },
         defaultRenderingPipelines: {
             bloomEnabled: true,

+ 2 - 0
Viewer/src/index.ts

@@ -42,5 +42,7 @@ function disposeAll() {
 
 const Version = BABYLON.Engine.Version;
 
+console.log("Babylon.js viewer (v" + Version + ")");
+
 // public API for initialization
 export { BABYLON, Version, InitTags, DefaultViewer, AbstractViewer, viewerGlobals, telemetryManager, disableInit, viewerManager, mapperManager, disposeAll, ModelLoader, ViewerModel, AnimationPlayMode, AnimationState, ModelState, ILoaderPlugin };

+ 25 - 3
Viewer/src/loader/modelLoader.ts

@@ -63,8 +63,18 @@ export class ModelLoader {
             return model;
         }
 
-        let filename = Tools.GetFilename(modelConfiguration.url) || modelConfiguration.url;
-        let base = modelConfiguration.root || Tools.GetFolderPath(modelConfiguration.url);
+        let base: string;
+        let filename: any;
+        if (modelConfiguration.file) {
+            base = "file:";
+            filename = modelConfiguration.file;
+        }
+        else {
+            filename = Tools.GetFilename(modelConfiguration.url) || modelConfiguration.url;
+            base = modelConfiguration.root || Tools.GetFolderPath(modelConfiguration.url);
+        }
+
+
         let plugin = modelConfiguration.loader;
 
         model.loader = SceneLoader.ImportMesh(undefined, base, filename, this._viewer.sceneManager.scene, (meshes, particleSystems, skeletons, animationGroups) => {
@@ -95,6 +105,12 @@ export class ModelLoader {
             let gltfLoader = (<GLTFFileLoader>model.loader);
             gltfLoader.animationStartMode = GLTFLoaderAnimationStartMode.NONE;
             gltfLoader.compileMaterials = true;
+
+            if (!modelConfiguration.file) {
+                gltfLoader.rewriteRootURL = (rootURL, responseURL) => {
+                    return modelConfiguration.root || Tools.GetFolderPath(responseURL || modelConfiguration.url || '');
+                };
+            }
             // if ground is set to "mirror":
             if (this._viewer.configuration.ground && typeof this._viewer.configuration.ground === 'object' && this._viewer.configuration.ground.mirror) {
                 gltfLoader.useClipPlane = true;
@@ -109,7 +125,13 @@ export class ModelLoader {
                 if (data && data.json && data.json['asset']) {
                     model.loadInfo = data.json['asset'];
                 }
-            })
+            });
+
+            gltfLoader.onCompleteObservable.add(() => {
+                model.loaderDone = true;
+            });
+        } else {
+            model.loaderDone = true;
         }
 
         this._checkAndRun("onInit", model.loader, model);

+ 48 - 3
Viewer/src/model/modelAnimation.ts

@@ -1,9 +1,9 @@
-import { AnimationGroup, Animatable, Skeleton } from "babylonjs";
+import { AnimationGroup, Animatable, Skeleton, Vector3 } from "babylonjs";
 
 /**
  * Animation play mode enum - is the animation looping or playing once
  */
-export enum AnimationPlayMode {
+export const enum AnimationPlayMode {
     ONCE,
     LOOP
 }
@@ -11,7 +11,7 @@ export enum AnimationPlayMode {
 /**
  * An enum representing the current state of an animation object
  */
-export enum AnimationState {
+export const enum AnimationState {
     INIT,
     PLAYING,
     PAUSED,
@@ -20,6 +20,51 @@ export enum AnimationState {
 }
 
 /**
+ * The different type of easing functions available 
+ */
+export const enum EasingFunction {
+    Linear = 0,
+    CircleEase = 1,
+    BackEase = 2,
+    BounceEase = 3,
+    CubicEase = 4,
+    ElasticEase = 5,
+    ExponentialEase = 6,
+    PowerEase = 7,
+    QuadraticEase = 8,
+    QuarticEase = 9,
+    QuinticEase = 10,
+    SineEase = 11
+}
+
+/**
+ * Defines a simple animation to be applied to a model (scale).
+ */
+export interface ModelAnimationConfiguration {
+    /**
+     * Time of animation, in seconds
+     */
+    time: number;
+
+    /**
+     * Scale to apply
+     */
+    scaling?: Vector3;
+
+    /**
+     * Easing function to apply
+     * See SPECTRE.EasingFunction
+     */
+    easingFunction?: number;
+
+    /**
+     * An Easing mode to apply to the easing function
+     * See BABYLON.EasingFunction
+     */
+    easingMode?: number;
+}
+
+/**
  * This interface can be implemented to define new types of ModelAnimation objects.
  */
 export interface IModelAnimation {

+ 273 - 20
Viewer/src/model/viewerModel.ts

@@ -1,7 +1,7 @@
-import { ISceneLoaderPlugin, ISceneLoaderPluginAsync, AnimationGroup, Animatable, AbstractMesh, Tools, Scene, SceneLoader, Observable, SceneLoaderProgressEvent, Tags, ParticleSystem, Skeleton, IDisposable, Nullable, Animation, Quaternion, Material, Vector3, AnimationPropertiesOverride } from "babylonjs";
+import { ISceneLoaderPlugin, ISceneLoaderPluginAsync, AnimationGroup, Animatable, AbstractMesh, Tools, Scene, SceneLoader, Observable, SceneLoaderProgressEvent, Tags, ParticleSystem, Skeleton, IDisposable, Nullable, Animation, Quaternion, Material, Vector3, AnimationPropertiesOverride, QuinticEase, SineEase, CircleEase, BackEase, BounceEase, CubicEase, ElasticEase, ExponentialEase, PowerEase, QuadraticEase, QuarticEase } from "babylonjs";
 import { GLTFFileLoader, GLTF2 } from "babylonjs-loaders";
-import { IModelConfiguration } from "../configuration/configuration";
-import { IModelAnimation, GroupModelAnimation, AnimationPlayMode } from "./modelAnimation";
+import { IModelConfiguration, IModelAnimationConfiguration } from "../configuration/configuration";
+import { IModelAnimation, GroupModelAnimation, AnimationPlayMode, ModelAnimationConfiguration, EasingFunction } from "./modelAnimation";
 
 import * as deepmerge from '../../assets/deepmerge.min.js';
 import { AbstractViewer } from "..";
@@ -12,6 +12,9 @@ export enum ModelState {
     INIT,
     LOADING,
     LOADED,
+    ENTRY,
+    ENTRYDONE,
+    COMPLETE,
     CANCELED,
     ERROR
 }
@@ -20,7 +23,6 @@ export enum ModelState {
  * The viewer model is a container for all assets representing a sngle loaded model.
  */
 export class ViewerModel implements IDisposable {
-
     /**
      * The loader used to load this model.
      */
@@ -67,6 +69,10 @@ export class ViewerModel implements IDisposable {
     public onLoadErrorObservable: Observable<{ message: string; exception: any }>;
 
     /**
+     * Will be executed after the model finished loading and complete, including entry animation and lod
+     */
+    public onCompleteObservable: Observable<ViewerModel>;
+    /**
      * Observers registered here will be executed every time the model is being configured.
      * This can be used to extend the model's configuration without extending the class itself
      */
@@ -85,10 +91,19 @@ export class ViewerModel implements IDisposable {
     private _loadedUrl: string;
     private _modelConfiguration: IModelConfiguration;
 
+    private _loaderDone: boolean = false;
+
+    private _entryAnimation: ModelAnimationConfiguration;
+    private _exitAnimation: ModelAnimationConfiguration;
+    private _scaleTransition: BABYLON.Animation;
+    private _animatables: Array<BABYLON.Animatable> = [];
+    private _frameRate: number = 60;
+
     constructor(protected _viewer: AbstractViewer, modelConfiguration: IModelConfiguration) {
         this.onLoadedObservable = new Observable();
         this.onLoadErrorObservable = new Observable();
         this.onLoadProgressObservable = new Observable();
+        this.onCompleteObservable = new Observable();
         this.onAfterConfigure = new Observable();
 
         this.state = ModelState.INIT;
@@ -99,6 +114,8 @@ export class ViewerModel implements IDisposable {
         // rotate 180, gltf fun
         this._pivotMesh.rotation.y += Math.PI;
 
+        this._scaleTransition = new Animation("scaleAnimation", "scaling", this._frameRate, BABYLON.Animation.ANIMATIONTYPE_VECTOR3, BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
+
         this._animations = [];
         //create a copy of the configuration to make sure it doesn't change even after it is changed in the viewer
         this._modelConfiguration = deepmerge(this._viewer.configuration.model || {}, modelConfiguration);
@@ -106,9 +123,14 @@ export class ViewerModel implements IDisposable {
         this._viewer.sceneManager.models.push(this);
         this._viewer.onModelAddedObservable.notifyObservers(this);
         this.onLoadedObservable.add(() => {
+            this.updateConfiguration(this._modelConfiguration);
             this._viewer.onModelLoadedObservable.notifyObservers(this);
             this._initAnimations();
         });
+
+        this.onCompleteObservable.add(() => {
+            this.state = ModelState.COMPLETE;
+        });
     }
 
     /**
@@ -125,6 +147,17 @@ export class ViewerModel implements IDisposable {
         this.rootMesh.setEnabled(enable);
     }
 
+    public set loaderDone(done: boolean) {
+        this._loaderDone = done;
+        this._checkCompleteState();
+    }
+
+    private _checkCompleteState() {
+        if (this._loaderDone && (this.state === ModelState.ENTRYDONE)) {
+            this._modelComplete();
+        }
+    }
+
     /**
      * Get the viewer showing this model
      */
@@ -157,8 +190,6 @@ export class ViewerModel implements IDisposable {
         return this._meshes;
     }
 
-    public get
-
     /**
      * Get the model's configuration
      */
@@ -201,7 +232,9 @@ export class ViewerModel implements IDisposable {
             });
         }
 
-        if (!this._modelConfiguration) return;
+        let completeCallback = () => {
+
+        }
 
         if (this._modelConfiguration.animation) {
             if (this._modelConfiguration.animation.playOnce) {
@@ -212,9 +245,50 @@ export class ViewerModel implements IDisposable {
             if (this._modelConfiguration.animation.autoStart && this._animations.length) {
                 let animationName = this._modelConfiguration.animation.autoStart === true ?
                     this._animations[0].name : this._modelConfiguration.animation.autoStart;
-                this.playAnimation(animationName);
+
+                completeCallback = () => {
+                    this.playAnimation(animationName);
+                }
             }
         }
+
+        this._enterScene(completeCallback);
+    }
+
+    /**
+     * Animates the model from the current position to the default position
+     * @param completeCallback A function to call when the animation has completed
+     */
+    private _enterScene(completeCallback?: () => void): void {
+        let callback = () => {
+            this.state = ModelState.ENTRYDONE;
+            this._checkCompleteState();
+            if (completeCallback) completeCallback();
+        }
+        if (!this._entryAnimation) {
+            callback();
+            return;
+        }
+
+        this._applyAnimation(this._entryAnimation, true, callback);
+    }
+
+    /**
+     * Animates the model from the current position to the exit-screen position
+     * @param completeCallback A function to call when the animation has completed
+     */
+    private _exitScene(completeCallback: () => void): void {
+        if (!this._exitAnimation) {
+            completeCallback();
+            return;
+        }
+
+        this._applyAnimation(this._exitAnimation, false, completeCallback);
+    }
+
+    private _modelComplete() {
+        this.state = ModelState.COMPLETE;
+        this.onCompleteObservable.notifyObservers(this);
     }
 
     /**
@@ -317,7 +391,7 @@ export class ViewerModel implements IDisposable {
             if (parentIndex !== undefined) {
                 meshesToNormalize.push(this._meshes[parentIndex]);
             } else {
-                meshesToNormalize = [this._pivotMesh];
+                meshesToNormalize = this._pivotMesh.getChildMeshes(true).length === 1 ? [this._pivotMesh] : meshesWithNoParent;
             }
 
             if (unitSize) {
@@ -341,17 +415,7 @@ export class ViewerModel implements IDisposable {
                 });
             }
         } else {
-            //center automatically
-            //meshesWithNoParent.forEach(mesh => {
-            const boundingInfo = this._pivotMesh.getHierarchyBoundingVectors(true);
-            const sizeVec = boundingInfo.max.subtract(boundingInfo.min);
-            const halfSizeVec = sizeVec.scale(0.5);
-            const center = boundingInfo.min.add(halfSizeVec);
-            this._pivotMesh.position = center.scale(-1);
-
-            // Recompute Info.
-            this._pivotMesh.computeWorldMatrix(true);
-            //});
+            // if centered, should be done here
         }
 
         // position?
@@ -398,9 +462,35 @@ export class ViewerModel implements IDisposable {
             this._applyModelMaterialConfiguration(mesh.material!);
         });
 
+        if (this._modelConfiguration.entryAnimation) {
+            this._entryAnimation = this._modelAnimationConfigurationToObject(this._modelConfiguration.entryAnimation);
+        }
+
+        if (this._modelConfiguration.exitAnimation) {
+            this._exitAnimation = this._modelAnimationConfigurationToObject(this._modelConfiguration.exitAnimation);
+        }
+
+
         this.onAfterConfigure.notifyObservers(this);
     }
 
+    private _modelAnimationConfigurationToObject(animConfig: IModelAnimationConfiguration): ModelAnimationConfiguration {
+        let anim: ModelAnimationConfiguration = {
+            time: 0.5
+        };
+        if (animConfig.scaling) {
+            anim.scaling = Vector3.Zero();
+        }
+        if (animConfig.easingFunction !== undefined) {
+            anim.easingFunction = animConfig.easingFunction;
+        }
+        if (animConfig.easingMode !== undefined) {
+            anim.easingMode = animConfig.easingMode;
+        }
+        extendClassWithConfig(anim, animConfig);
+        return anim;
+    }
+
     /**
      * Apply a material configuration to a material
      * @param material Material to apply configuration to
@@ -441,9 +531,172 @@ export class ViewerModel implements IDisposable {
     }
 
     /**
+     * Start entry/exit animation given an animation configuration
+     * @param animationConfiguration Entry/Exit animation configuration
+     * @param isEntry Pass true if the animation is an entry animation
+     * @param completeCallback Callback to execute when the animation completes
+     */
+    private _applyAnimation(animationConfiguration: ModelAnimationConfiguration, isEntry: boolean, completeCallback?: () => void) {
+        let animations: BABYLON.Animation[] = [];
+
+        //scale
+        if (animationConfiguration.scaling) {
+
+            let scaleStart: BABYLON.Vector3 = isEntry ? animationConfiguration.scaling : new BABYLON.Vector3(1, 1, 1);
+            let scaleEnd: BABYLON.Vector3 = isEntry ? new BABYLON.Vector3(1, 1, 1) : animationConfiguration.scaling;
+
+            if (!scaleStart.equals(scaleEnd)) {
+                this.rootMesh.scaling = scaleStart;
+                this._setLinearKeys(
+                    this._scaleTransition,
+                    this.rootMesh.scaling,
+                    scaleEnd,
+                    animationConfiguration.time
+                );
+                animations.push(this._scaleTransition);
+            }
+        }
+
+        //Start the animation(s)
+        this.transitionTo(
+            animations,
+            animationConfiguration.time,
+            this._createEasingFunction(animationConfiguration.easingFunction),
+            animationConfiguration.easingMode,
+            () => { if (completeCallback) completeCallback(); }
+        );
+    }
+
+    /**
+    * Begin @animations with the specified @easingFunction
+    * @param animations The BABYLON Animations to begin
+    * @param duration of transition, in seconds
+    * @param easingFunction An easing function to apply
+    * @param easingMode A easing mode to apply to the easingFunction
+    * @param onAnimationEnd Call back trigger at the end of the animation.
+    */
+    public transitionTo(
+        animations: Animation[],
+        duration: number,
+        easingFunction: any,
+        easingMode: number = BABYLON.EasingFunction.EASINGMODE_EASEINOUT,
+        onAnimationEnd: () => void): void {
+
+        if (easingFunction) {
+            for (let animation of animations) {
+                easingFunction.setEasingMode(easingMode);
+                animation.setEasingFunction(easingFunction);
+            }
+        }
+
+        //Stop any current animations before starting the new one - merging not yet supported.
+        this.stopAllAnimations();
+
+        this.rootMesh.animations = animations;
+
+        if (this._viewer.sceneManager.scene.beginAnimation) {
+            let animatable: Animatable = this._viewer.sceneManager.scene.beginAnimation(this.rootMesh, 0, this._frameRate * duration, false, 1, () => {
+                console.log(this.rootMesh.scaling);
+                if (onAnimationEnd) {
+                    onAnimationEnd();
+                }
+            });
+            this._animatables.push(animatable);
+        }
+    }
+
+    /**
+     * Sets key values on an Animation from first to last frame.
+     * @param animation The Babylon animation object to set keys on
+     * @param startValue The value of the first key
+     * @param endValue The value of the last key
+     * @param duration The duration of the animation, used to determine the end frame
+     */
+    private _setLinearKeys(animation: BABYLON.Animation, startValue: any, endValue: any, duration: number) {
+        animation.setKeys([
+            {
+                frame: 0,
+                value: startValue
+            },
+            {
+                frame: this._frameRate * duration,
+                value: endValue
+            }
+        ]);
+    }
+
+    /**
+     * Creates and returns a Babylon easing funtion object based on a string representing the Easing function
+     * @param easingFunctionID The enum of the easing funtion to create
+     * @return The newly created Babylon easing function object
+     */
+    private _createEasingFunction(easingFunctionID?: number): any {
+        let easingFunction;
+
+        switch (easingFunctionID) {
+            case EasingFunction.CircleEase:
+                easingFunction = new CircleEase();
+                break;
+            case EasingFunction.BackEase:
+                easingFunction = new BackEase(0.3);
+                break;
+            case EasingFunction.BounceEase:
+                easingFunction = new BounceEase();
+                break;
+            case EasingFunction.CubicEase:
+                easingFunction = new CubicEase();
+                break;
+            case EasingFunction.ElasticEase:
+                easingFunction = new ElasticEase();
+                break;
+            case EasingFunction.ExponentialEase:
+                easingFunction = new ExponentialEase();
+                break;
+            case EasingFunction.PowerEase:
+                easingFunction = new PowerEase();
+                break;
+            case EasingFunction.QuadraticEase:
+                easingFunction = new QuadraticEase();
+                break;
+            case EasingFunction.QuarticEase:
+                easingFunction = new QuarticEase();
+                break;
+            case EasingFunction.QuinticEase:
+                easingFunction = new QuinticEase();
+                break;
+            case EasingFunction.SineEase:
+                easingFunction = new SineEase();
+                break;
+            default:
+                Tools.Log("No ease function found");
+                break;
+        }
+
+        return easingFunction;
+    }
+
+    /**
+     * Stops and removes all animations that have been applied to the model
+     */
+    public stopAllAnimations(): void {
+        if (this.rootMesh) {
+            this.rootMesh.animations = [];
+        }
+        if (this.currentAnimation) {
+            this.currentAnimation.stop();
+        }
+        while (this._animatables.length) {
+            this._animatables[0].onAnimationEnd = null;
+            this._animatables[0].stop();
+            this._animatables.shift();
+        }
+    }
+
+    /**
      * Will remove this model from the viewer (but NOT dispose it).
      */
     public remove() {
+        this.stopAllAnimations();
         this._viewer.sceneManager.models.splice(this._viewer.sceneManager.models.indexOf(this), 1);
         // hide it
         this.rootMesh.isVisible = false;

Разлика између датотеке није приказан због своје велике величине
+ 51 - 29
Viewer/src/viewer/sceneManager.ts


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

@@ -211,8 +211,6 @@ export abstract class AbstractViewer {
                 this.engine.runRenderLoop(this._render);
             });
         });
-
-        Tools.Log("Babylon.js viewer (v" + Version + ") launched");
     }
 
     /**
@@ -465,11 +463,6 @@ export abstract class AbstractViewer {
                 } else {
                     return this.sceneManager.scene || this.sceneManager.initScene(this._configuration.scene);
                 }
-            }).then((scene) => {
-                /*if (!autoLoad) {
-                    this.updateConfiguration();
-                }*/
-                return this.onSceneInitObservable.notifyObserversWithPromise(scene);
             }).then(() => {
                 return this.onInitDoneObservable.notifyObserversWithPromise(this);
             }).catch(e => {