Source: ui/widget/manipulation-handles.js

import { VRSPACEUI } from "../vrspace-ui.js";
import { World } from "../../world/world.js";
/**
 * Plane manipulation UI: adds handles around a plane, and installs pointer drag observable to the scene.
 * Top and bottom handles are used to move the plane around, with 6DOF.
 * Left and right handles resize the plane.
 * If this.canMinimize is set, also adds a top-right box that disables plane's parent and all of it's children, 
 * except ones specified in dontMinimize.
 * Material and parent are taken from the plane.
 * While initially intended to be used for manipulation of a plane, this can be used to manipulate any mesh.
 */
export class ManipulationHandles {
  /**
   * Create the control.
   * @param plane mesh to manipulate
   * @param width mesh width, determines how wide are handles
   * @param height mesh height, how far up and down are handles
   * @param scene babylon scene
   */
  constructor(plane, width, height, scene) {
    this.plane = plane;
    this.material = plane.material;
    this.width = width;
    this.height = height;
    this.group = plane.parent;
    this.scene = scene;
    this.segments = 8;
    this.canMinimize = true;
    this.canClose = false;
    this.onClose = null;
    this.minimized = false;
    this.dontMinimize = [];
    this.sizeCallback = null;
    this.positionCallback = null;

    this.selectedMaterial = new BABYLON.StandardMaterial("selectedMaterial", this.scene);
    this.selectedMaterial.alpha = this.material.alpha;
    this.selectedMaterial.diffuseColor = new BABYLON.Color3(.2, .5, .2);

    this.alertMaterial = new BABYLON.StandardMaterial("alertMaterial", this.scene);
    this.alertMaterial.alpha = this.material.alpha;
    this.alertMaterial.diffuseColor = new BABYLON.Color3(.3, 0, 0);
    /** minimization button TODO rename */
    this.box = null;
    this.closeButton = null;
    /** Callback on area minimized/maximized, passed a minimized/hidden flag */
    this.onMinMax = null;
  }
  /**
   * Creates manipulation handles. 
   * Left and right handle resize, and top and bottom move it, optional box disables/reenables everything.
   */
  show() {
    let handleWidth = this.height / 25;
    this.leftHandle = BABYLON.MeshBuilder.CreateSphere("leftHandle", { segments: this.segments }, this.scene);
    this.leftHandle.scaling = new BABYLON.Vector3(handleWidth, this.height, handleWidth);
    this.leftHandle.position = new BABYLON.Vector3(-this.width / 2 - this.width / 20, 0, 0);
    this.leftHandle.parent = this.group;
    this.leftHandle.material = this.material;

    this.rightHandle = BABYLON.MeshBuilder.CreateSphere("rightHandle", { segments: this.segments }, this.scene);
    this.rightHandle.scaling = new BABYLON.Vector3(handleWidth, this.height, handleWidth);
    this.rightHandle.position = new BABYLON.Vector3(this.width / 2 + this.width / 20, 0, 0);
    this.rightHandle.parent = this.group;
    this.rightHandle.material = this.material;

    this.topHandle = BABYLON.MeshBuilder.CreateSphere("topHandle", { segments: this.segments }, this.scene);
    this.topHandle.scaling = new BABYLON.Vector3(this.width, handleWidth, handleWidth);
    this.topHandle.position = new BABYLON.Vector3(0, this.height / 2 + this.height / 20, 0);
    this.topHandle.parent = this.group;
    this.topHandle.material = this.material;

    this.bottomHandle = BABYLON.MeshBuilder.CreateSphere("bottomHandle", { segments: this.segments }, this.scene);
    this.bottomHandle.scaling = new BABYLON.Vector3(this.width, handleWidth, handleWidth);
    this.bottomHandle.position = new BABYLON.Vector3(0, -this.height / 2 - this.height / 20, 0);
    this.bottomHandle.parent = this.group;
    this.bottomHandle.material = this.material;

    if (this.canMinimize) {
      // CHECKME 1 may be too small on mobiles
      this.box = BABYLON.MeshBuilder.CreatePlane("MinMaxButon", { width: 1, height: 1 }, this.scene);
      this.box.scaling = new BABYLON.Vector3(handleWidth, handleWidth, this.height / 100);
      this.box.position = new BABYLON.Vector3(-this.width / 2 - this.width / 20, -this.height / 2 - this.height / 20, 0);
      this.box.parent = this.group;
      this.box.material = this.material.clone();
      this.box.material.diffuseTexture = new BABYLON.Texture(VRSPACEUI.contentBase + "/content/icons/minimize.png", this.scene);
      this.box.material.diffuseTexture.hasAlpha = true;
      this.box.material.emissiveColor = BABYLON.Color3.White();
    }
    if (this.canClose) {
      this.closeButton = BABYLON.MeshBuilder.CreatePlane("MinMaxButon", { width: 1, height: 1 }, this.scene);
      this.closeButton.scaling = new BABYLON.Vector3(handleWidth, handleWidth, this.height / 100);
      this.closeButton.position = new BABYLON.Vector3(this.width / 2 + this.width / 20, -this.height / 2 - this.height / 20, 0);
      this.closeButton.parent = this.group;
      this.closeButton.material = this.material.clone();
      this.closeButton.material.diffuseTexture = new BABYLON.Texture(VRSPACEUI.contentBase + "/content/icons/close.png", this.scene);
      this.closeButton.material.diffuseTexture.hasAlpha = true;
      this.closeButton.material.emissiveColor = BABYLON.Color3.White();

    }

    this.bottomHandle.opposite = this.topHandle;
    this.topHandle.opposite = this.bottomHandle;
    this.leftHandle.opposite = this.rightHandle;
    this.rightHandle.opposite = this.leftHandle;

    this.handles = [this.leftHandle, this.topHandle, this.rightHandle, this.bottomHandle];

    this.resizeHandler = this.scene.onPointerObservable.add((pointerInfo) => {
      if (pointerInfo.type == BABYLON.PointerEventTypes.POINTERDOWN) {
        //if ( pointerInfo.pickInfo.hit && this.handles.includes(pointerInfo.pickInfo.pickedMesh) ) {
        if (pointerInfo.pickInfo.hit) {
          if (pointerInfo.pickInfo.pickedMesh == this.bottomHandle || pointerInfo.pickInfo.pickedMesh == this.topHandle) {
            // moving around
            if (!this.behavior) {
              this.behavior = this.createBehavior();
              // does not work if group.parent is camera
              this.group.addBehavior(this.behavior);
              pointerInfo.pickInfo.pickedMesh.material = this.selectedMaterial;
              this.selectedHandle = pointerInfo.pickInfo.pickedMesh;
            }
          } else if (pointerInfo.pickInfo.pickedMesh == this.leftHandle || pointerInfo.pickInfo.pickedMesh == this.rightHandle) {
            // scaling
            if (!this.selectedHandle) {
              this.selectedHandle = pointerInfo.pickInfo.pickedMesh;
              this.point = pointerInfo.pickInfo.pickedPoint;
              pointerInfo.pickInfo.pickedMesh.material = this.selectedMaterial;
            }
          } else if (pointerInfo.pickInfo.pickedMesh == this.box) {
            // minimizing/maximizing (hiding/showing)
            this.hide(!this.minimized);
          } else if (pointerInfo.pickInfo.pickedMesh == this.closeButton) {
            if (this.onClose) {
              this.onClose();
            }
          }
        } else if (this.selectedHandle) {
          this.selectedHandle.material = this.material;
          this.selectedHandle = null;
          if (this.behavior) {
            this.group.removeBehavior(this.behavior);
            this.behavior = null;
          }
        }
      }
      if (pointerInfo.type == BABYLON.PointerEventTypes.POINTERUP && this.selectedHandle) {
        if (pointerInfo.pickInfo.hit && (pointerInfo.pickInfo.pickedMesh == this.leftHandle || pointerInfo.pickInfo.pickedMesh == this.rightHandle)) {
          let diff = pointerInfo.pickInfo.pickedPoint.y - this.point.y;
          let scale = (this.height + diff) / this.height;
          this.group.scaling = this.group.scaling.scale(scale);
          if (this.sizeCallback) {
            this.sizeCallback(this.group.scaling);
          }
        }
        if (this.selectedHandle) {
          this.selectedHandle.material = this.material;
          this.selectedHandle = null;
          if (this.behavior) {
            this.group.removeBehavior(this.behavior);
            this.behavior = null;
            if (this.positionCallback) {
              this.positionCallback(this.group.position, this.group.rotationQuaternion.toEulerAngles());
              //this.positionCallback(this.group.position, this.group.rotationQuaternion);
            }
          }
        }
      }
    });

  }

