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 }