src/widgets/widgets.annotation.js

Summary

Maintainability
F
6 days
Test Coverage
import { widgetsBase } from './widgets.base';
import { widgetsHandle as widgetsHandleFactory } from './widgets.handle';

/**
 * @module widgets/annotation
 * @todo: add option to show only label (without mesh, dots and lines)
 */
const widgetsAnnotation = (three = window.THREE) => {
  if (three === undefined || three.Object3D === undefined) {
    return null;
  }

  const Constructor = widgetsBase(three);
  return class extends Constructor {
    constructor(targetMesh, controls, params = {}) {
      super(targetMesh, controls, params);

      this._widgetType = 'Annotation';

      // incoming parameters (optional: worldPosition)

      this._initialized = false; // set to true when the name of the label is entered
      this._movinglabel = null; // bool that turns true when the label is moving with the mouse
      this._labelmoved = false; // bool that turns true once the label is moved by the user (at least once)
      this._labelhovered = false;
      this._manuallabeldisplay = false; // Make true to force the label to be displayed

      // mesh stuff
      this._material = null;
      this._geometry = null;
      this._meshline = null;
      this._cone = null;

      // dom stuff
      this._line = null;
      this._dashline = null;
      this._label = null;
      this._labeltext = null;

      // var
      this._labelOffset = new three.Vector3(); // difference between label center and second handle
      this._mouseLabelOffset = new three.Vector3(); // difference between mouse coordinates and label center

      // add handles
      this._handles = [];

      let handle;
      const WidgetsHandle = widgetsHandleFactory(three);
      for (let i = 0; i < 2; i++) {
        handle = new WidgetsHandle(targetMesh, controls, params);
        this.add(handle);
        this._handles.push(handle);
      }
      this._handles[1].active = true;

      this.create();
      this.initOffsets();

      this.onResize = this.onResize.bind(this);
      this.onMove = this.onMove.bind(this);
      this.onHoverlabel = this.onHoverlabel.bind(this);
      this.notonHoverlabel = this.notonHoverlabel.bind(this);
      this.changelabeltext = this.changelabeltext.bind(this);

      this.addEventListeners();
    }

    addEventListeners() {
      window.addEventListener('resize', this.onResize);

      this._label.addEventListener('mouseenter', this.onHoverlabel);
      this._label.addEventListener('mouseleave', this.notonHoverlabel);
      this._label.addEventListener('dblclick', this.changelabeltext);

      this._container.addEventListener('wheel', this.onMove);
    }

    removeEventListeners() {
      window.removeEventListener('resize', this.onResize);

      this._label.removeEventListener('mouseenter', this.onHoverlabel);
      this._label.removeEventListener('mouseleave', this.notonHoverlabel);
      this._label.removeEventListener('dblclick', this.changelabeltext);

      this._container.removeEventListener('wheel', this.onMove);
    }

    onResize() {
      this.initOffsets();
    }

    onHoverlabel() {
      // this function is called when mouse enters the label with "mouseenter" event
      this._labelhovered = true;
      this._container.style.cursor = 'pointer';
    }

    notonHoverlabel() {
      // this function is called when mouse leaves the label with "mouseleave" event
      this._labelhovered = false;
      this._container.style.cursor = 'default';
    }

    onStart(evt) {
      if (this._labelhovered) {
        // if label hovered then it should be moved
        // save mouse coordinates offset from label center
        const offsets = this.getMouseOffsets(evt, this._container);
        const paddingPoint = this._handles[1].screenPosition.clone().sub(this._labelOffset);

        this._mouseLabelOffset = new three.Vector3(
          offsets.screenX - paddingPoint.x,
          offsets.screenY - paddingPoint.y,
          0
        );
        this._movinglabel = true;
        this._labelmoved = true;
      }

      this._handles[0].onStart(evt);
      this._handles[1].onStart(evt);

      this._active = this._handles[0].active || this._handles[1].active || this._labelhovered;

      this.update();
    }

    onMove(evt) {
      if (this._movinglabel) {
        const offsets = this.getMouseOffsets(evt, this._container);

        this._labelOffset = new three.Vector3(
          this._handles[1].screenPosition.x - offsets.screenX + this._mouseLabelOffset.x,
          this._handles[1].screenPosition.y - offsets.screenY + this._mouseLabelOffset.y,
          0
        );
        this._controls.enabled = false;
      }

      if (this._active) {
        this._dragged = true;
      }

      this._handles[0].onMove(evt);
      this._handles[1].onMove(evt);

      this._hovered = this._handles[0].hovered || this._handles[1].hovered || this._labelhovered;

      this.update();
    }

    onEnd() {
      this._handles[0].onEnd(); // First Handle

      // Second Handle
      if (this._dragged || !this._handles[1].tracking) {
        this._handles[1].tracking = false;
        this._handles[1].onEnd();
      } else {
        this._handles[1].tracking = false;
      }

      if (!this._dragged && this._active && this._initialized) {
        this._selected = !this._selected; // change state if there was no dragging
        this._handles[0].selected = this._selected;
        this._handles[1].selected = this._selected;
      }

      if (!this._initialized) {
        this._labelOffset = this._handles[1].screenPosition
          .clone()
          .sub(this._handles[0].screenPosition)
          .multiplyScalar(0.5);
        this.setlabeltext();
        this._initialized = true;
      }

      this._active = this._handles[0].active || this._handles[1].active;
      this._dragged = false;
      this._movinglabel = false;
      this.update();
    }

    setlabeltext() {
      // called when the user creates a new arrow
      while (!this._labeltext) {
        this._labeltext = prompt('Please enter the annotation text', '');
      }
      this.displaylabel();
    }

    changelabeltext() {
      // called when the user does double click in the label
      this._labeltext = prompt('Please enter a new annotation text', this._label.innerHTML);
      this.displaylabel();
    }

    displaylabel() {
      this._label.innerHTML =
        typeof this._labeltext === 'string' && this._labeltext.length > 0 // avoid error
          ? this._labeltext
          : ''; // empty string is passed or Cancel is pressed
      // show the label (in css an empty string is used to revert display=none)
      this._label.style.display = '';
      this._dashline.style.display = '';
      this._label.style.transform = `translate3D(
        ${this._handles[1].screenPosition.x - this._labelOffset.x - this._label.offsetWidth / 2}px,
        ${this._handles[1].screenPosition.y -
          this._labelOffset.y -
          this._label.offsetHeight / 2 -
          this._container.offsetHeight}px, 0)`;
    }

    create() {
      this.createMesh();
      this.createDOM();
    }

    createMesh() {
      // material
      this._material = new three.LineBasicMaterial();

      this.updateMeshColor();

      // line geometry
      this._geometry = new three.Geometry();
      this._geometry.vertices.push(this._handles[0].worldPosition);
      this._geometry.vertices.push(this._handles[1].worldPosition);

      // line mesh
      this._meshline = new three.Line(this._geometry, this._material);
      this._meshline.visible = true;

      this.add(this._meshline);

      // cone geometry
      this._conegeometry = new three.CylinderGeometry(0, 2, 10);
      this._conegeometry.translate(0, -5, 0);
      this._conegeometry.rotateX(-Math.PI / 2);

      // cone mesh
      this._cone = new three.Mesh(this._conegeometry, this._material);
      this._cone.visible = true;

      this.add(this._cone);
    }

    createDOM() {
      this._line = document.createElement('div');
      this._line.className = 'widgets-line';
      this._container.appendChild(this._line);

      this._dashline = document.createElement('div');
      this._dashline.className = 'widgets-dashline';
      this._dashline.style.display = 'none';
      this._container.appendChild(this._dashline);

      this._label = document.createElement('div');
      this._label.className = 'widgets-label';
      this._label.style.display = 'none';
      this._container.appendChild(this._label);

      this.updateDOMColor();
    }

    update() {
      this.updateColor();

      this._handles[0].update();
      this._handles[1].update();

      this.updateMeshColor();
      this.updateMeshPosition();

      this.updateDOM();
    }

    updateMeshColor() {
      if (this._material) {
        this._material.color.set(this._color);
      }
    }

    updateMeshPosition() {
      if (this._geometry) {
        this._geometry.verticesNeedUpdate = true;
      }

      if (this._cone) {
        this._cone.position.copy(this._handles[1].worldPosition);
        this._cone.lookAt(this._handles[0].worldPosition);
      }
    }

    updateDOM() {
      this.updateDOMColor();

      // update line
      const lineData = this.getLineData(
        this._handles[0].screenPosition,
        this._handles[1].screenPosition
      );

      this._line.style.transform = `translate3D(${lineData.transformX}px, ${
        lineData.transformY
      }px, 0)
        rotate(${lineData.transformAngle}rad)`;
      this._line.style.width = lineData.length + 'px';

      // update label
      const paddingVector = lineData.line.multiplyScalar(0.5);
      const paddingPoint = this._handles[1].screenPosition.clone().sub(
        this._labelmoved
          ? this._labelOffset // if the label is moved, then its position is defined by labelOffset
          : paddingVector
      ); // otherwise it's placed in the center of the line
      const labelPosition = this.adjustLabelTransform(this._label, paddingPoint);

      this._label.style.transform = `translate3D(${labelPosition.x}px, ${labelPosition.y}px, 0)`;

      // create the label without the interaction of the user. Useful when we need to create the label manually
      if (this._manuallabeldisplay) {
        this.displaylabel();
      }

      // update dash line
      let minLine = this.getLineData(this._handles[0].screenPosition, paddingPoint);
      let lineCL = this.getLineData(lineData.center, paddingPoint);
      let line1L = this.getLineData(this._handles[1].screenPosition, paddingPoint);

      if (minLine.length > lineCL.length) {
        minLine = lineCL;
      }
      if (minLine.length > line1L.length) {
        minLine = line1L;
      }

      this._dashline.style.transform = `translate3D(${minLine.transformX}px, ${
        minLine.transformY
      }px, 0)
        rotate(${minLine.transformAngle}rad)`;
      this._dashline.style.width = minLine.length + 'px';
    }

    updateDOMColor() {
      this._line.style.backgroundColor = this._color;
      this._dashline.style.borderTop = '1.5px dashed ' + this._color;
      this._label.style.borderColor = this._color;
    }

    hideDOM() {
      this._line.style.display = 'none';
      this._dashline.style.display = 'none';
      this._label.style.display = 'none';
      this._handles.forEach(elem => elem.hideDOM());
    }

    showDOM() {
      this._line.style.display = '';
      this._dashline.style.display = '';
      this._label.style.display = '';
      this._handles.forEach(elem => elem.showDOM());
    }

    free() {
      this.removeEventListeners();

      this._handles.forEach(h => {
        this.remove(h);
        h.free();
      });
      this._handles = [];

      this._container.removeChild(this._line);
      this._container.removeChild(this._dashline);
      this._container.removeChild(this._label);

      // mesh, geometry, material
      this.remove(this._meshline);
      this._meshline.geometry.dispose();
      this._meshline.geometry = null;
      this._meshline.material.dispose();
      this._meshline.material = null;
      this._meshline = null;
      this._geometry.dispose();
      this._geometry = null;
      this._material.vertexShader = null;
      this._material.fragmentShader = null;
      this._material.uniforms = null;
      this._material.dispose();
      this._material = null;
      this.remove(this._cone);
      this._cone.geometry.dispose();
      this._cone.geometry = null;
      this._cone.material.dispose();
      this._cone.material = null;
      this._cone = null;
      this._conegeometry.dispose();
      this._conegeometry = null;

      super.free();
    }

    get targetMesh() {
      return this._targetMesh;
    }

    set targetMesh(targetMesh) {
      this._targetMesh = targetMesh;
      this._handles.forEach(elem => (elem.targetMesh = targetMesh));
      this.update();
    }

    get worldPosition() {
      return this._worldPosition;
    }

    set worldPosition(worldPosition) {
      this._handles[0].worldPosition.copy(worldPosition);
      this._handles[1].worldPosition.copy(worldPosition);
      this._worldPosition.copy(worldPosition);
      this.update();
    }
  };
};

export { widgetsAnnotation };
export default widgetsAnnotation();