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
    18  package org.apache.commons.geometry.euclidean.threed;
    19
    20  import java.util.ArrayList;
    21  import java.util.Arrays;
    22  import java.util.Collections;
    23  import java.util.Comparator;
    24  import java.util.List;
    25  import java.util.regex.Pattern;
    26
    27  import org.apache.commons.geometry.core.GeometryTestUtils;
    28  import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
    29  import org.apache.commons.numbers.angle.Angle;
    30  import org.apache.commons.numbers.core.Precision;
    31  import org.apache.commons.rng.UniformRandomProvider;
    32  import org.apache.commons.rng.simple.RandomSource;
    33  import org.junit.jupiter.api.Assertions;
    34  import org.junit.jupiter.api.Test;
    35
    36  class Vector3DTest {
    37
    38      private static final double EPS = 1e-15;
    39
    40      @Test
    41      void testConstants() {
    42          // act/assert
    43          checkVector(Vector3D.ZERO, 0, 0, 0);
    44
    45          checkVector(Vector3D.Unit.PLUS_X, 1, 0, 0);
    46          checkVector(Vector3D.Unit.MINUS_X, -1, 0, 0);
    47
    48          checkVector(Vector3D.Unit.PLUS_Y, 0, 1, 0);
    49          checkVector(Vector3D.Unit.MINUS_Y, 0, -1, 0);
    50
    51          checkVector(Vector3D.Unit.PLUS_Z, 0, 0, 1);
    52          checkVector(Vector3D.Unit.MINUS_Z, 0, 0, -1);
    53
    54          checkVector(Vector3D.NaN, Double.NaN, Double.NaN, Double.NaN);
    55          checkVector(Vector3D.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
    56          checkVector(Vector3D.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY);
    57      }
    58
    59      @Test
    60      void testConstants_normalize() {
    61          // act/assert
    62          Assertions.assertThrows(IllegalArgumentException.class, Vector3D.ZERO::normalize);
    63          Assertions.assertThrows(IllegalArgumentException.class, Vector3D.NaN::normalize);
    64          Assertions.assertThrows(IllegalArgumentException.class, Vector3D.POSITIVE_INFINITY::normalize);
    65          Assertions.assertThrows(IllegalArgumentException.class, Vector3D.NEGATIVE_INFINITY::normalize);
    66
    67          Assertions.assertSame(Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_X.normalize());
    68          Assertions.assertSame(Vector3D.Unit.MINUS_X, Vector3D.Unit.MINUS_X.normalize());
    69
    70          Assertions.assertSame(Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_Y.normalize());
    71          Assertions.assertSame(Vector3D.Unit.MINUS_Y, Vector3D.Unit.MINUS_Y.normalize());
    72
    73          Assertions.assertSame(Vector3D.Unit.PLUS_Z, Vector3D.Unit.PLUS_Z.normalize());
    74          Assertions.assertSame(Vector3D.Unit.MINUS_Z, Vector3D.Unit.MINUS_Z.normalize());
    75      }
    76
    77      @Test
    78      void testCoordinateAscendingOrder() {
    79          // arrange
    80          final Comparator<Vector3D> cmp = Vector3D.COORDINATE_ASCENDING_ORDER;
    81
    82          // act/assert
    83          Assertions.assertEquals(0, cmp.compare(Vector3D.of(1, 2, 3), Vector3D.of(1, 2, 3)));
    84
    85          Assertions.assertEquals(-1, cmp.compare(Vector3D.of(0, 2, 3), Vector3D.of(1, 2, 3)));
    86          Assertions.assertEquals(-1, cmp.compare(Vector3D.of(1, 1, 3), Vector3D.of(1, 2, 3)));
    87          Assertions.assertEquals(-1, cmp.compare(Vector3D.of(1, 2, 2), Vector3D.of(1, 2, 3)));
    88
    89          Assertions.assertEquals(1, cmp.compare(Vector3D.of(2, 2, 3), Vector3D.of(1, 2, 3)));
    90          Assertions.assertEquals(1, cmp.compare(Vector3D.of(1, 3, 3), Vector3D.of(1, 2, 3)));
    91          Assertions.assertEquals(1, cmp.compare(Vector3D.of(1, 2, 4), Vector3D.of(1, 2, 3)));
    92
    93          Assertions.assertEquals(-1, cmp.compare(Vector3D.of(1, 2, 3), null));
    94          Assertions.assertEquals(1, cmp.compare(null, Vector3D.of(1, 2, 3)));
    95          Assertions.assertEquals(0, cmp.compare(null, null));
    96      }
    97
    98      @Test
    99      void testCoordinates() {
    100         // arrange
    101         final Vector3D c = Vector3D.of(1, 2, 3);
    102
    103         // act/assert
    104         Assertions.assertEquals(1.0, c.getX(), EPS);
    105         Assertions.assertEquals(2.0, c.getY(), EPS);
    106         Assertions.assertEquals(3.0, c.getZ(), EPS);
    107     }
    108
    109     @Test
    110     void testToArray() {
    111         // arrange
    112         final Vector3D c = Vector3D.of(1, 2, 3);
    113
    114         // act
    115         final double[] arr = c.toArray();
    116
    117         // assert
    118         Assertions.assertEquals(3, arr.length);
    119         Assertions.assertEquals(1.0, arr[0], EPS);
    120         Assertions.assertEquals(2.0, arr[1], EPS);
    121         Assertions.assertEquals(3.0, arr[2], EPS);
    122     }
    123
    124     @Test
    125     void testDimension() {
    126         // arrange
    127         final Vector3D c = Vector3D.of(1, 2, 3);
    128
    129         // act/assert
    130         Assertions.assertEquals(3, c.getDimension());
    131     }
    132
    133     @Test
    134     void testNaN() {
    135         // act/assert
    136         Assertions.assertTrue(Vector3D.of(0, 0, Double.NaN).isNaN());
    137         Assertions.assertTrue(Vector3D.of(0, Double.NaN, 0).isNaN());
    138         Assertions.assertTrue(Vector3D.of(Double.NaN, 0, 0).isNaN());
    139
    140         Assertions.assertFalse(Vector3D.of(1, 1, 1).isNaN());
    141         Assertions.assertFalse(Vector3D.of(1, 1, Double.NEGATIVE_INFINITY).isNaN());
    142         Assertions.assertFalse(Vector3D.of(1, Double.POSITIVE_INFINITY, 1).isNaN());
    143         Assertions.assertFalse(Vector3D.of(Double.NEGATIVE_INFINITY, 1, 1).isNaN());
    144     }
    145
    146     @Test
    147     void testInfinite() {
    148         // act/assert
    149         Assertions.assertTrue(Vector3D.of(0, 0, Double.NEGATIVE_INFINITY).isInfinite());
    150         Assertions.assertTrue(Vector3D.of(0, Double.NEGATIVE_INFINITY, 0).isInfinite());
    151         Assertions.assertTrue(Vector3D.of(Double.NEGATIVE_INFINITY, 0, 0).isInfinite());
    152         Assertions.assertTrue(Vector3D.of(0, 0, Double.POSITIVE_INFINITY).isInfinite());
    153         Assertions.assertTrue(Vector3D.of(0, Double.POSITIVE_INFINITY, 0).isInfinite());
    154         Assertions.assertTrue(Vector3D.of(Double.POSITIVE_INFINITY, 0, 0).isInfinite());
    155
    156         Assertions.assertFalse(Vector3D.of(1, 1, 1).isInfinite());
    157         Assertions.assertFalse(Vector3D.of(0, 0, Double.NaN).isInfinite());
    158         Assertions.assertFalse(Vector3D.of(0, Double.NEGATIVE_INFINITY, Double.NaN).isInfinite());
    159         Assertions.assertFalse(Vector3D.of(Double.NaN, 0, Double.NEGATIVE_INFINITY).isInfinite());
    160         Assertions.assertFalse(Vector3D.of(Double.POSITIVE_INFINITY, Double.NaN, 0).isInfinite());
    161         Assertions.assertFalse(Vector3D.of(0, Double.NaN, Double.POSITIVE_INFINITY).isInfinite());
    162     }
    163
    164     @Test
    165     void testFinite() {
    166         // act/assert
    167         Assertions.assertTrue(Vector3D.ZERO.isFinite());
    168         Assertions.assertTrue(Vector3D.of(1, 1, 1).isFinite());
    169
    170         Assertions.assertFalse(Vector3D.of(0, 0, Double.NEGATIVE_INFINITY).isFinite());
    171         Assertions.assertFalse(Vector3D.of(0, Double.NEGATIVE_INFINITY, 0).isFinite());
    172         Assertions.assertFalse(Vector3D.of(Double.NEGATIVE_INFINITY, 0, 0).isFinite());
    173         Assertions.assertFalse(Vector3D.of(0, 0, Double.POSITIVE_INFINITY).isFinite());
    174         Assertions.assertFalse(Vector3D.of(0, Double.POSITIVE_INFINITY, 0).isFinite());
    175         Assertions.assertFalse(Vector3D.of(Double.POSITIVE_INFINITY, 0, 0).isFinite());
    176
    177         Assertions.assertFalse(Vector3D.of(0, 0, Double.NaN).isFinite());
    178         Assertions.assertFalse(Vector3D.of(0, Double.NEGATIVE_INFINITY, Double.NaN).isFinite());
    179         Assertions.assertFalse(Vector3D.of(Double.NaN, 0, Double.NEGATIVE_INFINITY).isFinite());
    180         Assertions.assertFalse(Vector3D.of(Double.POSITIVE_INFINITY, Double.NaN, 0).isFinite());
    181         Assertions.assertFalse(Vector3D.of(0, Double.NaN, Double.POSITIVE_INFINITY).isFinite());
    182     }
    183
    184     @Test
    185     void testZero() {
    186         // act
    187         final Vector3D zero = Vector3D.of(1, 2, 3).getZero();
    188
    189         // assert
    190         checkVector(zero, 0, 0, 0);
    191         Assertions.assertEquals(0, zero.norm(), EPS);
    192     }
    193
    194     @Test
    195     void testNorm() {
    196         // act/assert
    197         Assertions.assertEquals(0.0, Vector3D.ZERO.norm(), 0);
    198         Assertions.assertEquals(Math.sqrt(29), Vector3D.of(2, 3, 4).norm(), EPS);
    199         Assertions.assertEquals(Math.sqrt(29), Vector3D.of(-2, -3, -4).norm(), EPS);
    200     }
    201
    202     @Test
    203     void testNorm_unitVectors() {
    204         // arrange
    205         final Vector3D v = Vector3D.of(1.0, 2.0, 3.0).normalize();
    206
    207         // act/assert
    208         Assertions.assertEquals(1.0, v.norm(), 0.0);
    209     }
    210
    211     @Test
    212     void testNormSq() {
    213         // act/assert
    214         Assertions.assertEquals(0.0, Vector3D.ZERO.normSq(), 0);
    215         Assertions.assertEquals(29, Vector3D.of(2, 3, 4).normSq(), EPS);
    216         Assertions.assertEquals(29, Vector3D.of(-2, -3, -4).normSq(), EPS);
    217     }
    218
    219     @Test
    220     void testNormSq_unitVectors() {
    221         // arrange
    222         final Vector3D v = Vector3D.of(1.0, 2.0, 3.0).normalize();
    223
    224         // act/assert
    225         Assertions.assertEquals(1.0, v.normSq(), 0.0);
    226     }
    227
    228     @Test
    229     void testWithNorm() {
    230         // arrange
    231         final double x = 2;
    232         final double y = 3;
    233         final double z = 4;
    234
    235         final double len = Math.sqrt((x * x) + (y * y) + (z * z));
    236
    237         final double normX = x / len;
    238         final double normY = y / len;
    239         final double normZ = z / len;
    240
    241         // act/assert
    242         checkVector(Vector3D.of(x, y, z).withNorm(0.0), 0.0, 0.0, 0.0);
    243
    244         checkVector(Vector3D.of(x, y, z).withNorm(1.0), normX, normY, normZ);
    245         checkVector(Vector3D.of(x, y, -z).withNorm(1.0), normX, normY, -normZ);
    246         checkVector(Vector3D.of(x, -y, z).withNorm(1.0), normX, -normY, normZ);
    247         checkVector(Vector3D.of(x, -y, -z).withNorm(1.0), normX, -normY, -normZ);
    248         checkVector(Vector3D.of(-x, y, z).withNorm(1.0), -normX, normY, normZ);
    249         checkVector(Vector3D.of(-x, y, -z).withNorm(1.0), -normX, normY, -normZ);
    250         checkVector(Vector3D.of(-x, -y, z).withNorm(1.0), -normX, -normY, normZ);
    251         checkVector(Vector3D.of(-x, -y, -z).withNorm(1.0), -normX, -normY, -normZ);
    252
    253         checkVector(Vector3D.of(x, y, z).withNorm(0.5), 0.5 * normX, 0.5 * normY, 0.5 * normZ);
    254         checkVector(Vector3D.of(x, y, z).withNorm(3), 3 * normX, 3 * normY, 3 * normZ);
    255
    256         checkVector(Vector3D.of(x, y, z).withNorm(-0.5), -0.5 * normX, -0.5 * normY, -0.5 * normZ);
    257         checkVector(Vector3D.of(x, y, z).withNorm(-3), -3 * normX, -3 * normY, -3 * normZ);
    258
    259         for (int i = 0; i <= 10; i++) {
    260             final double mag = i * 0.12345 - 5;
    261             Assertions.assertEquals(Math.abs(mag), Vector3D.of(x, y, z).withNorm(mag).norm(), EPS);
    262         }
    263     }
    264
    265     @Test
    266     void testWithNorm_illegalNorm() {
    267         // act/assert
    268         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector3D.ZERO.withNorm(2.0));
    269         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector3D.NaN.withNorm(2.0));
    270         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector3D.POSITIVE_INFINITY.withNorm(2.0));
    271         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector3D.NEGATIVE_INFINITY.withNorm(2.0));
    272     }
    273
    274     @Test
    275     void testWithNorm_unitVectors() {
    276         // arrange
    277         final Vector3D v = Vector3D.of(2.0, -3.0, 4.0).normalize();
    278
    279         // act/assert
    280         checkVector(Vector3D.Unit.PLUS_X.withNorm(2.5), 2.5, 0.0, 0.0);
    281         checkVector(Vector3D.Unit.MINUS_Y.withNorm(3.14), 0.0, -3.14, 0.0);
    282         checkVector(Vector3D.Unit.PLUS_Z.withNorm(-1.1), 0.0, 0.0, -1.1);
    283
    284         for (double mag = -10.0; mag <= 10.0; ++mag) {
    285             Assertions.assertEquals(Math.abs(mag), v.withNorm(mag).norm(), EPS);
    286         }
    287     }
    288
    289     @Test
    290     void testAdd() {
    291         // arrange
    292         final Vector3D v1 = Vector3D.of(1, 2, 3);
    293         final Vector3D v2 = Vector3D.of(-4, -5, -6);
    294         final Vector3D v3 = Vector3D.of(7, 8, 9);
    295
    296         // act/assert
    297         checkVector(v1.add(v1), 2, 4, 6);
    298
    299         checkVector(v1.add(v2), -3, -3, -3);
    300         checkVector(v2.add(v1), -3, -3, -3);
    301
    302         checkVector(v1.add(v3), 8, 10, 12);
    303         checkVector(v3.add(v1), 8, 10, 12);
    304     }
    305
    306     @Test
    307     void testAdd_scaled() {
    308         // arrange
    309         final Vector3D v1 = Vector3D.of(1, 2, 3);
    310         final Vector3D v2 = Vector3D.of(-4, -5, -6);
    311         final Vector3D v3 = Vector3D.of(7, 8, 9);
    312
    313         // act/assert
    314         checkVector(v1.add(0, v1), 1, 2, 3);
    315         checkVector(v1.add(0.5, v1), 1.5, 3, 4.5);
    316         checkVector(v1.add(1, v1), 2, 4, 6);
    317
    318         checkVector(v1.add(2, v2), -7, -8, -9);
    319         checkVector(v2.add(2, v1), -2, -1, -0);
    320
    321         checkVector(v1.add(-2, v3), -13, -14, -15);
    322         checkVector(v3.add(-2, v1), 5, 4, 3);
    323     }
    324
    325     @Test
    326     void testSubtract() {
    327         // arrange
    328         final Vector3D v1 = Vector3D.of(1, 2, 3);
    329         final Vector3D v2 = Vector3D.of(-4, -5, -6);
    330         final Vector3D v3 = Vector3D.of(7, 8, 9);
    331
    332         // act/assert
    333         checkVector(v1.subtract(v1), 0, 0, 0);
    334
    335         checkVector(v1.subtract(v2), 5, 7, 9);
    336         checkVector(v2.subtract(v1), -5, -7, -9);
    337
    338         checkVector(v1.subtract(v3), -6, -6, -6);
    339         checkVector(v3.subtract(v1), 6, 6, 6);
    340     }
    341
    342     @Test
    343     void testSubtract_scaled() {
    344         // arrange
    345         final Vector3D v1 = Vector3D.of(1, 2, 3);
    346         final Vector3D v2 = Vector3D.of(-4, -5, -6);
    347         final Vector3D v3 = Vector3D.of(7, 8, 9);
    348
    349         // act/assert
    350         checkVector(v1.subtract(0, v1), 1, 2, 3);
    351         checkVector(v1.subtract(0.5, v1), 0.5, 1, 1.5);
    352         checkVector(v1.subtract(1, v1), 0, 0, 0);
    353
    354         checkVector(v1.subtract(2, v2), 9, 12, 15);
    355         checkVector(v2.subtract(2, v1), -6, -9, -12);
    356
    357         checkVector(v1.subtract(-2, v3), 15, 18, 21);
    358         checkVector(v3.subtract(-2, v1), 9, 12, 15);
    359     }
    360
    361     @Test
    362     void testNegate() {
    363         // act/assert
    364         checkVector(Vector3D.of(0.1, 2.5, 1.3).negate(), -0.1, -2.5, -1.3);
    365         checkVector(Vector3D.of(-0.1, -2.5, -1.3).negate(), 0.1, 2.5, 1.3);
    366     }
    367
    368     @Test
    369     void testNegate_unitVectors() {
    370         // arrange
    371         final Vector3D v1 = Vector3D.of(1.0, 2.0, 3.0).normalize();
    372         final Vector3D v2 = Vector3D.of(-2.0, -4.0, -3.0).normalize();
    373
    374         // act/assert
    375         checkVector(v1.negate(), -1.0 / Math.sqrt(14.0), -Math.sqrt(2.0 / 7.0), -3.0 / Math.sqrt(14.0));
    376         checkVector(v2.negate(), 2.0 / Math.sqrt(29.0), 4.0 / Math.sqrt(29.0), 3.0 / Math.sqrt(29.0));
    377     }
    378
    379     @Test
    380     void testNormalize() {
    381         // arrange
    382         final double invSqrt3 = 1 / Math.sqrt(3);
    383
    384         // act/assert
    385         checkVector(Vector3D.of(100, 0, 0).normalize(), 1, 0, 0);
    386         checkVector(Vector3D.of(-100, 0, 0).normalize(), -1, 0, 0);
    387
    388         checkVector(Vector3D.of(0, 100, 0).normalize(), 0, 1, 0);
    389         checkVector(Vector3D.of(0, -100, 0).normalize(), 0, -1, 0);
    390
    391         checkVector(Vector3D.of(0, 0, 100).normalize(), 0, 0, 1);
    392         checkVector(Vector3D.of(0, 0, -100).normalize(), 0, 0, -1);
    393
    394         checkVector(Vector3D.of(2, 2, 2).normalize(), invSqrt3, invSqrt3, invSqrt3);
    395         checkVector(Vector3D.of(-2, -2, -2).normalize(), -invSqrt3, -invSqrt3, -invSqrt3);
    396
    397         checkVector(Vector3D.of(Double.MIN_VALUE, 0, 0).normalize(), 1, 0, 0);
    398         checkVector(Vector3D.of(0, Double.MIN_VALUE, 0).normalize(), 0, 1, 0);
    399         checkVector(Vector3D.of(0, 0, Double.MIN_VALUE).normalize(), 0, 0, 1);
    400
    401         checkVector(Vector3D.of(-Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE).normalize(),
    402                 -invSqrt3, invSqrt3, invSqrt3);
    403
    404         checkVector(Vector3D.of(Double.MIN_NORMAL, 0, 0).normalize(), 1, 0, 0);
    405         checkVector(Vector3D.of(0, Double.MIN_NORMAL, 0).normalize(), 0, 1, 0);
    406         checkVector(Vector3D.of(0, 0, Double.MIN_NORMAL).normalize(), 0, 0, 1);
    407
    408         checkVector(Vector3D.of(Double.MIN_NORMAL, Double.MIN_NORMAL, -Double.MIN_NORMAL).normalize(),
    409                 invSqrt3, invSqrt3, -invSqrt3);
    410
    411         checkVector(Vector3D.of(Double.MAX_VALUE, -Double.MAX_VALUE, Double.MAX_VALUE).normalize(),
    412                 invSqrt3, -invSqrt3, invSqrt3);
    413
    414         Assertions.assertEquals(1.0, Vector3D.of(5, -4, 2).normalize().norm(), EPS);
    415     }
    416
    417     @Test
    418     void testNormalize_illegalNorm() {
    419         // arrange
    420         final Pattern illegalNorm = Pattern.compile("^Illegal norm: (0\\.0|-?Infinity|NaN)");
    421
    422         // act/assert
    423         GeometryTestUtils.assertThrowsWithMessage(Vector3D.ZERO::normalize,
    424                 IllegalArgumentException.class, illegalNorm);
    425         GeometryTestUtils.assertThrowsWithMessage(Vector3D.NaN::normalize,
    426                 IllegalArgumentException.class, illegalNorm);
    427         GeometryTestUtils.assertThrowsWithMessage(Vector3D.POSITIVE_INFINITY::normalize,
    428                 IllegalArgumentException.class, illegalNorm);
    429         GeometryTestUtils.assertThrowsWithMessage(Vector3D.NEGATIVE_INFINITY::normalize,
    430                 IllegalArgumentException.class, illegalNorm);
    431     }
    432
    433     @Test
    434     void testNormalize_isIdempotent() {
    435         // arrange
    436         final double invSqrt3 = 1 / Math.sqrt(3);
    437         final Vector3D v = Vector3D.of(2, 2, 2).normalize();
    438
    439         // act/assert
    440         Assertions.assertSame(v, v.normalize());
    441         checkVector(v.normalize(), invSqrt3, invSqrt3, invSqrt3);
    442     }
    443
    444     @Test
    445     void testNormalizeOrNull() {
    446         // arrange
    447         final double invSqrt3 = 1 / Math.sqrt(3);
    448
    449         // act/assert
    450         checkVector(Vector3D.of(100, 0, 0).normalizeOrNull(), 1, 0, 0);
    451         checkVector(Vector3D.of(-100, 0, 0).normalizeOrNull(), -1, 0, 0);
    452
    453         checkVector(Vector3D.of(2, 2, 2).normalizeOrNull(), invSqrt3, invSqrt3, invSqrt3);
    454         checkVector(Vector3D.of(-2, -2, -2).normalizeOrNull(), -invSqrt3, -invSqrt3, -invSqrt3);
    455
    456         checkVector(Vector3D.of(Double.MIN_VALUE, 0, 0).normalizeOrNull(), 1, 0, 0);
    457         checkVector(Vector3D.of(0, Double.MIN_VALUE, 0).normalizeOrNull(), 0, 1, 0);
    458         checkVector(Vector3D.of(0, 0, Double.MIN_VALUE).normalizeOrNull(), 0, 0, 1);
    459
    460         checkVector(Vector3D.of(-Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE).normalizeOrNull(),
    461                 -invSqrt3, invSqrt3, invSqrt3);
    462
    463         checkVector(Vector3D.of(Double.MIN_NORMAL, Double.MIN_NORMAL, -Double.MIN_NORMAL).normalizeOrNull(),
    464                 invSqrt3, invSqrt3, -invSqrt3);
    465
    466         checkVector(Vector3D.of(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE).normalizeOrNull(),
    467                 -invSqrt3, -invSqrt3, -invSqrt3);
    468
    469         Assertions.assertNull(Vector3D.ZERO.normalizeOrNull());
    470         Assertions.assertNull(Vector3D.NaN.normalizeOrNull());
    471         Assertions.assertNull(Vector3D.POSITIVE_INFINITY.normalizeOrNull());
    472         Assertions.assertNull(Vector3D.NEGATIVE_INFINITY.normalizeOrNull());
    473     }
    474
    475     @Test
    476     void testNormalizeOrNull_isIdempotent() {
    477         // arrange
    478         final double invSqrt3 = 1 / Math.sqrt(3);
    479         final Vector3D v = Vector3D.of(2, 2, 2).normalizeOrNull();
    480
    481         // act/assert
    482         Assertions.assertSame(v, v.normalizeOrNull());
    483         checkVector(v.normalizeOrNull(), invSqrt3, invSqrt3, invSqrt3);
    484     }
    485
    486     @Test
    487     void testOrthogonal() {
    488         // arrange
    489         final Vector3D v1 = Vector3D.of(0.1, 2.5, 1.3);
    490         final Vector3D v2 = Vector3D.of(2.3, -0.003, 7.6);
    491         final Vector3D v3 = Vector3D.of(-1.7, 1.4, 0.2);
    492         final Vector3D v4 = Vector3D.of(4.2, 0.1, -1.8);
    493
    494         // act/assert
    495         Assertions.assertEquals(0.0, v1.dot(v1.orthogonal()), EPS);
    496         Assertions.assertEquals(0.0, v2.dot(v2.orthogonal()), EPS);
    497         Assertions.assertEquals(0.0, v3.dot(v3.orthogonal()), EPS);
    498         Assertions.assertEquals(0.0, v4.dot(v4.orthogonal()), EPS);
    499     }
    500
    501     @Test
    502     void testOrthogonal_illegalNorm() {
    503         // act/assert
    504         Assertions.assertThrows(IllegalArgumentException.class, Vector3D.ZERO::orthogonal);
    505         Assertions.assertThrows(IllegalArgumentException.class, Vector3D.NaN::orthogonal);
    506         Assertions.assertThrows(IllegalArgumentException.class, Vector3D.POSITIVE_INFINITY::orthogonal);
    507         Assertions.assertThrows(IllegalArgumentException.class, Vector3D.NEGATIVE_INFINITY::orthogonal);
    508     }
    509
    510     @Test
    511     void testOrthogonal_givenDirection() {
    512         // arrange
    513         final double invSqrt2 = 1.0 / Math.sqrt(2.0);
    514
    515         // act/assert
    516         checkVector(Vector3D.Unit.PLUS_X.orthogonal(Vector3D.of(-1.0, 0.1, 0.0)), 0.0, 1.0, 0.0);
    517         checkVector(Vector3D.Unit.PLUS_Y.orthogonal(Vector3D.of(2.0, 2.0, 2.0)), invSqrt2, 0.0, invSqrt2);
    518         checkVector(Vector3D.Unit.PLUS_Z.orthogonal(Vector3D.of(3.0, 3.0, -3.0)), invSqrt2, invSqrt2, 0.0);
    519
    520         checkVector(Vector3D.of(invSqrt2, invSqrt2, 0.0).orthogonal(Vector3D.of(1.0, 1.0, 0.2)), 0.0, 0.0, 1.0);
    521     }
    522
    523     @Test
    524     void testOrthogonal_givenDirection_illegalNorm() {
    525         // act/assert
    526
    527         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.ZERO.orthogonal(Vector3D.Unit.PLUS_X));
    528         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.NaN.orthogonal(Vector3D.Unit.PLUS_X));
    529         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.POSITIVE_INFINITY.orthogonal(Vector3D.Unit.PLUS_X));
    530         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.NEGATIVE_INFINITY.orthogonal(Vector3D.Unit.PLUS_X));
    531         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.Unit.PLUS_X.orthogonal(Vector3D.ZERO));
    532         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.Unit.PLUS_X.orthogonal(Vector3D.NaN));
    533         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.Unit.PLUS_X.orthogonal(Vector3D.POSITIVE_INFINITY));
    534         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.Unit.PLUS_X.orthogonal(Vector3D.NEGATIVE_INFINITY));
    535     }
    536
    537     @Test
    538     void testOrthogonal_givenDirection_directionIsCollinear() {
    539         // act/assert
    540         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.Unit.PLUS_X.orthogonal(Vector3D.Unit.PLUS_X));
    541         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.Unit.PLUS_X.orthogonal(Vector3D.Unit.MINUS_X));
    542         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.of(1.0, 1.0, 1.0).orthogonal(Vector3D.of(2.0, 2.0, 2.0)));
    543         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.of(-1.01, -1.01, -1.01).orthogonal(Vector3D.of(20.1, 20.1, 20.1)));
    544     }
    545
    546     @Test
    547     void testAngle() {
    548         // arrange
    549         final double tolerance = 1e-10;
    550
    551         final Vector3D v1 = Vector3D.of(1, 2, 3);
    552         final Vector3D v2 = Vector3D.of(4, 5, 6);
    553
    554         // act/assert
    555         Assertions.assertEquals(0.22572612855273393616, v1.angle(v2), tolerance);
    556         Assertions.assertEquals(7.98595620686106654517199e-8, v1.angle(Vector3D.of(2, 4, 6.000001)), tolerance);
    557         Assertions.assertEquals(3.14159257373023116985197793156, v1.angle(Vector3D.of(-2, -4, -6.000001)), tolerance);
    558
    559         Assertions.assertEquals(0.0, Vector3D.Unit.PLUS_X.angle(Vector3D.Unit.PLUS_X), tolerance);
    560         Assertions.assertEquals(Math.PI, Vector3D.Unit.PLUS_X.angle(Vector3D.Unit.MINUS_X), tolerance);
    561
    562         Assertions.assertEquals(Angle.PI_OVER_TWO, Vector3D.Unit.PLUS_X.angle(Vector3D.Unit.PLUS_Y), tolerance);
    563         Assertions.assertEquals(Angle.PI_OVER_TWO, Vector3D.Unit.PLUS_X.angle(Vector3D.Unit.MINUS_Y), tolerance);
    564         Assertions.assertEquals(Angle.PI_OVER_TWO, Vector3D.Unit.PLUS_X.angle(Vector3D.Unit.PLUS_Z), tolerance);
    565         Assertions.assertEquals(Angle.PI_OVER_TWO, Vector3D.Unit.PLUS_X.angle(Vector3D.Unit.MINUS_Z), tolerance);
    566     }
    567
    568     @Test
    569     void testAngle_illegalNorm() {
    570         // arrange
    571         final Vector3D v = Vector3D.of(1.0, 1.0, 1.0);
    572
    573         // act/assert
    574
    575         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.ZERO.angle(v));
    576         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.NaN.angle(v));
    577         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.POSITIVE_INFINITY.angle(v));
    578         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.NEGATIVE_INFINITY.angle(v));
    579         Assertions.assertThrows(IllegalArgumentException.class, () ->  v.angle(Vector3D.ZERO));
    580         Assertions.assertThrows(IllegalArgumentException.class, () ->  v.angle(Vector3D.NaN));
    581         Assertions.assertThrows(IllegalArgumentException.class, () ->  v.angle(Vector3D.POSITIVE_INFINITY));
    582         Assertions.assertThrows(IllegalArgumentException.class, () ->  v.angle(Vector3D.NEGATIVE_INFINITY));
    583     }
    584
    585     @Test
    586     void testAngle_angularSeparation() {
    587         // arrange
    588         final Vector3D v1 = Vector3D.of(2, -1, 4);
    589
    590         final Vector3D  k = v1.normalize();
    591         final Vector3D  i = k.orthogonal();
    592         final Vector3D v2 = k.multiply(Math.cos(1.2)).add(i.multiply(Math.sin(1.2)));
    593
    594         // act/assert
    595         Assertions.assertTrue(Math.abs(v1.angle(v2) - 1.2) < 1.0e-12);
    596     }
    597
    598     @Test
    599     void testCrossProduct() {
    600         // act/assert
    601         checkVector(Vector3D.Unit.PLUS_X.cross(Vector3D.Unit.PLUS_Y), 0, 0, 1);
    602         checkVector(Vector3D.Unit.PLUS_X.cross(Vector3D.Unit.MINUS_Y), 0, 0, -1);
    603
    604         checkVector(Vector3D.Unit.MINUS_X.cross(Vector3D.Unit.MINUS_Y), 0, 0, 1);
    605         checkVector(Vector3D.Unit.MINUS_X.cross(Vector3D.Unit.PLUS_Y), 0, 0, -1);
    606
    607         checkVector(Vector3D.of(2, 1, -4).cross(Vector3D.of(3, 1, -1)), 3, -10, -1);
    608
    609         final double invSqrt6 = 1 / Math.sqrt(6);
    610         checkVector(Vector3D.of(1, 1, 1).cross(Vector3D.of(-1, 0, 1)).normalize(), invSqrt6, -2 * invSqrt6, invSqrt6);
    611     }
    612
    613     @Test
    614     void testCrossProduct_nearlyAntiParallel() {
    615         // the vectors u1 and u2 are nearly but not exactly anti-parallel
    616         // (7.31e-16 degrees from 180 degrees) naive cross product (i.e.
    617         // computing u1.x * u2.x + u1.y * u2.y + u1.z * u2.z
    618         // leads to a result of   [0.0009765, -0.0001220, -0.0039062],
    619         // instead of the correct [0.0006913, -0.0001254, -0.0007909]
    620
    621         // arrange
    622         final Vector3D u1 = Vector3D.of(-1321008684645961.0 / 268435456.0,
    623                                          -5774608829631843.0 / 268435456.0,
    624                                          -7645843051051357.0 / 8589934592.0);
    625         final Vector3D u2 = Vector3D.of(1796571811118507.0 / 2147483648.0,
    626                                           7853468008299307.0 / 2147483648.0,
    627                                           2599586637357461.0 / 17179869184.0);
    628         final Vector3D u3 = Vector3D.of(12753243807587107.0 / 18446744073709551616.0,
    629                                          -2313766922703915.0 / 18446744073709551616.0,
    630                                           -227970081415313.0 / 288230376151711744.0);
    631
    632         // act
    633         final Vector3D cNaive = Vector3D.of(u1.getY() * u2.getZ() - u1.getZ() * u2.getY(),
    634                                        u1.getZ() * u2.getX() - u1.getX() * u2.getZ(),
    635                                        u1.getX() * u2.getY() - u1.getY() * u2.getX());
    636         final Vector3D cAccurate = u1.cross(u2);
    637
    638         // assert
    639         Assertions.assertTrue(u3.distance(cNaive) > 2.9 * u3.norm());
    640         Assertions.assertEquals(0.0, u3.distance(cAccurate), 1.0e-30 * cAccurate.norm());
    641     }
    642
    643     @Test
    644     void testCrossProduct_accuracy() {
    645         // we compare accurate versus naive cross product implementations
    646         // on regular vectors (i.e. not extreme cases like in the previous test)
    647         final UniformRandomProvider random = RandomSource.create(RandomSource.WELL_1024_A, 885362227452043215L);
    648         for (int i = 0; i < 10000; ++i) {
    649             // arrange
    650             final double ux = 10000 * random.nextDouble();
    651             final double uy = 10000 * random.nextDouble();
    652             final double uz = 10000 * random.nextDouble();
    653             final double vx = 10000 * random.nextDouble();
    654             final double vy = 10000 * random.nextDouble();
    655             final double vz = 10000 * random.nextDouble();
    656
    657             // act
    658             final Vector3D cNaive = Vector3D.of(uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx);
    659             final Vector3D cAccurate = Vector3D.of(ux, uy, uz).cross(Vector3D.of(vx, vy, vz));
    660
    661             // assert
    662             Assertions.assertEquals(0.0, cAccurate.distance(cNaive), 6.0e-15 * cAccurate.norm());
    663         }
    664     }
    665
    666     @Test
    667     void testCrossProduct_cancellation() {
    668         // act/assert
    669         final Vector3D v1 = Vector3D.of(9070467121.0, 4535233560.0, 1);
    670         final Vector3D v2 = Vector3D.of(9070467123.0, 4535233561.0, 1);
    671         checkVector(v1.cross(v2), -1, 2, 1);
    672
    673         final double scale    = Math.scalb(1.0, 100);
    674         final Vector3D big1   = v1.multiply(scale);
    675         final Vector3D small2 = v2.multiply(1 / scale);
    676         checkVector(big1.cross(small2), -1, 2, 1);
    677     }
    678
    679     @Test
    680     void testScalarMultiply() {
    681         // arrange
    682         final Vector3D v1 = Vector3D.of(2, 3, 4);
    683         final Vector3D v2 = Vector3D.of(-2, -3, -4);
    684
    685         // act/assert
    686         checkVector(v1.multiply(0), 0, 0, 0);
    687         checkVector(v1.multiply(0.5), 1, 1.5, 2);
    688         checkVector(v1.multiply(1), 2, 3, 4);
    689         checkVector(v1.multiply(2), 4, 6, 8);
    690         checkVector(v1.multiply(-2), -4, -6, -8);
    691
    692         checkVector(v2.multiply(0), 0, 0, 0);
    693         checkVector(v2.multiply(0.5), -1, -1.5, -2);
    694         checkVector(v2.multiply(1), -2, -3, -4);
    695         checkVector(v2.multiply(2), -4, -6, -8);
    696         checkVector(v2.multiply(-2), 4, 6, 8);
    697     }
    698
    699     @Test
    700     void testDistance() {
    701         // arrange
    702         final Vector3D v1 = Vector3D.of(1, -2, 3);
    703         final Vector3D v2 = Vector3D.of(-4, 2, 0);
    704         final Vector3D v3 = Vector3D.of(5, -6, -7);
    705
    706         // act/assert
    707         Assertions.assertEquals(0.0, v1.distance(v1), EPS);
    708         Assertions.assertEquals(0.0, v2.distance(v2), EPS);
    709
    710         Assertions.assertEquals(Math.sqrt(50), v1.distance(v2), EPS);
    711         Assertions.assertEquals(Math.sqrt(50), v2.distance(v1), EPS);
    712
    713         Assertions.assertEquals(v1.subtract(v2).norm(), v1.distance(v2), EPS);
    714
    715         Assertions.assertEquals(Math.sqrt(132), v1.distance(v3), EPS);
    716         Assertions.assertEquals(Math.sqrt(132), v3.distance(v1), EPS);
    717     }
    718
    719     @Test
    720     void testDistanceSq() {
    721         // arrange
    722         final Vector3D v1 = Vector3D.of(1, -2, 3);
    723         final Vector3D v2 = Vector3D.of(-4, 2, 0);
    724         final Vector3D v3 = Vector3D.of(5, -6, -7);
    725
    726         // act/assert
    727         Assertions.assertEquals(0.0, v1.distanceSq(v1), EPS);
    728         Assertions.assertEquals(0.0, v2.distanceSq(v2), EPS);
    729
    730         Assertions.assertEquals(50, v1.distanceSq(v2), EPS);
    731         Assertions.assertEquals(50, v2.distanceSq(v1), EPS);
    732
    733         Assertions.assertEquals(v1.subtract(v2).normSq(), v1.distanceSq(v2), EPS);
    734
    735         Assertions.assertEquals(132, v1.distanceSq(v3), EPS);
    736         Assertions.assertEquals(132, v3.distanceSq(v1), EPS);
    737     }
    738
    739     @Test
    740     void testDotProduct() {
    741         // arrange
    742         final Vector3D v1 = Vector3D.of(1, -2, 3);
    743         final Vector3D v2 = Vector3D.of(-4, 5, -6);
    744         final Vector3D v3 = Vector3D.of(7, 8, 9);
    745
    746         // act/assert
    747         Assertions.assertEquals(14, v1.dot(v1), EPS);
    748
    749         Assertions.assertEquals(-32, v1.dot(v2), EPS);
    750         Assertions.assertEquals(-32, v2.dot(v1), EPS);
    751
    752         Assertions.assertEquals(18, v1.dot(v3), EPS);
    753         Assertions.assertEquals(18, v3.dot(v1), EPS);
    754     }
    755
    756     @Test
    757     void testDotProduct_nearlyOrthogonal() {
    758         // the following two vectors are nearly but not exactly orthogonal
    759         // naive dot product (i.e. computing u1.x * u2.x + u1.y * u2.y + u1.z * u2.z
    760         // leads to a result of 0.0, instead of the correct -1.855129...
    761
    762         // arrange
    763         final Vector3D u1 = Vector3D.of(-1321008684645961.0 /  268435456.0,
    764                                    -5774608829631843.0 /  268435456.0,
    765                                    -7645843051051357.0 / 8589934592.0);
    766         final Vector3D u2 = Vector3D.of(-5712344449280879.0 /    2097152.0,
    767                                    -4550117129121957.0 /    2097152.0,
    768                                     8846951984510141.0 /     131072.0);
    769
    770         // act
    771         final double sNaive = u1.getX() * u2.getX() + u1.getY() * u2.getY() + u1.getZ() * u2.getZ();
    772         final double sAccurate = u1.dot(u2);
    773
    774         // assert
    775         Assertions.assertEquals(0.0, sNaive, 1.0e-30);
    776         Assertions.assertEquals(-2088690039198397.0 / 1125899906842624.0, sAccurate, 1.0e-15);
    777     }
    778
    779     @Test
    780     void testDotProduct_accuracy() {
    781         // we compare accurate versus naive dot product implementations
    782         // on regular vectors (i.e. not extreme cases like in the previous test)
    783         final UniformRandomProvider random = RandomSource.create(RandomSource.WELL_1024_A, 553267312521321237L);
    784         for (int i = 0; i < 10000; ++i) {
    785             // arrange
    786             final double ux = 10000 * random.nextDouble();
    787             final double uy = 10000 * random.nextDouble();
    788             final double uz = 10000 * random.nextDouble();
    789             final double vx = 10000 * random.nextDouble();
    790             final double vy = 10000 * random.nextDouble();
    791             final double vz = 10000 * random.nextDouble();
    792
    793             // act
    794             final double sNaive = ux * vx + uy * vy + uz * vz;
    795             final double sAccurate = Vector3D.of(ux, uy, uz).dot(Vector3D.of(vx, vy, vz));
    796
    797             // assert
    798             Assertions.assertEquals(sNaive, sAccurate, 2.5e-16 * sAccurate);
    799         }
    800     }
    801
    802     @Test
    803     void testProject() {
    804         // arrange
    805         final Vector3D v1 = Vector3D.of(2.0, 3.0, 4.0);
    806         final Vector3D v2 = Vector3D.of(-5.0, -6.0, -7.0);
    807
    808         // act/assert
    809         checkVector(Vector3D.ZERO.project(Vector3D.Unit.PLUS_X), 0.0, 0.0, 0.0);
    810
    811         checkVector(v1.project(Vector3D.Unit.PLUS_X), 2.0, 0.0, 0.0);
    812         checkVector(v1.project(Vector3D.Unit.MINUS_X), 2.0, 0.0, 0.0);
    813         checkVector(v1.project(Vector3D.Unit.PLUS_Y), 0.0, 3.0, 0.0);
    814         checkVector(v1.project(Vector3D.Unit.MINUS_Y), 0.0, 3.0, 0.0);
    815         checkVector(v1.project(Vector3D.Unit.PLUS_Z), 0.0, 0.0, 4.0);
    816         checkVector(v1.project(Vector3D.Unit.MINUS_Z), 0.0, 0.0, 4.0);
    817
    818         checkVector(v2.project(Vector3D.Unit.PLUS_X), -5.0, 0.0, 0.0);
    819         checkVector(v2.project(Vector3D.Unit.MINUS_X), -5.0, 0.0, 0.0);
    820         checkVector(v2.project(Vector3D.Unit.PLUS_Y), 0.0, -6.0, 0.0);
    821         checkVector(v2.project(Vector3D.Unit.MINUS_Y), 0.0, -6.0, 0.0);
    822         checkVector(v2.project(Vector3D.Unit.PLUS_Z), 0.0, 0.0, -7.0);
    823         checkVector(v2.project(Vector3D.Unit.MINUS_Z), 0.0, 0.0, -7.0);
    824
    825         checkVector(v1.project(Vector3D.of(1.0, 1.0, 1.0)), 3.0, 3.0, 3.0);
    826         checkVector(v1.project(Vector3D.of(-1.0, -1.0, -1.0)), 3.0, 3.0, 3.0);
    827
    828         checkVector(v2.project(Vector3D.of(1.0, 1.0, 1.0)), -6.0, -6.0, -6.0);
    829         checkVector(v2.project(Vector3D.of(-1.0, -1.0, -1.0)), -6.0, -6.0, -6.0);
    830     }
    831
    832     @Test
    833     void testProject_baseHasIllegalNorm() {
    834         // arrange
    835         final Vector3D v = Vector3D.of(1.0, 1.0, 1.0);
    836
    837         // act/assert
    838         Assertions.assertThrows(IllegalArgumentException.class, () -> v.project(Vector3D.ZERO));
    839         Assertions.assertThrows(IllegalArgumentException.class, () -> v.project(Vector3D.NaN));
    840         Assertions.assertThrows(IllegalArgumentException.class, () -> v.project(Vector3D.POSITIVE_INFINITY));
    841         Assertions.assertThrows(IllegalArgumentException.class, () ->  v.project(Vector3D.NEGATIVE_INFINITY));
    842     }
    843
    844     @Test
    845     void testReject() {
    846         // arrange
    847         final Vector3D v1 = Vector3D.of(2.0, 3.0, 4.0);
    848         final Vector3D v2 = Vector3D.of(-5.0, -6.0, -7.0);
    849
    850         // act/assert
    851         checkVector(Vector3D.ZERO.reject(Vector3D.Unit.PLUS_X), 0.0, 0.0, 0.0);
    852
    853         checkVector(v1.reject(Vector3D.Unit.PLUS_X), 0.0, 3.0, 4.0);
    854         checkVector(v1.reject(Vector3D.Unit.MINUS_X), 0.0, 3.0, 4.0);
    855         checkVector(v1.reject(Vector3D.Unit.PLUS_Y), 2.0, 0.0, 4.0);
    856         checkVector(v1.reject(Vector3D.Unit.MINUS_Y), 2.0, 0.0, 4.0);
    857         checkVector(v1.reject(Vector3D.Unit.PLUS_Z), 2.0, 3.0, 0.0);
    858         checkVector(v1.reject(Vector3D.Unit.MINUS_Z), 2.0, 3.0, 0.0);
    859
    860         checkVector(v2.reject(Vector3D.Unit.PLUS_X), 0.0, -6.0, -7.0);
    861         checkVector(v2.reject(Vector3D.Unit.MINUS_X), 0.0, -6.0, -7.0);
    862         checkVector(v2.reject(Vector3D.Unit.PLUS_Y), -5.0, 0.0, -7.0);
    863         checkVector(v2.reject(Vector3D.Unit.MINUS_Y), -5.0, 0.0, -7.0);
    864         checkVector(v2.reject(Vector3D.Unit.PLUS_Z), -5.0, -6.0, 0.0);
    865         checkVector(v2.reject(Vector3D.Unit.MINUS_Z), -5.0, -6.0, 0.0);
    866
    867         checkVector(v1.reject(Vector3D.of(1.0, 1.0, 1.0)), -1.0, 0.0, 1.0);
    868         checkVector(v1.reject(Vector3D.of(-1.0, -1.0, -1.0)), -1.0, 0.0, 1.0);
    869
    870         checkVector(v2.reject(Vector3D.of(1.0, 1.0, 1.0)), 1.0, 0.0, -1.0);
    871         checkVector(v2.reject(Vector3D.of(-1.0, -1.0, -1.0)), 1.0, 0.0, -1.0);
    872     }
    873
    874     @Test
    875     void testReject_baseHasIllegalNorm() {
    876         // arrange
    877         final Vector3D v = Vector3D.of(1.0, 1.0, 1.0);
    878
    879         // act/assert
    880
    881         Assertions.assertThrows(IllegalArgumentException.class, () -> v.reject(Vector3D.ZERO));
    882         Assertions.assertThrows(IllegalArgumentException.class, () -> v.reject(Vector3D.NaN));
    883         Assertions.assertThrows(IllegalArgumentException.class, () -> v.reject(Vector3D.POSITIVE_INFINITY));
    884         Assertions.assertThrows(IllegalArgumentException.class, () -> v.reject(Vector3D.NEGATIVE_INFINITY));
    885
    886     }
    887
    888     @Test
    889     void testProjectAndReject_areComplementary() {
    890         // arrange
    891         final double eps = 1e-12;
    892
    893         // act/assert
    894         checkProjectAndRejectFullSphere(Vector3D.of(1.0, 0.0, 0.0), 1.0, eps);
    895         checkProjectAndRejectFullSphere(Vector3D.of(0.0, 1.0, 0.0), 2.0, eps);
    896         checkProjectAndRejectFullSphere(Vector3D.of(0.0, 0.0, 1.0), 2.0, eps);
    897         checkProjectAndRejectFullSphere(Vector3D.of(1.0, 1.0, 1.0), 3.0, eps);
    898
    899         checkProjectAndRejectFullSphere(Vector3D.of(-2.0, 0.0, 0.0), 1.0, eps);
    900         checkProjectAndRejectFullSphere(Vector3D.of(0.0, -2.0, 0.0), 2.0, eps);
    901         checkProjectAndRejectFullSphere(Vector3D.of(0.0, 0.0, -2.0), 2.0, eps);
    902         checkProjectAndRejectFullSphere(Vector3D.of(-2.0, -2.0, -2.0), 3.0, eps);
    903     }
    904
    905     private void checkProjectAndRejectFullSphere(final Vector3D vec, final double baseMag, final double eps) {
    906         for (double polar = 0.0; polar <= Math.PI; polar += 0.5) {
    907             for (double azimuth = 0.0; azimuth <= Angle.TWO_PI; azimuth += 0.5) {
    908                 final Vector3D base = SphericalCoordinates.toCartesian(baseMag, azimuth, polar);
    909
    910                 final Vector3D proj = vec.project(base);
    911                 final Vector3D rej = vec.reject(base);
    912
    913                 // ensure that the projection and rejection sum to the original vector
    914                 EuclideanTestUtils.assertCoordinatesEqual(vec, proj.add(rej), eps);
    915
    916                 final double angle = base.angle(vec);
    917
    918                 // check the angle between the projection and the base; this will
    919                 // be undefined when the angle between the original vector and the
    920                 // base is pi/2 (which means that the projection is the zero vector)
    921                 if (angle < Angle.PI_OVER_TWO) {
    922                     Assertions.assertEquals(0.0, proj.angle(base), eps);
    923                 } else if (angle > Angle.PI_OVER_TWO) {
    924                     Assertions.assertEquals(Math.PI, proj.angle(base), eps);
    925                 }
    926
    927                 // check the angle between the rejection and the base; this should
    928                 // always be pi/2 except for when the angle between the original vector
    929                 // and the base is 0 or pi, in which case the rejection is the zero vector.
    930                 if (angle > 0.0 && angle < Math.PI) {
    931                     Assertions.assertEquals(Angle.PI_OVER_TWO, rej.angle(base), eps);
    932                 }
    933             }
    934         }
    935     }
    936
    937     @Test
    938     void testVectorTo() {
    939         // act/assert
    940         final Vector3D p1 = Vector3D.of(1, 2, 3);
    941         final Vector3D p2 = Vector3D.of(4, 5, 6);
    942         final Vector3D p3 = Vector3D.of(-7, -8, -9);
    943
    944         // act/assert
    945         checkVector(p1.vectorTo(p1), 0, 0, 0);
    946         checkVector(p2.vectorTo(p2), 0, 0, 0);
    947         checkVector(p3.vectorTo(p3), 0, 0, 0);
    948
    949         checkVector(p1.vectorTo(p2), 3, 3, 3);
    950         checkVector(p2.vectorTo(p1), -3, -3, -3);
    951
    952         checkVector(p1.vectorTo(p3), -8, -10, -12);
    953         checkVector(p3.vectorTo(p1), 8, 10, 12);
    954     }
    955
    956     @Test
    957     void testDirectionTo() {
    958         // act/assert
    959         final double invSqrt3 = 1.0 / Math.sqrt(3);
    960
    961         final Vector3D p1 = Vector3D.of(1, 1, 1);
    962         final Vector3D p2 = Vector3D.of(1, 5, 1);
    963         final Vector3D p3 = Vector3D.of(-2, -2, -2);
    964
    965         // act/assert
    966         checkVector(p1.directionTo(p2), 0, 1, 0);
    967         checkVector(p2.directionTo(p1), 0, -1, 0);
    968
    969         checkVector(p1.directionTo(p3), -invSqrt3, -invSqrt3, -invSqrt3);
    970         checkVector(p3.directionTo(p1), invSqrt3, invSqrt3, invSqrt3);
    971     }
    972
    973     @Test
    974     void testDirectionTo_illegalNorm() {
    975         // arrange
    976         final Vector3D p = Vector3D.of(1, 2, 3);
    977
    978         // act/assert
    979         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.ZERO.directionTo(Vector3D.ZERO));
    980         Assertions.assertThrows(IllegalArgumentException.class, () ->  p.directionTo(p));
    981         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.NEGATIVE_INFINITY.directionTo(p));
    982         Assertions.assertThrows(IllegalArgumentException.class, () ->  p.directionTo(Vector3D.POSITIVE_INFINITY));
    983     }
    984
    985     @Test
    986     void testLerp() {
    987         // arrange
    988         final Vector3D v1 = Vector3D.of(1, -5, 2);
    989         final Vector3D v2 = Vector3D.of(-4, 0, 2);
    990         final Vector3D v3 = Vector3D.of(10, -4, 0);
    991
    992         // act/assert
    993         checkVector(v1.lerp(v1, 0), 1, -5, 2);
    994         checkVector(v1.lerp(v1, 1), 1, -5, 2);
    995
    996         checkVector(v1.lerp(v2, -0.25), 2.25, -6.25, 2);
    997         checkVector(v1.lerp(v2, 0), 1, -5, 2);
    998         checkVector(v1.lerp(v2, 0.25), -0.25, -3.75, 2);
    999         checkVector(v1.lerp(v2, 0.5), -1.5, -2.5, 2);
    1000         checkVector(v1.lerp(v2, 0.75), -2.75, -1.25, 2);
    1001         checkVector(v1.lerp(v2, 1), -4, 0, 2);
    1002         checkVector(v1.lerp(v2, 1.25), -5.25, 1.25, 2);
    1003
    1004         checkVector(v1.lerp(v3, 0), 1, -5, 2);
    1005         checkVector(v1.lerp(v3, 0.25), 3.25, -4.75, 1.5);
    1006         checkVector(v1.lerp(v3, 0.5), 5.5, -4.5, 1);
    1007         checkVector(v1.lerp(v3, 0.75), 7.75, -4.25, 0.5);
    1008         checkVector(v1.lerp(v3, 1), 10, -4, 0);
    1009     }
    1010
    1011     @Test
    1012     void testTransform() {
    1013         // arrange
    1014         final AffineTransformMatrix3D transform = AffineTransformMatrix3D.identity()
    1015                 .scale(2)
    1016                 .translate(1, 2, 3);
    1017
    1018         final Vector3D v1 = Vector3D.of(1, 2, 3);
    1019         final Vector3D v2 = Vector3D.of(-4, -5, -6);
    1020
    1021         // act/assert
    1022         checkVector(v1.transform(transform), 3, 6, 9);
    1023         checkVector(v2.transform(transform), -7, -8, -9);
    1024     }
    1025
    1026     @Test
    1027     void testPrecisionEquals() {
    1028         // arrange
    1029         final Precision.DoubleEquivalence smallEps = Precision.doubleEquivalenceOfEpsilon(1e-6);
    1030         final Precision.DoubleEquivalence largeEps = Precision.doubleEquivalenceOfEpsilon(1e-1);
    1031
    1032         final Vector3D vec = Vector3D.of(1, -2, 3);
    1033
    1034         // act/assert
    1035         Assertions.assertTrue(vec.eq(vec, smallEps));
    1036         Assertions.assertTrue(vec.eq(vec, largeEps));
    1037
    1038         Assertions.assertTrue(vec.eq(Vector3D.of(1.0000007, -2.0000009, 3.0000009), smallEps));
    1039         Assertions.assertTrue(vec.eq(Vector3D.of(1.0000007, -2.0000009, 3.0000009), largeEps));
    1040
    1041         Assertions.assertFalse(vec.eq(Vector3D.of(1.004, -2, 3), smallEps));
    1042         Assertions.assertFalse(vec.eq(Vector3D.of(1, -2.004, 3), smallEps));
    1043         Assertions.assertFalse(vec.eq(Vector3D.of(1, -2, 2.999), smallEps));
    1044         Assertions.assertTrue(vec.eq(Vector3D.of(1.004, -2.004, 2.999), largeEps));
    1045
    1046         Assertions.assertFalse(vec.eq(Vector3D.of(2, -2, 3), smallEps));
    1047         Assertions.assertFalse(vec.eq(Vector3D.of(1, -3, 3), smallEps));
    1048         Assertions.assertFalse(vec.eq(Vector3D.of(1, -2, 4), smallEps));
    1049         Assertions.assertFalse(vec.eq(Vector3D.of(2, -3, 4), smallEps));
    1050
    1051         Assertions.assertFalse(vec.eq(Vector3D.of(2, -2, 3), largeEps));
    1052         Assertions.assertFalse(vec.eq(Vector3D.of(1, -3, 3), largeEps));
    1053         Assertions.assertFalse(vec.eq(Vector3D.of(1, -2, 4), largeEps));
    1054         Assertions.assertFalse(vec.eq(Vector3D.of(2, -3, 4), largeEps));
    1055     }
    1056
    1057     @Test
    1058     void testIsZero() {
    1059         // arrange
    1060         final Precision.DoubleEquivalence smallEps = Precision.doubleEquivalenceOfEpsilon(1e-6);
    1061         final Precision.DoubleEquivalence largeEps = Precision.doubleEquivalenceOfEpsilon(1e-1);
    1062
    1063         // act/assert
    1064         Assertions.assertTrue(Vector3D.of(0.0, -0.0, 0.0).isZero(smallEps));
    1065         Assertions.assertTrue(Vector3D.of(-0.0, 0.0, -0.0).isZero(largeEps));
    1066
    1067         Assertions.assertTrue(Vector3D.of(-1e-7, 1e-7, -1e-8).isZero(smallEps));
    1068         Assertions.assertTrue(Vector3D.of(1e-7, -1e-7, 1e-8).isZero(largeEps));
    1069
    1070         Assertions.assertFalse(Vector3D.of(1e-2, 0.0, 0.0).isZero(smallEps));
    1071         Assertions.assertFalse(Vector3D.of(0.0, 1e-2, 0.0).isZero(smallEps));
    1072         Assertions.assertFalse(Vector3D.of(0.0, 0.0, 1e-2).isZero(smallEps));
    1073         Assertions.assertTrue(Vector3D.of(1e-2, -1e-2, 1e-2).isZero(largeEps));
    1074
    1075         Assertions.assertFalse(Vector3D.of(0.2, 0.0, 0.0).isZero(smallEps));
    1076         Assertions.assertFalse(Vector3D.of(0.0, 0.2, 0.0).isZero(smallEps));
    1077         Assertions.assertFalse(Vector3D.of(0.0, 0.0, 0.2).isZero(smallEps));
    1078         Assertions.assertFalse(Vector3D.of(0.2, 0.2, 0.2).isZero(smallEps));
    1079
    1080         Assertions.assertFalse(Vector3D.of(0.2, 0.0, 0.0).isZero(largeEps));
    1081         Assertions.assertFalse(Vector3D.of(0.0, 0.2, 0.0).isZero(largeEps));
    1082         Assertions.assertFalse(Vector3D.of(0.0, 0.0, 0.2).isZero(largeEps));
    1083         Assertions.assertFalse(Vector3D.of(0.2, 0.2, 0.2).isZero(largeEps));
    1084     }
    1085
    1086     @Test
    1087     void testHashCode() {
    1088         // arrange
    1089         final double delta = 10 * Precision.EPSILON;
    1090         final Vector3D u = Vector3D.of(1, 1, 1);
    1091         final Vector3D v = Vector3D.of(1 + delta, 1 + delta, 1 + delta);
    1092         final Vector3D w = Vector3D.of(1, 1, 1);
    1093
    1094         // act/assert
    1095         Assertions.assertTrue(u.hashCode() != v.hashCode());
    1096         Assertions.assertEquals(u.hashCode(), w.hashCode());
    1097
    1098         Assertions.assertEquals(Vector3D.of(0, 0, Double.NaN).hashCode(), Vector3D.NaN.hashCode());
    1099         Assertions.assertEquals(Vector3D.of(0, Double.NaN, 0).hashCode(), Vector3D.NaN.hashCode());
    1100         Assertions.assertEquals(Vector3D.of(Double.NaN, 0, 0).hashCode(), Vector3D.NaN.hashCode());
    1101         Assertions.assertEquals(Vector3D.of(0, 0, Double.NaN).hashCode(), Vector3D.of(Double.NaN, 0, 0).hashCode());
    1102     }
    1103
    1104     @Test
    1105     void testEquals() {
    1106         // arrange
    1107         final double delta = 10 * Precision.EPSILON;
    1108         final Vector3D u1 = Vector3D.of(1, 2, 3);
    1109         final Vector3D u2 = Vector3D.of(1, 2, 3);
    1110
    1111         // act/assert
    1112         GeometryTestUtils.assertSimpleEqualsCases(u1);
    1113         Assertions.assertEquals(u1, u2);
    1114
    1115         Assertions.assertNotEquals(u1, Vector3D.of(-1, -2, -3));
    1116         Assertions.assertNotEquals(u1, Vector3D.of(1 + delta, 2, 3));
    1117         Assertions.assertNotEquals(u1, Vector3D.of(1, 2 + delta, 3));
    1118         Assertions.assertNotEquals(u1, Vector3D.of(1, 2, 3 + delta));
    1119
    1120         Assertions.assertEquals(Vector3D.of(0, Double.NaN, 0), Vector3D.of(Double.NaN, 0, 0));
    1121
    1122         Assertions.assertEquals(Vector3D.of(0, 0, Double.POSITIVE_INFINITY), Vector3D.of(0, 0, Double.POSITIVE_INFINITY));
    1123         Assertions.assertNotEquals(Vector3D.of(0, Double.POSITIVE_INFINITY, 0), Vector3D.of(0, 0, Double.POSITIVE_INFINITY));
    1124         Assertions.assertNotEquals(Vector3D.of(Double.POSITIVE_INFINITY, 0, 0), Vector3D.of(0, 0, Double.POSITIVE_INFINITY));
    1125
    1126         Assertions.assertEquals(Vector3D.of(Double.NEGATIVE_INFINITY, 0, 0), Vector3D.of(Double.NEGATIVE_INFINITY, 0, 0));
    1127         Assertions.assertNotEquals(Vector3D.of(0, Double.NEGATIVE_INFINITY, 0), Vector3D.of(Double.NEGATIVE_INFINITY, 0, 0));
    1128         Assertions.assertNotEquals(Vector3D.of(0, 0, Double.NEGATIVE_INFINITY), Vector3D.of(Double.NEGATIVE_INFINITY, 0, 0));
    1129     }
    1130
    1131     @Test
    1132     void testEqualsAndHashCode_signedZeroConsistency() {
    1133         // arrange
    1134         final Vector3D a = Vector3D.of(0.0, -0.0, 0.0);
    1135         final Vector3D b = Vector3D.of(-0.0, 0.0, -0.0);
    1136         final Vector3D c = Vector3D.of(0.0, -0.0, 0.0);
    1137         final Vector3D d = Vector3D.of(-0.0, 0.0, -0.0);
    1138
    1139         // act/assert
    1140         Assertions.assertFalse(a.equals(b));
    1141
    1142         Assertions.assertTrue(a.equals(c));
    1143         Assertions.assertEquals(a.hashCode(), c.hashCode());
    1144
    1145         Assertions.assertTrue(b.equals(d));
    1146         Assertions.assertEquals(b.hashCode(), d.hashCode());
    1147     }
    1148
    1149     @Test
    1150     void testToString() {
    1151         // arrange
    1152         final Vector3D v = Vector3D.of(1, 2, 3);
    1153         final Pattern pattern = Pattern.compile("\\(1.{0,2}, 2.{0,2}, 3.{0,2}\\)");
    1154
    1155         // act
    1156         final String str = v.toString();
    1157
    1158         // assert
    1159         Assertions.assertTrue(pattern.matcher(str).matches(),
    1160                 "Expected string " + str + " to match regex " + pattern);
    1161     }
    1162
    1163     @Test
    1164     void testParse() {
    1165         // act/assert
    1166         checkVector(Vector3D.parse("(1, 2, 3)"), 1, 2, 3);
    1167         checkVector(Vector3D.parse("(-1, -2, -3)"), -1, -2, -3);
    1168
    1169         checkVector(Vector3D.parse("(0.01, -1e-3, 0)"), 1e-2, -1e-3, 0);
    1170
    1171         checkVector(Vector3D.parse("(NaN, -Infinity, Infinity)"), Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
    1172
    1173         checkVector(Vector3D.parse(Vector3D.ZERO.toString()), 0, 0, 0);
    1174         checkVector(Vector3D.parse(Vector3D.Unit.MINUS_X.toString()), -1, 0, 0);
    1175     }
    1176
    1177     @Test
    1178     void testParse_failure() {
    1179         // act/assert
    1180         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector3D.parse("abc"));
    1181     }
    1182
    1183     @Test
    1184     void testOf() {
    1185         // act/assert
    1186         checkVector(Vector3D.of(1, 2, 3), 1, 2, 3);
    1187         checkVector(Vector3D.of(-1, -2, -3), -1, -2, -3);
    1188         checkVector(Vector3D.of(Math.PI, Double.NaN, Double.POSITIVE_INFINITY),
    1189                 Math.PI, Double.NaN, Double.POSITIVE_INFINITY);
    1190         checkVector(Vector3D.of(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Math.E),
    1191                 Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Math.E);
    1192     }
    1193
    1194     @Test
    1195     void testOf_arrayArg() {
    1196         // act/assert
    1197         checkVector(Vector3D.of(new double[] {1, 2, 3}), 1, 2, 3);
    1198         checkVector(Vector3D.of(new double[] {-1, -2, -3}), -1, -2, -3);
    1199         checkVector(Vector3D.of(new double[] {Math.PI, Double.NaN, Double.POSITIVE_INFINITY}),
    1200                 Math.PI, Double.NaN, Double.POSITIVE_INFINITY);
    1201         checkVector(Vector3D.of(new double[] {Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Math.E}),
    1202                 Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Math.E);
    1203     }
    1204
    1205     @Test
    1206     void testOf_arrayArg_invalidDimensions() {
    1207         // act/assert
    1208         Assertions.assertThrows(IllegalArgumentException.class, () -> Vector3D.of(new double[] {0.0, 0.0}));
    1209     }
    1210
    1211     @Test
    1212     void testUnitFrom_coordinates() {
    1213         // arrange
    1214         final double invSqrt3 = 1.0 / Math.sqrt(3.0);
    1215
    1216         // act/assert
    1217         checkVector(Vector3D.Unit.from(2.0, -2.0, 2.0), invSqrt3, -invSqrt3, invSqrt3);
    1218         checkVector(Vector3D.Unit.from(-4.0, 4.0, -4.0), -invSqrt3, invSqrt3, -invSqrt3);
    1219     }
    1220
    1221     @Test
    1222     void testUnitFrom_vector() {
    1223         // arrange
    1224         final double invSqrt3 = 1.0 / Math.sqrt(3.0);
    1225         final Vector3D vec = Vector3D.of(2.0, -2.0, 2.0);
    1226         final Vector3D.Unit unitVec = Vector3D.Unit.from(2.0, -2.0, 2.0);
    1227
    1228         // act/assert
    1229         checkVector(Vector3D.Unit.from(vec), invSqrt3, -invSqrt3, invSqrt3);
    1230         Assertions.assertSame(unitVec, Vector3D.Unit.from(unitVec));
    1231     }
    1232
    1233     @Test
    1234     void testUnitFrom_static_illegalNorm() {
    1235         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.Unit.from(0.0, 0.0, 0.0));
    1236         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.Unit.from(Double.NaN, 1.0, 1.0));
    1237         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.Unit.from(1.0, Double.NEGATIVE_INFINITY, 1.0));
    1238         Assertions.assertThrows(IllegalArgumentException.class, () ->  Vector3D.Unit.from(1.0, 1.0, Double.POSITIVE_INFINITY));
    1239     }
    1240
    1241     @Test
    1242     void testMax() {
    1243         // act/assert
    1244         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-100, 1, 100),
    1245                 Vector3D.max(Collections.singletonList(Vector3D.of(-100, 1, 100))), EPS);
    1246
    1247         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 1, 100),
    1248                 Vector3D.max(Arrays.asList(Vector3D.of(-100, 1, 100), Vector3D.of(0, 1, 0))), EPS);
    1249
    1250         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 2),
    1251                 Vector3D.max(Vector3D.of(-2, 0, 0), Vector3D.of(-1, -5, 1), Vector3D.of(-10, -10, 2)), EPS);
    1252     }
    1253
    1254     @Test
    1255     void testMax_noPointsGiven() {
    1256         // arrange
    1257         final String msg = "Cannot compute vector max: no vectors given";
    1258
    1259         // act/assert
    1260         GeometryTestUtils.assertThrowsWithMessage(() -> {
    1261             Vector3D.max(new ArrayList<>());
    1262         }, IllegalArgumentException.class, msg);
    1263     }
    1264
    1265     @Test
    1266     void testMin() {
    1267         // act/assert
    1268         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-100, 1, 100),
    1269                 Vector3D.min(Collections.singletonList(Vector3D.of(-100, 1, 100))), EPS);
    1270
    1271         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-100, 1, 0),
    1272                 Vector3D.min(Arrays.asList(Vector3D.of(-100, 1, 100), Vector3D.of(0, 1, 0))), EPS);
    1273
    1274         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-10, -10, 0),
    1275                 Vector3D.min(Vector3D.of(-2, 0, 0), Vector3D.of(-1, -5, 1), Vector3D.of(-10, -10, 2)), EPS);
    1276     }
    1277
    1278     @Test
    1279     void testMin_noPointsGiven() {
    1280         // arrange
    1281         final String msg = "Cannot compute vector min: no vectors given";
    1282
    1283         // act/assert
    1284         GeometryTestUtils.assertThrowsWithMessage(() -> {
    1285             Vector3D.min(new ArrayList<>());
    1286         }, IllegalArgumentException.class, msg);
    1287     }
    1288
    1289     @Test
    1290     void testCentroid() {
    1291         // act/assert
    1292         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 3),
    1293                 Vector3D.centroid(Vector3D.of(1, 2, 3)), EPS);
    1294
    1295         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2.5, 3.5, 4.5),
    1296                 Vector3D.centroid(Vector3D.of(1, 2, 3), Vector3D.of(2, 3, 4),
    1297                         Vector3D.of(3, 4, 5), Vector3D.of(4, 5, 6)), EPS);
    1298
    1299         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 3),
    1300                 Vector3D.centroid(Collections.singletonList(Vector3D.of(1, 2, 3))), EPS);
    1301
    1302         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 1, 1.5),
    1303                 Vector3D.centroid(Arrays.asList(Vector3D.of(1, 2, 3), Vector3D.of(1, 2, 3),
    1304                         Vector3D.ZERO, Vector3D.ZERO)), EPS);
    1305     }
    1306
    1307     @Test
    1308     void testCentroid_noPointsGiven() {
    1309         // arrange
    1310         final String msg = "Cannot compute centroid: no points given";
    1311
    1312         // act/assert
    1313         GeometryTestUtils.assertThrowsWithMessage(() -> {
    1314             Vector3D.centroid(new ArrayList<>());
    1315         }, IllegalArgumentException.class, msg);
    1316     }
    1317
    1318     @Test
    1319     void testSum_factoryMethods() {
    1320         // act/assert
    1321         checkVector(Vector3D.Sum.create().get(), 0, 0, 0);
    1322         checkVector(Vector3D.Sum.of(Vector3D.of(1, 2, 3)).get(), 1, 2, 3);
    1323         checkVector(Vector3D.Sum.of(
    1324                 Vector3D.of(1, 2, 3),
    1325                 Vector3D.Unit.PLUS_X,
    1326                 Vector3D.Unit.PLUS_Y,
    1327                 Vector3D.Unit.PLUS_Z).get(), 2, 3, 4);
    1328     }
    1329
    1330     @Test
    1331     void testSum_instanceMethods() {
    1332         // arrange
    1333         final Vector3D p1 = Vector3D.of(1, 2, 3);
    1334         final Vector3D p2 = Vector3D.of(4, 6, 8);
    1335
    1336         // act/assert
    1337         checkVector(Vector3D.Sum.create()
    1338                 .add(p1)
    1339                 .addScaled(0.5, p2)
    1340                 .get(), 3, 5, 7);
    1341     }
    1342
    1343     @Test
    1344     void testSum_accept() {
    1345         // arrange
    1346         final Vector3D p1 = Vector3D.of(1, 2, -3);
    1347         final Vector3D p2 = Vector3D.of(3, -6, 8);
    1348
    1349         final List<Vector3D.Unit> units = Arrays.asList(
    1350                 Vector3D.Unit.PLUS_X,
    1351                 Vector3D.Unit.PLUS_Y,
    1352                 Vector3D.Unit.PLUS_Z);
    1353
    1354         final Vector3D.Sum s = Vector3D.Sum.create();
    1355
    1356         // act/assert
    1357         Arrays.asList(p1, Vector3D.ZERO, p2).forEach(s);
    1358         units.forEach(s);
    1359
    1360         // assert
    1361         checkVector(s.get(), 5, -3, 6);
    1362     }
    1363
    1364     @Test
    1365     void testUnitFactoryOptimization() {
    1366         // An already normalized vector will avoid unnecessary creation.
    1367         final Vector3D v = Vector3D.of(3, 4, 5).normalize();
    1368         Assertions.assertSame(v, v.normalize());
    1369     }
    1370
    1371     private void checkVector(final Vector3D v, final double x, final double y, final double z) {
    1372         Assertions.assertEquals(x, v.getX(), EPS);
    1373         Assertions.assertEquals(y, v.getY(), EPS);
    1374         Assertions.assertEquals(z, v.getZ(), EPS);
    1375     }
    1376 }