Plane.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.geometry.euclidean.threed;

  18. import java.util.Objects;

  19. import org.apache.commons.geometry.core.Transform;
  20. import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
  21. import org.apache.commons.geometry.core.partitioning.Hyperplane;
  22. import org.apache.commons.geometry.euclidean.threed.line.Line3D;
  23. import org.apache.commons.geometry.euclidean.threed.line.Lines3D;
  24. import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
  25. import org.apache.commons.geometry.euclidean.twod.ConvexArea;
  26. import org.apache.commons.numbers.core.Precision;

  27. /** Class representing a plane in 3 dimensional Euclidean space. Each plane is defined by a
  28.  * {@link #getNormal() normal} and an {@link #getOriginOffset() origin offset}. If \(\vec{n}\) is the plane normal,
  29.  * \(d\) is the origin offset, and \(p\) and \(q\) are any points in the plane, then the following are true:
  30.  * <ul>
  31.  *  <li>\(\lVert \vec{n} \rVert\) = 1</li>
  32.  *  <li>\(\vec{n} \cdot (p - q) = 0\)</li>
  33.  *  <li>\(d = - (\vec{n} \cdot q)\)</li>
  34.  *  </ul>
  35.  *  In other words, the normal is a unit vector such that the dot product of the normal and the difference of
  36.  *  any two points in the plane is always equal to \(0\). Similarly, the {@code origin offset} is equal to the
  37.  *  negation of the dot product of the normal and any point in the plane. The projection of the origin onto the
  38.  *  plane (given by {@link #getOrigin()}), is computed as \(-d \vec{n}\).
  39.  *
  40.  * <p>Instances of this class are guaranteed to be immutable.</p>
  41.  * @see Planes
  42.  */
  43. public class Plane extends AbstractHyperplane<Vector3D> {

  44.     /** Plane normal. */
  45.     private final Vector3D.Unit normal;

  46.     /** Offset of the origin with respect to the plane. */
  47.     private final double originOffset;

  48.     /** Construct a plane from its component parts.
  49.      * @param normal unit normal vector
  50.      * @param originOffset offset of the origin with respect to the plane
  51.      * @param precision precision context used to compare floating point values
  52.      */
  53.     Plane(final Vector3D.Unit normal, final double originOffset,
  54.           final Precision.DoubleEquivalence precision) {

  55.         super(precision);

  56.         this.normal = normal;
  57.         this.originOffset = originOffset;
  58.     }

  59.     /** Get the orthogonal projection of the 3D-space origin in the plane.
  60.      * @return the origin point of the plane frame (point closest to the 3D-space
  61.      *         origin)
  62.      */
  63.     public Vector3D getOrigin() {
  64.         return normal.multiply(-originOffset);
  65.     }

  66.     /** Get the offset of the spatial origin ({@code 0, 0, 0}) with respect to the plane.
  67.      * @return the offset of the origin with respect to the plane.
  68.      */
  69.     public double getOriginOffset() {
  70.         return originOffset;
  71.     }

  72.     /** Get the plane normal vector.
  73.      * @return plane normal vector
  74.      */
  75.     public Vector3D.Unit getNormal() {
  76.         return normal;
  77.     }

  78.     /** Return an {@link EmbeddingPlane} instance suitable for embedding 2D geometric objects
  79.      * into this plane. Returned instances are guaranteed to be equal between invocations.
  80.      * @return a plane instance suitable for embedding 2D subspaces
  81.      */
  82.     public EmbeddingPlane getEmbedding() {
  83.         final Vector3D.Unit u = normal.orthogonal();
  84.         final Vector3D.Unit v = normal.cross(u).normalize();

  85.         return new EmbeddingPlane(u, v, normal, originOffset, getPrecision());
  86.     }

  87.     /** {@inheritDoc} */
  88.     @Override
  89.     public double offset(final Vector3D point) {
  90.         return point.dot(normal) + originOffset;
  91.     }

  92.     /** Get the offset (oriented distance) of the given line with respect to the plane. The value
  93.      * closest to zero is returned, which will always be zero if the line is not parallel to the plane.
  94.      * @param line line to calculate the offset of
  95.      * @return the offset of the line with respect to the plane or 0.0 if the line
  96.      *      is not parallel to the plane.
  97.      */
  98.     public double offset(final Line3D line) {
  99.         if (!isParallel(line)) {
  100.             return 0.0;
  101.         }
  102.         return offset(line.getOrigin());
  103.     }

  104.     /** Get the offset (oriented distance) of the given plane with respect to this instance. The value
  105.      * closest to zero is returned, which will always be zero if the planes are not parallel.
  106.      * @param plane plane to calculate the offset of
  107.      * @return the offset of the plane with respect to this instance or 0.0 if the planes
  108.      *      are not parallel.
  109.      */
  110.     public double offset(final Plane plane) {
  111.         if (!isParallel(plane)) {
  112.             return 0.0;
  113.         }
  114.         return originOffset + (similarOrientation(plane) ? -plane.originOffset : plane.originOffset);
  115.     }

  116.     /** Check if the instance contains a point.
  117.      * @param p point to check
  118.      * @return true if p belongs to the plane
  119.      */
  120.     @Override
  121.     public boolean contains(final Vector3D p) {
  122.         return getPrecision().eqZero(offset(p));
  123.     }

  124.     /** Check if the instance contains a line.
  125.      * @param line line to check
  126.      * @return true if line is contained in this plane
  127.      */
  128.     public boolean contains(final Line3D line) {
  129.         return isParallel(line) && contains(line.getOrigin());
  130.     }

  131.     /** Check if the instance contains another plane. Planes are considered similar if they contain
  132.      * the same points. This does not mean they are equal since they can have opposite normals.
  133.      * @param plane plane to which the instance is compared
  134.      * @return true if the planes are similar
  135.      */
  136.     public boolean contains(final Plane plane) {
  137.         final double angle = normal.angle(plane.normal);
  138.         final Precision.DoubleEquivalence precision = getPrecision();

  139.         return ((precision.eqZero(angle)) && precision.eq(originOffset, plane.originOffset)) ||
  140.                 ((precision.eq(angle, Math.PI)) && precision.eq(originOffset, -plane.originOffset));
  141.     }

  142.     /** {@inheritDoc} */
  143.     @Override
  144.     public Vector3D project(final Vector3D point) {
  145.         return getOrigin().add(point.reject(normal));
  146.     }

  147.     /** Project a 3D line onto the plane.
  148.      * @param line the line to project
  149.      * @return the projection of the given line onto the plane.
  150.      */
  151.     public Line3D project(final Line3D line) {
  152.         final Vector3D direction = line.getDirection();
  153.         final Vector3D projection = normal.multiply(direction.dot(normal) * (1 / normal.normSq()));

  154.         final Vector3D projectedLineDirection = direction.subtract(projection);
  155.         final Vector3D p1 = project(line.getOrigin());
  156.         final Vector3D p2 = p1.add(projectedLineDirection);

  157.         return Lines3D.fromPoints(p1, p2, getPrecision());
  158.     }

  159.     /** {@inheritDoc} */
  160.     @Override
  161.     public PlaneConvexSubset span() {
  162.         return Planes.subsetFromConvexArea(getEmbedding(), ConvexArea.full());
  163.     }

  164.     /** Check if the line is parallel to the instance.
  165.      * @param line line to check.
  166.      * @return true if the line is parallel to the instance, false otherwise.
  167.      */
  168.     public boolean isParallel(final Line3D line) {
  169.         final double dot = normal.dot(line.getDirection());

  170.         return getPrecision().eqZero(dot);
  171.     }

  172.     /** Check if the plane is parallel to the instance.
  173.      * @param plane plane to check.
  174.      * @return true if the plane is parallel to the instance, false otherwise.
  175.      */
  176.     public boolean isParallel(final Plane plane) {
  177.         return getPrecision().eqZero(normal.cross(plane.normal).norm());
  178.     }

  179.     /** {@inheritDoc} */
  180.     @Override
  181.     public boolean similarOrientation(final Hyperplane<Vector3D> other) {
  182.         return (((Plane) other).normal).dot(normal) > 0;
  183.     }

  184.     /** Get the intersection of a line with this plane.
  185.      * @param line line intersecting the instance
  186.      * @return intersection point between between the line and the instance (null if
  187.      *         the line is parallel to the instance)
  188.      */
  189.     public Vector3D intersection(final Line3D line) {
  190.         final Vector3D direction = line.getDirection();
  191.         final double dot = normal.dot(direction);

  192.         if (getPrecision().eqZero(dot)) {
  193.             return null;
  194.         }

  195.         final Vector3D point = line.pointAt(0);
  196.         final double k = -(originOffset + normal.dot(point)) / dot;

  197.         return Vector3D.Sum.of(point)
  198.                 .addScaled(k, direction)
  199.                 .get();
  200.     }

  201.     /** Get the line formed by the intersection of this instance with the given plane.
  202.      * The returned line lies in both planes and points in the direction of
  203.      * the cross product <code>n<sub>1</sub> x n<sub>2</sub></code>, where <code>n<sub>1</sub></code>
  204.      * is the normal of the current instance and <code>n<sub>2</sub></code> is the normal
  205.      * of the argument.
  206.      *
  207.      * <p>Null is returned if the planes are parallel.</p>
  208.      *
  209.      * @param other other plane
  210.      * @return line at the intersection of the instance and the other plane, or null
  211.      *      if no such line exists
  212.      */
  213.     public Line3D intersection(final Plane other) {
  214.         final Vector3D direction = normal.cross(other.normal);

  215.         if (getPrecision().eqZero(direction.norm())) {
  216.             return null;
  217.         }

  218.         final Vector3D point = intersection(this, other, Planes.fromNormal(direction, getPrecision()));

  219.         return Lines3D.fromPointAndDirection(point, direction, getPrecision());
  220.     }

  221.     /** Build a new reversed version of this plane, with opposite orientation.
  222.      * @return a new reversed plane
  223.      */
  224.     @Override
  225.     public Plane reverse() {
  226.         return new Plane(normal.negate(), -originOffset, getPrecision());
  227.     }

  228.     /** {@inheritDoc}
  229.      *
  230.      * <p>Instances are transformed by selecting 3 representative points from the
  231.      * plane, transforming them, and constructing a new plane from the transformed points.
  232.      * Since the normal is not transformed directly, but rather is constructed new from the
  233.      * transformed points, the relative orientations of points in the plane are preserved,
  234.      * even for transforms that do not
  235.      * {@link Transform#preservesOrientation() preserve orientation}. The example below shows
  236.      * a plane being transformed by a non-orientation-preserving transform. The normal of the
  237.      * transformed plane retains its counterclockwise relationship to the points in the plane,
  238.      * in contrast with the normal that is transformed directly by the transform.
  239.      * </p>
  240.      * <pre>
  241.      * // construct a plane from 3 points; the normal will be selected such that the
  242.      * // points are ordered counterclockwise when looking down the plane normal.
  243.      * Vector3D p1 = Vector3D.of(0, 0, 0);
  244.      * Vector3D p2 = Vector3D.of(+1, 0, 0);
  245.      * Vector3D p3 = Vector3D.of(0, +1, 0);
  246.      *
  247.      * Plane plane = Planes.fromPoints(p1, p2, p3, precision); // normal is (0, 0, +1)
  248.      *
  249.      * // create a transform that negates all x-values; this transform does not
  250.      * // preserve orientation, i.e. it will convert a right-handed system into a left-handed
  251.      * // system and vice versa
  252.      * AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(-1, 1,  1);
  253.      *
  254.      * // transform the plane
  255.      * Plane transformedPlane = plane.transform(transform);
  256.      *
  257.      * // the plane normal is oriented such that transformed points are still ordered
  258.      * // counterclockwise when looking down the plane normal; since the point (1, 0, 0) has
  259.      * // now become (-1, 0, 0), the normal has flipped to (0, 0, -1)
  260.      * transformedPlane.getNormal();
  261.      *
  262.      * // directly transform the original plane normal; the normal is unchanged by the transform
  263.      * // since the target space of the transform is left-handed
  264.      * AffineTransformMatrix3D normalTransform = transform.normalTransform();
  265.      * Vector3D directlyTransformedNormal = normalTransform.apply(plane.getNormal()); // (0, 0, +1)
  266.      * </pre>
  267.      */
  268.     @Override
  269.     public Plane transform(final Transform<Vector3D> transform) {
  270.         // create 3 representation points lying on the plane, transform them,
  271.         // and use the transformed points to create a new plane

  272.         final Vector3D u = normal.orthogonal();
  273.         final Vector3D v = normal.cross(u);

  274.         final Vector3D p1 = getOrigin();
  275.         final Vector3D p2 = p1.add(u);
  276.         final Vector3D p3 = p1.add(v);

  277.         final Vector3D t1 = transform.apply(p1);
  278.         final Vector3D t2 = transform.apply(p2);
  279.         final Vector3D t3 = transform.apply(p3);

  280.         return Planes.fromPoints(t1, t2, t3, getPrecision());
  281.     }

  282.     /** Translate the plane by the specified amount.
  283.      * @param translation translation to apply
  284.      * @return a new plane
  285.      */
  286.     public Plane translate(final Vector3D translation) {
  287.         final Vector3D tOrigin = getOrigin().add(translation);

  288.         return Planes.fromPointAndNormal(tOrigin, normal, getPrecision());
  289.     }

  290.     /** Rotate the plane around the specified point.
  291.      * @param center rotation center
  292.      * @param rotation 3-dimensional rotation
  293.      * @return a new plane
  294.      */
  295.     public Plane rotate(final Vector3D center, final QuaternionRotation rotation) {
  296.         final Vector3D delta = getOrigin().subtract(center);
  297.         final Vector3D tOrigin = center.add(rotation.apply(delta));

  298.         // we can directly apply the rotation to the normal since it will transform
  299.         // it properly (there is no translation or scaling involved)
  300.         final Vector3D.Unit tNormal = rotation.apply(normal).normalize();

  301.         return Planes.fromPointAndNormal(tOrigin, tNormal, getPrecision());
  302.     }

  303.     /** Return true if this instance should be considered equivalent to the argument, using the
  304.      * given precision context for comparison. Instances are considered equivalent if they contain
  305.      * the same points, which is determined by comparing the plane {@code origins} and {@code normals}.
  306.      * @param other the point to compare with
  307.      * @param precision precision context to use for the comparison
  308.      * @return true if this instance should be considered equivalent to the argument
  309.      * @see Vector3D#eq(Vector3D, Precision.DoubleEquivalence)
  310.      */
  311.     public boolean eq(final Plane other, final Precision.DoubleEquivalence precision) {
  312.         return getOrigin().eq(other.getOrigin(), precision) &&
  313.                 normal.eq(other.normal, precision);
  314.     }

  315.     /** {@inheritDoc} */
  316.     @Override
  317.     public int hashCode() {
  318.         return Objects.hash(normal, originOffset, getPrecision());
  319.     }

  320.     /** {@inheritDoc} */
  321.     @Override
  322.     public boolean equals(final Object obj) {
  323.         if (this == obj) {
  324.             return true;
  325.         } else if (obj == null || obj.getClass() != this.getClass()) {
  326.             return false;
  327.         }

  328.         final Plane other = (Plane) obj;

  329.         return Objects.equals(this.normal, other.normal) &&
  330.                 Double.compare(this.originOffset, other.originOffset) == 0 &&
  331.                 Objects.equals(this.getPrecision(), other.getPrecision());
  332.     }

  333.     /** {@inheritDoc} */
  334.     @Override
  335.     public String toString() {
  336.         final StringBuilder sb = new StringBuilder();
  337.         sb.append(getClass().getSimpleName())
  338.             .append("[origin= ")
  339.             .append(getOrigin())
  340.             .append(", normal= ")
  341.             .append(normal)
  342.             .append(']');

  343.         return sb.toString();
  344.     }

  345.     /** Get the intersection point of three planes. Returns null if no unique intersection point
  346.      * exists (ie, there are no intersection points or an infinite number).
  347.      * @param plane1 first plane1
  348.      * @param plane2 second plane2
  349.      * @param plane3 third plane2
  350.      * @return intersection point of the three planes or null if no unique intersection point exists
  351.      */
  352.     public static Vector3D intersection(final Plane plane1, final Plane plane2, final Plane plane3) {

  353.         // coefficients of the three planes linear equations
  354.         final double a1 = plane1.normal.getX();
  355.         final double b1 = plane1.normal.getY();
  356.         final double c1 = plane1.normal.getZ();
  357.         final double d1 = plane1.originOffset;

  358.         final double a2 = plane2.normal.getX();
  359.         final double b2 = plane2.normal.getY();
  360.         final double c2 = plane2.normal.getZ();
  361.         final double d2 = plane2.originOffset;

  362.         final double a3 = plane3.normal.getX();
  363.         final double b3 = plane3.normal.getY();
  364.         final double c3 = plane3.normal.getZ();
  365.         final double d3 = plane3.originOffset;

  366.         // direct Cramer resolution of the linear system
  367.         // (this is still feasible for a 3x3 system)
  368.         final double a23 = (b2 * c3) - (b3 * c2);
  369.         final double b23 = (c2 * a3) - (c3 * a2);
  370.         final double c23 = (a2 * b3) - (a3 * b2);
  371.         final double determinant = (a1 * a23) + (b1 * b23) + (c1 * c23);

  372.         // use the precision context of the first plane to determine equality
  373.         if (plane1.getPrecision().eqZero(determinant)) {
  374.             return null;
  375.         }

  376.         final double r = 1.0 / determinant;
  377.         return Vector3D.of((-a23 * d1 - (c1 * b3 - c3 * b1) * d2 - (c2 * b1 - c1 * b2) * d3) * r,
  378.                 (-b23 * d1 - (c3 * a1 - c1 * a3) * d2 - (c1 * a2 - c2 * a1) * d3) * r,
  379.                 (-c23 * d1 - (b1 * a3 - b3 * a1) * d2 - (b2 * a1 - b1 * a2) * d3) * r);
  380.     }
  381. }