ClippingPlane.js

import {
  createLocalStorageCheckbox,
  createLocalStorageDetails,
} from '@ud-viz/utils_browser';

import { TransformControls } from 'three/examples/jsm/controls/TransformControls';
import { PlanarView } from 'itowns';
import {
  Vector3,
  Matrix3,
  Plane,
  PlaneGeometry,
  Mesh,
  MeshBasicMaterial,
  DoubleSide,
} from 'three';

/**
 * Representing a clipping plane in a 3D visualization
 * UI controls for manipulating the plane (managing its visibility and transformation)
 */
export class ClippingPlane {
  constructor(itownsView) {
    /** @type {Plane} */
    this.plane = new Plane();

    /** @type {PlanarView} */
    this.itownsView = itownsView;

    /** @type {HTMLDivElement} */
    this.domElement = document.createElement('div');

    /** @type {HTMLDetailsElement}*/
    this.details = null;
    /** @type {HTMLInputElement}*/
    this.planeVisible = null;
    /** @type {HTMLInputElement}*/
    this.clippingEnable = null;
    /** @type {Array<HTMLButtonElement>}*/
    this.buttons = {};

    /** @type {TransformControls} */
    this.transformControls = null;

    /** @type {Mesh} */
    this.quad = null;

    this.initUI();
    this.initQuad();
    this.initTransformControls();
    this.initCallback();
  }

  initUI() {
    this.details = createLocalStorageDetails(
      'clipping_plane_local_storage_key_point_cloud_visualizer',
      'Clipping plane',
      this.domElement
    );

    this.planeVisible = createLocalStorageCheckbox(
      'plane_visibility_key_loacal_storage',
      'Visible: ',
      this.details,
      false
    );

    this.clippingEnable = createLocalStorageCheckbox(
      'plane_enable_key_loacal_storage',
      'Enable: ',
      this.details
    );

    const mode = ['translate', 'rotate', 'scale'];
    mode.forEach((m) => {
      const buttonMode = document.createElement('button');
      buttonMode.innerText = m;
      buttonMode.mode = m;
      this.buttons[m + 'Button'] = buttonMode;
      this.details.appendChild(buttonMode);
    });
  }

  initQuad() {
    this.quad = new Mesh(
      new PlaneGeometry(),
      new MeshBasicMaterial({
        opacity: 0.5,
        transparent: true,
        side: DoubleSide,
      })
    );

    this.quad.visible = false;
    this.quad.name = 'quadOfClippingPlane';
  }

  update() {
    this.plane.normal.copy(
      new Vector3(0, 0, 1).applyNormalMatrix(
        new Matrix3().getNormalMatrix(this.quad.matrixWorld)
      )
    );
    // quad.position belongs to plane => quad.position.dot(plane.normal) = -constant
    this.plane.constant = -(
      this.plane.normal.x * this.quad.position.x +
      this.plane.normal.y * this.quad.position.y +
      this.plane.normal.z * this.quad.position.z
    );
  }

  initTransformControls() {
    this.transformControls = new TransformControls(
      this.itownsView.camera.camera3D,
      this.itownsView.mainLoop.gfxEngine.label2dRenderer.domElement
    );

    this.transformControls.addEventListener('change', this.update.bind(this));

    this.update();

    this.itownsView.scene.add(this.transformControls);
  }

  initCallback() {
    this.planeVisible.addEventListener('change', (event) => {
      this.quad.visible = event.target.checked;
      if (this.quad.visible) {
        this.transformControls.attach(this.quad);
      } else {
        this.transformControls.detach();
      }
      this.transformControls.updateMatrixWorld();
      this.itownsView.notifyChange();
    });

    this.clippingEnable.addEventListener('change', (event) => {
      this.itownsView.mainLoop.gfxEngine.renderer.localClippingEnabled =
        event.target.checked;
      this.itownsView.notifyChange();
    });

    for (const key in this.buttons) {
      const button = this.buttons[key];
      button.addEventListener('click', () => {
        this.transformControls.setMode(button.mode);
        this.planeVisible.dispatchEvent(new Event('change'));
      });
    }
  }
}