View Javadoc
1   /*
2    * Copyright (C) 2012 Alberto Irurueta Carro (alberto@irurueta.com)
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *         http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package com.irurueta.geometry;
17  
18  import com.irurueta.algebra.AlgebraException;
19  import com.irurueta.algebra.ArrayUtils;
20  import com.irurueta.algebra.Matrix;
21  import com.irurueta.algebra.SingularValueDecomposer;
22  
23  import java.io.Serializable;
24  import java.util.Objects;
25  
26  /**
27   * Class defining a plane.
28   * Planes can be expressed using the following expression:
29   * A * x + B * y + C * z + D = 0
30   */
31  @SuppressWarnings("DuplicatedCode")
32  public class Plane implements Serializable {
33  
34      /**
35       * Constant defining the size of the vector that contains plane parameters.
36       */
37      public static final int PLANE_NUMBER_PARAMS = 4;
38  
39      /**
40       * Constant defining the distance threshold to determine whether a point
41       * lays inside (is locus) this plane or not.
42       */
43      public static final double DEFAULT_LOCUS_THRESHOLD = 1e-12;
44  
45      /**
46       * Minimum allowed threshold.
47       */
48      public static final double MIN_THRESHOLD = 0.0;
49  
50      /**
51       * Defines the threshold used when comparing two values.
52       */
53      public static final double DEFAULT_COMPARISON_THRESHOLD = 1e-10;
54  
55      /**
56       * Machine precision.
57       */
58      private static final double PRECISION = 1e-12;
59  
60      /**
61       * Constant defining error threshold, which is a small value close to
62       * machine precision.
63       */
64      private static final double DEFAULT_ERROR_THRESHOLD = 1e-12;
65  
66      /**
67       * Constant defining the size of vector that define the direction of a plane.
68       */
69      private static final int INHOM_VECTOR_SIZE = 3;
70  
71      /**
72       * Parameter A of a plane.
73       */
74      private double a;
75  
76      /**
77       * Parameter B of a plane.
78       */
79      private double b;
80  
81      /**
82       * Parameter C of a plane.
83       */
84      private double c;
85  
86      /**
87       * Parameter D of a plane.
88       */
89      private double d;
90  
91      /**
92       * Defines whether the plane is already normalized or not.
93       */
94      private boolean normalized;
95  
96      /**
97       * Default constructor of this class.
98       */
99      public Plane() {
100         a = b = c = d = 0.0;
101         normalized = false;
102     }
103 
104     /**
105      * Constructor.
106      * This constructor accepts every parameter describing a plane in its
107      * homogeneous form:
108      * Ax + By + Cz + D = 0
109      *
110      * @param a Parameter A of this plane.
111      * @param b Parameter B of this plane.
112      * @param c Parameter C of this plane.
113      * @param d Parameter D of this plane.
114      */
115     public Plane(final double a, final double b, final double c, final double d) {
116         setParameters(a, b, c, d);
117     }
118 
119     /**
120      * Constructor.
121      * This constructor accepts an array containing all the parameters (a, b, c,
122      * d) describing a plane.
123      *
124      * @param array Array containing plane parameters.
125      * @throws IllegalArgumentException Raised if length of array is not 4.
126      */
127     public Plane(final double[] array) {
128         setParameters(array);
129     }
130 
131     /**
132      * Constructor.
133      * This constructor accepts three 3D points and computes the plane
134      * parameters so that the plane passes through provided points (they are
135      * locus).
136      *
137      * @param pointA First 3D point to compute the plane.
138      * @param pointB Second 3D point to compute the plane.
139      * @param pointC Third 3D point to compute the plane.
140      * @throws ColinearPointsException Raised if provided points lay in a line
141      *                                 preventing a single plane to be estimated. This happens in degenerate
142      *                                 configurations where points are co-linear and an infinite set of planes
143      *                                 pass through them.
144      */
145     public Plane(final Point3D pointA, final Point3D pointB, final Point3D pointC) throws ColinearPointsException {
146         setParametersFromThreePoints(pointA, pointB, pointC);
147     }
148 
149     /**
150      * Constructor.
151      *
152      * @param point   Point laying inside the plane.
153      * @param vectorA First vector laying in the plane.
154      * @param vectorB Second vector laying in the plane.
155      * @throws IllegalArgumentException Raised if vectors length is not 3.
156      * @throws ParallelVectorsException Raised if provided vectors are parallel.
157      */
158     public Plane(final Point3D point, final double[] vectorA, final double[] vectorB) throws ParallelVectorsException {
159         setParametersFrom1PointAnd2Vectors(point, vectorA, vectorB);
160     }
161 
162 
163     /**
164      * Constructor of a plane from one point and its director vector.
165      *
166      * @param point  Point laying inside the plane.
167      * @param vector Director vector.
168      * @throws IllegalArgumentException Raised if vector length is not 3.
169      */
170     public Plane(final Point3D point, final double[] vector) {
171         setParametersFromPointAndDirectorVector(point, vector);
172     }
173 
174     /**
175      * Returns parameter A of this plane.
176      *
177      * @return Parameter A of this plane.
178      */
179     public double getA() {
180         return a;
181     }
182 
183     /**
184      * Returns parameter B of this plane.
185      *
186      * @return Parameter B of this plane.
187      */
188     public double getB() {
189         return b;
190     }
191 
192     /**
193      * Returns parameter C of this plane.
194      *
195      * @return Parameter C of this plane.
196      */
197     public double getC() {
198         return c;
199     }
200 
201     /**
202      * Returns parameter D of this plane.
203      *
204      * @return Parameter D of this plane.
205      */
206     public double getD() {
207         return d;
208     }
209 
210     /**
211      * Sets parameters of this plane.
212      *
213      * @param a Parameter A of this plane.
214      * @param b Parameter B of this plane.
215      * @param c Parameter C of this plane.
216      * @param d Parameter D of this plane.
217      */
218     public final void setParameters(final double a, final double b, final double c, final double d) {
219         this.a = a;
220         this.b = b;
221         this.c = c;
222         this.d = d;
223         normalized = false;
224     }
225 
226     /**
227      * Sets parameters of this plane.
228      *
229      * @param array Array containing parameters of this plane.
230      * @throws IllegalArgumentException Raised if provided array does not have length equal to 4.
231      */
232     public final void setParameters(final double[] array) {
233         if (array.length != PLANE_NUMBER_PARAMS) {
234             throw new IllegalArgumentException();
235         }
236 
237         a = array[0];
238         b = array[1];
239         c = array[2];
240         d = array[3];
241         normalized = false;
242     }
243 
244     /**
245      * Computes and sets plane parameters using provided 3D points.
246      * A plane can be defined from just 3 points.
247      *
248      * @param pointA 1st point.
249      * @param pointB 2nd point.
250      * @param pointC 3rd point.
251      * @throws ColinearPointsException if provided points are in a co-linear or degenerate configuration.
252      */
253     public final void setParametersFromThreePoints(final Point3D pointA, final Point3D pointB, final Point3D pointC)
254             throws ColinearPointsException {
255 
256         // normalize points to increase accuracy
257         pointA.normalize();
258         pointB.normalize();
259         pointC.normalize();
260 
261         // we use 3 points to find one plane
262         try {
263             // set homogeneous coordinates of each point on each row of the matrix
264             final var m = new Matrix(3, PLANE_NUMBER_PARAMS);
265 
266             m.setElementAt(0, 0, pointA.getHomX());
267             m.setElementAt(0, 1, pointA.getHomY());
268             m.setElementAt(0, 2, pointA.getHomZ());
269             m.setElementAt(0, 3, pointA.getHomW());
270 
271             m.setElementAt(1, 0, pointB.getHomX());
272             m.setElementAt(1, 1, pointB.getHomY());
273             m.setElementAt(1, 2, pointB.getHomZ());
274             m.setElementAt(1, 3, pointB.getHomW());
275 
276             m.setElementAt(2, 0, pointC.getHomX());
277             m.setElementAt(2, 1, pointC.getHomY());
278             m.setElementAt(2, 2, pointC.getHomZ());
279             m.setElementAt(2, 3, pointC.getHomW());
280 
281             final var decomposer = new SingularValueDecomposer(m);
282             decomposer.decompose();
283 
284             if (decomposer.getRank() < 3) {
285                 // points where co-linear, and so the null-space of those 3 points
286                 // has dimension greater than one (a pencil of planes instead
287                 // of just one plane can be defined)
288                 throw new ColinearPointsException();
289             }
290 
291             // V is a 4x4 orthonormal matrix
292             final var mV = decomposer.getV();
293 
294             // last column of V will contain the right null-space of m, which is
295             // the plane where provided points belong to.
296             a = mV.getElementAt(0, 3);
297             b = mV.getElementAt(1, 3);
298             c = mV.getElementAt(2, 3);
299             d = mV.getElementAt(3, 3);
300 
301             // Because V is orthonormal, its columns have norm equal to 1 and
302             // there is no need to normalize this plane to increase accuracy
303             normalized = true;
304 
305         } catch (final AlgebraException e) {
306             // should only fail if decomposition fails for numerical reasons
307             throw new ColinearPointsException(e);
308         }
309     }
310 
311     /**
312      * Determines if provided points are co-linear or have a degenerate
313      * configuration. If returned value is true, then such points cannot be used
314      * to estimate a plane.
315      *
316      * @param pointA 1st plane.
317      * @param pointB 2nd plane.
318      * @param pointC 3rd plane.
319      * @return true if provided points co-linear, false otherwise.
320      */
321     public static boolean areColinearPoints(final Point3D pointA, final Point3D pointB, final Point3D pointC) {
322         // normalize points to increase accuracy
323         pointA.normalize();
324         pointB.normalize();
325         pointC.normalize();
326 
327         // we use 3 points to find one plane
328         try {
329             // set homogeneous coordinates of each point on each row of the matrix
330             final var m = new Matrix(3, PLANE_NUMBER_PARAMS);
331 
332             m.setElementAt(0, 0, pointA.getHomX());
333             m.setElementAt(0, 1, pointA.getHomY());
334             m.setElementAt(0, 2, pointA.getHomZ());
335             m.setElementAt(0, 3, pointA.getHomW());
336 
337             m.setElementAt(1, 0, pointB.getHomX());
338             m.setElementAt(1, 1, pointB.getHomY());
339             m.setElementAt(1, 2, pointB.getHomZ());
340             m.setElementAt(1, 3, pointB.getHomW());
341 
342             m.setElementAt(2, 0, pointC.getHomX());
343             m.setElementAt(2, 1, pointC.getHomY());
344             m.setElementAt(2, 2, pointC.getHomZ());
345             m.setElementAt(2, 3, pointC.getHomW());
346 
347             final var decomposer = new SingularValueDecomposer(m);
348             decomposer.decompose();
349 
350             // if points were co-linear, their null-space has dimension greater
351             // than one (a pencil of planes instead of just one plane can be
352             // defined)
353             return (decomposer.getRank() < 3);
354         } catch (final AlgebraException e) {
355             return true;
356         }
357     }
358 
359     /**
360      * Sets parameter A of this plane.
361      *
362      * @param a Parameter A.
363      */
364     public void setA(final double a) {
365         this.a = a;
366         normalized = false;
367     }
368 
369     /**
370      * Sets parameter B of this plane.
371      *
372      * @param b Parameter B.
373      */
374     public void setB(final double b) {
375         this.b = b;
376         normalized = false;
377     }
378 
379     /**
380      * Sets parameter C of this plane.
381      *
382      * @param c Parameter C.
383      */
384     public void setC(final double c) {
385         this.c = c;
386         normalized = false;
387     }
388 
389     /**
390      * Sets parameter D of this plane.
391      *
392      * @param d Parameter D.
393      */
394     public void setD(final double d) {
395         this.d = d;
396         normalized = false;
397     }
398 
399     /**
400      * Sets the parameters of a plane from one point and two vectors.
401      *
402      * @param point   Point laying inside the plane.
403      * @param vectorA First vector laying in the plane.
404      * @param vectorB Second vector laying in the plane.
405      * @throws IllegalArgumentException Raised if vectors length is not 3.
406      * @throws ParallelVectorsException Raised if provided vectors are parallel.
407      */
408     public final void setParametersFrom1PointAnd2Vectors(
409             final Point3D point, final double[] vectorA, final double[] vectorB) throws ParallelVectorsException {
410 
411         if (vectorA.length != INHOM_VECTOR_SIZE || vectorB.length != INHOM_VECTOR_SIZE) {
412             throw new IllegalArgumentException();
413         }
414 
415         // normalize vectors to increase accuracy (we make a copy to avoid
416         // changing provided arrays)
417         var norm = com.irurueta.algebra.Utils.normF(vectorA);
418         final var vA = ArrayUtils.multiplyByScalarAndReturnNew(vectorA, 1.0 / norm);
419         norm = com.irurueta.algebra.Utils.normF(vectorB);
420         final var vB = ArrayUtils.multiplyByScalarAndReturnNew(vectorB, 1.0 / norm);
421 
422         try {
423             final var cross = com.irurueta.algebra.Utils.crossProduct(vA, vB);
424 
425             // check if resulting vector from cross product is too small (vectors
426             // are almost parallel, and machine precision might worsen things)
427             if (Math.abs(cross[0]) < DEFAULT_ERROR_THRESHOLD && Math.abs(cross[1]) < DEFAULT_ERROR_THRESHOLD
428                     && Math.abs(cross[2]) < DEFAULT_ERROR_THRESHOLD) {
429                 throw new ParallelVectorsException();
430             }
431 
432             // the point and the two vectors will define a plane computing the
433             // cross product of the two vectors gives the values for (a,b,c),
434             // which it is its director vector, but d is still unknown.
435             // Given a point (xp, yp, zp, wp) and forcing this expression
436             // point'*plane = 0 results in
437             // a*xp + b*xy + c*xz + d*wp = 0
438             // and solving
439             // d = -(a*xp + b*xy + c*xz) / wp -> the plane is fully defined
440             setParametersFromPointAndDirectorVector(point, cross);
441         } catch (final AlgebraException e) {
442             throw new ParallelVectorsException(e);
443         }
444     }
445 
446     /**
447      * Sets parameters of a plane from one point and its director vector.
448      *
449      * @param point  Point laying inside the plane.
450      * @param vector Director vector.
451      * @throws IllegalArgumentException Raised if vector length is not 3.
452      */
453     public final void setParametersFromPointAndDirectorVector(final Point3D point, final double[] vector) {
454 
455         if (vector.length != INHOM_VECTOR_SIZE) {
456             throw new IllegalArgumentException();
457         }
458 
459         // normalize point to increase accuracy
460         point.normalize();
461 
462         a = vector[0];
463         b = vector[1];
464         c = vector[2];
465 
466         d = -(a * point.getHomX() + b * point.getHomY() + c * point.getHomZ()) / point.getHomW();
467 
468         normalized = false;
469     }
470 
471     /**
472      * Check if provided point is locus (lays into) of the plane.
473      *
474      * @param point Point to be checked.
475      * @return True if point is locus of this plane, false otherwise.
476      */
477     public boolean isLocus(final Point3D point) {
478         return isLocus(point, DEFAULT_LOCUS_THRESHOLD);
479     }
480 
481     /**
482      * Check if provided point is locus (lays into) of the plane.
483      *
484      * @param point     Point to be checked.
485      * @param threshold Threshold (non-negative small value) to decide if a
486      *                  point is locus of this plane.
487      * @return True if point is locus of this plane, false otherwise.
488      * @throws IllegalArgumentException Raised if threshold is negative.
489      */
490     public boolean isLocus(final Point3D point, final double threshold) {
491         if (threshold < MIN_THRESHOLD) {
492             throw new IllegalArgumentException();
493         }
494 
495         // make dot product of homogeneous coordinates with plane
496         // m = [x, y, z, w], P = [a, b, c, d], then
497         // x * a + y * b + z * c + w * d must be very small to be locus
498 
499         point.normalize();
500         normalize();
501 
502         final var dotProd = point.getHomX() * a + point.getHomY() * b + point.getHomZ() * c + point.getHomW() * d;
503 
504         return Math.abs(dotProd) < threshold;
505     }
506 
507     /**
508      * Distance between a plane and a 3D point. Returned distance equals to the
509      * Euclidean distance between this plane and provided point but having sign.
510      * Sign indicates whether point is at one side or the other of the plane.
511      *
512      * @param point Point whose distance to this line will be computed.
513      * @return Distance between this line and provided point.
514      */
515     public double signedDistance(final Point3D point) {
516         point.normalize();
517         normalize();
518 
519         // numerator is the dot product of point and line
520         final var num = point.getHomX() * a + point.getHomY() * b + point.getHomZ() * c + point.getHomW() * d;
521 
522         final var den = Math.sqrt(a * a + b * b + c * c) * point.getHomW();
523 
524         return num / den;
525     }
526 
527     /**
528      * Returns the point belonging to this line closest to provided point, which
529      * will be located at signedDistance(Point2D) from this line.
530      * If provided point belong to this line, then the same point will be
531      * returned as a result.
532      *
533      * @param point Point to be checked.
534      * @return Closest point.
535      */
536     public Point3D getClosestPoint(final Point3D point) {
537         return getClosestPoint(point, DEFAULT_LOCUS_THRESHOLD);
538     }
539 
540     /**
541      * Returns the point belonging to this line closest to provided point, which
542      * will be located at signedDistance(Point2D) from this line.
543      * If provided point belong to this line, then the same point will be
544      * returned as a result.
545      *
546      * @param point     Point to be checked.
547      * @param threshold Threshold to determine whether point is locus of line or
548      *                  not.
549      * @return Closest point.
550      * @throws IllegalArgumentException Raised if threshold is negative.
551      */
552     public Point3D getClosestPoint(final Point3D point, final double threshold) {
553         final var result = Point3D.create();
554         closestPoint(point, result, threshold);
555         return result;
556     }
557 
558     /**
559      * Computes the point belonging to this plane closest to provided point,
560      * which will be located at signedDistance(Point3D) from this plane.
561      * If provided point belongs to this plane, then the same point will be
562      * returned as a result.
563      *
564      * @param point  Point to be checked.
565      * @param result Instance where the closest point will be stored.
566      */
567     public void closestPoint(final Point3D point, final Point3D result) {
568         closestPoint(point, result, DEFAULT_LOCUS_THRESHOLD);
569     }
570 
571     /**
572      * Computes the point belonging to this plane closest to provided point,
573      * which will be located at signedDistance(Point3D) from this plane.
574      * If provided point belongs to this plane, then the same point will be
575      * returned as a result.
576      *
577      * @param point     Point to be checked.
578      * @param result    Instance where the closest point will be stored.
579      * @param threshold threshold to determine whether a point is locus of
580      *                  this plane.
581      * @throws IllegalArgumentException Raised if threshold is negative.
582      */
583     public void closestPoint(final Point3D point, final Point3D result, final double threshold) {
584         if (threshold < MIN_THRESHOLD) {
585             throw new IllegalArgumentException();
586         }
587 
588         // normalize point to increase accuracy
589         point.normalize();
590 
591         if (isLocus(point, threshold)) {
592             // if point belongs to line, then it is returned as result
593             result.setCoordinates(point);
594             return;
595         }
596 
597         // move point in director vector direction until it belongs to this plane
598         // (point.getInhomX() + mA * amount) * mA + (point.getInhomY() +
599         // mB * amount) * mB + (point.getInhomZ + mC * amount) * mC + mD = 0
600 
601         final var amount = -(point.getHomX() * a + point.getHomY() * b + point.getHomZ() * c + point.getHomW() * d)
602                 / (point.getHomW() * (a * a + b * b + c * c));
603         result.setHomogeneousCoordinates(point.getHomX() + a * amount * point.getHomW(),
604                 point.getHomY() + b * amount * point.getHomW(),
605                 point.getHomZ() + c * amount * point.getHomW(),
606                 point.getHomW());
607         result.normalize();
608     }
609 
610     /**
611      * Returns parameters of this plane as an array containing [a, b, c, d].
612      *
613      * @return Array containing all the parameters that describe this plane.
614      */
615     public double[] asArray() {
616         final var array = new double[PLANE_NUMBER_PARAMS];
617         asArray(array);
618         return array;
619     }
620 
621     /**
622      * Stores the parameters of this plane in provided array as [a, b, c, d].
623      *
624      * @param array Array where parameters of this plane will be stored.
625      * @throws IllegalArgumentException Raised if provided array doesn't have
626      *                                  length 4.
627      */
628     public void asArray(final double[] array) {
629         if (array.length != PLANE_NUMBER_PARAMS) {
630             throw new IllegalArgumentException();
631         }
632 
633         array[0] = a;
634         array[1] = b;
635         array[2] = c;
636         array[3] = d;
637     }
638 
639     /**
640      * Normalizes the parameters of this line to increase the accuracy of some
641      * computations.
642      */
643     public void normalize() {
644         if (!normalized) {
645             final var norm = Math.sqrt(a * a + b * b + c * c + d * d);
646 
647             if (norm > PRECISION) {
648                 a /= norm;
649                 b /= norm;
650                 c /= norm;
651                 d /= norm;
652 
653                 normalized = true;
654             }
655         }
656     }
657 
658     /**
659      * Returns boolean indicating whether this plane has already been
660      * normalized.
661      *
662      * @return True if this plane is normalized, false otherwise.
663      */
664     public boolean isNormalized() {
665         return normalized;
666     }
667 
668     /**
669      * Returns director vector of this plane.
670      *
671      * @return Director vector of this plane.
672      */
673     public double[] getDirectorVector() {
674         final var out = new double[INHOM_VECTOR_SIZE];
675         directorVector(out);
676         return out;
677     }
678 
679     /**
680      * Computes director vector of this plane and stores the result in provided
681      * array.
682      *
683      * @param directorVector Array containing director vector.
684      * @throws IllegalArgumentException Raised if provided array does not have
685      *                                  length 3.
686      */
687     public void directorVector(final double[] directorVector) {
688         if (directorVector.length != INHOM_VECTOR_SIZE) {
689             throw new IllegalArgumentException();
690         }
691 
692         directorVector[0] = a;
693         directorVector[1] = b;
694         directorVector[2] = c;
695     }
696 
697     /**
698      * Computes and returns the intersection point between this plane and the
699      * other 2 provided planes.
700      *
701      * @param otherPlane1 other plane 1.
702      * @param otherPlane2 other plane 2.
703      * @return point where the three planes intersect.
704      * @throws NoIntersectionException if the three planes do not intersect in
705      *                                 a single point.
706      */
707     public Point3D getIntersection(final Plane otherPlane1, final Plane otherPlane2) throws NoIntersectionException {
708         final var result = Point3D.create();
709         intersection(otherPlane1, otherPlane2, result);
710         return result;
711     }
712 
713     /**
714      * Computes the intersection point between this plane and the other 2
715      * provided planes.
716      *
717      * @param otherPlane1 other plane 1.
718      * @param otherPlane2 other plane 2.
719      * @param result      point where the intersection will be stored.
720      * @throws NoIntersectionException if the three planes do not intersect in
721      *                                 a single point.
722      */
723     public void intersection(final Plane otherPlane1, final Plane otherPlane2, final Point3D result)
724             throws NoIntersectionException {
725 
726         // normalize planes to increase accuracy
727         normalize();
728         otherPlane1.normalize();
729         otherPlane2.normalize();
730 
731         // set matrix where each row contains the parameters of the plane
732         try {
733             final var m = new Matrix(3, 4);
734             m.setElementAt(0, 0, a);
735             m.setElementAt(0, 1, b);
736             m.setElementAt(0, 2, c);
737             m.setElementAt(0, 3, d);
738 
739             m.setElementAt(1, 0, otherPlane1.getA());
740             m.setElementAt(1, 1, otherPlane1.getB());
741             m.setElementAt(1, 2, otherPlane1.getC());
742             m.setElementAt(1, 3, otherPlane1.getD());
743 
744             m.setElementAt(2, 0, otherPlane2.getA());
745             m.setElementAt(2, 1, otherPlane2.getB());
746             m.setElementAt(2, 2, otherPlane2.getC());
747             m.setElementAt(2, 3, otherPlane2.getD());
748 
749             // If planes are not parallel, then matrix has rank 3, and its right
750             // null-space is equal to their intersection.
751             final var decomposer = new SingularValueDecomposer(m);
752             decomposer.decompose();
753 
754             // planes are parallel
755             if (decomposer.getRank() < 3) {
756                 throw new NoIntersectionException();
757             }
758 
759             final var v = decomposer.getV();
760 
761             // last column of V contains the right null-space of m, which is the
762             // intersection of lines expressed in homogeneous coordinates.
763             // because column is already normalized by SVD decomposition, point
764             // will also be normalized
765             result.setHomogeneousCoordinates(v.getElementAt(0, 3), v.getElementAt(1, 3),
766                     v.getElementAt(2, 3), v.getElementAt(3, 3));
767         } catch (final AlgebraException e) {
768             // lines are numerically unstable
769             throw new NoIntersectionException(e);
770         }
771     }
772 
773     /**
774      * Computes the dot product between the parameters A, B, C, D of this plane
775      * and the ones of provided plane.
776      * This method normalizes both planes to compute dot product.
777      *
778      * @param plane plane to compute dot product with.
779      * @return dot product value.
780      */
781     public double dotProduct(final Plane plane) {
782         normalize();
783         plane.normalize();
784         return a * plane.a + b * plane.b + c * plane.c + d * plane.d;
785     }
786 
787     /**
788      * Checks if the plane described by this instance equals provided plane
789      * up to provided threshold.
790      *
791      * @param plane     plane to be compared to.
792      * @param threshold threshold grade of tolerance to determine whether the
793      *                  planes are equal or not. It is used because due to machine precision,
794      *                  the values might not be exactly equal (if not provided
795      *                  DEFAULT_COMPARISON_THRESHOLD is used).
796      * @return true if current plane and provided one are the same, false
797      * otherwise.
798      * @throws IllegalArgumentException if threshold is negative.
799      */
800     public boolean equals(final Plane plane, final double threshold) {
801 
802         if (threshold < MIN_THRESHOLD) {
803             throw new IllegalArgumentException();
804         }
805 
806         normalize();
807         plane.normalize();
808 
809         return (1.0 - Math.abs(dotProduct(plane))) <= threshold;
810     }
811 
812     /**
813      * Checks if the plane described by this instance equals provided plane
814      * up to default comparison threshold.
815      *
816      * @param plane plane to be compared to.
817      * @return true if current plane and provided one are the same, false
818      * otherwise.
819      */
820     public boolean equals(final Plane plane) {
821         return equals(plane, DEFAULT_COMPARISON_THRESHOLD);
822     }
823 
824     /**
825      * Checks if provided object equals current plane.
826      *
827      * @param obj object to compare.
828      * @return true if both objects are considered to be equal, false otherwise.
829      */
830     @Override
831     public boolean equals(final Object obj) {
832         if (!(obj instanceof Plane plane)) {
833             return false;
834         }
835         if (obj == this) {
836             return true;
837         }
838 
839         return equals(plane);
840     }
841 
842     /**
843      * Returns hash code value. This is only defined to keep the compiler happy.
844      * This method must be overridden in subclasses of this class.
845      *
846      * @return Hash code.
847      */
848     @Override
849     public int hashCode() {
850         return Objects.hash(a, b, c, d);
851     }
852 
853     /**
854      * Creates a new instance of a plane located the canonical infinity.
855      * The canonical infinity corresponds to all 3D points located at infinity
856      * (i.e. M = (X,Y,Z,W = 0), hence P = (A = 0,B = 0,C = 0, W = 1))
857      *
858      * @return a new instance of a plane located at the canonical infinity.
859      */
860     public static Plane createCanonicalPlaneAtInfinity() {
861         final var p = new Plane();
862         setAsCanonicalPlaneAtInfinity(p);
863         return p;
864     }
865 
866     /**
867      * Sets provided plane into the canonical infinity.
868      * The canonical infinity corresponds to all 3D points located at infinity
869      * (i.e. M = (X,Y,Z,W = 0), hence P = (A = 0,B = 0,C = 0, W = 1))
870      *
871      * @param plane plane to be set at infinity.
872      */
873     public static void setAsCanonicalPlaneAtInfinity(final Plane plane) {
874         plane.a = plane.b = plane.c = 0.0;
875         plane.d = 1.0;
876         plane.normalized = true;
877     }
878 }