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.function.UnaryOperator;
20
21 import org.apache.commons.geometry.core.GeometryTestUtils;
22 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
23 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
24 import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
25 import org.apache.commons.geometry.euclidean.EuclideanTestUtils.PermuteCallback3D;
26 import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
27 import org.apache.commons.geometry.euclidean.threed.rotation.StandardRotations;
28 import org.apache.commons.numbers.angle.PlaneAngleRadians;
29 import org.junit.Assert;
30 import org.junit.Test;
31
32 public class AffineTransformMatrix3DTest {
33
34 private static final double EPS = 1e-12;
35
36 private static final DoublePrecisionContext TEST_PRECISION =
37 new EpsilonDoublePrecisionContext(EPS);
38
39 @Test
40 public void testOf() {
41
42 final double[] arr = {
43 1, 2, 3, 4,
44 5, 6, 7, 8,
45 9, 10, 11, 12
46 };
47
48
49 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.of(arr);
50
51
52 final double[] result = transform.toArray();
53 Assert.assertNotSame(arr, result);
54 Assert.assertArrayEquals(arr, result, 0.0);
55 }
56
57 @Test
58 public void testOf_invalidDimensions() {
59
60 GeometryTestUtils.assertThrows(() -> AffineTransformMatrix3D.of(1, 2),
61 IllegalArgumentException.class, "Dimension mismatch: 2 != 12");
62 }
63
64 @Test
65 public void testFromColumnVectors_threeVectors() {
66
67 final Vector3D u = Vector3D.of(1, 2, 3);
68 final Vector3D v = Vector3D.of(4, 5, 6);
69 final Vector3D w = Vector3D.of(7, 8, 9);
70
71
72 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.fromColumnVectors(u, v, w);
73
74
75 Assert.assertArrayEquals(new double[] {
76 1, 4, 7, 0,
77 2, 5, 8, 0,
78 3, 6, 9, 0
79 }, transform.toArray(), 0.0);
80 }
81
82 @Test
83 public void testFromColumnVectors_fourVectors() {
84
85 final Vector3D u = Vector3D.of(1, 2, 3);
86 final Vector3D v = Vector3D.of(4, 5, 6);
87 final Vector3D w = Vector3D.of(7, 8, 9);
88 final Vector3D t = Vector3D.of(10, 11, 12);
89
90
91 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.fromColumnVectors(u, v, w, t);
92
93
94 Assert.assertArrayEquals(new double[] {
95 1, 4, 7, 10,
96 2, 5, 8, 11,
97 3, 6, 9, 12
98 }, transform.toArray(), 0.0);
99 }
100
101 @Test
102 public void testFrom() {
103
104 Assert.assertArrayEquals(new double[] {
105 1, 0, 0, 0,
106 0, 1, 0, 0,
107 0, 0, 1, 0
108 }, AffineTransformMatrix3D.from(UnaryOperator.identity()).toArray(), EPS);
109 Assert.assertArrayEquals(new double[] {
110 1, 0, 0, 2,
111 0, 1, 0, 3,
112 0, 0, 1, -4
113 }, AffineTransformMatrix3D.from(v -> v.add(Vector3D.of(2, 3, -4))).toArray(), EPS);
114 Assert.assertArrayEquals(new double[] {
115 3, 0, 0, 0,
116 0, 3, 0, 0,
117 0, 0, 3, 0
118 }, AffineTransformMatrix3D.from(v -> v.multiply(3)).toArray(), EPS);
119 Assert.assertArrayEquals(new double[] {
120 3, 0, 0, 6,
121 0, 3, 0, 9,
122 0, 0, 3, 12
123 }, AffineTransformMatrix3D.from(v -> v.add(Vector3D.of(2, 3, 4)).multiply(3)).toArray(), EPS);
124 }
125
126 @Test
127 public void testFrom_invalidFunction() {
128
129 GeometryTestUtils.assertThrows(() -> {
130 AffineTransformMatrix3D.from(v -> v.multiply(0));
131 }, IllegalArgumentException.class);
132 }
133
134 @Test
135 public void testIdentity() {
136
137 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity();
138
139
140 final double[] expected = {
141 1, 0, 0, 0,
142 0, 1, 0, 0,
143 0, 0, 1, 0
144 };
145 Assert.assertArrayEquals(expected, transform.toArray(), 0.0);
146 }
147
148 @Test
149 public void testCreateTranslation_xyz() {
150
151 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createTranslation(2, 3, 4);
152
153
154 final double[] expected = {
155 1, 0, 0, 2,
156 0, 1, 0, 3,
157 0, 0, 1, 4
158 };
159 Assert.assertArrayEquals(expected, transform.toArray(), 0.0);
160 }
161
162 @Test
163 public void testCreateTranslation_vector() {
164
165 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createTranslation(Vector3D.of(5, 6, 7));
166
167
168 final double[] expected = {
169 1, 0, 0, 5,
170 0, 1, 0, 6,
171 0, 0, 1, 7
172 };
173 Assert.assertArrayEquals(expected, transform.toArray(), 0.0);
174 }
175
176 @Test
177 public void testCreateScale_xyz() {
178
179 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(2, 3, 4);
180
181
182 final double[] expected = {
183 2, 0, 0, 0,
184 0, 3, 0, 0,
185 0, 0, 4, 0
186 };
187 Assert.assertArrayEquals(expected, transform.toArray(), 0.0);
188 }
189
190 @Test
191 public void testTranslate_xyz() {
192
193 final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(
194 2, 0, 0, 10,
195 0, 3, 0, 11,
196 0, 0, 4, 12
197 );
198
199
200 final AffineTransformMatrix3D result = a.translate(4, 5, 6);
201
202
203 final double[] expected = {
204 2, 0, 0, 14,
205 0, 3, 0, 16,
206 0, 0, 4, 18
207 };
208 Assert.assertArrayEquals(expected, result.toArray(), 0.0);
209 }
210
211 @Test
212 public void testTranslate_vector() {
213
214 final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(
215 2, 0, 0, 10,
216 0, 3, 0, 11,
217 0, 0, 4, 12
218 );
219
220
221 final AffineTransformMatrix3D result = a.translate(Vector3D.of(7, 8, 9));
222
223
224 final double[] expected = {
225 2, 0, 0, 17,
226 0, 3, 0, 19,
227 0, 0, 4, 21
228 };
229 Assert.assertArrayEquals(expected, result.toArray(), 0.0);
230 }
231
232 @Test
233 public void testCreateScale_vector() {
234
235 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(Vector3D.of(4, 5, 6));
236
237
238 final double[] expected = {
239 4, 0, 0, 0,
240 0, 5, 0, 0,
241 0, 0, 6, 0
242 };
243 Assert.assertArrayEquals(expected, transform.toArray(), 0.0);
244 }
245
246 @Test
247 public void testCreateScale_singleValue() {
248
249 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(7);
250
251
252 final double[] expected = {
253 7, 0, 0, 0,
254 0, 7, 0, 0,
255 0, 0, 7, 0
256 };
257 Assert.assertArrayEquals(expected, transform.toArray(), 0.0);
258 }
259
260 @Test
261 public void testScale_xyz() {
262
263 final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(
264 2, 0, 0, 10,
265 0, 3, 0, 11,
266 0, 0, 4, 12
267 );
268
269
270 final AffineTransformMatrix3D result = a.scale(4, 5, 6);
271
272
273 final double[] expected = {
274 8, 0, 0, 40,
275 0, 15, 0, 55,
276 0, 0, 24, 72
277 };
278 Assert.assertArrayEquals(expected, result.toArray(), 0.0);
279 }
280
281 @Test
282 public void testScale_vector() {
283
284 final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(
285 2, 0, 0, 10,
286 0, 3, 0, 11,
287 0, 0, 4, 12
288 );
289
290
291 final AffineTransformMatrix3D result = a.scale(Vector3D.of(7, 8, 9));
292
293
294 final double[] expected = {
295 14, 0, 0, 70,
296 0, 24, 0, 88,
297 0, 0, 36, 108
298 };
299 Assert.assertArrayEquals(expected, result.toArray(), 0.0);
300 }
301
302 @Test
303 public void testScale_singleValue() {
304
305 final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(
306 2, 0, 0, 10,
307 0, 3, 0, 11,
308 0, 0, 4, 12
309 );
310
311
312 final AffineTransformMatrix3D result = a.scale(10);
313
314
315 final double[] expected = {
316 20, 0, 0, 100,
317 0, 30, 0, 110,
318 0, 0, 40, 120
319 };
320 Assert.assertArrayEquals(expected, result.toArray(), 0.0);
321 }
322
323 @Test
324 public void testCreateRotation() {
325
326 final Vector3D center = Vector3D.of(1, 2, 3);
327 final QuaternionRotation rotation = QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Z, PlaneAngleRadians.PI_OVER_TWO);
328
329
330 final AffineTransformMatrix3D result = AffineTransformMatrix3D.createRotation(center, rotation);
331
332
333 final double[] expected = {
334 0, -1, 0, 3,
335 1, 0, 0, 1,
336 0, 0, 1, 0
337 };
338 Assert.assertArrayEquals(expected, result.toArray(), EPS);
339 }
340
341 @Test
342 public void testRotate() {
343
344 final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(
345 1, 2, 3, 4,
346 5, 6, 7, 8,
347 9, 10, 11, 12
348 );
349
350 final QuaternionRotation rotation = QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Z, PlaneAngleRadians.PI_OVER_TWO);
351
352
353 final AffineTransformMatrix3D result = a.rotate(rotation);
354
355
356 final double[] expected = {
357 -5, -6, -7, -8,
358 1, 2, 3, 4,
359 9, 10, 11, 12
360 };
361 Assert.assertArrayEquals(expected, result.toArray(), EPS);
362 }
363
364 @Test
365 public void testRotate_aroundCenter() {
366
367 final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(
368 1, 2, 3, 4,
369 5, 6, 7, 8,
370 9, 10, 11, 12
371 );
372
373 final Vector3D center = Vector3D.of(1, 2, 3);
374 final QuaternionRotation rotation = QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Z, PlaneAngleRadians.PI_OVER_TWO);
375
376
377 final AffineTransformMatrix3D result = a.rotate(center, rotation);
378
379
380 final double[] expected = {
381 -5, -6, -7, -5,
382 1, 2, 3, 5,
383 9, 10, 11, 12
384 };
385 Assert.assertArrayEquals(expected, result.toArray(), EPS);
386 }
387
388 @Test
389 public void testApply_identity() {
390
391 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity();
392
393
394 runWithCoordinates((x, y, z) -> {
395 final Vector3D v = Vector3D.of(x, y, z);
396
397 EuclideanTestUtils.assertCoordinatesEqual(v, transform.apply(v), EPS);
398 });
399 }
400
401 @Test
402 public void testApply_translate() {
403
404 final Vector3D translation = Vector3D.of(1.1, -PlaneAngleRadians.PI, 5.5);
405
406 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
407 .translate(translation);
408
409
410 runWithCoordinates((x, y, z) -> {
411 final Vector3D vec = Vector3D.of(x, y, z);
412
413 final Vector3D expectedVec = vec.add(translation);
414
415 EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
416 });
417 }
418
419 @Test
420 public void testApply_scale() {
421
422 final Vector3D factors = Vector3D.of(2.0, -3.0, 4.0);
423
424 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
425 .scale(factors);
426
427
428 runWithCoordinates((x, y, z) -> {
429 final Vector3D vec = Vector3D.of(x, y, z);
430
431 final Vector3D expectedVec = Vector3D.of(factors.getX() * x, factors.getY() * y, factors.getZ() * z);
432
433 EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
434 });
435 }
436
437 @Test
438 public void testApply_translateThenScale() {
439
440 final Vector3D translation = Vector3D.of(-2.0, -3.0, -4.0);
441 final Vector3D scale = Vector3D.of(5.0, 6.0, 7.0);
442
443 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
444 .translate(translation)
445 .scale(scale);
446
447
448 EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-5, -12, -21), transform.apply(Vector3D.of(1, 1, 1)), EPS);
449
450 runWithCoordinates((x, y, z) -> {
451 final Vector3D vec = Vector3D.of(x, y, z);
452
453 final Vector3D expectedVec = Vector3D.of(
454 (x + translation.getX()) * scale.getX(),
455 (y + translation.getY()) * scale.getY(),
456 (z + translation.getZ()) * scale.getZ()
457 );
458
459 EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
460 });
461 }
462
463 @Test
464 public void testApply_scaleThenTranslate() {
465
466 final Vector3D scale = Vector3D.of(5.0, 6.0, 7.0);
467 final Vector3D translation = Vector3D.of(-2.0, -3.0, -4.0);
468
469 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
470 .scale(scale)
471 .translate(translation);
472
473
474 runWithCoordinates((x, y, z) -> {
475 final Vector3D vec = Vector3D.of(x, y, z);
476
477 final Vector3D expectedVec = Vector3D.of(
478 (x * scale.getX()) + translation.getX(),
479 (y * scale.getY()) + translation.getY(),
480 (z * scale.getZ()) + translation.getZ()
481 );
482
483 EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
484 });
485 }
486
487 @Test
488 public void testApply_rotate() {
489
490 final QuaternionRotation rotation = QuaternionRotation.fromAxisAngle(Vector3D.of(1, 1, 1), 2.0 * PlaneAngleRadians.PI / 3.0);
491
492 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity().rotate(rotation);
493
494
495 runWithCoordinates((x, y, z) -> {
496 final Vector3D vec = Vector3D.of(x, y, z);
497
498 final Vector3D expectedVec = StandardRotations.PLUS_DIAGONAL_TWO_THIRDS_PI.apply(vec);
499
500 EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
501 });
502 }
503
504 @Test
505 public void testApply_rotate_aroundCenter() {
506
507 final double scaleFactor = 2;
508 final Vector3D center = Vector3D.of(3, -4, 5);
509 final QuaternionRotation rotation = QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Z, PlaneAngleRadians.PI_OVER_TWO);
510
511 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
512 .scale(scaleFactor)
513 .rotate(center, rotation);
514
515
516 EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, -3, 2), transform.apply(Vector3D.of(2, -2, 1)), EPS);
517
518 runWithCoordinates((x, y, z) -> {
519 final Vector3D vec = Vector3D.of(x, y, z);
520
521 final Vector3D expectedVec = StandardRotations.PLUS_Z_HALF_PI.apply(vec.multiply(scaleFactor).subtract(center)).add(center);
522
523 EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
524 });
525 }
526
527 @Test
528 public void testApplyVector_identity() {
529
530 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity();
531
532
533 runWithCoordinates((x, y, z) -> {
534 final Vector3D v = Vector3D.of(x, y, z);
535
536 EuclideanTestUtils.assertCoordinatesEqual(v, transform.applyVector(v), EPS);
537 });
538 }
539
540 @Test
541 public void testApplyVector_translate() {
542
543 final Vector3D translation = Vector3D.of(1.1, -PlaneAngleRadians.PI, 5.5);
544
545 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
546 .translate(translation);
547
548
549 runWithCoordinates((x, y, z) -> {
550 final Vector3D vec = Vector3D.of(x, y, z);
551
552 EuclideanTestUtils.assertCoordinatesEqual(vec, transform.applyVector(vec), EPS);
553 });
554 }
555
556 @Test
557 public void testApplyVector_scale() {
558
559 final Vector3D factors = Vector3D.of(2.0, -3.0, 4.0);
560
561 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
562 .scale(factors);
563
564
565 runWithCoordinates((x, y, z) -> {
566 final Vector3D vec = Vector3D.of(x, y, z);
567
568 final Vector3D expectedVec = Vector3D.of(factors.getX() * x, factors.getY() * y, factors.getZ() * z);
569
570 EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.applyVector(vec), EPS);
571 });
572 }
573
574 @Test
575 public void testApplyVector_representsDisplacement() {
576
577 final Vector3D p1 = Vector3D.of(1, 2, 3);
578
579 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
580 .scale(1.5)
581 .translate(4, 6, 5)
582 .rotate(QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Z, PlaneAngleRadians.PI_OVER_TWO));
583
584
585 runWithCoordinates((x, y, z) -> {
586 final Vector3D p2 = Vector3D.of(x, y, z);
587 final Vector3D input = p1.subtract(p2);
588
589 final Vector3D expected = transform.apply(p1).subtract(transform.apply(p2));
590
591 EuclideanTestUtils.assertCoordinatesEqual(expected, transform.applyVector(input), EPS);
592 });
593 }
594
595 @Test
596 public void testApplyDirection_identity() {
597
598 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity();
599
600
601 EuclideanTestUtils.permuteSkipZero(-5, 5, 0.5, (x, y, z) -> {
602 final Vector3D v = Vector3D.of(x, y, z);
603
604 EuclideanTestUtils.assertCoordinatesEqual(v.normalize(), transform.applyDirection(v), EPS);
605 });
606 }
607
608 @Test
609 public void testApplyDirection_translate() {
610
611 final Vector3D translation = Vector3D.of(1.1, -PlaneAngleRadians.PI, 5.5);
612
613 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
614 .translate(translation);
615
616
617 EuclideanTestUtils.permuteSkipZero(-5, 5, 0.5, (x, y, z) -> {
618 final Vector3D vec = Vector3D.of(x, y, z);
619
620 EuclideanTestUtils.assertCoordinatesEqual(vec.normalize(), transform.applyDirection(vec), EPS);
621 });
622 }
623
624 @Test
625 public void testApplyDirection_scale() {
626
627 final Vector3D factors = Vector3D.of(2.0, -3.0, 4.0);
628
629 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
630 .scale(factors);
631
632
633 EuclideanTestUtils.permuteSkipZero(-5, 5, 0.5, (x, y, z) -> {
634 final Vector3D vec = Vector3D.of(x, y, z);
635
636 final Vector3D expectedVec = Vector3D.of(factors.getX() * x, factors.getY() * y, factors.getZ() * z).normalize();
637
638 EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.applyDirection(vec), EPS);
639 });
640 }
641
642 @Test
643 public void testApplyDirection_representsNormalizedDisplacement() {
644
645 final Vector3D p1 = Vector3D.of(1, 2, 3);
646
647 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
648 .scale(1.5)
649 .translate(4, 6, 5)
650 .rotate(QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Z, PlaneAngleRadians.PI_OVER_TWO));
651
652
653 runWithCoordinates((x, y, z) -> {
654 final Vector3D p2 = Vector3D.of(x, y, z);
655 final Vector3D input = p1.subtract(p2);
656
657 final Vector3D expected = transform.apply(p1).subtract(transform.apply(p2)).normalize();
658
659 EuclideanTestUtils.assertCoordinatesEqual(expected, transform.applyDirection(input), EPS);
660 });
661 }
662
663 @Test
664 public void testApplyDirection_illegalNorm() {
665
666 GeometryTestUtils.assertThrows(() -> AffineTransformMatrix3D.createScale(1, 0, 1).applyDirection(Vector3D.Unit.PLUS_Y),
667 IllegalArgumentException.class);
668 GeometryTestUtils.assertThrows(() -> AffineTransformMatrix3D.createScale(2).applyDirection(Vector3D.ZERO),
669 IllegalArgumentException.class);
670 }
671
672 @Test
673 public void testMultiply() {
674
675 final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(
676 1, 2, 3, 4,
677 5, 6, 7, 8,
678 9, 10, 11, 12
679 );
680 final AffineTransformMatrix3D b = AffineTransformMatrix3D.of(
681 13, 14, 15, 16,
682 17, 18, 19, 20,
683 21, 22, 23, 24
684 );
685
686
687 final AffineTransformMatrix3D result = a.multiply(b);
688
689
690 final double[] arr = result.toArray();
691 Assert.assertArrayEquals(new double[] {
692 110, 116, 122, 132,
693 314, 332, 350, 376,
694 518, 548, 578, 620
695 }, arr, EPS);
696 }
697
698 @Test
699 public void testDeterminant() {
700
701 Assert.assertEquals(1.0, AffineTransformMatrix3D.identity().determinant(), EPS);
702 Assert.assertEquals(1.0, AffineTransformMatrix3D.of(
703 1, 0, 0, 10,
704 0, 1, 0, 11,
705 0, 0, 1, 12
706 ).determinant(), EPS);
707 Assert.assertEquals(-1.0, AffineTransformMatrix3D.of(
708 -1, 0, 0, 10,
709 0, 1, 0, 11,
710 0, 0, 1, 12
711 ).determinant(), EPS);
712 Assert.assertEquals(1.0, AffineTransformMatrix3D.of(
713 -1, 0, 0, 10,
714 0, -1, 0, 11,
715 0, 0, 1, 12
716 ).determinant(), EPS);
717 Assert.assertEquals(-1.0, AffineTransformMatrix3D.of(
718 -1, 0, 0, 10,
719 0, -1, 0, 11,
720 0, 0, -1, 12
721 ).determinant(), EPS);
722 Assert.assertEquals(49.0, AffineTransformMatrix3D.of(
723 2, -3, 1, 10,
724 2, 0, -1, 11,
725 1, 4, 5, -12
726 ).determinant(), EPS);
727 Assert.assertEquals(0.0, AffineTransformMatrix3D.of(
728 1, 2, 3, 0,
729 4, 5, 6, 0,
730 7, 8, 9, 0
731 ).determinant(), EPS);
732 }
733
734 @Test
735 public void testPreservesOrientation() {
736
737 Assert.assertTrue(AffineTransformMatrix3D.identity().preservesOrientation());
738 Assert.assertTrue(AffineTransformMatrix3D.of(
739 1, 0, 0, 10,
740 0, 1, 0, 11,
741 0, 0, 1, 12
742 ).preservesOrientation());
743 Assert.assertTrue(AffineTransformMatrix3D.of(
744 2, -3, 1, 10,
745 2, 0, -1, 11,
746 1, 4, 5, -12
747 ).preservesOrientation());
748
749 Assert.assertFalse(AffineTransformMatrix3D.of(
750 -1, 0, 0, 10,
751 0, 1, 0, 11,
752 0, 0, 1, 12
753 ).preservesOrientation());
754
755 Assert.assertTrue(AffineTransformMatrix3D.of(
756 -1, 0, 0, 10,
757 0, -1, 0, 11,
758 0, 0, 1, 12
759 ).preservesOrientation());
760
761 Assert.assertFalse(AffineTransformMatrix3D.of(
762 -1, 0, 0, 10,
763 0, -1, 0, 11,
764 0, 0, -1, 12
765 ).preservesOrientation());
766 Assert.assertFalse(AffineTransformMatrix3D.of(
767 1, 2, 3, 0,
768 4, 5, 6, 0,
769 7, 8, 9, 0
770 ).preservesOrientation());
771 }
772
773 @Test
774 public void testMultiply_combinesTransformOperations() {
775
776 final Vector3D translation1 = Vector3D.of(1, 2, 3);
777 final double scale = 2.0;
778 final Vector3D translation2 = Vector3D.of(4, 5, 6);
779
780 final AffineTransformMatrix3D a = AffineTransformMatrix3D.createTranslation(translation1);
781 final AffineTransformMatrix3D b = AffineTransformMatrix3D.createScale(scale);
782 final AffineTransformMatrix3D c = AffineTransformMatrix3D.identity();
783 final AffineTransformMatrix3D d = AffineTransformMatrix3D.createTranslation(translation2);
784
785
786 final AffineTransformMatrix3D transform = d.multiply(c).multiply(b).multiply(a);
787
788
789 runWithCoordinates((x, y, z) -> {
790 final Vector3D vec = Vector3D.of(x, y, z);
791
792 final Vector3D expectedVec = vec
793 .add(translation1)
794 .multiply(scale)
795 .add(translation2);
796
797 EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
798 });
799 }
800
801 @Test
802 public void testPremultiply() {
803
804 final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(
805 1, 2, 3, 4,
806 5, 6, 7, 8,
807 9, 10, 11, 12
808 );
809 final AffineTransformMatrix3D b = AffineTransformMatrix3D.of(
810 13, 14, 15, 16,
811 17, 18, 19, 20,
812 21, 22, 23, 24
813 );
814
815
816 final AffineTransformMatrix3D result = b.premultiply(a);
817
818
819 final double[] arr = result.toArray();
820 Assert.assertArrayEquals(new double[] {
821 110, 116, 122, 132,
822 314, 332, 350, 376,
823 518, 548, 578, 620
824 }, arr, EPS);
825 }
826
827 @Test
828 public void testPremultiply_combinesTransformOperations() {
829
830 final Vector3D translation1 = Vector3D.of(1, 2, 3);
831 final double scale = 2.0;
832 final Vector3D translation2 = Vector3D.of(4, 5, 6);
833
834 final AffineTransformMatrix3D a = AffineTransformMatrix3D.createTranslation(translation1);
835 final AffineTransformMatrix3D b = AffineTransformMatrix3D.createScale(scale);
836 final AffineTransformMatrix3D c = AffineTransformMatrix3D.identity();
837 final AffineTransformMatrix3D d = AffineTransformMatrix3D.createTranslation(translation2);
838
839
840 final AffineTransformMatrix3D transform = a.premultiply(b).premultiply(c).premultiply(d);
841
842
843 runWithCoordinates((x, y, z) -> {
844 final Vector3D vec = Vector3D.of(x, y, z);
845
846 final Vector3D expectedVec = vec
847 .add(translation1)
848 .multiply(scale)
849 .add(translation2);
850
851 EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
852 });
853 }
854
855 @Test
856 public void testInverse_identity() {
857
858 final AffineTransformMatrix3D inverse = AffineTransformMatrix3D.identity().inverse();
859
860
861 final double[] expected = {
862 1, 0, 0, 0,
863 0, 1, 0, 0,
864 0, 0, 1, 0
865 };
866 Assert.assertArrayEquals(expected, inverse.toArray(), 0.0);
867 }
868
869 @Test
870 public void testInverse_multiplyByInverse_producesIdentity() {
871
872 final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(
873 1, 3, 7, 8,
874 2, 4, 9, 12,
875 5, 6, 10, 11
876 );
877
878 final AffineTransformMatrix3D inv = a.inverse();
879
880
881 final AffineTransformMatrix3D result = inv.multiply(a);
882
883
884 final double[] expected = {
885 1, 0, 0, 0,
886 0, 1, 0, 0,
887 0, 0, 1, 0
888 };
889 Assert.assertArrayEquals(expected, result.toArray(), EPS);
890 }
891
892 @Test
893 public void testInverse_translate() {
894
895 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createTranslation(1, -2, 4);
896
897
898 final AffineTransformMatrix3D inverse = transform.inverse();
899
900
901 final double[] expected = {
902 1, 0, 0, -1,
903 0, 1, 0, 2,
904 0, 0, 1, -4
905 };
906 Assert.assertArrayEquals(expected, inverse.toArray(), 0.0);
907 }
908
909 @Test
910 public void testInverse_scale() {
911
912 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(10, -2, 4);
913
914
915 final AffineTransformMatrix3D inverse = transform.inverse();
916
917
918 final double[] expected = {
919 0.1, 0, 0, 0,
920 0, -0.5, 0, 0,
921 0, 0, 0.25, 0
922 };
923 Assert.assertArrayEquals(expected, inverse.toArray(), 0.0);
924 }
925
926 @Test
927 public void testInverse_rotate() {
928
929 final Vector3D center = Vector3D.of(1, 2, 3);
930 final QuaternionRotation rotation = QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Z, PlaneAngleRadians.PI_OVER_TWO);
931
932 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createRotation(center, rotation);
933
934
935 final AffineTransformMatrix3D inverse = transform.inverse();
936
937
938 final double[] expected = {
939 0, 1, 0, -1,
940 -1, 0, 0, 3,
941 0, 0, 1, 0
942 };
943 Assert.assertArrayEquals(expected, inverse.toArray(), EPS);
944 }
945
946 @Test
947 public void testInverse_undoesOriginalTransform() {
948
949 final Vector3D v1 = Vector3D.ZERO;
950 final Vector3D v2 = Vector3D.Unit.PLUS_X;
951 final Vector3D v3 = Vector3D.of(1, 1, 1);
952 final Vector3D v4 = Vector3D.of(-2, 3, 4);
953
954 final Vector3D center = Vector3D.of(1, 2, 3);
955 final QuaternionRotation rotation = QuaternionRotation.fromAxisAngle(Vector3D.of(1, 2, 3), 0.25);
956
957
958 runWithCoordinates((x, y, z) -> {
959 final AffineTransformMatrix3D transform = AffineTransformMatrix3D
960 .createTranslation(x, y, z)
961 .scale(2, 3, 4)
962 .rotate(center, rotation)
963 .translate(x / 3, y / 3, z / 3);
964
965 final AffineTransformMatrix3D inverse = transform.inverse();
966
967 EuclideanTestUtils.assertCoordinatesEqual(v1, inverse.apply(transform.apply(v1)), EPS);
968 EuclideanTestUtils.assertCoordinatesEqual(v2, inverse.apply(transform.apply(v2)), EPS);
969 EuclideanTestUtils.assertCoordinatesEqual(v3, inverse.apply(transform.apply(v3)), EPS);
970 EuclideanTestUtils.assertCoordinatesEqual(v4, inverse.apply(transform.apply(v4)), EPS);
971 });
972 }
973
974 @Test
975 public void testInverse_nonInvertible() {
976
977 GeometryTestUtils.assertThrows(() -> {
978 AffineTransformMatrix3D.of(
979 0, 0, 0, 0,
980 0, 0, 0, 0,
981 0, 0, 0, 0).inverse();
982 }, IllegalStateException.class, "Matrix is not invertible; matrix determinant is 0.0");
983
984 GeometryTestUtils.assertThrows(() -> {
985 AffineTransformMatrix3D.of(
986 1, 0, 0, 0,
987 0, 1, 0, 0,
988 0, 0, Double.NaN, 0).inverse();
989 }, IllegalStateException.class, "Matrix is not invertible; matrix determinant is NaN");
990
991 GeometryTestUtils.assertThrows(() -> {
992 AffineTransformMatrix3D.of(
993 1, 0, 0, 0,
994 0, Double.NEGATIVE_INFINITY, 0, 0,
995 0, 0, 1, 0).inverse();
996 }, IllegalStateException.class, "Matrix is not invertible; matrix determinant is NaN");
997
998 GeometryTestUtils.assertThrows(() -> {
999 AffineTransformMatrix3D.of(
1000 Double.POSITIVE_INFINITY, 0, 0, 0,
1001 0, 1, 0, 0,
1002 0, 0, 1, 0).inverse();
1003 }, IllegalStateException.class, "Matrix is not invertible; matrix determinant is NaN");
1004
1005 GeometryTestUtils.assertThrows(() -> {
1006 AffineTransformMatrix3D.of(
1007 1, 0, 0, Double.NaN,
1008 0, 1, 0, 0,
1009 0, 0, 1, 0).inverse();
1010 }, IllegalStateException.class, "Matrix is not invertible; invalid matrix element: NaN");
1011
1012 GeometryTestUtils.assertThrows(() -> {
1013 AffineTransformMatrix3D.of(
1014 1, 0, 0, 0,
1015 0, 1, 0, Double.POSITIVE_INFINITY,
1016 0, 0, 1, 0).inverse();
1017 }, IllegalStateException.class, "Matrix is not invertible; invalid matrix element: Infinity");
1018
1019 GeometryTestUtils.assertThrows(() -> {
1020 AffineTransformMatrix3D.of(
1021 1, 0, 0, 0,
1022 0, 1, 0, 0,
1023 0, 0, 1, Double.NEGATIVE_INFINITY).inverse();
1024 }, IllegalStateException.class, "Matrix is not invertible; invalid matrix element: -Infinity");
1025 }
1026
1027 @Test
1028 public void testLinear() {
1029
1030 final AffineTransformMatrix3D mat = AffineTransformMatrix3D.of(
1031 2, 3, 4, 5,
1032 6, 7, 8, 9,
1033 10, 11, 12, 13);
1034
1035
1036 final AffineTransformMatrix3D result = mat.linear();
1037
1038
1039 final double[] expected = {
1040 2, 3, 4, 0,
1041 6, 7, 8, 0,
1042 10, 11, 12, 0
1043 };
1044 Assert.assertArrayEquals(expected, result.toArray(), 0.0);
1045 }
1046
1047 @Test
1048 public void testLinearTranspose() {
1049
1050 final AffineTransformMatrix3D mat = AffineTransformMatrix3D.of(
1051 2, 3, 4, 5,
1052 6, 7, 8, 9,
1053 10, 11, 12, 13);
1054
1055
1056 final AffineTransformMatrix3D result = mat.linearTranspose();
1057
1058
1059 final double[] expected = {
1060 2, 6, 10, 0,
1061 3, 7, 11, 0,
1062 4, 8, 12, 0
1063 };
1064 Assert.assertArrayEquals(expected, result.toArray(), 0.0);
1065 }
1066
1067 @Test
1068 public void testNormalTransform() {
1069
1070 checkNormalTransform(AffineTransformMatrix3D.identity());
1071
1072 checkNormalTransform(AffineTransformMatrix3D.createTranslation(2, 3, 4));
1073 checkNormalTransform(AffineTransformMatrix3D.createTranslation(-3, -4, -5));
1074
1075 checkNormalTransform(AffineTransformMatrix3D.createScale(2, 5, 0.5));
1076 checkNormalTransform(AffineTransformMatrix3D.createScale(-3, 4, 2));
1077 checkNormalTransform(AffineTransformMatrix3D.createScale(-0.1, -0.5, 0.8));
1078 checkNormalTransform(AffineTransformMatrix3D.createScale(-2, -5, -8));
1079
1080 final QuaternionRotation rotA = QuaternionRotation.fromAxisAngle(Vector3D.of(2, 3, 4), 0.75 * Math.PI);
1081 final QuaternionRotation rotB = QuaternionRotation.fromAxisAngle(Vector3D.of(-1, 1, -1), 1.75 * Math.PI);
1082
1083 checkNormalTransform(AffineTransformMatrix3D.createRotation(Vector3D.of(1, 1, 1), rotA));
1084 checkNormalTransform(AffineTransformMatrix3D.createRotation(Vector3D.of(-1, -1, -1), rotB));
1085
1086 checkNormalTransform(AffineTransformMatrix3D.createTranslation(2, 3, 4)
1087 .scale(7, 5, 4)
1088 .rotate(rotA));
1089 checkNormalTransform(AffineTransformMatrix3D.createRotation(Vector3D.ZERO, rotB)
1090 .translate(7, 5, 4)
1091 .rotate(rotA)
1092 .scale(2, 3, 0.5));
1093 }
1094
1095 private void checkNormalTransform(final AffineTransformMatrix3D transform) {
1096 final AffineTransformMatrix3D normalTransform = transform.normalTransform();
1097
1098 final Vector3D p1 = Vector3D.of(-0.25, 0.75, 0.5);
1099 final Vector3D p2 = Vector3D.of(0.5, -0.75, 0.25);
1100
1101 final Vector3D t1 = transform.apply(p1);
1102 final Vector3D t2 = transform.apply(p2);
1103
1104 EuclideanTestUtils.permute(-10, 10, 1, (x, y, z) -> {
1105 final Vector3D p3 = Vector3D.of(x, y, z);
1106 final Vector3D n = Planes.fromPoints(p1, p2, p3, TEST_PRECISION).getNormal();
1107
1108 final Vector3D t3 = transform.apply(p3);
1109
1110 final Plane tPlane = transform.preservesOrientation() ?
1111 Planes.fromPoints(t1, t2, t3, TEST_PRECISION) :
1112 Planes.fromPoints(t1, t3, t2, TEST_PRECISION);
1113 final Vector3D expected = tPlane.getNormal();
1114
1115 final Vector3D actual = normalTransform.apply(n).normalize();
1116
1117 EuclideanTestUtils.assertCoordinatesEqual(expected, actual, EPS);
1118 });
1119 }
1120
1121 @Test
1122 public void testNormalTransform_nonInvertible() {
1123
1124 GeometryTestUtils.assertThrows(() -> {
1125 AffineTransformMatrix3D.createScale(0).normalTransform();
1126 }, IllegalStateException.class);
1127 }
1128
1129 @Test
1130 public void testHashCode() {
1131
1132 final double[] values = {
1133 1, 2, 3, 4,
1134 5, 6, 7, 8,
1135 9, 10, 11, 12
1136 };
1137
1138
1139 final int orig = AffineTransformMatrix3D.of(values).hashCode();
1140 final int same = AffineTransformMatrix3D.of(values).hashCode();
1141
1142 Assert.assertEquals(orig, same);
1143
1144 double[] temp;
1145 for (int i = 0; i < values.length; ++i) {
1146 temp = values.clone();
1147 temp[i] = 0;
1148
1149 final int modified = AffineTransformMatrix3D.of(temp).hashCode();
1150
1151 Assert.assertNotEquals(orig, modified);
1152 }
1153 }
1154
1155 @Test
1156 public void testEquals() {
1157
1158 final double[] values = {
1159 1, 2, 3, 4,
1160 5, 6, 7, 8,
1161 9, 10, 11, 12
1162 };
1163
1164 final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(values);
1165
1166
1167 Assert.assertEquals(a, a);
1168
1169 Assert.assertFalse(a.equals(null));
1170 Assert.assertFalse(a.equals(new Object()));
1171
1172 double[] temp;
1173 for (int i = 0; i < values.length; ++i) {
1174 temp = values.clone();
1175 temp[i] = 0;
1176
1177 final AffineTransformMatrix3D modified = AffineTransformMatrix3D.of(temp);
1178
1179 Assert.assertNotEquals(a, modified);
1180 }
1181 }
1182
1183 @Test
1184 public void testEqualsAndHashCode_signedZeroConsistency() {
1185
1186 final double[] arrWithPosZero = {
1187 1.0, 0.0, 0.0, 0.0,
1188 0.0, 1.0, 0.0, 0.0,
1189 0.0, 0.0, 1.0, 0.0,
1190 };
1191 final double[] arrWithNegZero = {
1192 1.0, 0.0, 0.0, 0.0,
1193 0.0, 1.0, 0.0, 0.0,
1194 0.0, 0.0, 1.0, -0.0,
1195 };
1196 final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(arrWithPosZero);
1197 final AffineTransformMatrix3D b = AffineTransformMatrix3D.of(arrWithNegZero);
1198 final AffineTransformMatrix3D c = AffineTransformMatrix3D.of(arrWithPosZero);
1199 final AffineTransformMatrix3D d = AffineTransformMatrix3D.of(arrWithNegZero);
1200
1201
1202 Assert.assertFalse(a.equals(b));
1203 Assert.assertNotEquals(a.hashCode(), b.hashCode());
1204
1205 Assert.assertTrue(a.equals(c));
1206 Assert.assertEquals(a.hashCode(), c.hashCode());
1207
1208 Assert.assertTrue(b.equals(d));
1209 Assert.assertEquals(b.hashCode(), d.hashCode());
1210 }
1211
1212 @Test
1213 public void testToString() {
1214
1215 final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(
1216 1, 2, 3, 4,
1217 5, 6, 7, 8,
1218 9, 10, 11, 12
1219 );
1220
1221
1222 final String result = a.toString();
1223
1224
1225 Assert.assertEquals(
1226 "[ 1.0, 2.0, 3.0, 4.0; " +
1227 "5.0, 6.0, 7.0, 8.0; " +
1228 "9.0, 10.0, 11.0, 12.0 ]", result);
1229 }
1230
1231
1232
1233
1234
1235 private static void runWithCoordinates(final PermuteCallback3D test) {
1236 EuclideanTestUtils.permute(-1e-2, 1e-2, 5e-3, test);
1237 EuclideanTestUtils.permute(-1e2, 1e2, 5, test);
1238 }
1239 }