import { EventEmitter } from 'events' ; import { FetchDataTypeName , FetchDataType , FetchResult , fetchDataType } from '../common/fetchdatatype' ; import WebGL from './gl/gl' ; import Scene from './scene' ; import { Resource } from './resource' ; import { PathSolver , HandlerResourceData , HandlerResource , SolverParams } from './handlerresource' ; import GenericResource from './genericresource' ; import ClientBuffer from './gl/clientbuffer' ; import { isImageSource , ImageTexture , detectMime } from './imagetexture' ; import { blobToImage } from '../common/canvas' ; export enum DebugRenderMode { None , Diffuse , NormalMap , Occlusion , Roughness , Metallic , TCFactor , Emissive , TexCoords , Normals , Tangents } /**
 * A viewer handler.
 */ export interface Handler { load ? : ( viewer : ModelViewer , ... args : any [ ] ) => void ; isValidSource : ( src : unknown ) => boolean ; resource : new ( src , resourceData ) => HandlerResource ; } /**
 * A model viewer.
 */ export default class ModelViewer extends EventEmitter { /**
     * The viewer's canvas.
     */ canvas : HTMLCanvasElement ; /**
     * The viewer's WebGL context.
     */ gl : WebGLRenderingContext ; /**
     * A wrapper around the viewer's WebGL context with utility functions.
     */ webgl : WebGL ; /**
     * All of the loaded resources.
     */ resources : Resource [ ] = [ ] ; /**
     * A map from urls to their resources.
     *
     * Only used by fetched resources.
     */ resourceMap : Map < string , Resource > = new Map ( ) ; /**
     * A map from urls to the promises that load them.
     *
     * Only used by fetched resources.
     */ promiseMap : Map < string , Promise < Resource | undefined > > = new Map ( ) ; /**
     * The viewer's handlers, added with `addHandler()`.
     */ handlers : Set < Handler > = new Set ( ) ; /**
     * The viewer's scenes, added with `addScene()`.
     */ scenes : Scene [ ] = [ ] ; /**
     * The current frame.
     */ frame = 0 ; /**
     * The number of visible cells on the current frame.
     */ visibleCells = 0 ; /**
     * The number of visible instances on the current frame.
     */ visibleInstances = 0 ; /**
     * The number of particles being updated on the current frame.
     */ updatedParticles = 0 ; /**
     * A viewer-wide flag.
     *
     * If it is false, not only will audio not run, but in fact audio files won't even be fetched in the first place.
     *
     * If audio is desired, this should be set to true before loading models that use audio.
     *
     * Note that it is preferable to call enableAudio(), which checks for the existence of AudioContext.
     */ audioEnabled = false ; /**
     * A resizeable buffer that can be used by any part of the library.
     *
     * The data it contains is temporary, and can be overwritten at any time.
     */ buffer : ClientBuffer ; /**
     * A cache of arbitrary data, shared between all of the handlers.
     */ sharedCache : Map < unknown , unknown > = new Map ( ) ; /**
     * Debug rendering mode.
     *
     * How it affects the rendering ultimately depends on the handlers.
     */ debugRenderMode = DebugRenderMode . None ; directLoadId = 0 ; constructor ( canvas , options ? ) { super ( ) ; const webgl = new WebGL ( canvas , options ) ; const gl = webgl . gl ; this . canvas = canvas ; this . gl = gl ; this . webgl = webgl ; this . buffer = webgl . createClientBuffer ( ) ; gl . depthFunc ( gl . LEQUAL ) ; gl . enable ( gl . DEPTH_TEST ) ; gl . enable ( gl . SCISSOR_TEST ) ; } /**
     * Enable audio if AudioContext is available.
     */ enableAudio ( ) { if ( typeof AudioContext === 'function' ) { this . audioEnabled = true ; return true ; } return false ; } /**
     * Add an handler.
     */ addHandler ( handler , ... args ) { if ( handler ) { const handlers = this . handlers ; if ( ! handlers . has ( handler ) ) { if ( ! handler . isValidSource ) { this . emit ( 'error' , { viewer : this , error : 'Handler missing the isValidSource function' , handler } ) ; return false ; } if ( handler . load ) { try { handler . load ( this , ... args ) ; } catch ( e ) { this . emit ( 'error' , { viewer : this , error : `Handler failed to load` , handler , reason : e } ) ; return false ; } } handlers . add ( handler ) ; return true ; } } return false ; } /**
     * Add a scene.
     */ addScene ( ) { const scene = new Scene ( this ) ; this . scenes . push ( scene ) ; return scene ; } /**
     * Remove a scene.
     */ removeScene ( scene ) { const scenes = this . scenes ; const index = scenes . indexOf ( scene ) ; if ( index !== - 1 ) { scenes . splice ( index , 1 ) ; return true ; } return false ; } /**
     * Removes all of the scenes in the viewer.
     */ clear ( ) { this . scenes . length = 0 ; } /**
     * Given a source and an optional path solver, loads a resource and returns a promise to it.
     */ async load ( src , pathSolver ? , solverParams ? ) { let finalSrc ; let fetchUrl = '' ; let promise ; if ( pathSolver ) { try { finalSrc = pathSolver ( src , solverParams ) ; } catch ( e ) { this . emit ( 'error' , { viewer : this , error : `Path solver failed` , src , reason : e , pathSolver , solverParams } ) ; return ; } } else { finalSrc = src ; } if ( ! finalSrc ) { return ; } if ( finalSrc instanceof Promise ) { finalSrc = await finalSrc ; } if ( finalSrc instanceof Resource ) { return finalSrc ; } if ( typeof finalSrc === 'string' && ! this . detectFormat ( finalSrc ) ) { fetchUrl = finalSrc ; promise = this . promiseMap . get ( fetchUrl ) ; if ( promise ) { return promise ; } const resource = this . resourceMap . get ( fetchUrl ) ; if ( resource ) { return resource ; } promise = fetchDataType ( fetchUrl , 'arrayBuffer' ) . then ( ( value ) => { if ( value . ok ) { return value . data ; } else { this . emit ( 'error' , { viewer : this , error : `Failed to fetch a resource: ${ value . error }` , fetchUrl , reason : value . data } ) ; return ; } } ) ; } else { fetchUrl = `__DIRECT_LOAD_${ this . directLoadId ++ }` ; promise = Promise . resolve ( finalSrc ) ; } promise = promise . then ( async ( actualSrc ) => { if ( actualSrc ) { if ( actualSrc instanceof ArrayBuffer ) { actualSrc = new Uint8Array ( actualSrc ) ; } if ( isImageSource ( actualSrc ) ) { return new ImageTexture ( < TexImageSource > actualSrc , { viewer : this , fetchUrl , pathSolver } ) ; } if ( actualSrc instanceof Uint8Array ) { const type = detectMime ( actualSrc ) ; if ( type . length ) { return new ImageTexture ( await blobToImage ( new Blob ( [ actualSrc . buffer ] , { type } ) ) , { viewer : this , fetchUrl , pathSolver } ) ; } } const handler = this . detectFormat ( actualSrc ) ; if ( handler ) { try { const resource = new handler . resource ( actualSrc , { viewer : this , fetchUrl , pathSolver } ) ; await Promise . all ( resource . blockers ) ; resource . blockers . length = 0 ; return resource ; } catch ( e ) { this . emit ( 'error' , { viewer : this , error : 'Failed to create a resource' , fetchUrl , src , reason : e } ) ; } } else { this . emit ( 'error' , { viewer : this , error : 'Source has no matching handler' , fetchUrl , src } ) ; } } return ; } ) . then ( ( resource ) => { this . promiseMap . delete ( fetchUrl ) ; if ( resource ) { this . resourceMap . set ( fetchUrl , resource ) ; this . resources . push ( resource ) ; this . emit ( 'load' , { viewer : this , fetchUrl , resource } ) ; } this . emit ( 'loadend' , { viewer : this , fetchUrl , resource } ) ; this . checkLoadingStatus ( ) ; return resource ; } ) ; this . promiseMap . set ( fetchUrl , promise ) ; this . emit ( 'loadstart' , { viewer : this , fetchUrl , promise } ) ; return promise ; } detectFormat ( src ) { for ( const handler of this . handlers ) { if ( handler . isValidSource ( src ) ) { return handler ; } } return ; } /**
     * Check whether the given string maps to a resource in the cache.
     */ has ( key ) { return this . resourceMap . has ( key ) ; } /**
     * Get a resource from the cache.
     */ get ( key ) { return this . resourceMap . get ( key ) ; } /**
     * Load something generic.
     *
     * Unlike load(), this does not use handlers or construct any internal objects.
     *
     * `dataType` can be one of: `"image"`, `"string"`, `"arrayBuffer"`, `"bytes"`, `"blob"`.
     *
     * If `callback` isn't given, the resource's `data` is the fetch data, according to `dataType`.
     *
     * If `callback` is given, the resource's `data` is the value returned by it when called with the fetch data.
     *
     * If `callback` returns a promise, the resource's `data` will be whatever the promise resolved to.
     */ async loadGeneric ( fetchUrl , dataType , callback ? ) { const promise = this . promiseMap . get ( fetchUrl ) ; if ( promise ) { return < Promise < GenericResource > > promise ; } const resource = this . resourceMap . get ( fetchUrl ) ; if ( resource ) { return < GenericResource > resource ; } const fetchPromise = fetchDataType ( fetchUrl , dataType ) . then ( async ( value ) => { this . promiseMap . delete ( fetchUrl ) ; let resource ; if ( value . ok ) { let data = value . data ; if ( callback ) { data = await callback ( < FetchDataType > data ) ; } resource = new GenericResource ( data , { viewer : this , fetchUrl } ) ; this . resourceMap . set ( fetchUrl , resource ) ; this . resources . push ( resource ) ; this . emit ( 'load' , { viewer : this , fetchUrl , resource } ) ; } else { this . emit ( 'error' , { viewer : this , error : 'Failed to fetch a generic resource' , fetchUrl } ) ; } this . emit ( 'loadend' , { viewer : this , fetchUrl , resource } ) ; this . checkLoadingStatus ( ) ; return resource ; } ) ; this . promiseMap . set ( fetchUrl , fetchPromise ) ; this . emit ( 'loadstart' , { viewer : this , fetchUrl } ) ; return fetchPromise ; } /**
     * Unload a resource.
     * Note that this only removes the resource from the viewer's cache.
     * If it's being referenced and used e.g. by a scene, it will not be garbage collected.
     */ unload ( resource ) { const fetchUrl = resource . fetchUrl ; if ( fetchUrl !== '' ) { this . resourceMap . delete ( fetchUrl ) ; } const resources = this . resources ; const index = resources . indexOf ( resource ) ; if ( index !== - 1 ) { resources . splice ( index , 1 ) ; return true ; } return false ; } /**
     * Starts loading a new empty resource, and returns it.
     * This empty resource will block the "idle" event (and thus whenAllLoaded) until it's resolved.
     * This is used when a resource might get loaded in the future, but it is not known what it is yet.
     */ promise ( ) { const promise = Promise . resolve ( undefined ) ; const key = `${ performance . now ( ) }` ; this . promiseMap . set ( key , promise ) ; return ( ) : void => { this . promiseMap . delete ( key ) ; this . checkLoadingStatus ( ) ; } ; } checkLoadingStatus ( ) { if ( this . promiseMap . size === 0 ) { setTimeout ( ( ) => this . emit ( 'idle' ) , 0 ) ; } } /**
     * Wait for all of the resources to load.
     *
     * If a callback is given, it will be called, otherwise, a promise is returned.
     */ whenAllLoaded ( ) ; whenAllLoaded ( callback ) ; whenAllLoaded ( callback ? ) { const promise = new Promise ( ( resolve ) => { if ( this . promiseMap . size === 0 ) { resolve ( this ) ; } else { this . once ( 'idle' , ( ) => resolve ( this ) ) ; } } ) ; if ( callback ) { promise . then ( ( ) => callback ( this ) ) ; } else { return promise ; } } /**
     * Get a blob representing the contents of the viewer's canvas.
     *
     * If a callback is given, it will be called, otherwise, a promise is returned.
     */ toBlob ( ) ; toBlob ( callback ) ; toBlob ( callback ? ) { const promise = new Promise ( ( resolve ) => this . canvas . toBlob ( ( blob ) => resolve ( blob ) ) ) ; if ( callback ) { promise . then ( ( blob ) => callback ( blob ) ) ; } else { return promise ; } } /**
     * Update and render a frame.
     */ updateAndRender ( dt = 1000 / 60 ) { this . update ( dt ) ; this . startFrame ( ) ; this . render ( ) ; } /**
     * Update all of the scenes, which includes updating their cameras, audio context if one exists, and all of the instances they hold.
     */ update ( dt = 1000 / 60 ) { dt *= 0.001 ; this . frame += 1 ; this . visibleCells = 0 ; this . visibleInstances = 0 ; this . updatedParticles = 0 ; for ( const scene of this . scenes ) { scene . update ( dt ) ; this . visibleCells += scene . visibleCells ; this . visibleInstances += scene . visibleInstances ; this . updatedParticles += scene . updatedParticles ; } } /**
     * Clears the WebGL buffer.
     *
     * Called automatically by updateAndRender().
     *
     * Call this at some point before render() if you need more control.
     */ startFrame ( ) { const gl = this . gl ; gl . depthMask ( true ) ; gl . clearColor ( 0 , 0 , 0 , 1 ) ; gl . clear ( gl . COLOR_BUFFER_BIT | gl . DEPTH_BUFFER_BIT ) ; } /**
     * Render.
     */ render ( ) { for ( const scene of this . scenes ) { scene . render ( ) ; } } /**
     * Clear all of the emitted objects in this viewer.
     */ clearEmittedObjects ( ) { for ( const scene of this . scenes ) { scene . clearEmittedObjects ( ) ; } } }