src/cameras/cameras.orthographic.js

Summary

Maintainability
F
1 wk
Test Coverage
import Intersections from '../core/core.intersections';
import Validators from '../core/core.validators';

/**
 * Orthographic camera from THREE.JS with some extra convenience
 * functionalities.
 *
 * @example
 * //
 * //
 *
 * @module cameras/orthographic
 */

const camerasOrthographic = (three = window.THREE) => {
  if (three === undefined || three.OrthographicCamera === undefined) {
    return null;
  }

  const Constructor = three.OrthographicCamera;
  return class extends Constructor {
    constructor(left, right, top, bottom, near, far) {
      super(left, right, top, bottom, near, far);

      this._front = null;
      this._back = null;

      this._directions = [
        new three.Vector3(1, 0, 0),
        new three.Vector3(0, 1, 0),
        new three.Vector3(0, 0, 1),
      ];

      this._directionsLabel = [
        'A',
        'P', // TOP/BOTTOM
        'L',
        'R', // LEFT/RIGHT
        'I',
        'S', // FROM/TO
      ];

      this._orientation = 'default';
      this._convention = 'radio';
      this._stackOrientation = 0;

      this._right = null;
      this._up = null;
      this._direction = null;

      this._controls = null;
      this._box = null;
      this._canvas = {
        width: null,
        height: null,
      };

      this._fromFront = true;
      this._angle = 0;
    }

    /**
     * Initialize orthographic camera variables
     */
    init(xCosine, yCosine, zCosine, controls, box, canvas) {
      // DEPRECATION NOTICE
      window.console.warn(
        `cameras.orthographic.init(...) is deprecated.
        Use .cosines, .controls, .box and .canvas instead.`
      );

      //
      if (
        !(
          Validators.vector3(xCosine) &&
          Validators.vector3(yCosine) &&
          Validators.vector3(zCosine) &&
          Validators.box(box) &&
          controls
        )
      ) {
        window.console.log('Invalid input provided.');

        return false;
      }

      this._right = xCosine;
      this._up = this._adjustTopDirection(xCosine, yCosine);
      this._direction = new three.Vector3().crossVectors(this._right, this._up);
      this._controls = controls;
      this._box = box;
      this._canvas = canvas;

      let ray = {
        position: this._box.center,
        direction: this._direction,
      };

      let intersections = this._orderIntersections(
        Intersections.rayBox(ray, this._box),
        this._direction
      );
      this._front = intersections[0];
      this._back = intersections[1];

      // set default values
      this.up.set(this._up.x, this._up.y, this._up.z);
      this._updateCanvas();
      this._updatePositionAndTarget(this._front, this._back);
      this._updateMatrices();
      this._updateDirections();
    }

    update() {
      // http://www.grahamwideman.com/gw/brain/orientation/orientterms.htm
      // do magics depending on orientation and convention
      // also needs a default mode

      if (this._orientation === 'default') {
        switch (this._getMaxIndex(this._directions[2])) {
          case 0:
            this._orientation = 'sagittal';
            break;

          case 1:
            this._orientation = 'coronal';
            break;

          case 2:
            this._orientation = 'axial';
            break;

          default:
            this._orientation = 'free';
            break;
        }
      }

      if (this._orientation === 'free') {
        this._right = this._directions[0];
        this._up = this._directions[1];
        this._direction = this._directions[2];
      } else {
        let leftIndex = this.leftDirection();
        let leftDirection = this._directions[leftIndex];
        let posteriorIndex = this.posteriorDirection();
        let posteriorDirection = this._directions[posteriorIndex];
        let superiorIndex = this.superiorDirection();
        let superiorDirection = this._directions[superiorIndex];

        if (this._convention === 'radio') {
          switch (this._orientation) {
            case 'axial':
              // up vector is 'anterior'
              if (posteriorDirection.y > 0) {
                posteriorDirection.negate();
              }

              // looking towards superior
              if (superiorDirection.z < 0) {
                superiorDirection.negate();
              }

              //
              this._right = leftDirection; // does not matter right/left
              this._up = posteriorDirection;
              this._direction = superiorDirection;
              break;

            case 'coronal':
              // up vector is 'superior'
              if (superiorDirection.z < 0) {
                superiorDirection.negate();
              }

              // looking towards posterior
              if (posteriorDirection.y < 0) {
                posteriorDirection.negate();
              }

              //
              this._right = leftDirection; // does not matter right/left
              this._up = superiorDirection;
              this._direction = posteriorDirection;
              break;

            case 'sagittal':
              // up vector is 'superior'
              if (superiorDirection.z < 0) {
                superiorDirection.negate();
              }

              // looking towards right
              if (leftDirection.x > 0) {
                leftDirection.negate();
              }

              //
              this._right = posteriorDirection; // does not matter right/left
              this._up = superiorDirection;
              this._direction = leftDirection;

              break;

            default:
              window.console.warn(
                `"${this._orientation}" orientation is not valid.
                  (choices: axial, coronal, sagittal)`
              );
              break;
          }
        } else if (this._convention === 'neuro') {
          switch (this._orientation) {
            case 'axial':
              // up vector is 'anterior'
              if (posteriorDirection.y > 0) {
                posteriorDirection.negate();
              }

              // looking towards inferior
              if (superiorDirection.z > 0) {
                superiorDirection.negate();
              }

              //
              this._right = leftDirection; // does not matter right/left
              this._up = posteriorDirection;
              this._direction = superiorDirection;
              break;

            case 'coronal':
              // up vector is 'superior'
              if (superiorDirection.z < 0) {
                superiorDirection.negate();
              }

              // looking towards anterior
              if (posteriorDirection.y > 0) {
                posteriorDirection.negate();
              }

              //
              this._right = leftDirection; // does not matter right/left
              this._up = superiorDirection;
              this._direction = posteriorDirection;
              break;

            case 'sagittal':
              // up vector is 'superior'
              if (superiorDirection.z < 0) {
                superiorDirection.negate();
              }

              // looking towards right
              if (leftDirection.x > 0) {
                leftDirection.negate();
              }

              //
              this._right = posteriorDirection; // does not matter right/left
              this._up = superiorDirection;
              this._direction = leftDirection;

              break;

            default:
              window.console.warn(
                `"${this._orientation}" orientation is not valid.
                  (choices: axial, coronal, sagittal)`
              );
              break;
          }
        } else {
          window.console.warn(`${this._convention} is not valid (choices: radio, neuro)`);
        }
      }

      // that is what determines left/right
      let ray = {
        position: this._box.center,
        direction: this._direction,
      };

      let intersections = this._orderIntersections(
        Intersections.rayBox(ray, this._box),
        this._direction
      );
      this._front = intersections[0];
      this._back = intersections[1];

      // set default values
      this.up.set(this._up.x, this._up.y, this._up.z);
      this._updateCanvas();
      this._updatePositionAndTarget(this._front, this._back);
      this._updateMatrices();
      this._updateDirections();
    }

    leftDirection() {
      return this._findMaxIndex(this._directions, 0);
    }

    posteriorDirection() {
      return this._findMaxIndex(this._directions, 1);
    }

    superiorDirection() {
      return this._findMaxIndex(this._directions, 2);
    }

    /**
     * Invert rows in the current slice.
     * Inverting rows in 2 steps:
     *   * Flip the "up" vector
     *   * Look at the slice from the other side
     */
    invertRows() {
      // flip "up" vector
      // we flip up first because invertColumns update projectio matrices
      this.up.multiplyScalar(-1);
      this.invertColumns();

      this._updateDirections();
    }

    /**
     * Invert rows in the current slice.
     * Inverting rows in 1 step:
     *   * Look at the slice from the other side
     */
    invertColumns() {
      this.center();
      // rotate 180 degrees around the up vector...
      let oppositePosition = this._oppositePosition(this.position);

      // update posistion and target
      // clone is needed because this.position is overwritten in method
      this._updatePositionAndTarget(oppositePosition, this.position.clone());
      this._updateMatrices();
      this._fromFront = !this._fromFront;

      this._angle %= 360;
      this._angle = 360 - this._angle;

      this._updateDirections();
    }

    /**
     * Center slice in the camera FOV.
     * It also updates the controllers properly.
     * We can center a camera from the front or from the back.
     */
    center() {
      if (this._fromFront) {
        this._updatePositionAndTarget(this._front, this._back);
      } else {
        this._updatePositionAndTarget(this._back, this._front);
      }

      this._updateMatrices();
      this._updateDirections();
    }

    /**
     * Pi/2 rotation around the zCosine axis.
     * Clock-wise rotation from the user point of view.
     */
    rotate(angle = null) {
      this.center();

      let rotationToApply = 90;
      if (angle === null) {
        rotationToApply *= -1;
        this._angle += 90;
      } else {
        rotationToApply = 360 -  (angle - this._angle);
        this._angle = angle;
      }

      this._angle %= 360;

      // Rotate the up vector around the "zCosine"
      let rotation = new three.Matrix4().makeRotationAxis(
        this._direction,
        (rotationToApply * Math.PI) / 180
      );
      this.up.applyMatrix4(rotation);

      this._updateMatrices();
      this._updateDirections();
    }

    // dimensions[0] // width
    // dimensions[1] // height
    // direction= 0 width, 1 height, 2 best
    // factor
    fitBox(direction = 0, factor = 1.5) {
      //
      // if (!(dimensions && dimensions.length >= 2)) {
      //   window.console.log('Invalid dimensions container.');
      //   window.console.log(dimensions);

      //   return false;
      // }

      //
      let zoom = 1;

      // update zoom
      switch (direction) {
        case 0:
          zoom = factor * this._computeZoom(this._canvas.width, this._right);
          break;
        case 1:
          zoom = factor * this._computeZoom(this._canvas.height, this._up);
          break;
        case 2:
          zoom =
            factor *
            Math.min(
              this._computeZoom(this._canvas.width, this._right),
              this._computeZoom(this._canvas.height, this._up)
            );
          break;
        default:
          break;
      }

      if (!zoom) {
        return false;
      }

      this.zoom = zoom;

      this.center();
    }

    _adjustTopDirection(horizontalDirection, verticalDirection) {
      const vMaxIndex = this._getMaxIndex(verticalDirection);

      // should handle vMax index === 0
      if (
        (vMaxIndex === 2 && verticalDirection.getComponent(vMaxIndex) < 0) ||
        (vMaxIndex === 1 && verticalDirection.getComponent(vMaxIndex) > 0) ||
        (vMaxIndex === 0 && verticalDirection.getComponent(vMaxIndex) > 0)
      ) {
        verticalDirection.negate();
      }

      return verticalDirection;
    }

    _getMaxIndex(vector) {
      // init with X value
      let maxValue = Math.abs(vector.x);
      let index = 0;

      if (Math.abs(vector.y) > maxValue) {
        maxValue = Math.abs(vector.y);
        index = 1;
      }

      if (Math.abs(vector.z) > maxValue) {
        index = 2;
      }

      return index;
    }

    _findMaxIndex(directions, target) {
      // get index of the most superior direction
      let maxIndices = this._getMaxIndices(directions);

      for (let i = 0; i < maxIndices.length; i++) {
        if (maxIndices[i] === target) {
          return i;
        }
      }
    }

    _getMaxIndices(directions) {
      let indices = [];
      indices.push(this._getMaxIndex(directions[0]));
      indices.push(this._getMaxIndex(directions[1]));
      indices.push(this._getMaxIndex(directions[2]));

      return indices;
    }

    _orderIntersections(intersections, direction) {
      const ordered = intersections[0].dot(direction) < intersections[1].dot(direction);

      if (!ordered) {
        return [intersections[1], intersections[0]];
      }

      return intersections;
    }

    _updateCanvas() {
      let camFactor = 2;
      this.left = -this._canvas.width / camFactor;
      this.right = this._canvas.width / camFactor;
      this.top = this._canvas.height / camFactor;
      this.bottom = -this._canvas.height / camFactor;

      this._updateMatrices();
      this.controls.handleResize();
    }

    _oppositePosition(position) {
      let oppositePosition = position.clone();
      // center world postion around box center
      oppositePosition.sub(this._box.center);
      // rotate
      let rotation = new three.Matrix4().makeRotationAxis(this.up, Math.PI);

      oppositePosition.applyMatrix4(rotation);
      // translate back to world position
      oppositePosition.add(this._box.center);
      return oppositePosition;
    }

    _computeZoom(dimension, direction) {
      if (!(dimension && dimension > 0)) {
        window.console.log('Invalid dimension provided.');
        window.console.log(dimension);
        return false;
      }

      // ray
      let ray = {
        position: this._box.center.clone(),
        direction: direction,
      };

      let intersections = Intersections.rayBox(ray, this._box);
      if (intersections.length < 2) {
        window.console.log('Can not adjust the camera ( < 2 intersections).');
        window.console.log(ray);
        window.console.log(this._box);
        return false;
      }

      return dimension / intersections[0].distanceTo(intersections[1]);
    }

    _updatePositionAndTarget(position, target) {
      // position
      this.position.set(position.x, position.y, position.z);

      // targets
      this.lookAt(target.x, target.y, target.z);
      this._controls.target.set(target.x, target.y, target.z);
    }

    _updateMatrices() {
      this._controls.update();
      // THEN camera
      this.updateProjectionMatrix();
      this.updateMatrixWorld();
    }

    _updateLabels() {
      this._directionsLabel = [
        this._vector2Label(this._up),
        this._vector2Label(this._up.clone().negate()),
        this._vector2Label(this._right),
        this._vector2Label(this._right.clone().negate()),
        this._vector2Label(this._direction),
        this._vector2Label(this._direction.clone().negate()),
      ];
    }

    _vector2Label(direction) {
      const index = this._getMaxIndex(direction);
      // set vector max value to 1
      const scaledDirection = direction
        .clone()
        .divideScalar(Math.abs(direction.getComponent(index)));
      const delta = 0.2;
      let label = '';

      // loop through components of the vector
      for (let i = 0; i < 3; i++) {
        if (i === 0) {
          if (scaledDirection.getComponent(i) + delta >= 1) {
            label += 'L';
          } else if (scaledDirection.getComponent(i) - delta <= -1) {
            label += 'R';
          }
        }

        if (i === 1) {
          if (scaledDirection.getComponent(i) + delta >= 1) {
            label += 'P';
          } else if (scaledDirection.getComponent(i) - delta <= -1) {
            label += 'A';
          }
        }

        if (i === 2) {
          if (scaledDirection.getComponent(i) + delta >= 1) {
            label += 'S';
          } else if (scaledDirection.getComponent(i) - delta <= -1) {
            label += 'I';
          }
        }
      }

      return label;
    }

    _updateDirections() {
      // up is correct
      this._up = this.up.clone();

      // direction
      let pLocal = new three.Vector3(0, 0, -1);
      let pWorld = pLocal.applyMatrix4(this.matrixWorld);
      this._direction = pWorld.sub(this.position).normalize();

      // right
      this._right = new three.Vector3().crossVectors(this._direction, this.up);

      // update labels accordingly
      this._updateLabels();
    }

    set controls(controls) {
      this._controls = controls;
    }

    get controls() {
      return this._controls;
    }

    set box(box) {
      this._box = box;
    }

    get box() {
      return this._box;
    }

    set canvas(canvas) {
      this._canvas = canvas;
      this._updateCanvas();
    }

    get canvas() {
      return this._canvas;
    }

    set angle(angle) {
      this.rotate(angle);
    }

    get angle() {
      return this._angle;
    }

    set directions(directions) {
      this._directions = directions;
    }

    get directions() {
      return this._directions;
    }

    set convention(convention) {
      this._convention = convention;
    }

    get convention() {
      return this._convention;
    }

    set orientation(orientation) {
      this._orientation = orientation;
    }

    get orientation() {
      return this._orientation;
    }

    set directionsLabel(directionsLabel) {
      this._directionsLabel = directionsLabel;
    }

    get directionsLabel() {
      return this._directionsLabel;
    }

    set stackOrientation(stackOrientation) {
      this._stackOrientation = stackOrientation;

      if (this._stackOrientation === 0) {
        this._orientation = 'default';
      } else {
        const maxIndex = this._getMaxIndex(this._directions[(this._stackOrientation + 2) % 3]);

        if (maxIndex === 0) {
          this._orientation = 'sagittal';
        } else if (maxIndex === 1) {
          this._orientation = 'coronal';
        } else if (maxIndex === 2) {
          this._orientation = 'axial';
        }
      }
    }

    get stackOrientation() {
      //
      if (this._orientation === 'default') {
        this._stackOrientation = 0;
      } else {
        let maxIndex = this._getMaxIndex(this._direction);

        if (maxIndex === this._getMaxIndex(this._directions[2])) {
          this._stackOrientation = 0;
        } else if (maxIndex === this._getMaxIndex(this._directions[0])) {
          this._stackOrientation = 1;
        } else if (maxIndex === this._getMaxIndex(this._directions[1])) {
          this._stackOrientation = 2;
        }
      }

      return this._stackOrientation;
    }
  };
};

// export factory
export { camerasOrthographic };
// default export to
export default camerasOrthographic();