Source: ui/load-progress-indicator.js

import { VRSPACEUI } from './vrspace-ui.js';
import { CameraHelper } from '../core/camera-helper.js';

/**
Default progress indicator: rotating vrspace.org logo, 30 cm ahead, 5 cm below camera.
Always bounds to active camera, to ensure same look and function on PC, mobile and VR devices.
 */
export class LoadProgressIndicator {
  /** Initializes VRSpaceUI, loading logo geometry so it can be reused.
  Installs active camera listener on the scene.
  @param scene
  @param camera current camera to bind to
   */
  constructor(scene, camera = scene.activeCamera) {
    this.scene = scene;
    this.camera = camera;
    this.position = new BABYLON.Vector3(0, -0.1, 0.5);
    this.xrPosition = new BABYLON.Vector3(0, -0.2, 0.5);
    this.mesh = null;
    this.totalItems = 0;
    this.currentItem = 0;
    this.zeroRotation = null;
    this.angle = Math.PI;
    /** Debug log flag */
    this.debug = false;
    /** Whether progress of individual items should be tracked.
    Default true rotates the logo only when an item loads.
    False results in continous rotation.
    */
    this.trackItems = true;
    if (VRSPACEUI.initialized) {
      this._createIndicatorMesh()
    } else {
      VRSPACEUI.init(scene).then((ui) => this._createIndicatorMesh());
    }
    CameraHelper.getInstance(this.scene).addCameraListener(() => {
      if (this.scene.activeCamera) {
        //console.log("Camera changed: "+this.scene.activeCamera.getClassName());
        this.attachToCamera();
      }
    });
  }
  _createIndicatorMesh() {
    this.mesh = VRSPACEUI.logo.clone("LoadingProgressIndicator");
    this.mesh.scaling.scaleInPlace(0.05);
    this.attachToCamera();
    this.zeroRotation = new BABYLON.Quaternion.RotationAxis(BABYLON.Axis.X, -Math.PI / 2);
    this.mesh.rotationQuaternion = this.zeroRotation;
    this.mesh.setEnabled(this.totalItems > this.currentItem);
    this.log("Loaded logo, current progress " + this.currentItem + "/" + this.totalItems);
  }
  _init() {
    this.totalItems = 0;
    this.currentItem = 0;
    this.angle = Math.PI;
  }
  _setPosition() {
    if (this.scene.activeCamera.getClassName() == 'WebXRCamera') {
      this.mesh.position = this.xrPosition;
    } else {
      this.mesh.position = this.position;
    }
  }
  attachToCamera() {
    this.camera = this.scene.activeCamera;
    if (this.mesh) {
      this.mesh.parent = this.scene.activeCamera;
      this._setPosition();
    }
  }
  /** Add an item to be tracked. First item added shows the indicator and starts the animation.
  @param item an item to track
   */
  add(item) {
    if (this.mesh && !this.mesh.isEnabled()) {
      this.mesh.setEnabled(true);
    }
    this.totalItems++;
    this.log("Added " + this.currentItem + "/" + this.totalItems);
    this._update();
  }
  /** Remove an item, e.g. loaded file. Last item removed stops the animation and hides the indicator.
  @param item to remove
   */
  remove(item) {
    this.currentItem++;
    this._update();
    this.log("Finished " + this.currentItem + "/" + this.totalItems);
    if (this.totalItems <= this.currentItem && this.mesh) {
      this.mesh.setEnabled(false);
      if (this.animation) {
        this.scene.unregisterBeforeRender(this.animation);
        delete this.animation;
      }
      this._init();
    }
  }
  /** Stops tracking individual items and runs contionous animation */
  animate() {
    this.trackItems = false;
    this.animation = () => { this._update() };
    this.scene.registerBeforeRender(this.animation);
  }
  /** 
  Call on load progress event.
  @param evt progress event, https://doc.babylonjs.com/typedoc/interfaces/BABYLON.ISceneLoaderProgressEvent
  @param item related item, not used, subclasses may use it
  */
  progress(evt, item) {
    if (evt) {
      this.trackItems = false;
      if (evt.lengthComputable) {
        var loaded = evt.loaded / evt.total;
        this.log("Loaded " + (loaded * 100) + "%");
        if (this.mesh && this.zeroRotation) {
          this.angle += 0.01;
          this.mesh.rotationQuaternion = this.zeroRotation.multiply(new BABYLON.Quaternion.RotationAxis(BABYLON.Axis.Y, this.angle));
        }
      } else {
        var dlCount = evt.loaded / (1024 * 1024);
        this.log("Loaded " + dlCount + " MB");
      }
    }
  }
  _update() {
    if (this.mesh && this.zeroRotation) {
      this._setPosition();
      if (this.trackItems) {
        this.angle = Math.PI * (1 + this.currentItem / this.totalItems);
      } else {
        this.angle += 0.01;
      }
      this.mesh.rotationQuaternion = this.zeroRotation.multiply(new BABYLON.Quaternion.RotationAxis(BABYLON.Axis.Y, this.angle));
    }
  }
  log(something) {
    if (this.debug) {
      console.log(something);
    }
  }
  dispose() {
    if (this.mesh) {
      this.mesh.dispose();
      this.mesh = null;
    }
  }
}