Source: ui/asset-loader.js

class AssetSync {
  constructor(url, scene) {
    this.url = url;
    this.scene = scene;
    this.container = null;
    this.numberOfInstances = 0;
    this.info = null;
  }
  load(callback,failure,progress) {
    this.loadAsset(callback,failure,progress);
    return this.promise;
  }
  async loadAsset(callback,failure,progress) {
    if ( this.promise ) {
      await this.promise;
      this.numberOfInstances++;
      this.instantiate(callback);
      return this.promise;
    }
    this.promise = new Promise( (resolve, reject) => {
      if ( this.container ) {
        resolve();
      } else {
        this.numberOfInstances++;
        console.log('loading sync '+this.url+" "+this.numberOfInstances);
        // load
        var path = "";
        var file = this.url;
        var pos = this.url.lastIndexOf('/');
        if ( pos >= 0 ) {
          path = this.url.substring(0,pos+1);
          file = this.url.substring(pos+1);
        }
        var plugin = BABYLON.SceneLoader.LoadAssetContainer(path, file, this.scene, (container) =>
          {
            console.log("Loaded asset "+this.url);
            //var root = container.createRootMesh();
            this.container = container;
            //container.addAllToScene();
            if ( callback ) {
              try {
                callback(this.url, container, this.info);                
              } catch ( err ) {
                console.log( "Error in callback for "+this.url, err);
              }
            }
            resolve(container);
          },
          (evt, name) => {if ( progress ) progress(evt, name)}, 
          (scene, message, exception)=>{
            if ( failure ) {
              failure(exception);
            } else {
              console.log(message, exception);
            }
            reject(exception);
          }
        );
        plugin.onParsedObservable.add(gltfBabylon => {
            var manifest = gltfBabylon.json;
            this.info = manifest.asset.extras;
            console.log("Asset extra info: ",this.info);
        });
      }
    });
    return this.promise;
  }
  instantiate(callback) {
    console.log('instantiating '+this.numberOfInstances+" of "+this.url);
    // instantiate
    // make sure to enable before instantiating, or we may get invisible copy
    var isEnabled = this.container.meshes[0].isEnabled();
    this.container.meshes.forEach(mesh=>mesh.setEnabled(true));
    var instances = this.container.instantiateModelsToScene();
    this.container.meshes[0].setEnabled(isEnabled);
    console.log("Instantiated "+this.numberOfInstances+" of "+this.url);
    try {
      callback(this.url, this.container, this.info, instances);
    } catch ( err ) {
      console.log( "Error in callback for "+this.url, err);
    }
  }
}

/** 
Loads assets from GLTF files and keeps references, creates clones of already loaded assets.
 */
export class AssetLoader {
  constructor(scene) {
    this.scene=scene;
    // contains asset containers - name and number of used instances
    this.containers={};
    this.debug=true;
  }
  /**
   * Load or instantiate an AssetContainer from given URL. Used to load avatars, all heavy lifting is done elsewhere.
   * @param url URL to load from
   * @param callback optional callback to be executed after successfull load; takes url, AssetContainer and GLTF metadata parameters
   * @param failure optional callback to be called with exception
   * @param progress optional ProgressIndicator instance
   * @returns promise 
   */
  async loadAsset( url, callback, failure, progress ) {
    await this.createAsset(url);
    return this.containers[url].load(callback, failure, progress);
  }
  /** Internal */
  async createAsset(url) {
    if ( !this.containers[url] ) {
      console.log("Creating asset "+url);
      this.containers[url] = new AssetSync(url, this.scene);
    }
  }
  /**
  Load or instantiate mesh of a VRObject: if loaded, create a container root and add all objects to scene;
  if already loaded, instantiate.
  Utility method that calls this.loadAsset() and post-processes the result to match VRObject and Mesh.
  Called from WorldManager for pretty everything except avatars.
  @param obj VRObject to load
  @param callback function executed on success
  @param failure function executed on failure
   */
  loadObject(obj, callback, failure, progress) {
    this.loadAsset(
      obj.mesh, 
      (loadedUrl, container, info, instantiatedEntries) => {
        if ( instantiatedEntries ) {
          obj.instantiatedEntries = instantiatedEntries;
    
          // Adds all elements to the scene
          var mesh = obj.instantiatedEntries.rootNodes[0];
          mesh.VRObject = obj;
          mesh.name = obj.mesh;
          mesh.scaling = new BABYLON.Vector3(1,1,1);
          mesh.refreshBoundingInfo();
          mesh.id = obj.className+" "+obj.id;
    
          console.log("Instantiated "+this.numberOfInstances+" of "+obj.mesh, obj);
    
          callback(mesh);
    
        } else {
          var mesh = container.createRootMesh();
          container.addAllToScene();
          
          // Adds all elements to the scene
          mesh.VRObject = obj;
          mesh.name = obj.mesh;
          mesh.id = obj.className+" "+obj.id;
          
          obj.container = container;
          
          console.log("Loaded "+obj.mesh);
          
          callback(mesh);
        }
      },
      failure,
      progress
    );
  }
  
