1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.geometry.euclidean.threed;
18
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.Collections;
22 import java.util.List;
23
24 import org.apache.commons.geometry.core.GeometryTestUtils;
25 import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
26 import org.apache.commons.geometry.euclidean.threed.line.Line3D;
27 import org.apache.commons.geometry.euclidean.threed.line.Lines3D;
28 import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
29 import org.apache.commons.numbers.angle.Angle;
30 import org.apache.commons.numbers.core.Precision;
31 import org.junit.jupiter.api.Assertions;
32 import org.junit.jupiter.api.Test;
33
34 class PlaneTest {
35
36 private static final double TEST_EPS = 1e-10;
37
38 private static final Precision.DoubleEquivalence TEST_PRECISION =
39 Precision.doubleEquivalenceOfEpsilon(TEST_EPS);
40
41 @Test
42 void testFromNormal() {
43
44 checkPlane(Planes.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION), Vector3D.ZERO, Vector3D.Unit.PLUS_X);
45 checkPlane(Planes.fromNormal(Vector3D.of(7, 0, 0), TEST_PRECISION), Vector3D.ZERO, Vector3D.Unit.PLUS_X);
46
47 checkPlane(Planes.fromNormal(Vector3D.Unit.PLUS_Y, TEST_PRECISION), Vector3D.ZERO, Vector3D.Unit.PLUS_Y);
48 checkPlane(Planes.fromNormal(Vector3D.of(0, 5, 0), TEST_PRECISION), Vector3D.ZERO, Vector3D.Unit.PLUS_Y);
49
50 checkPlane(Planes.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION), Vector3D.ZERO, Vector3D.Unit.PLUS_Z);
51 checkPlane(Planes.fromNormal(Vector3D.of(0, 0, 0.01), TEST_PRECISION), Vector3D.ZERO, Vector3D.Unit.PLUS_Z);
52 }
53
54 @Test
55 void testFromNormal_illegalArguments() {
56
57 Assertions.assertThrows(IllegalArgumentException.class, () -> Planes.fromNormal(Vector3D.ZERO, TEST_PRECISION));
58 }
59
60 @Test
61 void testFromPointAndNormal() {
62
63 final Vector3D pt = Vector3D.of(1, 2, 3);
64
65
66 checkPlane(Planes.fromPointAndNormal(pt, Vector3D.of(0.1, 0, 0), TEST_PRECISION),
67 Vector3D.of(1, 0, 0), Vector3D.Unit.PLUS_X);
68 checkPlane(Planes.fromPointAndNormal(pt, Vector3D.of(0, 2, 0), TEST_PRECISION),
69 Vector3D.of(0, 2, 0), Vector3D.Unit.PLUS_Y);
70 checkPlane(Planes.fromPointAndNormal(pt, Vector3D.of(0, 0, 5), TEST_PRECISION),
71 Vector3D.of(0, 0, 3), Vector3D.Unit.PLUS_Z);
72 }
73
74 @Test
75 void testFromPointAndNormal_illegalArguments() {
76
77 final Vector3D pt = Vector3D.of(1, 2, 3);
78
79
80 Assertions.assertThrows(IllegalArgumentException.class, () -> Planes.fromPointAndNormal(pt, Vector3D.ZERO, TEST_PRECISION));
81 }
82
83 @Test
84 void testFromPoints() {
85
86 final Vector3D a = Vector3D.of(1, 1, 1);
87 final Vector3D b = Vector3D.of(1, 1, 4.3);
88 final Vector3D c = Vector3D.of(2.5, 1, 1);
89
90
91 checkPlane(Planes.fromPoints(a, b, c, TEST_PRECISION),
92 Vector3D.of(0, 1, 0), Vector3D.Unit.PLUS_Y);
93
94 checkPlane(Planes.fromPoints(a, c, b, TEST_PRECISION),
95 Vector3D.of(0, 1, 0), Vector3D.Unit.MINUS_Y);
96 }
97
98 @Test
99 void testFromPoints_planeContainsSourcePoints() {
100
101 final Vector3D p1 = Vector3D.of(1.2, 3.4, -5.8);
102 final Vector3D p2 = Vector3D.of(3.4, -5.8, 1.2);
103 final Vector3D p3 = Vector3D.of(-2.0, 4.3, 0.7);
104
105
106 final Plane plane = Planes.fromPoints(p1, p2, p3, TEST_PRECISION);
107
108
109 Assertions.assertTrue(plane.contains(p1));
110 Assertions.assertTrue(plane.contains(p2));
111 Assertions.assertTrue(plane.contains(p3));
112 }
113
114 @Test
115 void testFromPoints_illegalArguments() {
116
117 final Vector3D a = Vector3D.of(1, 0, 0);
118 final Vector3D b = Vector3D.of(0, 1, 0);
119
120
121
122 Assertions.assertThrows(IllegalArgumentException.class, () -> Planes.fromPoints(a, a, a, TEST_PRECISION));
123 Assertions.assertThrows(IllegalArgumentException.class, () -> Planes.fromPoints(a, a, b, TEST_PRECISION));
124 Assertions.assertThrows(IllegalArgumentException.class, () -> Planes.fromPoints(a, b, a, TEST_PRECISION));
125 Assertions.assertThrows(IllegalArgumentException.class, () -> Planes.fromPoints(b, a, a, TEST_PRECISION));
126 }
127
128 @Test
129 void testFromPoints_collection_threePoints() {
130
131 final List<Vector3D> pts = Arrays.asList(
132 Vector3D.of(1, 1, 0),
133 Vector3D.of(1, 1, -1),
134 Vector3D.of(0, 1, 0)
135 );
136
137
138 final Plane plane = Planes.fromPoints(pts, TEST_PRECISION);
139
140
141 checkPlane(plane, Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_Y);
142
143 Assertions.assertTrue(plane.contains(pts.get(0)));
144 Assertions.assertTrue(plane.contains(pts.get(1)));
145 Assertions.assertTrue(plane.contains(pts.get(2)));
146 }
147
148 @Test
149 void testFromPoints_collection_someCollinearPoints() {
150
151 final List<Vector3D> pts = Arrays.asList(
152 Vector3D.of(1, 0, 2),
153 Vector3D.of(2, 0, 2),
154 Vector3D.of(3, 0, 2),
155 Vector3D.of(0, 1, 2)
156 );
157
158
159 final Plane plane = Planes.fromPoints(pts, TEST_PRECISION);
160
161
162 checkPlane(plane, Vector3D.of(0, 0, 2), Vector3D.Unit.PLUS_Z);
163
164 Assertions.assertTrue(plane.contains(pts.get(0)));
165 Assertions.assertTrue(plane.contains(pts.get(1)));
166 Assertions.assertTrue(plane.contains(pts.get(2)));
167 Assertions.assertTrue(plane.contains(pts.get(3)));
168 }
169
170 @Test
171 void testFromPoints_collection_concaveWithCollinearAndDuplicatePoints() {
172
173 final List<Vector3D> pts = Arrays.asList(
174 Vector3D.of(1, 0, 1),
175 Vector3D.of(1, 0, 0.5),
176
177 Vector3D.of(1, 0, 0),
178 Vector3D.of(1, 1, -1),
179 Vector3D.of(1, 2, 0),
180 Vector3D.of(1, 2, 1e-15),
181 Vector3D.of(1, 1, -0.5),
182 Vector3D.of(1, 1 + 1e-15, -0.5),
183 Vector3D.of(1 - 1e-15, 1, -0.5),
184 Vector3D.of(1, 0, 0),
185
186 Vector3D.of(1, 0, 0.5),
187 Vector3D.of(1, 0, 1)
188 );
189
190 final Vector3D origin = Vector3D.of(1, 0, 0);
191
192
193 checkPlane(Planes.fromPoints(pts, TEST_PRECISION), origin, Vector3D.Unit.PLUS_X);
194
195 for (int i = 1; i < 12; ++i) {
196 checkPlane(Planes.fromPoints(rotate(pts, i), TEST_PRECISION), origin, Vector3D.Unit.PLUS_X);
197 }
198 }
199
200 @Test
201 void testFromPoints_collection_choosesBestOrientation() {
202
203 checkPlane(Planes.fromPoints(Arrays.asList(
204 Vector3D.of(1, 0, 2),
205 Vector3D.of(2, 0, 2),
206 Vector3D.of(3, 0, 2),
207 Vector3D.of(3.5, 1, 2)
208 ), TEST_PRECISION), Vector3D.of(0, 0, 2), Vector3D.Unit.PLUS_Z);
209
210 checkPlane(Planes.fromPoints(Arrays.asList(
211 Vector3D.of(1, 0, 2),
212 Vector3D.of(2, 0, 2),
213 Vector3D.of(3, 0, 2),
214 Vector3D.of(3.5, -1, 2)
215 ), TEST_PRECISION), Vector3D.of(0, 0, 2), Vector3D.Unit.MINUS_Z);
216
217 checkPlane(Planes.fromPoints(Arrays.asList(
218 Vector3D.of(1, 0, 2),
219 Vector3D.of(2, 0, 2),
220 Vector3D.of(3, 0, 2),
221 Vector3D.of(3.5, -1, 2),
222 Vector3D.of(4, 0, 2)
223 ), TEST_PRECISION), Vector3D.of(0, 0, 2), Vector3D.Unit.PLUS_Z);
224
225 checkPlane(Planes.fromPoints(Arrays.asList(
226 Vector3D.of(1, 0, 2),
227 Vector3D.of(2, 0, 2),
228 Vector3D.of(3, 0, 2),
229 Vector3D.of(3.5, 1, 2),
230 Vector3D.of(4, -1, 2)
231 ), TEST_PRECISION), Vector3D.of(0, 0, 2), Vector3D.Unit.MINUS_Z);
232
233 checkPlane(Planes.fromPoints(Arrays.asList(
234 Vector3D.of(0, 0, 2),
235 Vector3D.of(1, 0, 2),
236 Vector3D.of(1, 1, 2),
237 Vector3D.of(0, 1, 2),
238 Vector3D.of(0, 0, 2)
239 ), TEST_PRECISION), Vector3D.of(0, 0, 2), Vector3D.Unit.PLUS_Z);
240
241 checkPlane(Planes.fromPoints(Arrays.asList(
242 Vector3D.of(0, 0, 2),
243 Vector3D.of(0, 1, 2),
244 Vector3D.of(1, 1, 2),
245 Vector3D.of(1, 0, 2),
246 Vector3D.of(0, 0, 2)
247 ), TEST_PRECISION), Vector3D.of(0, 0, 2), Vector3D.Unit.MINUS_Z);
248
249 checkPlane(Planes.fromPoints(Arrays.asList(
250 Vector3D.of(0, 0, 2),
251 Vector3D.of(1, 0, 2),
252 Vector3D.of(2, 1, 2),
253 Vector3D.of(3, 0, 2),
254 Vector3D.of(2, 4, 2),
255 Vector3D.of(0, 0, 2)
256 ), TEST_PRECISION), Vector3D.of(0, 0, 2), Vector3D.Unit.PLUS_Z);
257
258 checkPlane(Planes.fromPoints(Arrays.asList(
259 Vector3D.of(0, 0, 2),
260 Vector3D.of(0, 1, 2),
261 Vector3D.of(2, 4, 2),
262 Vector3D.of(3, 0, 2),
263 Vector3D.of(2, 1, 2),
264 Vector3D.of(0, 0, 2)
265 ), TEST_PRECISION), Vector3D.of(0, 0, 2), Vector3D.Unit.MINUS_Z);
266 }
267
268 @Test
269 void testFromPoints_collection_illegalArguments() {
270
271 final Vector3D a = Vector3D.ZERO;
272 final Vector3D b = Vector3D.Unit.PLUS_X;
273
274
275
276 Assertions.assertThrows(IllegalArgumentException.class, () -> Planes.fromPoints(Collections.emptyList(), TEST_PRECISION));
277 Assertions.assertThrows(IllegalArgumentException.class, () -> Planes.fromPoints(Collections.singletonList(a), TEST_PRECISION));
278 Assertions.assertThrows(IllegalArgumentException.class, () -> Planes.fromPoints(Arrays.asList(a, b), TEST_PRECISION));
279 }
280
281 @Test
282 void testFromPoints_collection_allPointsCollinear() {
283
284 Assertions.assertThrows(IllegalArgumentException.class, () -> Planes.fromPoints(Arrays.asList(
285 Vector3D.ZERO,
286 Vector3D.Unit.PLUS_X,
287 Vector3D.of(2, 0, 0)
288 ), TEST_PRECISION));
289 Assertions.assertThrows(IllegalArgumentException.class, () -> Planes.fromPoints(Arrays.asList(
290 Vector3D.ZERO,
291 Vector3D.Unit.PLUS_X,
292 Vector3D.of(2, 0, 0),
293 Vector3D.of(3, 0, 0)
294 ), TEST_PRECISION));
295 }
296
297 @Test
298 void testFromPoints_collection_notEnoughUniquePoints() {
299
300 Assertions.assertThrows(IllegalArgumentException.class, () -> Planes.fromPoints(Arrays.asList(
301 Vector3D.ZERO,
302 Vector3D.ZERO,
303 Vector3D.of(1e-12, 1e-12, 0),
304 Vector3D.Unit.PLUS_X
305 ), TEST_PRECISION));
306 Assertions.assertThrows(IllegalArgumentException.class, () -> Planes.fromPoints(Arrays.asList(
307 Vector3D.ZERO,
308 Vector3D.of(1e-12, 0, 0),
309 Vector3D.ZERO
310 ), TEST_PRECISION));
311 }
312
313 @Test
314 void testFromPoints_collection_pointsNotOnSamePlane() {
315
316 Assertions.assertThrows(IllegalArgumentException.class, () -> Planes.fromPoints(Arrays.asList(
317 Vector3D.ZERO,
318 Vector3D.Unit.PLUS_X,
319 Vector3D.Unit.PLUS_Y,
320 Vector3D.Unit.PLUS_Z
321 ), TEST_PRECISION));
322 }
323
324 @Test
325 void testGetEmbedding() {
326
327 final Vector3D pt = Vector3D.of(1, 2, 3);
328 EuclideanTestUtils.permuteSkipZero(-4, 4, 1, (x, y, z) -> {
329
330 final Plane plane = Planes.fromPointAndNormal(pt, Vector3D.of(x, y, z), TEST_PRECISION);
331
332
333 final EmbeddingPlane embeddingPlane = plane.getEmbedding();
334 final EmbeddingPlane nextEmbeddingPlane = plane.getEmbedding();
335
336
337 Assertions.assertSame(plane.getNormal(), embeddingPlane.getNormal());
338 Assertions.assertSame(plane.getNormal(), embeddingPlane.getW());
339 Assertions.assertEquals(plane.getOriginOffset(), embeddingPlane.getOriginOffset(), TEST_EPS);
340 Assertions.assertSame(plane.getPrecision(), embeddingPlane.getPrecision());
341
342 final Vector3D.Unit u = embeddingPlane.getU();
343 final Vector3D.Unit v = embeddingPlane.getV();
344 final Vector3D.Unit w = embeddingPlane.getW();
345
346 Assertions.assertEquals(0, u.dot(v), TEST_EPS);
347 Assertions.assertEquals(0, u.dot(w), TEST_EPS);
348 Assertions.assertEquals(0, v.dot(w), TEST_EPS);
349
350 Assertions.assertNotSame(embeddingPlane, nextEmbeddingPlane);
351 Assertions.assertEquals(embeddingPlane, nextEmbeddingPlane);
352 });
353 }
354
355 @Test
356 void testContains_point() {
357
358 final Plane plane = Planes.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), TEST_PRECISION);
359 final double halfEps = 0.5 * TEST_EPS;
360
361
362 EuclideanTestUtils.permute(-100, 100, 5, (x, y) -> {
363
364 Assertions.assertTrue(plane.contains(Vector3D.of(x, y, 1)));
365 Assertions.assertTrue(plane.contains(Vector3D.of(x, y, 1 + halfEps)));
366 Assertions.assertTrue(plane.contains(Vector3D.of(x, y, 1 - halfEps)));
367
368 Assertions.assertFalse(plane.contains(Vector3D.of(x, y, 0.5)));
369 Assertions.assertFalse(plane.contains(Vector3D.of(x, y, 1.5)));
370 });
371 }
372
373 @Test
374 void testContains_line() {
375
376 final Plane plane = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
377
378
379 Assertions.assertTrue(plane.contains(
380 Lines3D.fromPoints(Vector3D.of(1, 0, 0), Vector3D.of(2, 0, 0), TEST_PRECISION)));
381 Assertions.assertTrue(plane.contains(
382 Lines3D.fromPoints(Vector3D.of(-1, 0, 0), Vector3D.of(-2, 0, 0), TEST_PRECISION)));
383
384 Assertions.assertFalse(plane.contains(
385 Lines3D.fromPoints(Vector3D.of(1, 0, 2), Vector3D.of(2, 0, 2), TEST_PRECISION)));
386 Assertions.assertFalse(plane.contains(
387 Lines3D.fromPoints(Vector3D.ZERO, Vector3D.of(2, 0, 2), TEST_PRECISION)));
388 }
389
390 @Test
391 void testContains_plane() {
392
393 final Vector3D p1 = Vector3D.of(1.2, 3.4, -5.8);
394 final Vector3D p2 = Vector3D.of(3.4, -5.8, 1.2);
395 final Vector3D p3 = Vector3D.of(-2.0, 4.3, 0.7);
396 final Plane planeA = Planes.fromPoints(p1, p2, p3, TEST_PRECISION);
397
398
399 Assertions.assertTrue(planeA.contains(planeA));
400 Assertions.assertTrue(planeA.contains(Planes.fromPoints(p1, p3, p2, TEST_PRECISION)));
401 Assertions.assertTrue(planeA.contains(Planes.fromPoints(p3, p1, p2, TEST_PRECISION)));
402 Assertions.assertTrue(planeA.contains(Planes.fromPoints(p3, p2, p1, TEST_PRECISION)));
403
404 Assertions.assertFalse(planeA.contains(Planes.fromPoints(p1, Vector3D.of(11.4, -3.8, 5.1), p2, TEST_PRECISION)));
405
406 final Vector3D offset = planeA.getNormal().multiply(1e-8);
407 Assertions.assertFalse(planeA.contains(Planes.fromPoints(p1.add(offset), p2, p3, TEST_PRECISION)));
408 Assertions.assertFalse(planeA.contains(Planes.fromPoints(p1, p2.add(offset), p3, TEST_PRECISION)));
409 Assertions.assertFalse(planeA.contains(Planes.fromPoints(p1, p2, p3.add(offset), TEST_PRECISION)));
410
411 Assertions.assertFalse(planeA.contains(Planes.fromPoints(p1.add(offset),
412 p2.add(offset),
413 p3.add(offset), TEST_PRECISION)));
414 }
415
416 @Test
417 void testReverse() {
418
419 final Vector3D pt = Vector3D.of(0, 0, 1);
420 final Plane plane = Planes.fromPointAndNormal(pt, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
421
422
423 final Plane reversed = plane.reverse();
424
425
426 checkPlane(reversed, pt, Vector3D.Unit.MINUS_Z);
427
428 Assertions.assertTrue(reversed.contains(Vector3D.of(1, 1, 1)));
429 Assertions.assertTrue(reversed.contains(Vector3D.of(-1, -1, 1)));
430 Assertions.assertFalse(reversed.contains(Vector3D.ZERO));
431
432 Assertions.assertEquals(1.0, reversed.offset(Vector3D.ZERO), TEST_EPS);
433 }
434
435 @Test
436 void testIsParallelAndOffset_line() {
437
438 final Plane plane = Planes.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), TEST_PRECISION);
439
440 final Line3D parallelLine = Lines3D.fromPoints(Vector3D.of(1, 0, 2), Vector3D.of(2, 0, 2), TEST_PRECISION);
441 final Line3D nonParallelLine = Lines3D.fromPoints(Vector3D.of(1, 0, 2), Vector3D.of(2, 0, 1), TEST_PRECISION);
442 final Line3D containedLine = Lines3D.fromPoints(Vector3D.of(2, 0, 1), Vector3D.of(1, 0, 1), TEST_PRECISION);
443
444
445 Assertions.assertTrue(plane.isParallel(parallelLine));
446 Assertions.assertEquals(1.0, plane.offset(parallelLine), TEST_EPS);
447
448 Assertions.assertFalse(plane.isParallel(nonParallelLine));
449 Assertions.assertEquals(0.0, plane.offset(nonParallelLine), TEST_EPS);
450
451 Assertions.assertTrue(plane.isParallel(containedLine));
452 Assertions.assertEquals(0.0, plane.offset(containedLine), TEST_EPS);
453 }
454
455 @Test
456 void testIsParallelAndOffset_plane() {
457
458 final Plane plane = Planes.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), TEST_PRECISION);
459 final Plane parallelPlane = Planes.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0, 0, 1), TEST_PRECISION);
460 final Plane parallelPlane2 = Planes.fromPointAndNormal(Vector3D.of(0, 0, 2), Vector3D.of(0, 0, 1), TEST_PRECISION);
461 final Plane parallelPlane3 = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.of(0, 0, 1), TEST_PRECISION).reverse();
462 final Plane nonParallelPlane = Planes.fromPointAndNormal(Vector3D.of(0, 0, 1),
463 Vector3D.of(1, 1.5, 1).cross(Vector3D.of(0, 1, 1)), TEST_PRECISION);
464 final Plane reversedPlane = plane.reverse();
465
466
467 Assertions.assertTrue(plane.isParallel(parallelPlane));
468 Assertions.assertEquals(0.0, plane.offset(parallelPlane), TEST_EPS);
469
470 Assertions.assertTrue(plane.isParallel(parallelPlane2));
471 Assertions.assertEquals(1.0, plane.offset(parallelPlane2), TEST_EPS);
472
473 Assertions.assertTrue(plane.isParallel(parallelPlane3));
474 Assertions.assertEquals(-1.0, plane.offset(parallelPlane3), TEST_EPS);
475
476 Assertions.assertFalse(plane.isParallel(nonParallelPlane));
477 Assertions.assertEquals(0.0, plane.offset(nonParallelPlane), TEST_EPS);
478
479 Assertions.assertTrue(plane.isParallel(reversedPlane));
480 Assertions.assertEquals(0.0, plane.offset(nonParallelPlane), TEST_EPS);
481 }
482
483 @Test
484 void testOffset_point() {
485
486 final Vector3D p1 = Vector3D.of(1, 1, 1);
487 final Plane plane = Planes.fromPointAndNormal(p1, Vector3D.of(0.2, 0, 0), TEST_PRECISION);
488
489
490 Assertions.assertEquals(-5.0, plane.offset(Vector3D.of(-4, 0, 0)), TEST_EPS);
491 Assertions.assertEquals(+5.0, plane.offset(Vector3D.of(6, 10, -12)), TEST_EPS);
492 Assertions.assertEquals(0.3,
493 plane.offset(Vector3D.Sum.of(p1).addScaled(0.3, plane.getNormal()).get()),
494 TEST_EPS);
495 Assertions.assertEquals(-0.3,
496 plane.offset(Vector3D.Sum.of(p1).addScaled(-0.3, plane.getNormal()).get()),
497 TEST_EPS);
498 }
499
500 @Test
501 void testProject_point() {
502
503 final Vector3D pt = Vector3D.of(-3, -2, -1);
504 EuclideanTestUtils.permuteSkipZero(-4, 4, 1, (nx, ny, nz) -> {
505
506 final Plane plane = Planes.fromPointAndNormal(pt, Vector3D.of(nx, ny, nz), TEST_PRECISION);
507 EuclideanTestUtils.permute(-4, 4, 1, (x, y, z) -> {
508
509 final Vector3D p = Vector3D.of(x, y, z);
510
511
512 final Vector3D proj = plane.project(p);
513
514
515 Assertions.assertTrue(plane.contains(proj));
516 Assertions.assertEquals(0, plane.getOrigin().vectorTo(proj).dot(plane.getNormal()), TEST_EPS);
517 });
518 });
519 }
520
521 @Test
522 void testProject_line() {
523
524 final Plane plane = Planes.fromPointAndNormal(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
525 final Line3D line = Lines3D.fromPoints(Vector3D.of(1, 0, 1), Vector3D.of(2, 0, 2), TEST_PRECISION);
526
527
528 final Line3D projected = plane.project(line);
529
530
531 final Line3D expectedProjection = Lines3D.fromPoints(Vector3D.of(1, 0, 1), Vector3D.of(2, 0, 1), TEST_PRECISION);
532 Assertions.assertEquals(expectedProjection, projected);
533
534 Assertions.assertTrue(plane.contains(projected));
535
536 Assertions.assertTrue(projected.contains(Vector3D.of(1, 0, 1)));
537 Assertions.assertTrue(projected.contains(Vector3D.of(2, 0, 1)));
538 }
539
540 @Test
541 void testTransform_rotationAroundPoint() {
542
543 final Vector3D pt = Vector3D.of(0, 0, 1);
544 final Plane plane = Planes.fromPointAndNormal(pt, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
545
546 final AffineTransformMatrix3D mat = AffineTransformMatrix3D.createRotation(pt,
547 QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, Angle.PI_OVER_TWO));
548
549
550 final Plane result = plane.transform(mat);
551
552
553 checkPlane(result, Vector3D.ZERO, Vector3D.Unit.PLUS_X);
554 }
555
556 @Test
557 void testTransform_asymmetricScaling() {
558
559 final Vector3D pt = Vector3D.of(0, 1, 0);
560 final Plane plane = Planes.fromPointAndNormal(pt, Vector3D.of(1, 1, 0), TEST_PRECISION);
561
562 final AffineTransformMatrix3D t = AffineTransformMatrix3D.createScale(2, 1, 1);
563
564
565 final Plane result = plane.transform(t);
566
567
568 final Vector3D expectedNormal = Vector3D.Unit.from(1, 2, 0);
569 final Vector3D expectedOrigin = result.project(Vector3D.ZERO);
570
571 checkPlane(result, expectedOrigin, expectedNormal);
572
573 final Vector3D transformedPt = t.apply(Vector3D.of(0.5, 0.5, 1));
574 Assertions.assertTrue(result.contains(transformedPt));
575 Assertions.assertFalse(plane.contains(transformedPt));
576 }
577
578 @Test
579 void testTransform_negateOneComponent() {
580
581 final Vector3D pt = Vector3D.of(0, 0, 1);
582 final Plane plane = Planes.fromPointAndNormal(pt, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
583
584 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(-1, 1, 1);
585
586
587 final Plane result = plane.transform(transform);
588
589
590 checkPlane(result, Vector3D.of(0, 0, 1), Vector3D.Unit.MINUS_Z);
591
592 Assertions.assertFalse(transform.preservesOrientation());
593 EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_Z,
594 transform.normalTransform().apply(plane.getNormal()), TEST_EPS);
595 }
596
597 @Test
598 void testTransform_negateTwoComponents() {
599
600 final Vector3D pt = Vector3D.of(0, 0, 1);
601 final Plane plane = Planes.fromPointAndNormal(pt, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
602
603 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.from(v -> Vector3D.of(-v.getX(), -v.getY(), v.getZ()));
604
605
606 final Plane result = plane.transform(transform);
607
608
609 checkPlane(result, Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_Z);
610 }
611
612 @Test
613 void testTransform_negateAllComponents() {
614
615 final Vector3D pt = Vector3D.of(0, 0, 1);
616 final Plane plane = Planes.fromPointAndNormal(pt, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
617
618 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.from(Vector3D::negate);
619
620
621 final Plane result = plane.transform(transform);
622
623
624 checkPlane(result, Vector3D.of(0, 0, -1), Vector3D.Unit.PLUS_Z);
625 }
626
627 @Test
628 void testTransform_consistency() {
629
630 final Vector3D pt = Vector3D.of(1, 2, 3);
631 final Vector3D normal = Vector3D.Unit.of(1, 1, 1);
632
633 final Plane plane = Planes.fromPointAndNormal(pt, normal, TEST_PRECISION);
634
635 final Vector3D p1 = plane.project(Vector3D.of(4, 5, 6));
636 final Vector3D p2 = plane.project(Vector3D.of(-7, -8, -9));
637 final Vector3D p3 = plane.project(Vector3D.of(10, -11, 12));
638
639 final Vector3D notOnPlane1 = plane.getOrigin().add(plane.getNormal());
640 final Vector3D notOnPlane2 = plane.getOrigin().subtract(plane.getNormal());
641
642 EuclideanTestUtils.permuteSkipZero(-4, 4, 1, (a, b, c) -> {
643 final AffineTransformMatrix3D t = AffineTransformMatrix3D.identity()
644 .rotate(Vector3D.of(-1, 2, 3),
645 QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_X, 0.3 * a))
646 .scale(Math.max(a, 1), Math.max(b, 1), Math.max(c, 1))
647 .translate(c, b, a);
648
649
650 final Plane result = plane.transform(t);
651
652
653 Vector3D expectedNormal = t.normalTransform().apply(plane.getNormal()).normalize();
654 if (!t.preservesOrientation()) {
655 expectedNormal = expectedNormal.negate();
656 }
657
658 EuclideanTestUtils.assertCoordinatesEqual(expectedNormal, result.getNormal(), TEST_EPS);
659
660 Assertions.assertTrue(result.contains(t.apply(p1)));
661 Assertions.assertTrue(result.contains(t.apply(p2)));
662 Assertions.assertTrue(result.contains(t.apply(p3)));
663
664 Assertions.assertFalse(result.contains(t.apply(notOnPlane1)));
665 Assertions.assertFalse(result.contains(t.apply(notOnPlane2)));
666 });
667 }
668
669 @Test
670 void testRotate() {
671
672 final Vector3D p1 = Vector3D.of(1.2, 3.4, -5.8);
673 final Vector3D p2 = Vector3D.of(3.4, -5.8, 1.2);
674 final Vector3D p3 = Vector3D.of(-2.0, 4.3, 0.7);
675 Plane plane = Planes.fromPoints(p1, p2, p3, TEST_PRECISION);
676 final Vector3D oldNormal = plane.getNormal();
677
678
679 plane = plane.rotate(p2, QuaternionRotation.fromAxisAngle(p2.subtract(p1), 1.7));
680 Assertions.assertTrue(plane.contains(p1));
681 Assertions.assertTrue(plane.contains(p2));
682 Assertions.assertFalse(plane.contains(p3));
683
684 plane = plane.rotate(p2, QuaternionRotation.fromAxisAngle(oldNormal, 0.1));
685 Assertions.assertFalse(plane.contains(p1));
686 Assertions.assertTrue(plane.contains(p2));
687 Assertions.assertFalse(plane.contains(p3));
688
689 plane = plane.rotate(p1, QuaternionRotation.fromAxisAngle(oldNormal, 0.1));
690 Assertions.assertFalse(plane.contains(p1));
691 Assertions.assertFalse(plane.contains(p2));
692 Assertions.assertFalse(plane.contains(p3));
693 }
694
695 @Test
696 void testTranslate() {
697
698 final Vector3D p1 = Vector3D.of(1.2, 3.4, -5.8);
699 final Vector3D p2 = Vector3D.of(3.4, -5.8, 1.2);
700 final Vector3D p3 = Vector3D.of(-2.0, 4.3, 0.7);
701 Plane plane = Planes.fromPoints(p1, p2, p3, TEST_PRECISION);
702
703
704 plane = plane.translate(plane.getNormal().orthogonal().multiply(2.0));
705 Assertions.assertTrue(plane.contains(p1));
706 Assertions.assertTrue(plane.contains(p2));
707 Assertions.assertTrue(plane.contains(p3));
708
709 plane = plane.translate(plane.getNormal().multiply(-1.2));
710 Assertions.assertFalse(plane.contains(p1));
711 Assertions.assertFalse(plane.contains(p2));
712 Assertions.assertFalse(plane.contains(p3));
713
714 plane = plane.translate(plane.getNormal().multiply(+1.2));
715 Assertions.assertTrue(plane.contains(p1));
716 Assertions.assertTrue(plane.contains(p2));
717 Assertions.assertTrue(plane.contains(p3));
718 }
719
720 @Test
721 void testIntersection_withLine() {
722
723 final Plane plane = Planes.fromPointAndNormal(Vector3D.of(1, 2, 3), Vector3D.of(-4, 1, -5), TEST_PRECISION);
724 final Line3D line = Lines3D.fromPoints(Vector3D.of(0.2, -3.5, 0.7), Vector3D.of(1.2, -2.5, -0.3), TEST_PRECISION);
725
726
727 final Vector3D point = plane.intersection(line);
728
729
730 Assertions.assertTrue(plane.contains(point));
731 Assertions.assertTrue(line.contains(point));
732 Assertions.assertNull(plane.intersection(Lines3D.fromPoints(Vector3D.of(10, 10, 10),
733 Vector3D.of(10, 10, 10).add(plane.getNormal().orthogonal()),
734 TEST_PRECISION)));
735 }
736
737 @Test
738 void testIntersection_withLine_noIntersection() {
739
740 final Vector3D pt = Vector3D.of(1, 2, 3);
741 final Vector3D normal = Vector3D.of(-4, 1, -5);
742
743 final Plane plane = Planes.fromPointAndNormal(pt, normal, TEST_PRECISION);
744 final Vector3D u = plane.getNormal().orthogonal();
745 final Vector3D v = plane.getNormal().cross(u);
746
747
748 Assertions.assertNull(plane.intersection(Lines3D.fromPoints(pt, pt.add(u), TEST_PRECISION)));
749
750 final Vector3D offsetPt = pt.add(plane.getNormal());
751 Assertions.assertNull(plane.intersection(Lines3D.fromPoints(offsetPt, offsetPt.add(v), TEST_PRECISION)));
752 }
753
754 @Test
755 void testIntersection_withPlane() {
756
757 final Vector3D p1 = Vector3D.of(1.2, 3.4, -5.8);
758 final Vector3D p2 = Vector3D.of(3.4, -5.8, 1.2);
759 final Plane planeA = Planes.fromPoints(p1, p2, Vector3D.of(-2.0, 4.3, 0.7), TEST_PRECISION);
760 final Plane planeB = Planes.fromPoints(p1, Vector3D.of(11.4, -3.8, 5.1), p2, TEST_PRECISION);
761
762
763 final Line3D line = planeA.intersection(planeB);
764
765
766 Assertions.assertTrue(line.contains(p1));
767 Assertions.assertTrue(line.contains(p2));
768 EuclideanTestUtils.assertCoordinatesEqual(planeA.getNormal().cross(planeB.getNormal()).normalize(),
769 line.getDirection(), TEST_EPS);
770
771 Assertions.assertNull(planeA.intersection(planeA));
772 }
773
774 @Test
775 void testIntersection_withPlane_noIntersection() {
776
777 final Plane plane = Planes.fromPointAndNormal(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
778
779
780 Assertions.assertNull(plane.intersection(plane));
781 Assertions.assertNull(plane.intersection(plane.reverse()));
782
783 Assertions.assertNull(plane.intersection(Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION)));
784 Assertions.assertNull(plane.intersection(Planes.fromPointAndNormal(Vector3D.of(0, 0, 2), Vector3D.Unit.PLUS_Z, TEST_PRECISION)));
785 }
786
787 @Test
788 void testIntersection_threePlanes() {
789
790 final Vector3D pt = Vector3D.of(1.2, 3.4, -5.8);
791 final Plane a = Planes.fromPointAndNormal(pt, Vector3D.of(1, 3, 3), TEST_PRECISION);
792 final Plane b = Planes.fromPointAndNormal(pt, Vector3D.of(-2, 4, 0), TEST_PRECISION);
793 final Plane c = Planes.fromPointAndNormal(pt, Vector3D.of(7, 0, -4), TEST_PRECISION);
794
795
796 final Vector3D result = Plane.intersection(a, b, c);
797
798
799 EuclideanTestUtils.assertCoordinatesEqual(pt, result, TEST_EPS);
800 }
801
802 @Test
803 void testIntersection_threePlanes_intersectInLine() {
804
805 final Plane a = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.of(1, 0, 0), TEST_PRECISION);
806 final Plane b = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.of(1, 0.5, 0), TEST_PRECISION);
807 final Plane c = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.of(1, 1, 0), TEST_PRECISION);
808
809
810 final Vector3D result = Plane.intersection(a, b, c);
811
812
813 Assertions.assertNull(result);
814 }
815
816 @Test
817 void testIntersection_threePlanes_twoParallel() {
818
819 final Plane a = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
820 final Plane b = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_X, TEST_PRECISION);
821 final Plane c = Planes.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_Z, TEST_PRECISION);
822
823
824 final Vector3D result = Plane.intersection(a, b, c);
825
826
827 Assertions.assertNull(result);
828 }
829
830 @Test
831 void testIntersection_threePlanes_allParallel() {
832
833 final Plane a = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
834 final Plane b = Planes.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_Z, TEST_PRECISION);
835 final Plane c = Planes.fromPointAndNormal(Vector3D.of(0, 0, 2), Vector3D.Unit.PLUS_Z, TEST_PRECISION);
836
837
838 final Vector3D result = Plane.intersection(a, b, c);
839
840
841 Assertions.assertNull(result);
842 }
843
844 @Test
845 void testIntersection_threePlanes_coincidentPlanes() {
846
847 final Plane a = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
848 final Plane b = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
849 final Plane c = b.reverse();
850
851
852 final Vector3D result = Plane.intersection(a, b, c);
853
854
855 Assertions.assertNull(result);
856 }
857
858 @Test
859 void testSpan() {
860
861 final Plane plane = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
862
863
864 final PlaneConvexSubset sub = plane.span();
865
866
867 Assertions.assertNotSame(plane, sub.getPlane());
868 EuclideanTestUtils.assertCoordinatesEqual(plane.getOrigin(), sub.getPlane().getOrigin(), TEST_EPS);
869 EuclideanTestUtils.assertCoordinatesEqual(plane.getNormal(), sub.getPlane().getNormal(), TEST_EPS);
870
871 Assertions.assertTrue(sub.isFull());
872
873 Assertions.assertTrue(sub.contains(Vector3D.ZERO));
874 Assertions.assertTrue(sub.contains(Vector3D.of(1, 1, 0)));
875
876 Assertions.assertFalse(sub.contains(Vector3D.of(0, 0, 1)));
877 }
878
879 @Test
880 void testSimilarOrientation() {
881
882 final Plane plane = Planes.fromNormal(Vector3D.of(1, 0, 0), TEST_PRECISION);
883
884
885 Assertions.assertTrue(plane.similarOrientation(plane));
886 Assertions.assertTrue(plane.similarOrientation(Planes.fromNormal(Vector3D.of(1, 1, 0), TEST_PRECISION)));
887 Assertions.assertTrue(plane.similarOrientation(Planes.fromNormal(Vector3D.of(1, -1, 0), TEST_PRECISION)));
888
889 Assertions.assertFalse(plane.similarOrientation(Planes.fromNormal(Vector3D.of(0, 1, 0), TEST_PRECISION)));
890 Assertions.assertFalse(plane.similarOrientation(Planes.fromNormal(Vector3D.of(-1, 1, 0), TEST_PRECISION)));
891 Assertions.assertFalse(plane.similarOrientation(Planes.fromNormal(Vector3D.of(-1, 1, 0), TEST_PRECISION)));
892 Assertions.assertFalse(plane.similarOrientation(Planes.fromNormal(Vector3D.of(0, -1, 0), TEST_PRECISION)));
893 }
894
895 @Test
896 void testEq() {
897
898 final double eps = 1e-3;
899 final Precision.DoubleEquivalence precision = Precision.doubleEquivalenceOfEpsilon(eps);
900
901 final Vector3D pt = Vector3D.of(1, 2, 3);
902 final Vector3D normal = Vector3D.Unit.PLUS_X;
903
904 final Vector3D ptPrime = Vector3D.of(1.0001, 2.0001, 3.0001);
905 final Vector3D normalPrime = Vector3D.Unit.of(1, 1e-4, 0);
906
907 final Plane a = Planes.fromPointAndNormal(pt, normal, precision);
908
909 final Plane b = Planes.fromPointAndNormal(Vector3D.of(2, 2, 3), normal, precision);
910 final Plane c = Planes.fromPointAndNormal(pt, Vector3D.Unit.MINUS_X, precision);
911 final Plane d = Planes.fromPointAndNormal(pt, normal, TEST_PRECISION);
912
913 final Plane e = Planes.fromPointAndNormal(ptPrime, normalPrime, Precision.doubleEquivalenceOfEpsilon(eps));
914
915
916 Assertions.assertTrue(a.eq(a, precision));
917
918 Assertions.assertFalse(a.eq(b, precision));
919 Assertions.assertFalse(a.eq(c, precision));
920
921 Assertions.assertTrue(a.eq(d, precision));
922 Assertions.assertTrue(a.eq(e, precision));
923 Assertions.assertTrue(e.eq(a, precision));
924 }
925
926 @Test
927 void testHashCode() {
928
929 final Vector3D pt = Vector3D.of(1, 2, 3);
930 final Vector3D normal = Vector3D.Unit.PLUS_X;
931
932 final Plane a = Planes.fromPointAndNormal(pt, normal, TEST_PRECISION);
933 final Plane b = Planes.fromPointAndNormal(Vector3D.of(2, 2, 3), normal, TEST_PRECISION);
934 final Plane c = Planes.fromPointAndNormal(pt, Vector3D.of(1, 1, 0), TEST_PRECISION);
935 final Plane d = Planes.fromPointAndNormal(pt, normal, Precision.doubleEquivalenceOfEpsilon(1e-8));
936 final Plane e = Planes.fromPointAndNormal(pt, normal, TEST_PRECISION);
937
938
939 final int hash = a.hashCode();
940
941 Assertions.assertEquals(hash, a.hashCode());
942
943 Assertions.assertNotEquals(hash, b.hashCode());
944 Assertions.assertNotEquals(hash, c.hashCode());
945 Assertions.assertNotEquals(hash, d.hashCode());
946
947 Assertions.assertEquals(hash, e.hashCode());
948 }
949
950 @Test
951 void testEquals() {
952
953 final Vector3D pt = Vector3D.of(1, 2, 3);
954 final Vector3D normal = Vector3D.Unit.PLUS_X;
955
956 final Plane a = Planes.fromPointAndNormal(pt, normal, TEST_PRECISION);
957 final Plane b = Planes.fromPointAndNormal(Vector3D.of(2, 2, 3), normal, TEST_PRECISION);
958 final Plane c = Planes.fromPointAndNormal(pt, Vector3D.Unit.MINUS_X, TEST_PRECISION);
959 final Plane d = Planes.fromPointAndNormal(pt, normal, Precision.doubleEquivalenceOfEpsilon(1e-8));
960 final Plane e = Planes.fromPointAndNormal(pt, normal, TEST_PRECISION);
961
962
963 GeometryTestUtils.assertSimpleEqualsCases(a);
964
965 Assertions.assertNotEquals(a, b);
966 Assertions.assertNotEquals(a, c);
967 Assertions.assertNotEquals(a, d);
968
969 Assertions.assertEquals(a, e);
970 Assertions.assertEquals(e, a);
971 }
972
973 @Test
974 void testToString() {
975
976 final Plane plane = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
977
978
979 final String str = plane.toString();
980
981
982 Assertions.assertTrue(str.startsWith("Plane["));
983 Assertions.assertTrue(str.matches(".*origin= \\(0(\\.0)?, 0(\\.0)?\\, 0(\\.0)?\\).*"));
984 Assertions.assertTrue(str.matches(".*normal= \\(0(\\.0)?, 0(\\.0)?\\, 1(\\.0)?\\).*"));
985 }
986
987 private static void checkPlane(final Plane plane, final Vector3D origin, final Vector3D normal) {
988 EuclideanTestUtils.assertCoordinatesEqual(origin, plane.getOrigin(), TEST_EPS);
989 Assertions.assertTrue(plane.contains(origin));
990
991 EuclideanTestUtils.assertCoordinatesEqual(normal, plane.getNormal(), TEST_EPS);
992 Assertions.assertEquals(1.0, plane.getNormal().norm(), TEST_EPS);
993
994 final double offset = plane.getOriginOffset();
995 Assertions.assertEquals(Vector3D.ZERO.distance(plane.getOrigin()), Math.abs(offset), TEST_EPS);
996 EuclideanTestUtils.assertCoordinatesEqual(origin, plane.getNormal().multiply(-offset), TEST_EPS);
997 }
998
999 private static <T> List<T> rotate(final List<T> list, final int shift) {
1000 final int size = list.size();
1001
1002 final List<T> result = new ArrayList<>(size);
1003
1004 for (int i = 0; i < size; ++i) {
1005 result.add(list.get((i + shift) % size));
1006 }
1007
1008 return result;
1009 }
1010 }