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.shape;
    18
    19  import java.io.IOException;
    20  import java.util.List;
    21  import java.util.function.DoubleSupplier;
    22  import java.util.regex.Pattern;
    23  import java.util.stream.Collectors;
    24
    25  import org.apache.commons.geometry.core.GeometryTestUtils;
    26  import org.apache.commons.geometry.core.RegionLocation;
    27  import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
    28  import org.apache.commons.geometry.euclidean.threed.Bounds3D;
    29  import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
    30  import org.apache.commons.geometry.euclidean.threed.RegionBSPTree3D;
    31  import org.apache.commons.geometry.euclidean.threed.SphericalCoordinates;
    32  import org.apache.commons.geometry.euclidean.threed.Triangle3D;
    33  import org.apache.commons.geometry.euclidean.threed.Vector3D;
    34  import org.apache.commons.geometry.euclidean.threed.line.Line3D;
    35  import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
    36  import org.apache.commons.geometry.euclidean.threed.line.LinecastPoint3D;
    37  import org.apache.commons.geometry.euclidean.threed.line.Lines3D;
    38  import org.apache.commons.geometry.euclidean.threed.mesh.TriangleMesh;
    39  import org.apache.commons.numbers.angle.Angle;
    40  import org.apache.commons.numbers.core.Precision;
    41  import org.apache.commons.rng.UniformRandomProvider;
    42  import org.apache.commons.rng.simple.RandomSource;
    43  import org.junit.jupiter.api.Assertions;
    44  import org.junit.jupiter.api.Test;
    45
    46  class SphereTest {
    47
    48      private static final double TEST_EPS = 1e-10;
    49
    50      private static final Precision.DoubleEquivalence TEST_PRECISION =
    51              Precision.doubleEquivalenceOfEpsilon(TEST_EPS);
    52
    53      @Test
    54      void testFrom() {
    55          // arrange
    56          final Vector3D center = Vector3D.of(1, 2, 3);
    57
    58          // act
    59          final Sphere s = Sphere.from(center, 3, TEST_PRECISION);
    60
    61          // act/assert
    62          Assertions.assertFalse(s.isFull());
    63          Assertions.assertFalse(s.isEmpty());
    64
    65          Assertions.assertSame(center, s.getCenter());
    66          Assertions.assertSame(center, s.getCentroid());
    67
    68          Assertions.assertEquals(3, s.getRadius(), 0.0);
    69
    70          Assertions.assertSame(TEST_PRECISION, s.getPrecision());
    71      }
    72
    73      @Test
    74      void testFrom_illegalCenter() {
    75          // act/assert
    76          Assertions.assertThrows(IllegalArgumentException.class, () -> Sphere.from(Vector3D.of(Double.POSITIVE_INFINITY, 1, 2), 1, TEST_PRECISION));
    77          Assertions.assertThrows(IllegalArgumentException.class, () -> Sphere.from(Vector3D.of(Double.NaN, 1, 2), 1, TEST_PRECISION));
    78      }
    79
    80      @Test
    81      void testFrom_illegalRadius() {
    82          // arrange
    83          final Precision.DoubleEquivalence precision = Precision.doubleEquivalenceOfEpsilon(1e-2);
    84
    85          // act/assert
    86          Assertions.assertThrows(IllegalArgumentException.class, () -> Sphere.from(Vector3D.ZERO, -1, TEST_PRECISION));
    87          Assertions.assertThrows(IllegalArgumentException.class, () -> Sphere.from(Vector3D.ZERO, 0, TEST_PRECISION));
    88          Assertions.assertThrows(IllegalArgumentException.class, () -> Sphere.from(Vector3D.ZERO, Double.POSITIVE_INFINITY, TEST_PRECISION));
    89          Assertions.assertThrows(IllegalArgumentException.class, () -> Sphere.from(Vector3D.ZERO, Double.NaN, TEST_PRECISION));
    90          Assertions.assertThrows(IllegalArgumentException.class, () -> Sphere.from(Vector3D.ZERO, 1e-3, precision));
    91      }
    92
    93      @Test
    94      void testGeometricProperties() {
    95          // arrange
    96          final double r = 2;
    97          final Sphere s = Sphere.from(Vector3D.of(1, 2, 3), r, TEST_PRECISION);
    98
    99          // act/assert
    100         Assertions.assertEquals(4 * Math.PI * r * r, s.getBoundarySize(), TEST_EPS);
    101         Assertions.assertEquals((4.0 * Math.PI * r * r * r) / 3.0, s.getSize(), TEST_EPS);
    102     }
    103
    104     @Test
    105     void testClassify() {
    106         // arrange
    107         final Vector3D center = Vector3D.of(1, 2, 3);
    108         final double radius = 4;
    109         final Sphere s = Sphere.from(center, radius, TEST_PRECISION);
    110
    111         EuclideanTestUtils.permute(0, Angle.TWO_PI, 0.2, (azimuth, polar) -> {
    112             // act/assert
    113             EuclideanTestUtils.assertRegionLocation(s, RegionLocation.OUTSIDE,
    114                     SphericalCoordinates.of(radius + 1, azimuth, polar)
    115                         .toVector()
    116                         .add(center));
    117
    118             EuclideanTestUtils.assertRegionLocation(s, RegionLocation.BOUNDARY,
    119                     SphericalCoordinates.of(radius + 1e-12, azimuth, polar)
    120                         .toVector()
    121                         .add(center));
    122
    123             EuclideanTestUtils.assertRegionLocation(s, RegionLocation.INSIDE,
    124                     SphericalCoordinates.of(radius - 1, azimuth, polar)
    125                         .toVector()
    126                         .add(center));
    127         });
    128     }
    129
    130     @Test
    131     void testContains() {
    132      // arrange
    133         final Vector3D center = Vector3D.of(1, 2, 3);
    134         final double radius = 4;
    135         final Sphere s = Sphere.from(center, radius, TEST_PRECISION);
    136
    137         EuclideanTestUtils.permute(0, Angle.TWO_PI, 0.2, (azimuth, polar) -> {
    138             // act/assert
    139             checkContains(s, false,
    140                     SphericalCoordinates.of(radius + 1, azimuth, polar)
    141                         .toVector()
    142                         .add(center));
    143
    144             checkContains(s, true,
    145                     SphericalCoordinates.of(radius - 1, azimuth, polar)
    146                         .toVector()
    147                         .add(center),
    148                     SphericalCoordinates.of(radius + 1e-12, azimuth, polar)
    149                         .toVector()
    150                         .add(center));
    151         });
    152     }
    153
    154     @Test
    155     void testProject() {
    156         // arrange
    157         final Vector3D center = Vector3D.of(1.5, 2.5, 3.5);
    158         final double radius = 3;
    159         final Sphere s = Sphere.from(center, radius, TEST_PRECISION);
    160
    161         EuclideanTestUtils.permute(-4, 4, 1, (x, y, z) -> {
    162             final Vector3D pt = Vector3D.of(x, y, z);
    163
    164             // act
    165             final Vector3D projection = s.project(pt);
    166
    167             // assert
    168             Assertions.assertEquals(radius, center.distance(projection), TEST_EPS);
    169             EuclideanTestUtils.assertCoordinatesEqual(center.directionTo(pt),
    170                     center.directionTo(projection), TEST_EPS);
    171         });
    172     }
    173
    174     @Test
    175     void testProject_argumentEqualsCenter() {
    176         // arrange
    177         final Sphere c = Sphere.from(Vector3D.of(1, 2, 3), 2, TEST_PRECISION);
    178
    179         // act
    180         final Vector3D projection = c.project(Vector3D.of(1, 2, 3));
    181
    182         // assert
    183         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 2, 3), projection, TEST_EPS);
    184     }
    185
    186     @Test
    187     void testIntersections() {
    188         // --- arrange
    189         final Sphere s = Sphere.from(Vector3D.of(2, 1, 3), 2, TEST_PRECISION);
    190         final double sqrt3 = Math.sqrt(3);
    191
    192         // --- act/assert
    193         // descending along y in x-y plane
    194         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(-1, 4, 3), Vector3D.of(5, 4, 3), TEST_PRECISION));
    195         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(-1, 3, 3), Vector3D.of(5, 3, 3), TEST_PRECISION),
    196                 Vector3D.of(2, 3, 3));
    197         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(-1, 2, 3), Vector3D.of(5, 2, 3), TEST_PRECISION),
    198                 Vector3D.of(2 - sqrt3, 2, 3), Vector3D.of(2 + sqrt3, 2, 3));
    199         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(-1, 1, 3), Vector3D.of(5, 1, 3), TEST_PRECISION),
    200                 Vector3D.of(0, 1, 3), Vector3D.of(4, 1, 3));
    201         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(-1, 0, 3), Vector3D.of(5, 0, 3), TEST_PRECISION),
    202                 Vector3D.of(2 - sqrt3, 0, 3), Vector3D.of(2 + sqrt3, 0, 3));
    203         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(-1, -1, 3), Vector3D.of(5, -1, 3), TEST_PRECISION),
    204                 Vector3D.of(2, -1, 3));
    205         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(-1, -2, 3), Vector3D.of(5, -2, 3), TEST_PRECISION));
    206
    207         // ascending along x in x-y plane
    208         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(-1, -2, 3), Vector3D.of(-1, 5, 3), TEST_PRECISION));
    209         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(0, -2, 3), Vector3D.of(0, 5, 3), TEST_PRECISION),
    210                 Vector3D.of(0, 1, 3));
    211         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(1, -2, 3), Vector3D.of(1, 5, 3), TEST_PRECISION),
    212                 Vector3D.of(1, 1 - sqrt3, 3), Vector3D.of(1, 1 + sqrt3, 3));
    213         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(2, -2, 3), Vector3D.of(2, 5, 3), TEST_PRECISION),
    214                 Vector3D.of(2, -1, 3), Vector3D.of(2, 3, 3));
    215         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(3, -2, 3), Vector3D.of(3, 5, 3), TEST_PRECISION),
    216                 Vector3D.of(3, 1 - sqrt3, 3), Vector3D.of(3, 1 + sqrt3, 3));
    217         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(4, -2, 3), Vector3D.of(4, 5, 3), TEST_PRECISION),
    218                 Vector3D.of(4, 1, 3));
    219         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(5, -2, 3), Vector3D.of(5, 5, 3), TEST_PRECISION));
    220
    221         // descending along z in y-z plane
    222         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(2, -2, 6), Vector3D.of(2, 4, 6), TEST_PRECISION));
    223         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(2, -2, 5), Vector3D.of(2, 4, 5), TEST_PRECISION),
    224                 Vector3D.of(2, 1, 5));
    225         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(2, -2, 4), Vector3D.of(2, 4, 4), TEST_PRECISION),
    226                 Vector3D.of(2, 1 - sqrt3, 4), Vector3D.of(2, 1 + sqrt3, 4));
    227         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(2, -2, 3), Vector3D.of(2, 4, 3), TEST_PRECISION),
    228                 Vector3D.of(2, -1, 3), Vector3D.of(2, 3, 3));
    229         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(2, -2, 2), Vector3D.of(2, 4, 2), TEST_PRECISION),
    230                 Vector3D.of(2, 1 - sqrt3, 2), Vector3D.of(2, 1 + sqrt3, 2));
    231         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(2, -2, 1), Vector3D.of(2, 4, 1), TEST_PRECISION),
    232                 Vector3D.of(2, 1, 1));
    233         checkIntersections(s, Lines3D.fromPoints(Vector3D.of(2, -2, 0), Vector3D.of(2, 4, 0), TEST_PRECISION));
    234
    235         // diagonal from origin
    236         final Vector3D center = s.getCenter();
    237         checkIntersections(s, Lines3D.fromPoints(Vector3D.ZERO, s.getCenter(), TEST_PRECISION),
    238                 center.withNorm(center.norm() - s.getRadius()), center.withNorm(center.norm() + s.getRadius()));
    239     }
    240
    241     @Test
    242     void testLinecast() {
    243         // arrange
    244         final Sphere s = Sphere.from(Vector3D.of(2, 1, 3), 2, TEST_PRECISION);
    245         final double sqrt3 = Math.sqrt(3);
    246
    247         // act/assert
    248         checkLinecast(s, Lines3D.segmentFromPoints(Vector3D.of(-1, 0, 3), Vector3D.of(5, 0, 3), TEST_PRECISION),
    249                 Vector3D.of(2 - sqrt3, 0, 3), Vector3D.of(2 + sqrt3, 0, 3));
    250         checkLinecast(s, Lines3D.segmentFromPoints(Vector3D.of(-1, 3, 3), Vector3D.of(5, 3, 3), TEST_PRECISION),
    251                 Vector3D.of(2, 3, 3));
    252         checkLinecast(s, Lines3D.segmentFromPoints(Vector3D.of(-1, -2, 3), Vector3D.of(5, -2, 3), TEST_PRECISION));
    253     }
    254
    255     @Test
    256     void testLinecast_intersectionsNotInSegment() {
    257         // arrange
    258         final Sphere s = Sphere.from(Vector3D.of(2, 1, 3), 2, TEST_PRECISION);
    259         final Line3D line = Lines3D.fromPointAndDirection(Vector3D.of(0, 0, 3), Vector3D.Unit.PLUS_X, TEST_PRECISION);
    260
    261         // act/assert
    262         checkLinecast(s, line.segment(-1, 0));
    263         checkLinecast(s, line.segment(1.5, 2.5));
    264         checkLinecast(s, line.segment(1.5, 2.5));
    265         checkLinecast(s, line.segment(4, 5));
    266     }
    267
    268     @Test
    269     void testLinecast_segmentPointOnBoundary() {
    270         // arrange
    271         final Sphere s = Sphere.from(Vector3D.of(2, 1, 3), 2, TEST_PRECISION);
    272         final Line3D line = Lines3D.fromPointAndDirection(Vector3D.of(0, 0, 3), Vector3D.Unit.PLUS_X, TEST_PRECISION);
    273         final double sqrt3 = Math.sqrt(3);
    274         final double start = 2 - sqrt3;
    275         final double end = 2 + sqrt3;
    276
    277         // act/assert
    278         checkLinecast(s, line.segment(start, 2), Vector3D.of(start, 0, 3));
    279         checkLinecast(s, line.segment(start, end), Vector3D.of(start, 0, 3), Vector3D.of(end, 0, 3));
    280         checkLinecast(s, line.segment(end, 5), Vector3D.of(end, 0, 3));
    281     }
    282
    283     @Test
    284     void testToTree_zeroSubdivisions() throws IOException {
    285         // arrange
    286         final double r = 2;
    287         final Sphere s = Sphere.from(Vector3D.of(2, 1, 3), r, TEST_PRECISION);
    288
    289         // act
    290         final RegionBSPTree3D tree = s.toTree(0);
    291
    292         // assert
    293         checkBasicApproximationProperties(s, tree);
    294
    295         final List<PlaneConvexSubset> boundaries = tree.getBoundaries();
    296         Assertions.assertEquals(8, boundaries.size());
    297
    298         final List<Triangle3D> triangles = tree.triangleStream().collect(Collectors.toList());
    299         Assertions.assertEquals(8, triangles.size());
    300
    301         final double expectedSize = (4.0 / 3.0) * r * r * r;
    302         Assertions.assertEquals(expectedSize, tree.getSize(), TEST_EPS);
    303     }
    304
    305     @Test
    306     void testToTree_oneSubdivision() throws IOException {
    307         // arrange
    308         final double r = 2;
    309         final Sphere s = Sphere.from(Vector3D.of(2, 1, 3), r, TEST_PRECISION);
    310
    311         // act
    312         final RegionBSPTree3D tree = s.toTree(1);
    313
    314         // assert
    315         checkBasicApproximationProperties(s, tree);
    316
    317         final List<PlaneConvexSubset> boundaries = tree.getBoundaries();
    318         Assertions.assertEquals(32, boundaries.size());
    319
    320         final List<Triangle3D> triangles = tree.triangleStream().collect(Collectors.toList());
    321         Assertions.assertEquals(32, triangles.size());
    322
    323         Assertions.assertTrue(tree.getSize() <= s.getSize());
    324     }
    325
    326     @Test
    327     void testToTree_multipleSubdivisionCounts() {
    328         // -- arrange
    329         final Sphere s = Sphere.from(Vector3D.of(-3, 5, 1), 10, TEST_PRECISION);
    330
    331         final int min = 0;
    332         final int max = 5;
    333
    334         RegionBSPTree3D tree;
    335
    336         double sizeDiff;
    337         double prevSizeDiff = Double.POSITIVE_INFINITY;
    338
    339         for (int n = min; n <= max; ++n) {
    340             // -- act
    341             tree = s.toTree(n);
    342
    343             // -- assert
    344             checkBasicApproximationProperties(s, tree);
    345
    346             final int expectedTriangles = (int) (8 * Math.pow(4, n));
    347             final List<PlaneConvexSubset> boundaries = tree.getBoundaries();
    348             Assertions.assertEquals(expectedTriangles, boundaries.size());
    349
    350             final List<Triangle3D> triangles = tree.triangleStream().collect(Collectors.toList());
    351             Assertions.assertEquals(expectedTriangles, triangles.size());
    352
    353             // check that we get closer and closer to the correct size as we add more segments
    354             sizeDiff = s.getSize() - tree.getSize();
    355             Assertions.assertTrue(sizeDiff < prevSizeDiff, "Expected size difference to decrease: n= " +
    356                     n + ", prevSizeDiff= " + prevSizeDiff + ", sizeDiff= " + sizeDiff);
    357
    358             prevSizeDiff = sizeDiff;
    359         }
    360     }
    361
    362     @Test
    363     void testToTree_randomSpheres() {
    364         // arrange
    365         final UniformRandomProvider rand = RandomSource.create(RandomSource.XO_RO_SHI_RO_128_PP, 1L);
    366         final Precision.DoubleEquivalence precision = Precision.doubleEquivalenceOfEpsilon(1e-10);
    367         final double min = 1e-1;
    368         final double max = 1e2;
    369
    370         final DoubleSupplier randDouble = () -> (rand.nextDouble() * (max - min)) + min;
    371
    372         final int count = 10;
    373         for (int i = 0; i < count; ++i) {
    374             final Vector3D center = Vector3D.of(
    375                     randDouble.getAsDouble(),
    376                     randDouble.getAsDouble(),
    377                     randDouble.getAsDouble());
    378
    379             final double radius = randDouble.getAsDouble();
    380             final Sphere sphere = Sphere.from(center, radius, precision);
    381
    382             for (int s = 0; s < 7; ++s) {
    383                 // act
    384                 final RegionBSPTree3D tree = sphere.toTree(s);
    385
    386                 // assert
    387                 Assertions.assertEquals((int) (8 * Math.pow(4, s)), tree.getBoundaries().size());
    388                 Assertions.assertTrue(tree.isFinite());
    389                 Assertions.assertFalse(tree.isEmpty());
    390                 Assertions.assertTrue(tree.getSize() < sphere.getSize());
    391             }
    392         }
    393     }
    394
    395     @Test
    396     void testToTree_closeApproximation() throws IOException {
    397         // arrange
    398         final Sphere s = Sphere.from(Vector3D.ZERO, 1, TEST_PRECISION);
    399
    400         // act
    401         final RegionBSPTree3D tree = s.toTree(8);
    402
    403         // assert
    404         checkBasicApproximationProperties(s, tree);
    405
    406         final double eps = 1e-3;
    407         Assertions.assertTrue(tree.isFinite());
    408         Assertions.assertEquals(s.getSize(), tree.getSize(), eps);
    409         Assertions.assertEquals(s.getBoundarySize(), tree.getBoundarySize(), eps);
    410         EuclideanTestUtils.assertCoordinatesEqual(s.getCentroid(), tree.getCentroid(), eps);
    411     }
    412
    413     @Test
    414     void testToTree_subdivideFails() {
    415         // arrange
    416         final Precision.DoubleEquivalence precision = Precision.doubleEquivalenceOfEpsilon(1e-5);
    417         final Sphere s = Sphere.from(Vector3D.ZERO, 1, precision);
    418
    419         // act/assert
    420         GeometryTestUtils.assertThrowsWithMessage(() -> {
    421             s.toTree(6);
    422         }, IllegalStateException.class,
    423                 Pattern.compile("^Failed to construct sphere approximation with subdivision count 6:.*"));
    424     }
    425
    426     @Test
    427     void testToTree_invalidArgs() {
    428         // arrange
    429         final Sphere s = Sphere.from(Vector3D.of(2, 1, 3), 2, TEST_PRECISION);
    430
    431         // act/assert
    432         GeometryTestUtils.assertThrowsWithMessage(() -> {
    433             s.toTree(-1);
    434         }, IllegalArgumentException.class,
    435                 "Number of sphere approximation subdivisions must be greater than or equal to zero; was -1");
    436     }
    437
    438     @Test
    439     void testToMesh_zeroSubdivisions() {
    440         // arrange
    441         final Sphere s = Sphere.from(Vector3D.of(1, 2, 3), 2, TEST_PRECISION);
    442
    443         // act
    444         final TriangleMesh mesh = s.toTriangleMesh(0);
    445
    446         // assert
    447         Assertions.assertEquals(6, mesh.getVertexCount());
    448         Assertions.assertEquals(8, mesh.getFaceCount());
    449
    450         final Bounds3D bounds = mesh.getBounds();
    451         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 1), bounds.getMin(), TEST_EPS);
    452         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 4, 5), bounds.getMax(), TEST_EPS);
    453
    454         Assertions.assertTrue(mesh.toTree().isFinite());
    455     }
    456
    457     @Test
    458     void testToMesh_manySubdivisions() {
    459         // arrange
    460         final Sphere s = Sphere.from(Vector3D.of(1, 2, 3), 2, TEST_PRECISION);
    461         final int subdivisions = 5;
    462
    463         // act
    464         final TriangleMesh mesh = s.toTriangleMesh(subdivisions);
    465
    466         // assert
    467         Assertions.assertEquals((int) (8 * Math.pow(4, subdivisions)), mesh.getFaceCount());
    468
    469         final Bounds3D bounds = mesh.getBounds();
    470         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 1), bounds.getMin(), TEST_EPS);
    471         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 4, 5), bounds.getMax(), TEST_EPS);
    472
    473         final RegionBSPTree3D tree = RegionBSPTree3D.partitionedRegionBuilder()
    474                 .insertAxisAlignedGrid(bounds, 3, TEST_PRECISION)
    475                 .insertBoundaries(mesh)
    476                 .build();
    477
    478         Assertions.assertTrue(tree.isFinite());
    479
    480         final double approximationEps = 0.1;
    481         Assertions.assertEquals(s.getSize(), tree.getSize(), approximationEps);
    482         Assertions.assertEquals(s.getBoundarySize(), tree.getBoundarySize(), approximationEps);
    483
    484         EuclideanTestUtils.assertCoordinatesEqual(s.getCentroid(), tree.getCentroid(), TEST_EPS);
    485     }
    486
    487     @Test
    488     void testToMesh_invalidArgs() {
    489         // arrange
    490         final Sphere s = Sphere.from(Vector3D.of(2, 1, 3), 2, TEST_PRECISION);
    491
    492         // act/assert
    493         GeometryTestUtils.assertThrowsWithMessage(() -> {
    494             s.toTriangleMesh(-1);
    495         }, IllegalArgumentException.class,
    496                 "Number of sphere approximation subdivisions must be greater than or equal to zero; was -1");
    497     }
    498
    499     @Test
    500     void testHashCode() {
    501         // arrange
    502         final Precision.DoubleEquivalence otherPrecision = Precision.doubleEquivalenceOfEpsilon(1e-2);
    503
    504         final Sphere a = Sphere.from(Vector3D.of(1, 2, 3), 3, TEST_PRECISION);
    505         final Sphere b = Sphere.from(Vector3D.of(1, 1, 3), 3, TEST_PRECISION);
    506         final Sphere c = Sphere.from(Vector3D.of(1, 2, 3), 4, TEST_PRECISION);
    507         final Sphere d = Sphere.from(Vector3D.of(1, 2, 3), 3, otherPrecision);
    508         final Sphere e = Sphere.from(Vector3D.of(1, 2, 3), 3, TEST_PRECISION);
    509
    510         // act
    511         final int hash = a.hashCode();
    512
    513         // act/assert
    514         Assertions.assertEquals(hash, a.hashCode());
    515
    516         Assertions.assertNotEquals(hash, b.hashCode());
    517         Assertions.assertNotEquals(hash, c.hashCode());
    518         Assertions.assertNotEquals(hash, d.hashCode());
    519
    520         Assertions.assertEquals(hash, e.hashCode());
    521     }
    522
    523     @Test
    524     void testEquals() {
    525         // arrange
    526         final Precision.DoubleEquivalence precision = Precision.doubleEquivalenceOfEpsilon(1e-2);
    527
    528         final Sphere a = Sphere.from(Vector3D.of(1, 2, 3), 3, TEST_PRECISION);
    529         final Sphere b = Sphere.from(Vector3D.of(1, 1, 3), 3, TEST_PRECISION);
    530         final Sphere c = Sphere.from(Vector3D.of(1, 2, 3), 4, TEST_PRECISION);
    531         final Sphere d = Sphere.from(Vector3D.of(1, 2, 3), 3, precision);
    532         final Sphere e = Sphere.from(Vector3D.of(1, 2, 3), 3, TEST_PRECISION);
    533
    534         // act/assert
    535         GeometryTestUtils.assertSimpleEqualsCases(a);
    536
    537         Assertions.assertNotEquals(a, b);
    538         Assertions.assertNotEquals(a, c);
    539         Assertions.assertNotEquals(a, d);
    540
    541         Assertions.assertEquals(a, e);
    542     }
    543
    544     @Test
    545     void testToString() {
    546         // arrange
    547         final Sphere c = Sphere.from(Vector3D.of(1, 2, 3), 3, TEST_PRECISION);
    548
    549         // act
    550         final String str = c.toString();
    551
    552         // assert
    553         Assertions.assertEquals("Sphere[center= (1.0, 2.0, 3.0), radius= 3.0]", str);
    554     }
    555
    556     private static void checkContains(final Sphere sphere, final boolean contains, final Vector3D... pts) {
    557         for (final Vector3D pt : pts) {
    558             Assertions.assertEquals(contains, sphere.contains(pt),
    559                     "Expected circle to " + (contains ? "" : "not") + "contain point " + pt);
    560         }
    561     }
    562
    563     private static void checkIntersections(final Sphere sphere, final Line3D line, final Vector3D... expectedPts) {
    564         // --- act
    565         // compute the intersections forward and reverse
    566         final List<Vector3D> actualPtsForward = sphere.intersections(line);
    567         final List<Vector3D> actualPtsReverse = sphere.intersections(line.reverse());
    568
    569         final Vector3D actualFirstForward = sphere.firstIntersection(line);
    570         final Vector3D actualFirstReverse = sphere.firstIntersection(line.reverse());
    571
    572         // --- assert
    573         final int len = expectedPts.length;
    574
    575         // check the lists
    576         Assertions.assertEquals(len, actualPtsForward.size());
    577         Assertions.assertEquals(len, actualPtsReverse.size());
    578
    579         for (int i = 0; i < len; ++i) {
    580             EuclideanTestUtils.assertCoordinatesEqual(expectedPts[i], actualPtsForward.get(i), TEST_EPS);
    581             Assertions.assertEquals(sphere.getRadius(), sphere.getCenter().distance(actualPtsForward.get(i)), TEST_EPS);
    582
    583             EuclideanTestUtils.assertCoordinatesEqual(expectedPts[len - i - 1], actualPtsReverse.get(i), TEST_EPS);
    584             Assertions.assertEquals(sphere.getRadius(), sphere.getCenter().distance(actualPtsReverse.get(i)), TEST_EPS);
    585         }
    586
    587         // check the single intersection points
    588         if (len > 0) {
    589             Assertions.assertNotNull(actualFirstForward);
    590             Assertions.assertNotNull(actualFirstReverse);
    591
    592             EuclideanTestUtils.assertCoordinatesEqual(expectedPts[0], actualFirstForward, TEST_EPS);
    593             EuclideanTestUtils.assertCoordinatesEqual(expectedPts[len - 1], actualFirstReverse, TEST_EPS);
    594         } else {
    595             Assertions.assertNull(actualFirstForward);
    596             Assertions.assertNull(actualFirstReverse);
    597         }
    598     }
    599
    600     private static void checkLinecast(final Sphere s, final LineConvexSubset3D segment, final Vector3D... expectedPts) {
    601         // check linecast
    602         final List<LinecastPoint3D> results = s.linecast(segment);
    603         Assertions.assertEquals(expectedPts.length, results.size());
    604
    605         LinecastPoint3D actual;
    606         Vector3D expected;
    607         for (int i = 0; i < expectedPts.length; ++i) {
    608             expected = expectedPts[i];
    609             actual = results.get(i);
    610
    611             EuclideanTestUtils.assertCoordinatesEqual(expected, actual.getPoint(), TEST_EPS);
    612             EuclideanTestUtils.assertCoordinatesEqual(s.getCenter().directionTo(expected), actual.getNormal(), TEST_EPS);
    613             Assertions.assertSame(segment.getLine(), actual.getLine());
    614         }
    615
    616         // check linecastFirst
    617         final LinecastPoint3D firstResult = s.linecastFirst(segment);
    618         if (expectedPts.length > 0) {
    619             Assertions.assertEquals(results.get(0), firstResult);
    620         } else {
    621             Assertions.assertNull(firstResult);
    622         }
    623     }
    624
    625     /**
    626      * Check a number of standard properties for bsp trees generated as sphere approximations.
    627      */
    628     private static void checkBasicApproximationProperties(final Sphere s, final RegionBSPTree3D tree) {
    629         Assertions.assertFalse(tree.isFull());
    630         Assertions.assertFalse(tree.isEmpty());
    631         Assertions.assertTrue(tree.isFinite());
    632         Assertions.assertFalse(tree.isInfinite());
    633
    634         // volume must be less than the sphere
    635         Assertions.assertTrue(tree.getSize() < s.getSize(), "Expected approximation volume to be less than circle");
    636
    637         // all vertices must be inside the sphere or on the boundary
    638         for (final PlaneConvexSubset boundary : tree.getBoundaries()) {
    639             Assertions.assertTrue(boundary.isFinite());
    640
    641             for (final Vector3D vertex : boundary.getVertices()) {
    642                 Assertions.assertTrue(s.contains(vertex), "Expected vertex to be contained in sphere: " + vertex);
    643             }
    644         }
    645
    646         // sphere must contain centroid
    647         EuclideanTestUtils.assertRegionLocation(s, RegionLocation.INSIDE, tree.getCentroid());
    648     }
    649 }