src/core/core.intersections.js

Summary

Maintainability
F
3 days
Test Coverage
import CoreUtils from './core.utils';
import Validators from './core.validators';

import { Matrix4 } from 'three/src/math/Matrix4';
import { Vector3 } from 'three/src/math/Vector3';

/**
 * Compute/test intersection between different objects.
 *
 * @module core/intersections
 */

export default class Intersections {
  /**
   * Compute intersection between oriented bounding box and a plane.
   *
   * Returns intersection in plane's space.
   *
   * Should return at least 3 intersections. If not, the plane and the box do not
   * intersect.
   *
   * @param {Object} aabb - Axe Aligned Bounding Box representation.
   * @param {Vector3} aabb.halfDimensions - Half dimensions of the box.
   * @param {Vector3} aabb.center - Center of the box.
   * @param {Matrix4} aabb.toAABB - Transform to go from plane space to box space.
   * @param {Object} plane - Plane representation
   * @param {Vector3} plane.position - position of normal which describes the plane.
   * @param {Vector3} plane.direction - Direction of normal which describes the plane.
   *
   * @returns {Array<Vector3>} List of all intersections in plane's space.
   * @returns {boolean} false is invalid input provided.
   *
   * @example
   * //Returns array with intersection N intersections
   * let aabb = {
   *   center: new Vector3(150, 150, 150),
   *   halfDimensions: new Vector3(50, 60, 70),
   *   toAABB: new Matrix4()
   * }
   * let plane = {
   *   position: new Vector3(110, 120, 130),
   *   direction: new Vector3(1, 0, 0)
   * }
   *
   * let intersections = CoreIntersections.aabbPlane(aabb, plane);
   * // intersections ==
   * //[ { x : 110, y : 90,  z : 80 },
   * //  { x : 110, y : 210, z : 220 },
   * //  { x : 110, y : 210, z : 80 },
   * //  { x : 110, y : 90,  z : 220 } ]
   *
   * //Returns empty array with 0 intersections
   * let aabb = {
   *
   * }
   * let plane = {
   *
   * }
   *
   * let intersections = VJS.Core.Validators.matrix4(new Vector3());
   *
   * //Returns false if invalid input?
   *
   */
  static aabbPlane(aabb, plane) {
    //
    // obb = { halfDimensions, orientation, center, toAABB }
    // plane = { position, direction }
    //
    //
    // LOGIC:
    //
    // Test intersection of each edge of the Oriented Bounding Box with the Plane
    //
    // ALL EDGES
    //
    //      .+-------+
    //    .' |     .'|
    //   +---+---+'  |
    //   |   |   |   |
    //   |  ,+---+---+
    //   |.'     | .'
    //   +-------+'
    //
    // SPACE ORIENTATION
    //
    //       +
    //     j |
    //       |
    //       |   i
    //   k  ,+-------+
    //    .'
    //   +
    //
    //
    // 1- Move Plane position and orientation in IJK space
    // 2- Test Edges/ IJK Plane intersections
    // 3- Return intersection Edge/ IJK Plane if it touches the Oriented BBox

    let intersections = [];

    if (!(this.validateAabb(aabb) && this.validatePlane(plane))) {
      window.console.log('Invalid aabb or plane provided.');
      return false;
    }

    // invert space matrix
    let fromAABB = new Matrix4();
    fromAABB.getInverse(aabb.toAABB);

    let t1 = plane.direction.clone().applyMatrix4(aabb.toAABB);
    let t0 = new Vector3(0, 0, 0).applyMatrix4(aabb.toAABB);

    let planeAABB = this.posdir(
      plane.position.clone().applyMatrix4(aabb.toAABB),
      new Vector3(t1.x - t0.x, t1.y - t0.y, t1.z - t0.z).normalize()
    );

    let bbox = CoreUtils.bbox(aabb.center, aabb.halfDimensions);

    let orientation = new Vector3(new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, 1));

    // 12 edges (i.e. ray)/plane intersection tests
    // RAYS STARTING FROM THE FIRST CORNER (0, 0, 0)
    //
    //       +
    //       |
    //       |
    //       |
    //      ,+---+---+
    //    .'
    //   +

    let ray = this.posdir(
      new Vector3(
        aabb.center.x - aabb.halfDimensions.x,
        aabb.center.y - aabb.halfDimensions.y,
        aabb.center.z - aabb.halfDimensions.z
      ),
      orientation.x
    );
    this.rayPlaneInBBox(ray, planeAABB, bbox, intersections);

    ray.direction = orientation.y;
    this.rayPlaneInBBox(ray, planeAABB, bbox, intersections);

    ray.direction = orientation.z;
    this.rayPlaneInBBox(ray, planeAABB, bbox, intersections);

    // RAYS STARTING FROM THE LAST CORNER
    //
    //               +
    //             .'
    //   +-------+'
    //           |
    //           |
    //           |
    //           +
    //

