index.js

import { MAIN_LOOP_EVENTS } from 'itowns';
import { Scene, PointsMaterial, Box3, Raycaster } from 'three';
import {
  computeNearFarCamera,
  RequestAnimationFrameProcess,
} from '@ud-viz/utils_browser';

import { ClippingPlane } from './ClippingPlane';
import { TargetOrbitControlMesh } from './TargetOrbitControlMesh';
import { ViewManager } from './ViewManager';
import { LayerManager } from './LayerManager';
import { setUpCameraDefaults } from './cameraSetup';
import { setupLoadingUI, setUpSpeedControls } from './uiSetup';
import { Measure } from './Measure';

/**
 * @typedef {object} LayerConfig
 * @property {string} name - Name of the C3DTilesLayer.
 * @property {object} source - Source of the C3DTilesLayer.
 * @property {string} source.url - URL to the tileset.json.
 */

/**
 * Visualizer class for rendering 3D Tiles and managing interactions.
 *
 * @param {import("itowns").Extent} extent - The extent of the area being visualized in the itowns framework.
 * @param {Array<LayerConfig>} layerConfigs - Configuration parameters for 3DTiles layers.
 * @param {object} options - Configuration options for the visualizer.
 * @param {HTMLElement} options.parentDomElement - The DOM element where the planar view will be appended.
 * @param {string} options.domElementClass - CSS class to apply to the main DOM element.
 * @param {Array<string>} options.c3DTilesLoadingDomElementClasses - CSS classes for loading indicators.
 * @param {object} options.camera - Camera configuration options.
 * @param {object} options.camera.default - Default camera settings.
 * @param {object} options.camera.default.position - Default position for the camera.
 * @param {number} options.maxSubdivisionLevel - Maximum level of detail for base map texture.
 * @param {number} options.defaultPointCloudSize - Default size for point cloud points.
 * @param {number} [options.raycasterPointsThreshold=Visualizer.RAYCASTER_POINTS_THRESHOLD] - Threshold for raycaster points.
 * @param {number} [options.measure] - If true, initializes the measure tool.
 *
 * The constructor sets up the visualization environment, including initializing scenes,
 * camera settings, layer management, and UI components. It also handles the rendering loop
 * and event listeners for user interactions.
 *
 * Note: options.camera.default.quaternion is not available option since the camera
 * is initialized as pointing towards the center of the bounding box of the
 * observed 3DTiles.
 */
