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