View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.geometry.euclidean.threed;
18  
19  import java.util.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          // arrange
42          final double[] arr = {
43              1, 2, 3, 4,
44              5, 6, 7, 8,
45              9, 10, 11, 12
46          };
47  
48          // act
49          final AffineTransformMatrix3D transform = AffineTransformMatrix3D.of(arr);
50  
51          // assert
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          // act/assert
60          GeometryTestUtils.assertThrows(() -> AffineTransformMatrix3D.of(1, 2),
61                  IllegalArgumentException.class, "Dimension mismatch: 2 != 12");
62      }
63  
64      @Test
65      public void testFromColumnVectors_threeVectors() {
66          // arrange
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          // act
72          final AffineTransformMatrix3D transform = AffineTransformMatrix3D.fromColumnVectors(u, v, w);
73  
74          // assert
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          // arrange
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          // act
91          final AffineTransformMatrix3D transform = AffineTransformMatrix3D.fromColumnVectors(u, v, w, t);
92  
93          // assert
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         // act/assert
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         // act/assert
129         GeometryTestUtils.assertThrows(() -> {
130             AffineTransformMatrix3D.from(v -> v.multiply(0));
131         }, IllegalArgumentException.class);
132     }
133 
134     @Test
135     public void testIdentity() {
136         // act
137         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity();
138 
139         // assert
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         // act
151         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createTranslation(2, 3, 4);
152 
153         // assert
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         // act
165         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createTranslation(Vector3D.of(5, 6, 7));
166 
167         // assert
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         // act
179         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(2, 3, 4);
180 
181         // assert
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         // arrange
193         final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(
194                     2, 0, 0, 10,
195                     0, 3, 0, 11,
196                     0, 0, 4, 12
197                 );
198 
199         // act
200         final AffineTransformMatrix3D result = a.translate(4, 5, 6);
201 
202         // assert
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         // arrange
214         final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(
215                     2, 0, 0, 10,
216                     0, 3, 0, 11,
217                     0, 0, 4, 12
218                 );
219 
220         // act
221         final AffineTransformMatrix3D result = a.translate(Vector3D.of(7, 8, 9));
222 
223         // assert
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         // act
235         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(Vector3D.of(4, 5, 6));
236 
237         // assert
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         // act
249         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(7);
250 
251         // assert
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         // arrange
263         final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(
264                     2, 0, 0, 10,
265                     0, 3, 0, 11,
266                     0, 0, 4, 12
267                 );
268 
269         // act
270         final AffineTransformMatrix3D result = a.scale(4, 5, 6);
271 
272         // assert
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         // arrange
284         final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(
285                     2, 0, 0, 10,
286                     0, 3, 0, 11,
287                     0, 0, 4, 12
288                 );
289 
290         // act
291         final AffineTransformMatrix3D result = a.scale(Vector3D.of(7, 8, 9));
292 
293         // assert
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         // arrange
305         final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(
306                     2, 0, 0, 10,
307                     0, 3, 0, 11,
308                     0, 0, 4, 12
309                 );
310 
311         // act
312         final AffineTransformMatrix3D result = a.scale(10);
313 
314         // assert
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         // arrange
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         // act
330         final AffineTransformMatrix3D result = AffineTransformMatrix3D.createRotation(center, rotation);
331 
332         // assert
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         // arrange
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         // act
353         final AffineTransformMatrix3D result = a.rotate(rotation);
354 
355         // assert
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         // arrange
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         // act
377         final AffineTransformMatrix3D result = a.rotate(center, rotation);
378 
379         // assert
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         // arrange
391         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity();
392 
393         // act/assert
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         // arrange
404         final Vector3D translation = Vector3D.of(1.1, -PlaneAngleRadians.PI, 5.5);
405 
406         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
407                 .translate(translation);
408 
409         // act/assert
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         // arrange
422         final Vector3D factors = Vector3D.of(2.0, -3.0, 4.0);
423 
424         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
425                 .scale(factors);
426 
427         // act/assert
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         // arrange
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         // act/assert
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         // arrange
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         // act/assert
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         // arrange
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         // act/assert
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         // arrange
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         // act/assert
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         // arrange
530         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity();
531 
532         // act/assert
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         // arrange
543         final Vector3D translation = Vector3D.of(1.1, -PlaneAngleRadians.PI, 5.5);
544 
545         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
546                 .translate(translation);
547 
548         // act/assert
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         // arrange
559         final Vector3D factors = Vector3D.of(2.0, -3.0, 4.0);
560 
561         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
562                 .scale(factors);
563 
564         // act/assert
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         // arrange
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         // act/assert
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         // arrange
598         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity();
599 
600         // act/assert
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         // arrange
611         final Vector3D translation = Vector3D.of(1.1, -PlaneAngleRadians.PI, 5.5);
612 
613         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
614                 .translate(translation);
615 
616         // act/assert
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         // arrange
627         final Vector3D factors = Vector3D.of(2.0, -3.0, 4.0);
628 
629         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
630                 .scale(factors);
631 
632         // act/assert
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         // arrange
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         // act/assert
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         // act/assert
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         // arrange
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         // act
687         final AffineTransformMatrix3D result = a.multiply(b);
688 
689         // assert
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         // act/assert
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         // act/assert
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         // arrange
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         // act
786         final AffineTransformMatrix3D transform = d.multiply(c).multiply(b).multiply(a);
787 
788         // assert
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         // arrange
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         // act
816         final AffineTransformMatrix3D result = b.premultiply(a);
817 
818         // assert
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         // arrange
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         // act
840         final AffineTransformMatrix3D transform = a.premultiply(b).premultiply(c).premultiply(d);
841 
842         // assert
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         // act
858         final AffineTransformMatrix3D inverse = AffineTransformMatrix3D.identity().inverse();
859 
860         // assert
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         // arrange
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         // act
881         final AffineTransformMatrix3D result = inv.multiply(a);
882 
883         // assert
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         // arrange
895         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createTranslation(1, -2, 4);
896 
897         // act
898         final AffineTransformMatrix3D inverse = transform.inverse();
899 
900         // assert
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         // arrange
912         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(10, -2, 4);
913 
914         // act
915         final AffineTransformMatrix3D inverse = transform.inverse();
916 
917         // assert
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         // arrange
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         // act
935         final AffineTransformMatrix3D inverse = transform.inverse();
936 
937         // assert
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         // arrange
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         // act/assert
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         // act/assert
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         // arrange
1030         final AffineTransformMatrix3D mat = AffineTransformMatrix3D.of(
1031                 2, 3, 4, 5,
1032                 6, 7, 8, 9,
1033                 10, 11, 12, 13);
1034 
1035         // act
1036         final AffineTransformMatrix3D result = mat.linear();
1037 
1038         // assert
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         // arrange
1050         final AffineTransformMatrix3D mat = AffineTransformMatrix3D.of(
1051                 2, 3, 4, 5,
1052                 6, 7, 8, 9,
1053                 10, 11, 12, 13);
1054 
1055         // act
1056         final AffineTransformMatrix3D result = mat.linearTranspose();
1057 
1058         // assert
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         // act/assert
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         // act/assert
1124         GeometryTestUtils.assertThrows(() -> {
1125             AffineTransformMatrix3D.createScale(0).normalTransform();
1126         }, IllegalStateException.class);
1127     }
1128 
1129     @Test
1130     public void testHashCode() {
1131         // arrange
1132         final double[] values = {
1133             1, 2, 3, 4,
1134             5, 6, 7, 8,
1135             9, 10, 11, 12
1136         };
1137 
1138         // act/assert
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         // arrange
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         // act/assert
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         // arrange
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         // act/assert
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         // arrange
1215         final AffineTransformMatrix3D a = AffineTransformMatrix3D.of(
1216                     1, 2, 3, 4,
1217                     5, 6, 7, 8,
1218                     9, 10, 11, 12
1219                 );
1220 
1221         // act
1222         final String result = a.toString();
1223 
1224         // assert
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      * Run the given test callback with a wide range of (x, y, z) inputs.
1233      * @param test
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 }