    let ray2 = this.posdir(
      new Vector3(
        aabb.center.x + aabb.halfDimensions.x,
        aabb.center.y + aabb.halfDimensions.y,
        aabb.center.z + aabb.halfDimensions.z
      ),
      orientation.x
    );
    this.rayPlaneInBBox(ray2, planeAABB, bbox, intersections);

    ray2.direction = orientation.y;
    this.rayPlaneInBBox(ray2, planeAABB, bbox, intersections);

    ray2.direction = orientation.z;
    this.rayPlaneInBBox(ray2, planeAABB, bbox, intersections);

    // RAYS STARTING FROM THE SECOND CORNER
    //
    //               +
    //               |
    //               |
    //               |
    //               +
    //             .'
    //           +'

    let ray3 = this.posdir(
      new Vector3(
        aabb.center.x + aabb.halfDimensions.x,
        aabb.center.y - aabb.halfDimensions.y,
        aabb.center.z - aabb.halfDimensions.z
      ),
      orientation.y
    );
    this.rayPlaneInBBox(ray3, planeAABB, bbox, intersections);

    ray3.direction = orientation.z;
    this.rayPlaneInBBox(ray3, planeAABB, bbox, intersections);

    // RAYS STARTING FROM THE THIRD CORNER
    //
    //      .+-------+
    //    .'
    //   +
    //
    //
    //
    //

    let ray4 = this.posdir(
      new Vector3(
        aabb.center.x - aabb.halfDimensions.x,
        aabb.center.y + aabb.halfDimensions.y,
        aabb.center.z - aabb.halfDimensions.z
      ),
      orientation.x
    );
    this.rayPlaneInBBox(ray4, planeAABB, bbox, intersections);

    ray4.direction = orientation.z;
    this.rayPlaneInBBox(ray4, planeAABB, bbox, intersections);

    // RAYS STARTING FROM THE FOURTH CORNER
    //
    //
    //
    //   +
    //   |
    //   |
    //   |
    //   +-------+

    let ray5 = this.posdir(
      new Vector3(
        aabb.center.x - aabb.halfDimensions.x,
        aabb.center.y - aabb.halfDimensions.y,
        aabb.center.z + aabb.halfDimensions.z
      ),
      orientation.x
    );
    this.rayPlaneInBBox(ray5, planeAABB, bbox, intersections);

    ray5.direction = orientation.y;
    this.rayPlaneInBBox(ray5, planeAABB, bbox, intersections);

    // @todo make sure objects are unique...

    // back to original space
    intersections.map(function(element) {
      return element.applyMatrix4(fromAABB);
    });