export class Visualizer {
  constructor(extent, layerConfigs, options = {}) {
    /** @type {Raycaster} */
    this.raycaster = new Raycaster();

    /** @type {ViewManager} */
    this.viewManager = new ViewManager(extent, options);

    /** @type {Scene} */
    this.topScene = new Scene(); // Scene for additional rendering layers
    this.itownsView.mainLoop.gfxEngine.renderer.autoClear = false; // Prevent automatic clearing of the renderer

    // Custom render function to manage the rendering order of scenes
    this.itownsView.render = () => {
      this.itownsView.mainLoop.gfxEngine.renderer.clear(); // Clear buffers before rendering
      this.itownsView.mainLoop.gfxEngine.renderer.render(
        this.itownsView.scene,
        this.itownsView.camera.camera3D
      ); // Render the main scene
      this.itownsView.mainLoop.gfxEngine.renderer.clearDepth(); // Clear depth buffer
      this.itownsView.mainLoop.gfxEngine.renderer.render(
        this.topScene,
        this.itownsView.camera.camera3D
      ); // Render the top scene
    };

    /** @type {ClippingPlane} */
    this.clippingPlane = new ClippingPlane(this.itownsView);

    // Init layer manager
    this.layerManager = new LayerManager(
      layerConfigs,
      this.itownsView,
      new PointsMaterial({
        size: options.defaultPointCloudSize || Visualizer.DEFAULT_POINT_SIZE,
        vertexColors: true,
        clippingPlanes: [this.clippingPlane.plane], // Set clipping planes for the material
      })
    );

    // Create mesh for target orbit controls
    this.targetOrbitControlsMesh = new TargetOrbitControlMesh(
      this.viewManager.orbitControls,
      this.itownsView.camera.camera3D,
      this.layerManager
    );
    this.topScene.add(this.targetOrbitControlsMesh.mesh); // Add mesh to the top scene

    // Set up default camera settings
    setUpCameraDefaults(
      this.viewManager.itownsView,
      this.viewManager.orbitControls,
      this.layerManager.layers,
      options.camera
    );

    // Set up loading UI for C3DTiles
    this.c3DTilesLoadingDomElement = setupLoadingUI(
      this.viewManager.domElement,
      this.layerManager.layers,
      this.viewManager.itownsView,
      options
    );

    // Set up controls for adjusting zoom speed
    this.domElementSpeedControls = setUpSpeedControls(
      this.viewManager.orbitControls
    );

    // Add global bounding box mesh to the scene which embed all 3Dtiles layers
    this.viewManager.itownsView.scene.add(this.layerManager.globalBBMesh);

    /** @type {Measure} */
    this.measure = null;
    if (options.measure) {
      this.measure = new Measure(
        this.viewManager.itownsView,
        this.layerManager,
        this.viewManager.itownsView.mainLoop.gfxEngine.label2dRenderer.domElement
      );

      this.topScene.add(this.measure.group);

      window.addEventListener('keyup', (event) => {
        if (event.key == 'Escape') {
          this.measure.leaveMeasureMode();
        }
      });

      this.measure.update(this.itownsView);
    }

    // Add clipping plane to the scene
    this.viewManager.itownsView.scene.add(this.clippingPlane.quad);

    // Set position and scale for the clipping plane
    this.clippingPlane.quad.position.set(
      extent.center().x,
      extent.center().y,
      300
    );
    this.clippingPlane.quad.scale.set(1000, 1000, 1000);

    const transformControlsProcess = new RequestAnimationFrameProcess(30); // Process to manage transform control of the clipping plane

    // Event listener for dragging changes on the clipping plane
    this.clippingPlane.transformControls.addEventListener(
      'dragging-changed',
      (event) => {
        this.viewManager.orbitControls.enabled = !event.value;
      }
    );

    // Start rendering updates for the clipping plane when it's visible
    transformControlsProcess.start(() => {
      if (!this.clippingPlane.quad.visible) return;
      this.clippingPlane.transformControls.updateMatrixWorld();
      this.viewManager.itownsView.render();
    });

    // Add a frame requester to dynamically compute near and far planes for the camera
    this.viewManager.itownsView.addFrameRequester(
      MAIN_LOOP_EVENTS.AFTER_CAMERA_UPDATE,
      () => {
        const bb = new Box3().setFromObject(this.viewManager.itownsView.scene);
        computeNearFarCamera(
          this.viewManager.itownsView.camera.camera3D,
          bb.min,
          bb.max
        );
      }
    );

    // Notify changes to the view manager to trigger a redraw
    this.viewManager.itownsView.notifyChange(
      this.viewManager.itownsView.camera.camera3D
    );
  }

  /**
   * Getter for the itowns view instance.
   *
   * @returns {object} The current itowns view.
   */
  get itownsView() {
    return this.viewManager.itownsView;
  }

  /**
   * Getter for orbit controls.
   *
   * @returns {object} The current orbit controls.
   */
  get orbitControls() {
    return this.viewManager.orbitControls;
  }

  /**
   * Getter for the layer manager's layers.
   *
   * @returns {Array<object>} The layers managed by the layer manager.
   */
  get layers() {
    return this.layerManager.layers;
  }

  /**
   * Getter for the DOM element used in target dragging.
   *
   * @returns {HTMLElement} The target drag element.
   */
  get domElementTargetDragElement() {
    return this.targetOrbitControlsMesh.domElement;
  }

  /**
   * Getter for the DOM element associated with the measure tool.
   *
   * @returns {HTMLElement} The measure tool's DOM element.
   */
  get measureDomElement() {
    return this.measure.domElement;
  }

  /**
   * Getter for the clipping plane details.
   *
   * @returns {object} The details of the clipping plane.
   */
  get clippingPlaneDetails() {
    return this.clippingPlane.details;
  }

  /**
   * Default size for points in the point cloud visualization.
   *
   * @returns {number} The default point size.
   */
  static get DEFAULT_POINT_SIZE() {
    return 0.03;
  }

  /**
   * Key for storing target information in local storage.
   *
   * @returns {string} The key for target storage.
   */
  static get TARGET_LOCAL_STORAGE_KEY() {
    return 'target_local_storage_key_point_cloud_visualizer';
  }

  /**
   * Key for storing camera settings in local storage.
   *
   * @returns {string} The key for camera storage.
   */
  static get CAMERA_LOCAL_STORAGE_KEY() {
    return 'camera_local_storage_key_point_cloud_visualizer';
  }

  /**
   * Key for storing clipping plane settings in local storage.
   *
   * @returns {string} The key for clipping plane storage.
   */
  static get CLIPPING_PLANE_LOCAL_STORAGE_KEY() {
    return 'clipping_plane_local_storage_key_point_cloud_visualizer';
  }

  /**
   * Threshold for the raycaster points.
   *
   * @returns {number} The threshold value.
   */
  static get RAYCASTER_POINTS_THRESHOLD() {
    return 0.01;
  }
}