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 }