  /**
  Remove a loaded VRObject from the scene.
  @param obj VRObject to remove
  @returns number of remaining instances
   */
  unloadObject(obj) {
    if ( this.containers[obj.mesh] ) {
      // loaded by asset loader
      console.log("Unloading object ",obj);
      var container = this.containers[obj.mesh];
      this.unloadAsset(obj.mesh, obj.instantiatedEntries);
      return container.numberOfInstances;
    } else if ( obj.container ) {
      // TODO remove after refactoring
      // legacy, loaded by some other component (avatar.js)
      console.log("FIXME: disposing of "+obj.id);
      this.disposeOfContainer(obj.container);
      return 0;
    }
  }
  unloadAsset(url, instantiatedEntries) {
    if ( this.containers[url] ) {
      // loaded by asset loader
      var asset = this.containers[url];
      var container = this.containers[url].container;
      asset.numberOfInstances--;
      if ( instantiatedEntries ) {
        console.log("Removing an instance of "+url+", "+asset.numberOfInstances+" remaining");
        instantiatedEntries.rootNodes.forEach( node => node.dispose() );
        instantiatedEntries.skeletons.forEach( node => node.dispose() );
        instantiatedEntries.animationGroups.forEach( node => node.dispose() );
      } else {
        console.log("Disabling main instance of "+url+", "+asset.numberOfInstances+" remaining");
      // well we can't dispose of container just like that
        container.meshes[0].setEnabled(false);
      }
      if ( asset.numberOfInstances == 0 ) {
        console.log("Unloaded "+url);
        this.disposeOfContainer(container);
        delete this.containers[url];
      }
    }
    // TODO else error
  }
  // workaround for https://forum.babylonjs.com/t/assetcontainer-dispose-throws-typeerror-r-metadata-is-null/30360
  disposeOfContainer(container) {
    try {
      container.dispose();
    } catch ( error ) {
      console.log("Failed to dispose of container", error);
    }
  }
  /** 
  Returns all currently loaded assets, with spatial coordinates of all instances.
   */
  dump() {
    var dump = {};
    this.scene.rootNodes.forEach( (node) => {
      var url = node.name;
      // CHECKME: do we want also to return user avatars? (starts with Client)
      if ( node.id.startsWith("VRObject") && this.containers[url] ) {
        if ( ! dump[url] ) {
          dump[url] = {
            info: this.containers[url].info,
            numberOfInstances: this.containers[url].numberOfInstances,
            instances: []
          };
        } 
        var vrObject = node.VRObject;
        var obj = {
          id: vrObject.id,
          position: vrObject.position,
          rotation: vrObject.rotation,
          scale: vrObject.scale
        };
        dump[url].instances.push(obj);
      }
    });
    return dump;
  }
}