TargetOrbitControlMesh.js

import { Camera, Vector3, Mesh, MeshBasicMaterial, BoxGeometry } from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { RequestAnimationFrameProcess } from '@ud-viz/utils_browser';
import { LayerManager } from './LayerManager';

export class TargetOrbitControlMesh {
  /**
   * Constructor to initialize the camera controller and related UI elements.
   * Sets up the orbit controls, camera mesh, and initializes the UI for interaction.
   * Adds an event listener on the orbit controls to update the mesh when changes occur.
   *
   * @param {OrbitControls} orbitControls - The orbit controls used to manipulate the camera.
   * @param {Camera} camera3D - The 3D camera being controlled.
   * @param {LayerManager} layerManager - A manager handling different layers in the scene.
   */
  constructor(orbitControls, camera3D, layerManager) {
    /** @type {HTMLElement} */
    this.domElement = document.createElement('div');

    /** @type {OrbitControls} */
    this.orbitControls = orbitControls;

    /** @type {Mesh} */
    this.mesh = new Mesh(
      new BoxGeometry(),
      new MeshBasicMaterial({ color: 'red' })
    );

    this.mesh.name = 'target_orbit_controls';

    this.initUI(camera3D, layerManager);

    this.orbitControls.addEventListener('change', () => {
      this.update();
    });

    this.update();
  }

  /**
   * Initializes the UI, creating a draggable element that interacts with the 3D camera and layers.
   * Adds drag-and-drop functionality to allow users to move the camera to a new position based on interaction.
   *
   * @param {Camera} camera3D - The camera that will be moved on drag end.
   * @param {LayerManager} layerManager - The layer manager that handles intersections with 3D tiles.
   */
  initUI(camera3D, layerManager) {
    const element = document.createElement('div');
    element.classList.add('drag_element');
    element.draggable = true;

    element.ondragend = (event) => {
      if (event.target === element) {
        const i = layerManager.eventTo3DTilesIntersect(event, camera3D);
        if (i) this.moveCamera(camera3D, null, i.point, 500);
      }
    };
    this.domElement.appendChild(element);
  }

  /**
   * Updates the position and scale of the mesh to reflect the current orbit target.
   *
   * This function ensures that the red target mesh follows the orbit control's target position.
   * It also adjusts the scale of the mesh based on the camera's distance to the target,
   * making the mesh appear proportional regardless of zoom level.
   */
  update() {
    this.mesh.position.copy(this.orbitControls.target.clone());
    const scale =
      this.orbitControls.object.position.distanceTo(this.orbitControls.target) /
      150;
    this.mesh.scale.set(scale, scale, scale);
    this.mesh.updateMatrixWorld();
  }

  /**
   * Smoothly moves the camera to a specified position and target over a defined duration.
   *
   * @param {Camera} camera3D - The 3D camera object to move.
   * @param {Vector3} destPosition - The destination position to move the camera to. If null, the current camera position will be used.
   * @param {Vector3} destTarget - The destination target of the orbit controls (where the camera is looking). If null, the current orbit target will be used.
   * @param {number} duration - The duration of the animation in milliseconds (default is 1500 ms).
   * @returns {Promise} - A promise that resolves when the camera has finished moving to the destination.
   *
   * The function disables the camera's orbit controls during the transition, hides the mesh (presumably representing an object in the scene),
   * and smoothly interpolates between the camera's current position and the destination. The transition is controlled using a `RequestAnimationFrameProcess`
   * that updates every 30ms. When the transition is complete, the orbit controls are re-enabled, and the mesh is made visible again.
   */
  moveCamera(camera3D, destPosition, destTarget, duration = 1500) {
    if (!destPosition) destPosition = camera3D.position.clone();
    if (!destTarget) destTarget = this.orbitControls.target.clone();
    const startCameraPosition = camera3D.position.clone();
    const startCameraTargetPosition = this.orbitControls.target.clone();

    this.mesh.visible = false;

    this.orbitControls.enabled = false;
    const process = new RequestAnimationFrameProcess(30);

    let currentDuration = 0;

    return new Promise((resolve) => {
      process.start((dt) => {
        currentDuration += dt;
        const ratio = Math.min(Math.max(0, currentDuration / duration), 1);

        camera3D.position.lerpVectors(startCameraPosition, destPosition, ratio);

        this.orbitControls.target.lerpVectors(
          startCameraTargetPosition,
          destTarget,
          ratio
        );

        this.orbitControls.update();

        if (ratio == 1) {
          this.orbitControls.enabled = true;
          this.mesh.visible = true;
          process.stop();
          resolve();
        }
      });
    });
  }
}