import { IDisposable } from "babylonjs/scene" ; import { ISceneLoaderPlugin , ISceneLoaderPluginAsync } from "babylonjs/Loading/sceneLoader" ; import { AbstractMesh } from "babylonjs/Meshes/abstractMesh" ; import { IParticleSystem } from "babylonjs/Particles/IParticleSystem" ; import { Skeleton } from "babylonjs/Bones/skeleton" ; import { Observable } from "babylonjs/Misc/observable" ; import { ISceneLoaderProgressEvent } from "babylonjs/Loading/sceneLoader" ; import { AnimationGroup } from "babylonjs/Animations/animationGroup" ; import { Animation , Animatable , CircleEase , BackEase , BounceEase , CubicEase , ElasticEase , ExponentialEase , PowerEase , QuadraticEase , QuarticEase , QuinticEase , SineEase } from "babylonjs/Animations/index" ; import { Nullable } from "babylonjs/types" ; import { Quaternion , Vector3 } from "babylonjs/Maths/math.vector" ; import { Tags } from "babylonjs/Misc/tags" ; import { Material } from "babylonjs/Materials/material" ; import { PBRMaterial } from "babylonjs/Materials/PBR/pbrMaterial" ; import { MultiMaterial } from "babylonjs/Materials/multiMaterial" ; import { Tools } from "babylonjs/Misc/tools" ; import { GLTFFileLoader } from "babylonjs-loaders/glTF/index" ; import { IAsset } from "babylonjs-gltf2interface" ; import { IModelConfiguration } from "../configuration/interfaces/modelConfiguration" ; import { IModelAnimationConfiguration } from "../configuration/interfaces/modelAnimationConfiguration" ; import { IModelAnimation , GroupModelAnimation , AnimationPlayMode , ModelAnimationConfiguration , EasingFunction , AnimationState } from "./modelAnimation" ; import { deepmerge , extendClassWithConfig } from '../helper/' ; import { ObservablesManager } from "../managers/observablesManager" ; import { ConfigurationContainer } from "../configuration/configurationContainer" ; /**
 * The current state of the model
 */ export enum ModelState { INIT , LOADING , LOADED , ENTRY , ENTRYDONE , COMPLETE , CANCELED , ERROR } /**
 * 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.
     */ public loader : ISceneLoaderPlugin | ISceneLoaderPluginAsync ; private _animations : Array < IModelAnimation > ; /**
     * the list of meshes that are a part of this model
     */ private _meshes : Array < AbstractMesh > = [ ] ; /**
     * This model's root mesh (the parent of all other meshes).
     * This mesh does not(!) exist in the meshes array.
     */ public rootMesh : AbstractMesh ; private _pivotMesh : AbstractMesh ; /**
     * ParticleSystems connected to this model
     */ public particleSystems : Array < IParticleSystem > = [ ] ; /**
     * Skeletons defined in this model
     */ public skeletons : Array < Skeleton > = [ ] ; /**
     * The current model animation.
     * On init, this will be undefined.
     */ public currentAnimation : IModelAnimation ; /**
     * Observers registered here will be executed when the model is done loading
     */ public onLoadedObservable : Observable < ViewerModel > ; /**
     * Observers registered here will be executed when the loader notified of a progress event
     */ public onLoadProgressObservable : Observable < ISceneLoaderProgressEvent > ; /**
     * Observers registered here will be executed when the loader notified of an error.
     */ 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
     */ public onAfterConfigure : Observable < ViewerModel > ; /**
     * The current model state (loaded, error, etc)
     */ public state : ModelState ; /**
     * A loadID provided by the modelLoader, unique to ths (Abstract)Viewer instance.
     */ public loadId : number ; public loadInfo : IAsset ; private _loadedUrl : string ; private _modelConfiguration : IModelConfiguration ; private _loaderDone : boolean = false ; private _entryAnimation : ModelAnimationConfiguration ; private _exitAnimation : ModelAnimationConfiguration ; private _scaleTransition : Animation ; private _animatables : Array < Animatable > = [ ] ; private _frameRate : number = 60 ; private _shadowsRenderedAfterLoad : boolean = false ; constructor ( private _observablesManager , modelConfiguration , private _configurationContainer ? ) { this . onLoadedObservable = new Observable ( ) ; this . onLoadErrorObservable = new Observable ( ) ; this . onLoadProgressObservable = new Observable ( ) ; this . onCompleteObservable = new Observable ( ) ; this . onAfterConfigure = new Observable ( ) ; this . state = ModelState . INIT ; let scene = this . _configurationContainer && this . _configurationContainer . scene ; this . rootMesh = new AbstractMesh ( "modelRootMesh" , scene ) ; this . _pivotMesh = new AbstractMesh ( "pivotMesh" , scene ) ; this . _pivotMesh . parent = this . rootMesh ; this . _pivotMesh . rotation . y += Math . PI ; this . _scaleTransition = new Animation ( "scaleAnimation" , "scaling" , this . _frameRate , Animation . ANIMATIONTYPE_VECTOR3 , Animation . ANIMATIONLOOPMODE_CONSTANT ) ; this . _animations = [ ] ; this . _modelConfiguration = deepmerge ( ( this . _configurationContainer && this . _configurationContainer . configuration . model ) || { } , modelConfiguration ) ; if ( this . _observablesManager ) { this . _observablesManager . onModelAddedObservable . notifyObservers ( this ) ; } if ( this . _modelConfiguration . entryAnimation ) { this . rootMesh . setEnabled ( false ) ; } this . onLoadedObservable . add ( ( ) => { this . updateConfiguration ( this . _modelConfiguration ) ; if ( this . _observablesManager ) { this . _observablesManager . onModelLoadedObservable . notifyObservers ( this ) ; } this . _initAnimations ( ) ; } ) ; this . onCompleteObservable . add ( ( ) => { this . state = ModelState . COMPLETE ; } ) ; } public get shadowsRenderedAfterLoad ( ) { return this . _shadowsRenderedAfterLoad ; } public set shadowsRenderedAfterLoad ( rendered ) { if ( ! rendered ) { throw new Error ( "can only be enabled" ) ; } else { this . _shadowsRenderedAfterLoad = rendered ; } } public getViewerId ( ) { return this . _configurationContainer && this . _configurationContainer . viewerId ; } /**
     * Is this model enabled?
     */ public get enabled ( ) { return this . rootMesh . isEnabled ( ) ; } /**
     * Set whether this model is enabled or not.
     */ public set enabled ( enable ) { this . rootMesh . setEnabled ( enable ) ; } public set loaderDone ( done ) { this . _loaderDone = done ; this . _checkCompleteState ( ) ; } private _checkCompleteState ( ) { if ( this . _loaderDone && ( this . state === ModelState . ENTRYDONE ) ) { this . _modelComplete ( ) ; } } param mesh param triggerLoaded public addMesh ( mesh , triggerLoaded ? ) { if ( ! mesh . parent ) { mesh . parent = this . _pivotMesh ; } if ( mesh . getClassName ( ) !== "InstancedMesh" ) { mesh . receiveShadows = ! ! this . configuration . receiveShadows ; } this . _meshes . push ( mesh ) ; if ( triggerLoaded ) { return this . onLoadedObservable . notifyObserversWithPromise ( this ) ; } } /**
     * get the list of meshes (excluding the root mesh)
     */ public get meshes ( ) { return this . _meshes ; } /**
     * Get the model's configuration
     */ public get configuration ( ) : IModelConfiguration { return this . _modelConfiguration ; } param newConfiguration public set configuration ( newConfiguration ) { this . _modelConfiguration = newConfiguration ; this . _configureModel ( ) ; } param newConfiguration public updateConfiguration ( newConfiguration ) { this . _modelConfiguration = deepmerge ( this . _modelConfiguration , newConfiguration ) ; this . _configureModel ( ) ; } private _initAnimations ( ) { if ( this . skeletons . length ) { this . skeletons . forEach ( ( skeleton , idx ) => { let ag = new AnimationGroup ( "animation-" + idx , this . _configurationContainer && this . _configurationContainer . scene ) ; let add = false ; skeleton . getAnimatables ( ) . forEach ( ( a ) => { if ( a . animations && a . animations [ 0 ] ) { ag . addTargetedAnimation ( a . animations [ 0 ] , a ) ; add = true ; } } ) ; if ( add ) { this . addAnimationGroup ( ag ) ; } } ) ; } let completeCallback = ( ) => { } ; if ( this . _modelConfiguration . animation ) { if ( this . _modelConfiguration . animation . playOnce ) { this . _animations . forEach ( ( a ) => { a . playMode = AnimationPlayMode . ONCE ; } ) ; } if ( this . _modelConfiguration . animation . autoStart && this . _animations . length ) { let animationName = this . _modelConfiguration . animation . autoStart === true ? this . _animations [ 0 ] . name : this . _modelConfiguration . animation . autoStart ; completeCallback = ( ) => { this . playAnimation ( animationName ) ; } ; } } this . _enterScene ( completeCallback ) ; } param completeCallback private _enterScene ( completeCallback ? ) { const scene = this . rootMesh . getScene ( ) ; let previousValue = scene . animationPropertiesOverride ! . enableBlending ; let callback = ( ) => { this . state = ModelState . ENTRYDONE ; scene . animationPropertiesOverride ! . enableBlending = previousValue ; this . _checkCompleteState ( ) ; if ( completeCallback ) { completeCallback ( ) ; } } ; if ( ! this . _entryAnimation ) { callback ( ) ; return ; } this . rootMesh . setEnabled ( true ) ; scene . animationPropertiesOverride ! . enableBlending = false ; this . _applyAnimation ( this . _entryAnimation , true , callback ) ; } param completeCallback private _exitScene ( completeCallback ) { if ( ! this . _exitAnimation ) { completeCallback ( ) ; return ; } this . _applyAnimation ( this . _exitAnimation , false , completeCallback ) ; } private _modelComplete ( ) { let meshes = this . _pivotMesh . getChildMeshes ( false ) ; meshes . filter ( ( m ) => m . material ) . forEach ( ( mesh ) => { this . _applyModelMaterialConfiguration ( mesh . material ! ) ; } ) ; this . state = ModelState . COMPLETE ; this . onCompleteObservable . notifyObservers ( this ) ; } param animationGroup public addAnimationGroup ( animationGroup ) { this . _animations . push ( new GroupModelAnimation ( animationGroup ) ) ; } /**
     * Get the ModelAnimation array
     */ public getAnimations ( ) { return this . _animations ; } /**
     * Get the animations' names. Using the names you can play a specific animation.
     */ public getAnimationNames ( ) { return this . _animations . map ( ( a ) => a . name ) ; } param name protected _getAnimationByName ( name ) { let filtered = this . _animations . filter ( ( a ) => a . name === name . trim ( ) ) ; if ( filtered . length === 1 ) { return filtered [ 0 ] ; } else { return null ; } } param name returns public playAnimation ( name ) { let animation = this . setCurrentAnimationByName ( name ) ; if ( animation ) { animation . start ( ) ; } return animation ; } public setCurrentAnimationByName ( name ) { let animation = this . _getAnimationByName ( name . trim ( ) ) ; if ( animation ) { if ( this . currentAnimation && this . currentAnimation . state !== AnimationState . STOPPED ) { this . currentAnimation . stop ( ) ; } this . currentAnimation = animation ; return animation ; } else { throw new Error ( "animation not found - " + name ) ; } } private _configureModel ( ) { let meshesWithNoParent = [ this . rootMesh ] ; let updateMeshesWithNoParent = ( variable , value , param ? ) => { meshesWithNoParent . forEach ( ( mesh ) => { if ( param ) { mesh [ variable ] [ param ] = value ; } else { mesh [ variable ] = value ; } } ) ; } ; let updateXYZ = ( variable , configValues ) => { if ( configValues . x !== undefined ) { updateMeshesWithNoParent ( variable , configValues . x , 'x' ) ; } if ( configValues . y !== undefined ) { updateMeshesWithNoParent ( variable , configValues . y , 'y' ) ; } if ( configValues . z !== undefined ) { updateMeshesWithNoParent ( variable , configValues . z , 'z' ) ; } if ( configValues . w !== undefined ) { updateMeshesWithNoParent ( variable , configValues . w , 'w' ) ; } } ; if ( this . _modelConfiguration . normalize ) { let center = false ; let unitSize = false ; let parentIndex ; if ( this . _modelConfiguration . normalize === true ) { center = true ; unitSize = true ; } else { center = ! ! this . _modelConfiguration . normalize . center ; unitSize = ! ! this . _modelConfiguration . normalize . unitSize ; parentIndex = this . _modelConfiguration . normalize . parentIndex ; } let meshesToNormalize = [ ] ; if ( parentIndex !== undefined ) { meshesToNormalize . push ( this . _meshes [ parentIndex ] ) ; } else { meshesToNormalize = this . _pivotMesh . getChildMeshes ( true ) . length === 1 ? [ this . _pivotMesh ] : meshesWithNoParent ; } if ( unitSize ) { meshesToNormalize . forEach ( ( mesh ) => { mesh . normalizeToUnitCube ( true ) ; mesh . computeWorldMatrix ( true ) ; } ) ; } if ( center ) { meshesToNormalize . forEach ( ( mesh ) => { const boundingInfo = mesh . getHierarchyBoundingVectors ( true ) ; const sizeVec = boundingInfo . max . subtract ( boundingInfo . min ) ; const halfSizeVec = sizeVec . scale ( 0.5 ) ; const center = boundingInfo . min . add ( halfSizeVec ) ; mesh . position = center . scale ( - 1 ) ; mesh . position . y += halfSizeVec . y ; mesh . computeWorldMatrix ( true ) ; } ) ; } } else { } if ( this . _modelConfiguration . position ) { updateXYZ ( 'position' , this . _modelConfiguration . position ) ; } if ( this . _modelConfiguration . rotation ) { if ( this . _modelConfiguration . rotation . w ) { meshesWithNoParent . forEach ( ( mesh ) => { if ( ! mesh . rotationQuaternion ) { mesh . rotationQuaternion = new Quaternion ( ) ; } } ) ; updateXYZ ( 'rotationQuaternion' , this . _modelConfiguration . rotation ) ; } else { updateXYZ ( 'rotation' , this . _modelConfiguration . rotation ) ; } } if ( this . _modelConfiguration . rotationOffsetAxis ) { let rotationAxis = new Vector3 ( this . _modelConfiguration . rotationOffsetAxis . x , this . _modelConfiguration . rotationOffsetAxis . y , this . _modelConfiguration . rotationOffsetAxis . z ) ; meshesWithNoParent . forEach ( ( m ) => { if ( this . _modelConfiguration . rotationOffsetAngle ) { m . rotate ( rotationAxis , this . _modelConfiguration . rotationOffsetAngle ) ; } } ) ; } if ( this . _modelConfiguration . scaling ) { updateXYZ ( 'scaling' , this . _modelConfiguration . scaling ) ; } if ( this . _modelConfiguration . castShadow ) { this . _meshes . forEach ( ( mesh ) => { Tags . AddTagsTo ( mesh , 'castShadow' ) ; } ) ; } let meshes = this . _pivotMesh . getChildMeshes ( false ) ; meshes . filter ( ( m ) => m . material ) . forEach ( ( mesh ) => { 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 ) { let anim = { 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 ; } param material hidden public _applyModelMaterialConfiguration ( material ) { if ( ! this . _modelConfiguration . material ) { return ; } extendClassWithConfig ( material , this . _modelConfiguration . material ) ; if ( material instanceof PBRMaterial ) { if ( this . _modelConfiguration . material . directIntensity !== undefined ) { material . directIntensity = this . _modelConfiguration . material . directIntensity ; } if ( this . _modelConfiguration . material . emissiveIntensity !== undefined ) { material . emissiveIntensity = this . _modelConfiguration . material . emissiveIntensity ; } if ( this . _modelConfiguration . material . environmentIntensity !== undefined ) { material . environmentIntensity = this . _modelConfiguration . material . environmentIntensity ; } if ( this . _modelConfiguration . material . directEnabled !== undefined ) { material . disableLighting = ! this . _modelConfiguration . material . directEnabled ; } if ( this . _configurationContainer && this . _configurationContainer . reflectionColor ) { material . reflectionColor = this . _configurationContainer . reflectionColor . clone ( ) ; } } else if ( material instanceof MultiMaterial ) { for ( let i = 0 ; i < material . subMaterials . length ; i ++ ) { const subMaterial = material . subMaterials [ i ] ; if ( subMaterial ) { this . _applyModelMaterialConfiguration ( subMaterial ) ; } } } } param animationConfiguration param isEntry param completeCallback private _applyAnimation ( animationConfiguration , isEntry , completeCallback ? ) { let animations = [ ] ; if ( animationConfiguration . scaling ) { let scaleStart = isEntry ? animationConfiguration . scaling : new Vector3 ( 1 , 1 , 1 ) ; let scaleEnd = isEntry ? new 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 ) ; } } this . transitionTo ( animations , animationConfiguration . time , this . _createEasingFunction ( animationConfiguration . easingFunction ) , animationConfiguration . easingMode , ( ) => { if ( completeCallback ) { completeCallback ( ) ; } } ) ; } param animations param duration param easingFunction param easingMode param onAnimationEnd public transitionTo ( animations , duration , easingFunction , easingMode = 2 , onAnimationEnd ) { if ( easingFunction ) { for ( let animation of animations ) { easingFunction . setEasingMode ( easingMode ) ; animation . setEasingFunction ( easingFunction ) ; } } this . stopAllAnimations ( ) ; this . rootMesh . animations = animations ; if ( this . rootMesh . getScene ( ) . beginAnimation !== undefined ) { let animatable = this . rootMesh . getScene ( ) . beginAnimation ( this . rootMesh , 0 , this . _frameRate * duration , false , 1 , ( ) => { if ( onAnimationEnd ) { onAnimationEnd ( ) ; } } ) ; this . _animatables . push ( animatable ) ; } } param animation param startValue param endValue param duration private _setLinearKeys ( animation , startValue , endValue , duration ) { animation . setKeys ( [ { frame : 0 , value : startValue } , { frame : this . _frameRate * duration , value : endValue } ] ) ; } param easingFunctionID return private _createEasingFunction ( easingFunctionID ? ) { 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 ( ) { 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 . rootMesh . isVisible = false ; if ( this . _observablesManager ) { this . _observablesManager . onModelRemovedObservable . notifyObservers ( this ) ; } } /**
     * Dispose this model, including all of its associated assets.
     */ public dispose ( ) { this . remove ( ) ; this . onAfterConfigure . clear ( ) ; this . onLoadedObservable . clear ( ) ; this . onLoadErrorObservable . clear ( ) ; this . onLoadProgressObservable . clear ( ) ; if ( this . loader && this . loader . name === "gltf" ) { ( < GLTFFileLoader > this . loader ) . dispose ( ) ; } this . particleSystems . forEach ( ( ps ) => ps . dispose ( ) ) ; this . particleSystems . length = 0 ; this . skeletons . forEach ( ( s ) => s . dispose ( ) ) ; this . skeletons . length = 0 ; this . _animations . forEach ( ( ag ) => ag . dispose ( ) ) ; this . _animations . length = 0 ; this . rootMesh . dispose ( false , true ) ; } }