  createBehavior() {
    if (World.lastInstance.inXR()) {
      return new BABYLON.SixDofDragBehavior();
    }
    //if ( this.group.billboardMode == BABYLON.Mesh.BILLBOARDMODE_Y ) {
    return new BABYLON.PointerDragBehavior({ dragPlaneNormal: new BABYLON.Vector3(0, 0, 1) });
    //return new BABYLON.PointerDragBehavior({ dragAxis: new BABYLON.Vector3(0, 1, 0) });
  }

  /**
   * Minimize or maximize (hide or show all children of this.group)
   * @param flag boolean indicating whether to hide or show children
   */
  hide(flag) {
    if (this.canMinimize) {
      //console.log("Hiding handles: "+flag);
      this.group.getChildMeshes().forEach(h => {
        if (h !== this.box && !this.dontMinimize.includes(h)) {
          h.setEnabled(!flag);
        }
      });
      this.minimized = flag;
      if (this.minimized) {
        this.box.material.diffuseTexture = new BABYLON.Texture(VRSPACEUI.contentBase + "/content/icons/maximize.png", this.scene);
      } else {
        this.box.material.diffuseTexture = new BABYLON.Texture(VRSPACEUI.contentBase + "/content/icons/minimize.png", this.scene);
      }
      if (this.onMinMax) {
        this.onMinMax(this.minimized);
      }
    }
  }
  /**
   * Clean up
   */
  dispose() {
    this.scene.onPointerObservable.remove(this.resizeHandler);
    this.handles.forEach(h => h.dispose());
    if (this.box) {
      this.box.dispose();
    }
    if (this.closeButton) {
      this.closeButton.dispose();
    }
    this.selectedMaterial.dispose();
    this.alertMaterial.dispose();
  }
}