Source: widgets/widgets.handle.js

import { widgetsBase } from './widgets.base';
import CoreIntersections from '../core/core.intersections';

/**
 * @module widgets/handle
 */
const widgetsHandle = (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 = 'Handle';

      // incoming parameters (optional: worldPosition)
      if (params.hideHandleMesh === true) {
        this.visible = false;
      }

      // if no target mesh, use plane for FREE dragging.
      this._plane = {
        position: new three.Vector3(),
        direction: new three.Vector3(),
      };
      this._offset = new three.Vector3();
      this._raycaster = new three.Raycaster();

      this._active = false;
      this._hovered = false;
      this._tracking = false;

      this._mouse = new three.Vector2();

      this._initialized = false;

      // mesh stuff
      this._material = null;
      this._geometry = null;
      this._mesh = null;
      this._meshHovered = false;

      // dom stuff
      this._dom = null;
      this._domHovered = false;

      this._screenPosition = this.worldToScreen(this._worldPosition);

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

      // event listeners
      this.onResize = this.onResize.bind(this);
      this.onMove = this.onMove.bind(this);
      this.onHover = this.onHover.bind(this);
      this.addEventListeners();
    }

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

      this._dom.addEventListener('mouseenter', this.onHover);
      this._dom.addEventListener('mouseleave', this.onHover);

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

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

      this._dom.removeEventListener('mouseenter', this.onHover);
      this._dom.removeEventListener('mouseleave', this.onHover);

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

    onResize() {
      this.initOffsets();
    }

    onHover(evt) {
      if (evt) {
        this.hoverDom(evt);
      }

      this.hoverMesh();

      this._hovered = this._meshHovered || this._domHovered;
      this._container.style.cursor = this._hovered ? 'pointer' : 'default';
    }

    hoverMesh() {
      // check raycast intersection, do we want to hover on mesh or just css?
      let intersectsHandle = this._raycaster.intersectObject(this._mesh);
      this._meshHovered = intersectsHandle.length > 0;
    }

    hoverDom(evt) {
      this._domHovered = evt.type === 'mouseenter';
    }

    onStart(evt) {
      const offsets = this.getMouseOffsets(evt, this._container);
      this._mouse.set(offsets.x, offsets.y);

      // update raycaster
      this._raycaster.setFromCamera(this._mouse, this._camera);
      this._raycaster.ray.position = this._raycaster.ray.origin;

      if (this._hovered) {
        this._active = true;
        this._controls.enabled = false;

        if (this._targetMesh) {
          let intersectsTarget = this._raycaster.intersectObject(this._targetMesh);
          if (intersectsTarget.length > 0) {
            this._offset.copy(intersectsTarget[0].point).sub(this._worldPosition);
          }
        } else {
          this._plane.position.copy(this._worldPosition);
          this._plane.direction.copy(this._camera.getWorldDirection());
          let intersection = CoreIntersections.rayPlane(this._raycaster.ray, this._plane);
          if (intersection !== null) {
            this._offset.copy(intersection).sub(this._plane.position);
          }
        }

        this.update();
      }
    }

    /**
     * @param {Object} evt - Browser event
     * @param {Boolean} forced - true to move inactive handles
     */
    onMove(evt, forced) {
      const offsets = this.getMouseOffsets(evt, this._container);
      this._mouse.set(offsets.x, offsets.y);

      // update raycaster
      // set ray.position to satisfy CoreIntersections::rayPlane API
      this._raycaster.setFromCamera(this._mouse, this._camera);
      this._raycaster.ray.position = this._raycaster.ray.origin;

      if (this._active || forced) {
        this._dragged = true;

        if (this._targetMesh !== null) {
          let intersectsTarget = this._raycaster.intersectObject(this._targetMesh);
          if (intersectsTarget.length > 0) {
            this._worldPosition.copy(intersectsTarget[0].point.sub(this._offset));
          }
        } else {
          if (this._plane.direction.length() === 0) {
            // free mode!this._targetMesh
            this._plane.position.copy(this._worldPosition);
            this._plane.direction.copy(this._camera.getWorldDirection());
          }

          let intersection = CoreIntersections.rayPlane(this._raycaster.ray, this._plane);
          if (intersection !== null) {
            this._worldPosition.copy(intersection.sub(this._offset));
          }
        }
      } else {
        this.onHover(null);
      }

      this.update();
    }

    onEnd() {
      if (this._tracking === true) {
        // stay active and keep controls disabled
        return;
      }

      if (!this._dragged && this._active && this._initialized) {
        this._selected = !this._selected; // change state if there was no dragging
      }

      this._initialized = true;
      this._active = false;
      this._dragged = false;
      this._controls.enabled = true;

      this.update();
    }

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

    createMesh() {
      // geometry
      this._geometry = new three.SphereGeometry(1, 16, 16);

      // material
      this._material = new three.MeshBasicMaterial({
        wireframe: true,
        wireframeLinewidth: 2,
      });

      this.updateMeshColor();

      // mesh
      this._mesh = new three.Mesh(this._geometry, this._material);
      this._mesh.position.copy(this._worldPosition);
      this._mesh.visible = true;

      this.add(this._mesh);
    }

    createDOM() {
      this._dom = document.createElement('div');
      this._dom.className = 'widgets-handle';

      this._dom.style.transform = `translate3D(
      ${this._screenPosition.x}px,
      ${this._screenPosition.y - this._container.offsetHeight}px, 0)`;

      this.updateDOMColor();

      this._container.appendChild(this._dom);
    }

    update() {
      // general update
      this.updateColor();

      // update screen position of handle
      this._screenPosition = this.worldToScreen(this._worldPosition);

      // mesh stuff
      this.updateMeshColor();
      this.updateMeshPosition();

      // DOM stuff
      this.updateDOMColor();
      this.updateDOMPosition();
    }

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

    updateMeshPosition() {
      if (this._mesh) {
        this._mesh.position.copy(this._worldPosition);
      }
    }

    updateDOMPosition() {
      if (this._dom) {
        this._dom.style.transform = `translate3D(${this._screenPosition.x}px,
        ${this._screenPosition.y - this._container.offsetHeight}px, 0)`;
      }
    }

    updateDOMColor() {
      this._dom.style.borderColor = this._color;
    }

    showMesh() {
      if (this._params.hideMesh === true || this._params.hideHandleMesh === true) {
        return;
      }

      this.visible = true;
    }

    free() {
      // events
      this.removeEventListeners();
      // dom
      this._container.removeChild(this._dom);
      // mesh, geometry, material
      this.remove(this._mesh);
      this._mesh.geometry.dispose();
      this._mesh.geometry = null;
      this._mesh.material.dispose();
      this._mesh.material = null;
      this._mesh = 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;

      super.free();
    }

    hideDOM() {
      this._dom.style.display = 'none';
    }

    showDOM() {
      this._dom.style.display = '';
    }

    get screenPosition() {
      return this._screenPosition;
    }

    set screenPosition(screenPosition) {
      this._screenPosition = screenPosition;
    }

    get active() {
      return this._active;
    }

    set active(active) {
      this._active = active;
      // this._tracking = this._active;
      this._controls.enabled = !this._active;

      this.update();
    }

    get tracking() {
      return this._tracking;
    }

    set tracking(tracking) {
      this._tracking = tracking;
      this.update();
    }
  };
};

export { widgetsHandle };
export default widgetsHandle();