View Javadoc
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  
19  import java.util.Objects;
20  
21  import org.apache.commons.geometry.core.Transform;
22  import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
23  import org.apache.commons.geometry.core.partitioning.Hyperplane;
24  import org.apache.commons.geometry.euclidean.threed.line.Line3D;
25  import org.apache.commons.geometry.euclidean.threed.line.Lines3D;
26  import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
27  import org.apache.commons.geometry.euclidean.twod.ConvexArea;
28  import org.apache.commons.numbers.core.Precision;
29  
30  /** Class representing a plane in 3 dimensional Euclidean space. Each plane is defined by a
31   * {@link #getNormal() normal} and an {@link #getOriginOffset() origin offset}. If \(\vec{n}\) is the plane normal,
32   * \(d\) is the origin offset, and \(p\) and \(q\) are any points in the plane, then the following are true:
33   * <ul>
34   *  <li>\(\lVert \vec{n} \rVert\) = 1</li>
35   *  <li>\(\vec{n} \cdot (p - q) = 0\)</li>
36   *  <li>\(d = - (\vec{n} \cdot q)\)</li>
37   *  </ul>
38   *  In other words, the normal is a unit vector such that the dot product of the normal and the difference of
39   *  any two points in the plane is always equal to \(0\). Similarly, the {@code origin offset} is equal to the
40   *  negation of the dot product of the normal and any point in the plane. The projection of the origin onto the
41   *  plane (given by {@link #getOrigin()}), is computed as \(-d \vec{n}\).
42   *
43   * <p>Instances of this class are guaranteed to be immutable.</p>
44   * @see Planes
45   */
46  public class Plane extends AbstractHyperplane<Vector3D> {
47  
48      /** Plane normal. */
49      private final Vector3D.Unit normal;
50  
51      /** Offset of the origin with respect to the plane. */
52      private final double originOffset;
53  
54      /** Construct a plane from its component parts.
55       * @param normal unit normal vector
56       * @param originOffset offset of the origin with respect to the plane
57       * @param precision precision context used to compare floating point values
58       */
59      Plane(final Vector3D.Unit normal, final double originOffset,
60            final Precision.DoubleEquivalence precision) {
61  
62          super(precision);
63  
64          this.normal = normal;
65          this.originOffset = originOffset;
66      }
67  
68      /** Get the orthogonal projection of the 3D-space origin in the plane.
69       * @return the origin point of the plane frame (point closest to the 3D-space
70       *         origin)
71       */
72      public Vector3D getOrigin() {
73          return normal.multiply(-originOffset);
74      }
75  
76      /** Get the offset of the spatial origin ({@code 0, 0, 0}) with respect to the plane.
77       * @return the offset of the origin with respect to the plane.
78       */
79      public double getOriginOffset() {
80          return originOffset;
81      }
82  
83      /** Get the plane normal vector.
84       * @return plane normal vector
85       */
86      public Vector3D.Unit getNormal() {
87          return normal;
88      }
89  
90      /** Return an {@link EmbeddingPlane} instance suitable for embedding 2D geometric objects
91       * into this plane. Returned instances are guaranteed to be equal between invocations.
92       * @return a plane instance suitable for embedding 2D subspaces
93       */
94      public EmbeddingPlane getEmbedding() {
95          final Vector3D.Unit u = normal.orthogonal();
96          final Vector3D.Unit v = normal.cross(u).normalize();
97  
98          return new EmbeddingPlane(u, v, normal, originOffset, getPrecision());
99      }
100 
101     /** {@inheritDoc} */
102     @Override
103     public double offset(final Vector3D point) {
104         return point.dot(normal) + originOffset;
105     }
106 
107     /** Get the offset (oriented distance) of the given line with respect to the plane. The value
108      * closest to zero is returned, which will always be zero if the line is not parallel to the plane.
109      * @param line line to calculate the offset of
110      * @return the offset of the line with respect to the plane or 0.0 if the line
111      *      is not parallel to the plane.
112      */
113     public double offset(final Line3D line) {
114         if (!isParallel(line)) {
115             return 0.0;
116         }
117         return offset(line.getOrigin());
118     }
119 
120     /** Get the offset (oriented distance) of the given plane with respect to this instance. The value
121      * closest to zero is returned, which will always be zero if the planes are not parallel.
122      * @param plane plane to calculate the offset of
123      * @return the offset of the plane with respect to this instance or 0.0 if the planes
124      *      are not parallel.
125      */
126     public double offset(final Plane plane) {
127         if (!isParallel(plane)) {
128             return 0.0;
129         }
130         return originOffset + (similarOrientation(plane) ? -plane.originOffset : plane.originOffset);
131     }
132 
133     /** Check if the instance contains a point.
134      * @param p point to check
135      * @return true if p belongs to the plane
136      */
137     @Override
138     public boolean contains(final Vector3D p) {
139         return getPrecision().eqZero(offset(p));
140     }
141 
142     /** Check if the instance contains a line.
143      * @param line line to check
144      * @return true if line is contained in this plane
145      */
146     public boolean contains(final Line3D line) {
147         return isParallel(line) && contains(line.getOrigin());
148     }
149 
150     /** Check if the instance contains another plane. Planes are considered similar if they contain
151      * the same points. This does not mean they are equal since they can have opposite normals.
152      * @param plane plane to which the instance is compared
153      * @return true if the planes are similar
154      */
155     public boolean contains(final Plane plane) {
156         final double angle = normal.angle(plane.normal);
157         final Precision.DoubleEquivalence precision = getPrecision();
158 
159         return ((precision.eqZero(angle)) && precision.eq(originOffset, plane.originOffset)) ||
160                 ((precision.eq(angle, Math.PI)) && precision.eq(originOffset, -plane.originOffset));
161     }
162 
163     /** {@inheritDoc} */
164     @Override
165     public Vector3D project(final Vector3D point) {
166         return getOrigin().add(point.reject(normal));
167     }
168 
169     /** Project a 3D line onto the plane.
170      * @param line the line to project
171      * @return the projection of the given line onto the plane.
172      */
173     public Line3D project(final Line3D line) {
174         final Vector3D direction = line.getDirection();
175         final Vector3D projection = normal.multiply(direction.dot(normal) * (1 / normal.normSq()));
176 
177         final Vector3D projectedLineDirection = direction.subtract(projection);
178         final Vector3D p1 = project(line.getOrigin());
179         final Vector3D p2 = p1.add(projectedLineDirection);
180 
181         return Lines3D.fromPoints(p1, p2, getPrecision());
182     }
183 
184     /** {@inheritDoc} */
185     @Override
186     public PlaneConvexSubset span() {
187         return Planes.subsetFromConvexArea(getEmbedding(), ConvexArea.full());
188     }
189 
190     /** Check if the line is parallel to the instance.
191      * @param line line to check.
192      * @return true if the line is parallel to the instance, false otherwise.
193      */
194     public boolean isParallel(final Line3D line) {
195         final double dot = normal.dot(line.getDirection());
196 
197         return getPrecision().eqZero(dot);
198     }
199 
200     /** Check if the plane is parallel to the instance.
201      * @param plane plane to check.
202      * @return true if the plane is parallel to the instance, false otherwise.
203      */
204     public boolean isParallel(final Plane plane) {
205         return getPrecision().eqZero(normal.cross(plane.normal).norm());
206     }
207 
208     /** {@inheritDoc} */
209     @Override
210     public boolean similarOrientation(final Hyperplane<Vector3D> other) {
211         return (((Plane) other).normal).dot(normal) > 0;
212     }
213 
214     /** Get the intersection of a line with this plane.
215      * @param line line intersecting the instance
216      * @return intersection point between between the line and the instance (null if
217      *         the line is parallel to the instance)
218      */
219     public Vector3D intersection(final Line3D line) {
220         final Vector3D direction = line.getDirection();
221         final double dot = normal.dot(direction);
222 
223         if (getPrecision().eqZero(dot)) {
224             return null;
225         }
226 
227         final Vector3D point = line.pointAt(0);
228         final double k = -(originOffset + normal.dot(point)) / dot;
229 
230         return Vector3D.Sum.of(point)
231                 .addScaled(k, direction)
232                 .get();
233     }
234 
235     /** Get the line formed by the intersection of this instance with the given plane.
236      * The returned line lies in both planes and points in the direction of
237      * the cross product <code>n<sub>1</sub> x n<sub>2</sub></code>, where <code>n<sub>1</sub></code>
238      * is the normal of the current instance and <code>n<sub>2</sub></code> is the normal
239      * of the argument.
240      *
241      * <p>Null is returned if the planes are parallel.</p>
242      *
243      * @param other other plane
244      * @return line at the intersection of the instance and the other plane, or null
245      *      if no such line exists
246      */
247     public Line3D intersection(final Plane other) {
248         final Vector3D direction = normal.cross(other.normal);
249 
250         if (getPrecision().eqZero(direction.norm())) {
251             return null;
252         }
253 
254         final Vector3D point = intersection(this, other, Planes.fromNormal(direction, getPrecision()));
255 
256         return Lines3D.fromPointAndDirection(point, direction, getPrecision());
257     }
258 
259     /** Build a new reversed version of this plane, with opposite orientation.
260      * @return a new reversed plane
261      */
262     @Override
263     public Plane reverse() {
264         return new Plane(normal.negate(), -originOffset, getPrecision());
265     }
266 
267     /** {@inheritDoc}
268      *
269      * <p>Instances are transformed by selecting 3 representative points from the
270      * plane, transforming them, and constructing a new plane from the transformed points.
271      * Since the normal is not transformed directly, but rather is constructed new from the
272      * transformed points, the relative orientations of points in the plane are preserved,
273      * even for transforms that do not
274      * {@link Transform#preservesOrientation() preserve orientation}. The example below shows
275      * a plane being transformed by a non-orientation-preserving transform. The normal of the
276      * transformed plane retains its counterclockwise relationship to the points in the plane,
277      * in contrast with the normal that is transformed directly by the transform.
278      * </p>
279      * <pre>
280      * // construct a plane from 3 points; the normal will be selected such that the
281      * // points are ordered counterclockwise when looking down the plane normal.
282      * Vector3D p1 = Vector3D.of(0, 0, 0);
283      * Vector3D p2 = Vector3D.of(+1, 0, 0);
284      * Vector3D p3 = Vector3D.of(0, +1, 0);
285      *
286      * Plane plane = Planes.fromPoints(p1, p2, p3, precision); // normal is (0, 0, +1)
287      *
288      * // create a transform that negates all x-values; this transform does not
289      * // preserve orientation, i.e. it will convert a right-handed system into a left-handed
290      * // system and vice versa
291      * AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(-1, 1,  1);
292      *
293      * // transform the plane
294      * Plane transformedPlane = plane.transform(transform);
295      *
296      * // the plane normal is oriented such that transformed points are still ordered
297      * // counterclockwise when looking down the plane normal; since the point (1, 0, 0) has
298      * // now become (-1, 0, 0), the normal has flipped to (0, 0, -1)
299      * transformedPlane.getNormal();
300      *
301      * // directly transform the original plane normal; the normal is unchanged by the transform
302      * // since the target space of the transform is left-handed
303      * AffineTransformMatrix3D normalTransform = transform.normalTransform();
304      * Vector3D directlyTransformedNormal = normalTransform.apply(plane.getNormal()); // (0, 0, +1)
305      * </pre>
306      */
307     @Override
308     public Plane transform(final Transform<Vector3D> transform) {
309         // create 3 representation points lying on the plane, transform them,
310         // and use the transformed points to create a new plane
311 
312         final Vector3D u = normal.orthogonal();
313         final Vector3D v = normal.cross(u);
314 
315         final Vector3D p1 = getOrigin();
316         final Vector3D p2 = p1.add(u);
317         final Vector3D p3 = p1.add(v);
318 
319         final Vector3D t1 = transform.apply(p1);
320         final Vector3D t2 = transform.apply(p2);
321         final Vector3D t3 = transform.apply(p3);
322 
323         return Planes.fromPoints(t1, t2, t3, getPrecision());
324     }
325 
326     /** Translate the plane by the specified amount.
327      * @param translation translation to apply
328      * @return a new plane
329      */
330     public Plane translate(final Vector3D translation) {
331         final Vector3D tOrigin = getOrigin().add(translation);
332 
333         return Planes.fromPointAndNormal(tOrigin, normal, getPrecision());
334     }
335 
336     /** Rotate the plane around the specified point.
337      * @param center rotation center
338      * @param rotation 3-dimensional rotation
339      * @return a new plane
340      */
341     public Plane rotate(final Vector3D center, final QuaternionRotation rotation) {
342         final Vector3D delta = getOrigin().subtract(center);
343         final Vector3D tOrigin = center.add(rotation.apply(delta));
344 
345         // we can directly apply the rotation to the normal since it will transform
346         // it properly (there is no translation or scaling involved)
347         final Vector3D.Unit tNormal = rotation.apply(normal).normalize();
348 
349         return Planes.fromPointAndNormal(tOrigin, tNormal, getPrecision());
350     }
351 
352     /** Return true if this instance should be considered equivalent to the argument, using the
353      * given precision context for comparison. Instances are considered equivalent if they contain
354      * the same points, which is determined by comparing the plane {@code origins} and {@code normals}.
355      * @param other the point to compare with
356      * @param precision precision context to use for the comparison
357      * @return true if this instance should be considered equivalent to the argument
358      * @see Vector3D#eq(Vector3D, Precision.DoubleEquivalence)
359      */
360     public boolean eq(final Plane other, final Precision.DoubleEquivalence precision) {
361         return getOrigin().eq(other.getOrigin(), precision) &&
362                 normal.eq(other.normal, precision);
363     }
364 
365     /** {@inheritDoc} */
366     @Override
367     public int hashCode() {
368         return Objects.hash(normal, originOffset, getPrecision());
369     }
370 
371     /** {@inheritDoc} */
372     @Override
373     public boolean equals(final Object obj) {
374         if (this == obj) {
375             return true;
376         } else if (obj == null || obj.getClass() != this.getClass()) {
377             return false;
378         }
379 
380         final Plane other = (Plane) obj;
381 
382         return Objects.equals(this.normal, other.normal) &&
383                 Double.compare(this.originOffset, other.originOffset) == 0 &&
384                 Objects.equals(this.getPrecision(), other.getPrecision());
385     }
386 
387     /** {@inheritDoc} */
388     @Override
389     public String toString() {
390         final StringBuilder sb = new StringBuilder();
391         sb.append(getClass().getSimpleName())
392             .append("[origin= ")
393             .append(getOrigin())
394             .append(", normal= ")
395             .append(normal)
396             .append(']');
397 
398         return sb.toString();
399     }
400 
401     /** Get the intersection point of three planes. Returns null if no unique intersection point
402      * exists (ie, there are no intersection points or an infinite number).
403      * @param plane1 first plane1
404      * @param plane2 second plane2
405      * @param plane3 third plane2
406      * @return intersection point of the three planes or null if no unique intersection point exists
407      */
408     public static Vector3D intersection(final Plane plane1, final Plane plane2, final Plane plane3) {
409 
410         // coefficients of the three planes linear equations
411         final double a1 = plane1.normal.getX();
412         final double b1 = plane1.normal.getY();
413         final double c1 = plane1.normal.getZ();
414         final double d1 = plane1.originOffset;
415 
416         final double a2 = plane2.normal.getX();
417         final double b2 = plane2.normal.getY();
418         final double c2 = plane2.normal.getZ();
419         final double d2 = plane2.originOffset;
420 
421         final double a3 = plane3.normal.getX();
422         final double b3 = plane3.normal.getY();
423         final double c3 = plane3.normal.getZ();
424         final double d3 = plane3.originOffset;
425 
426         // direct Cramer resolution of the linear system
427         // (this is still feasible for a 3x3 system)
428         final double a23 = (b2 * c3) - (b3 * c2);
429         final double b23 = (c2 * a3) - (c3 * a2);
430         final double c23 = (a2 * b3) - (a3 * b2);
431         final double determinant = (a1 * a23) + (b1 * b23) + (c1 * c23);
432 
433         // use the precision context of the first plane to determine equality
434         if (plane1.getPrecision().eqZero(determinant)) {
435             return null;
436         }
437 
438         final double r = 1.0 / determinant;
439         return Vector3D.of((-a23 * d1 - (c1 * b3 - c3 * b1) * d2 - (c2 * b1 - c1 * b2) * d3) * r,
440                 (-b23 * d1 - (c3 * a1 - c1 * a3) * d2 - (c1 * a2 - c2 * a1) * d3) * r,
441                 (-c23 * d1 - (b1 * a3 - b3 * a1) * d2 - (b2 * a1 - b1 * a2) * d3) * r);
442     }
443 }