    return intersections;
  }

  /**
   * Compute intersection between a ray and a plane.
   *
   * @memberOf this
   * @public
   *
   * @param {Object} ray - Ray representation.
   * @param {Vector3} ray.position - position of normal which describes the ray.
   * @param {Vector3} ray.direction - Direction of normal which describes the ray.
   * @param {Object} plane - Plane representation
   * @param {Vector3} plane.position - position of normal which describes the plane.
   * @param {Vector3} plane.direction - Direction of normal which describes the plane.
   *
   * @returns {Vector3|null} Intersection between ray and plane or null.
   */
  static rayPlane(ray, plane) {
    // ray: {position, direction}
    // plane: {position, direction}

    if (ray.direction.dot(plane.direction) !== 0) {
      //
      // not parallel, move forward
      //
      // LOGIC:
      //
      // Ray equation: P = P0 + tV
      // P = <Px, Py, Pz>
      // P0 = <ray.position.x, ray.position.y, ray.position.z>
      // V = <ray.direction.x, ray.direction.y, ray.direction.z>
      //
      // Therefore:
      // Px = ray.position.x + t*ray.direction.x
      // Py = ray.position.y + t*ray.direction.y
      // Pz = ray.position.z + t*ray.direction.z
      //
      //
      //
      // Plane equation: ax + by + cz + d = 0
      // a = plane.direction.x
      // b = plane.direction.y
      // c = plane.direction.z
      // d = -( plane.direction.x*plane.position.x +
      //        plane.direction.y*plane.position.y +
      //        plane.direction.z*plane.position.z )
      //
      //
      // 1- in the plane equation, we replace x, y and z by Px, Py and Pz
      // 2- find t
      // 3- replace t in Px, Py and Pz to get the coordinate of the intersection
      //
      let t =
        (plane.direction.x * (plane.position.x - ray.position.x) +
          plane.direction.y * (plane.position.y - ray.position.y) +
          plane.direction.z * (plane.position.z - ray.position.z)) /
        (plane.direction.x * ray.direction.x +
          plane.direction.y * ray.direction.y +
          plane.direction.z * ray.direction.z);

      let intersection = new Vector3(
        ray.position.x + t * ray.direction.x,
        ray.position.y + t * ray.direction.y,
        ray.position.z + t * ray.direction.z
      );

      return intersection;
    }

    return null;
  }

  /**
   * Compute intersection between a ray and a box
   * @param {Object} ray
   * @param {Object} box
   * @return {Array}
   */
  static rayBox(ray, box) {
    // should also do the space transforms here
    // ray: {position, direction}
    // box: {halfDimensions, center}

    let intersections = [];

    let bbox = CoreUtils.bbox(box.center, box.halfDimensions);

    // window.console.log(bbox);

    // X min
    let plane = this.posdir(
      new Vector3(bbox.min.x, box.center.y, box.center.z),
      new Vector3(-1, 0, 0)
    );
    this.rayPlaneInBBox(ray, plane, bbox, intersections);

    // X max
    plane = this.posdir(new Vector3(bbox.max.x, box.center.y, box.center.z), new Vector3(1, 0, 0));
    this.rayPlaneInBBox(ray, plane, bbox, intersections);

    // Y min
    plane = this.posdir(new Vector3(box.center.x, bbox.min.y, box.center.z), new Vector3(0, -1, 0));
    this.rayPlaneInBBox(ray, plane, bbox, intersections);

    // Y max
    plane = this.posdir(new Vector3(box.center.x, bbox.max.y, box.center.z), new Vector3(0, 1, 0));
    this.rayPlaneInBBox(ray, plane, bbox, intersections);

    // Z min
    plane = this.posdir(new Vector3(box.center.x, box.center.y, bbox.min.z), new Vector3(0, 0, -1));
    this.rayPlaneInBBox(ray, plane, bbox, intersections);

    // Z max
    plane = this.posdir(new Vector3(box.center.x, box.center.y, bbox.max.z), new Vector3(0, 0, 1));
    this.rayPlaneInBBox(ray, plane, bbox, intersections);

    return intersections;
  }

  /**
   * Intersection between ray and a plane that are in a box.
   * @param {*} ray
   * @param {*} planeAABB
   * @param {*} bbox
   * @param {*} intersections
   */
  static rayPlaneInBBox(ray, planeAABB, bbox, intersections) {
    let intersection = this.rayPlane(ray, planeAABB);
    // window.console.log(intersection);
    if (intersection && this.inBBox(intersection, bbox)) {
      if (!intersections.find(this.findIntersection(intersection))) {
        intersections.push(intersection);
      }
    }
  }

  /**
   * Find intersection in array
   * @param {*} myintersection
   */
  static findIntersection(myintersection) {
    return function found(element, index, array) {
      if (
        myintersection.x === element.x &&
        myintersection.y === element.y &&
        myintersection.z === element.z
      ) {
        return true;
      }

      return false;
    };
  }

  /**
   * Is point in box.
   * @param {Object} point
   * @param {Object} bbox
   * @return {Boolean}
   */
  static inBBox(point, bbox) {
    //
    let epsilon = 0.0001;
    if (
      point &&
      point.x >= bbox.min.x - epsilon &&
      point.y >= bbox.min.y - epsilon &&
      point.z >= bbox.min.z - epsilon &&
      point.x <= bbox.max.x + epsilon &&
      point.y <= bbox.max.y + epsilon &&
      point.z <= bbox.max.z + epsilon
    ) {
      return true;
    }
    return false;
  }

  static posdir(position, direction) {
    return { position, direction };
  }

  static validatePlane(plane) {
    //
    if (plane === null) {
      window.console.log('Invalid plane.');
      window.console.log(plane);

      return false;
    }

    if (!Validators.vector3(plane.position)) {
      window.console.log('Invalid plane.position.');
      window.console.log(plane.position);

      return false;
    }

    if (!Validators.vector3(plane.direction)) {
      window.console.log('Invalid plane.direction.');
      window.console.log(plane.direction);

      return false;
    }

    return true;
  }

  static validateAabb(aabb) {
    //
    if (aabb === null) {
      window.console.log('Invalid aabb.');
      window.console.log(aabb);
      return false;
    }

    if (!Validators.matrix4(aabb.toAABB)) {
      window.console.log('Invalid aabb.toAABB: ');
      window.console.log(aabb.toAABB);

      return false;
    }

    if (!Validators.vector3(aabb.center)) {
      window.console.log('Invalid aabb.center.');
      window.console.log(aabb.center);

      return false;
    }

    if (
      !(
        Validators.vector3(aabb.halfDimensions) &&
        aabb.halfDimensions.x >= 0 &&
        aabb.halfDimensions.y >= 0 &&
        aabb.halfDimensions.z >= 0
      )
    ) {
      window.console.log('Invalid aabb.halfDimensions.');
      window.console.log(aabb.halfDimensions);

      return false;
    }

    return true;
  }
}