Source: ui/world/portal.js

import { ServerFolder } from '../../core/server-folder.js';
import { VRSPACEUI } from '../vrspace-ui.js';
import { TextArea } from '../widget/text-area.js';

/**
Portal is an entrance to other worlds, disabled by default.
 */
export class Portal {
  /** Create a portal
  @param scene babylonjs scene
  @param {ServerFolder} serverFolder containing world class and content
  @param callback to execute when portal is activated (clicked, tapped)
  @param shadowGenerator optionally, portal can cast shadows
   */
  constructor(scene, serverFolder, callback, shadowGenerator) {
    this.scene = scene;
    /** @type {ServerFolder} */
    this.serverFolder = serverFolder;
    this.callback = callback;
    this.name = serverFolder.name;
    this.description = null;
    this.subTitle = null;
    this.alwaysShowTitle = false;
    this.imageUrl = null;
    if (serverFolder.relatedUrl()) {
      this.imageUrl = serverFolder.relatedUrl();
      this.thumbnail = new BABYLON.Texture(this.imageUrl);
    }
    this.shadowGenerator = shadowGenerator;
    this.isEnabled = false;
    this.angle = 0;
    // used in dispose:
    this.controls = [];
    this.textures = [];
    this.materials = [];
    this.soundUrl = VRSPACEUI.contentBase + "/babylon/portal/couchhero_portal-idle.mp3";
    this.soundDistance = 5;
    this.soundVolume = .5;
  }
  /** handy, returns base url and folder name */
  worldUrl() {
    return this.serverFolder.baseUrl + this.serverFolder.name;
  }
  /** dispose of everything */
  dispose() {
    this.playSound(false);
    if (this.sound) {
      this.sound.dispose();
    }
    this.group.dispose();
    if (this.thumbnail) {
      this.thumbnail.dispose();
    }
    this.material.dispose();
    for (var i = 0;i < this.controls.length;i++) {
      // CHECKME doesn's seem required
      this.controls[i].dispose();
    }
    for (var i = 0;i < this.textures.length;i++) {
      this.textures[i].dispose();
    }
    for (var i = 0;i < this.materials.length;i++) {
      this.materials[i].dispose();
    }
    if (this.pointerTracker) {
      this.scene.onPointerObservable.remove(this.pointerTracker);
      delete this.pointerTracker;
    }
  }
  /** Load and display portal at given coordinates. Copies existing portal mesh to new coordinates and angle.
  @param x
  @param y
  @param z
  @param angle
   */
  async loadAt(x, y, z, angle) {
    this.angle = angle;
    this.group = new BABYLON.TransformNode('Portal:' + this.name);
    this.group.position = new BABYLON.Vector3(x, y, z);
    this.group.rotationQuaternion = new BABYLON.Quaternion.RotationAxis(BABYLON.Axis.Y, angle);
    this.group.Portal = this;

    if (this.shadowGenerator) {
      var clone = VRSPACEUI.portal.clone();
      clone.parent = this.group;
      var meshes = clone.getChildMeshes();
      for (var i = 0;i < meshes.length;i++) {
        this.shadowGenerator.getShadowMap().renderList.push(meshes[i]);
      }
    } else {
      VRSPACEUI.copyMesh(VRSPACEUI.portal, this.group);
    }

    var plane = BABYLON.Mesh.CreatePlane("PortalEntrance:" + this.name, 1.60, this.scene);
    plane.isNearPickable = VRSPACEUI.allowHands;
    plane.parent = this.group;
    plane.position = new BABYLON.Vector3(0, 1.32, 0);
    this.pointerTracker = (e) => {
      if (e.type == BABYLON.PointerEventTypes.POINTERDOWN) {
        var p = e.pickInfo;
        if (p.pickedMesh == plane) {
          if (this.isEnabled) {
            console.log("Entering " + this.name);
            this.enter();
          } else {
            console.log("Not entering " + this.name + " - disabled");
          }
        }
      }
    };
    this.scene.onPointerObservable.add(this.pointerTracker);

    this.material = new BABYLON.StandardMaterial(this.name + "-noise", this.scene);
    plane.material = this.material;

    this.material.disableLighting = true;
    this.material.backFaceCulling = false;
    var noiseTexture = new BABYLON.NoiseProceduralTexture(this.name + "-perlin", 256, this.scene);
    this.material.lightmapTexture = noiseTexture;
    noiseTexture.octaves = 4;
    noiseTexture.persistence = 1.2;
    noiseTexture.animationSpeedFactor = 2;
    plane.visibility = 0.85;
    this.textures.push(noiseTexture);

    this.showTitle();

    this.attachSound();

    return this;
  }
  attachSound() {
    if (this.soundUrl) {
      this.sound = new BABYLON.Sound(
        "portalSound:" + this.name,
        this.soundUrl,
        this.scene, null, {
        loop: true,
        autoplay: false,
        spatialSound: true,
        streaming: false,
        distanceModel: "linear",
        maxDistance: this.soundDistance, // default 100, used only when linear
        panningModel: "equalpower" // or "HRTF"
      });
      this.sound.attachToMesh(this.group);
      this.sound.setVolume(this.soundVolume);
    }
  }
  playSound(enable) {
    if (this.sound) {
      if (enable) {
        /*
        TODO audio V2 
        VRSPACEUI.audioEngine.audioContext?.resume();
        VRSPACEUI.audioEngine.setGlobalVolume(1);
        */
        this.sound.play();
        // chrome hacks
        BABYLON.Engine.audioEngine.audioContext?.resume();
        BABYLON.Engine.audioEngine.setGlobalVolume(1);
      } else if (this.sound) {
        this.sound.stop();
      }
    }
  }
  showTitle() {
    if (!this.title && (this.isEnabled || this.alwaysShowTitle)) {
      this.title = new TextArea(this.scene, this.name, this.name);
      this.title.autoScale = true;
      this.title.addHandles = false;
      this.title.addBackground = false;
      this.title.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER;
      this.title.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
      this.title.size = 0.1;
      //this.title.group.parent = this.group;
      this.title.position = new BABYLON.Vector3(0, 2.5, 0);
      //this.title.isVisible = this.alwaysShowTitle; // TODO
      this.title.text = ""; 
      if (this.subTitle) {
        this.title.text += this.subTitle+"\n";
      }
      if (this.description) {
        this.title.text += this.description;
      }
      this.title.show();
      this.title.group.parent = this.group;
    }
  }
  setTitle(title) {
    this.subTitle = title;
    this.clearTitle();
    this.showTitle();
  }
  getTitle() {
    return this.subTitle;
  }
  clearTitle() {
    if (this.title) {
      this.title.dispose();
      this.title == null;
    }
  }
  /** Enables or disables the portal
  @param enable
   */
  enabled(enable) {
    this.isEnabled = enable;
    if (enable) {
      this.material.emissiveTexture = this.thumbnail;
      this.showTitle();
    } else {
      this.material.emissiveTexture = null;
      this.clearTitle();
    }
    this.playSound(enable);
  }
  /** Executes callback on entry */
  enter() {
    if (this.callback) {
      this.callback(this);
    }
  }
}