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