View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.geometry.euclidean.threed;
18  
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.Collections;
22  import java.util.function.BiFunction;
23  import java.util.function.ToDoubleFunction;
24  import java.util.regex.Pattern;
25  
26  import org.apache.commons.geometry.core.GeometryTestUtils;
27  import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
28  import org.apache.commons.geometry.euclidean.threed.shape.Parallelepiped;
29  import org.apache.commons.numbers.core.Precision;
30  import org.junit.jupiter.api.Assertions;
31  import org.junit.jupiter.api.Test;
32  
33  class Bounds3DTest {
34  
35      private static final double TEST_EPS = 1e-10;
36  
37      private static final Precision.DoubleEquivalence TEST_PRECISION =
38              Precision.doubleEquivalenceOfEpsilon(TEST_EPS);
39  
40      private static final String NO_POINTS_MESSAGE = "Cannot construct bounds: no points given";
41  
42      private static final Pattern INVALID_BOUNDS_PATTERN =
43              Pattern.compile("^Invalid bounds: min= \\([^\\)]+\\), max= \\([^\\)]+\\)");
44  
45      @Test
46      void testFrom_varargs_singlePoint() {
47          // arrange
48          final Vector3D p1 = Vector3D.of(-1, 2, -3);
49  
50          // act
51          final Bounds3D b = Bounds3D.from(p1);
52  
53          // assert
54          EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMin(), TEST_EPS);
55          EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMax(), TEST_EPS);
56          EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, b.getDiagonal(), TEST_EPS);
57          EuclideanTestUtils.assertCoordinatesEqual(p1, b.getCentroid(), TEST_EPS);
58      }
59  
60      @Test
61      void testFrom_varargs_multiplePoints() {
62          // arrange
63          final Vector3D p1 = Vector3D.of(1, 6, 7);
64          final Vector3D p2 = Vector3D.of(0, 5, 11);
65          final Vector3D p3 = Vector3D.of(3, 6, 8);
66  
67          // act
68          final Bounds3D b = Bounds3D.from(p1, p2, p3);
69  
70          // assert
71          EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 5, 7), b.getMin(), TEST_EPS);
72          EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 6, 11), b.getMax(), TEST_EPS);
73          EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 1, 4), b.getDiagonal(), TEST_EPS);
74          EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.5, 5.5, 9), b.getCentroid(), TEST_EPS);
75      }
76  
77      @Test
78      void testFrom_iterable_singlePoint() {
79          // arrange
80          final Vector3D p1 = Vector3D.of(-1, 2, -3);
81  
82          // act
83          final Bounds3D b = Bounds3D.from(Collections.singletonList(p1));
84  
85          // assert
86          EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMin(), TEST_EPS);
87          EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMax(), TEST_EPS);
88          EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, b.getDiagonal(), TEST_EPS);
89          EuclideanTestUtils.assertCoordinatesEqual(p1, b.getCentroid(), TEST_EPS);
90      }
91  
92      @Test
93      void testFrom_iterable_multiplePoints() {
94          // arrange
95          final Vector3D p1 = Vector3D.of(1, 6, 7);
96          final Vector3D p2 = Vector3D.of(2, 5, 9);
97          final Vector3D p3 = Vector3D.of(3, 4, 8);
98  
99          // act
100         final Bounds3D b = Bounds3D.from(Arrays.asList(p1, p2, p3));
101 
102         // assert
103         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 4, 7), b.getMin(), TEST_EPS);
104         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 6, 9), b.getMax(), TEST_EPS);
105         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 2, 2), b.getDiagonal(), TEST_EPS);
106         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 5, 8), b.getCentroid(), TEST_EPS);
107     }
108 
109     @Test
110     void testFrom_iterable_noPoints() {
111         // act/assert
112         GeometryTestUtils.assertThrowsWithMessage(() -> {
113             Bounds3D.from(new ArrayList<>());
114         }, IllegalStateException.class, NO_POINTS_MESSAGE);
115     }
116 
117     @Test
118     void testFrom_invalidBounds() {
119         // arrange
120         final Vector3D good = Vector3D.of(1, 1, 1);
121 
122         final Vector3D nan = Vector3D.of(Double.NaN, 1, 1);
123         final Vector3D posInf = Vector3D.of(1, Double.POSITIVE_INFINITY, 1);
124         final Vector3D negInf = Vector3D.of(1, 1, Double.NEGATIVE_INFINITY);
125 
126         // act/assert
127         GeometryTestUtils.assertThrowsWithMessage(() -> {
128             Bounds3D.from(Vector3D.NaN);
129         }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
130 
131         GeometryTestUtils.assertThrowsWithMessage(() -> {
132             Bounds3D.from(Vector3D.POSITIVE_INFINITY);
133         }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
134 
135         GeometryTestUtils.assertThrowsWithMessage(() -> {
136             Bounds3D.from(Vector3D.NEGATIVE_INFINITY);
137         }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
138 
139         GeometryTestUtils.assertThrowsWithMessage(() -> {
140             Bounds3D.from(good, nan);
141         }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
142 
143         GeometryTestUtils.assertThrowsWithMessage(() -> {
144             Bounds3D.from(posInf, good);
145         }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
146 
147         GeometryTestUtils.assertThrowsWithMessage(() -> {
148             Bounds3D.from(good, negInf, good);
149         }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
150     }
151 
152     @Test
153     void testHasSize() {
154         // arrange
155         final Precision.DoubleEquivalence low = Precision.doubleEquivalenceOfEpsilon(1e-2);
156         final Precision.DoubleEquivalence high = Precision.doubleEquivalenceOfEpsilon(1e-10);
157 
158         final Vector3D p1 = Vector3D.ZERO;
159 
160         final Vector3D p2 = Vector3D.of(1e-5, 1, 1);
161         final Vector3D p3 = Vector3D.of(1, 1e-5, 1);
162         final Vector3D p4 = Vector3D.of(1, 1, 1e-5);
163 
164         final Vector3D p5 = Vector3D.of(1, 1, 1);
165 
166         // act/assert
167         Assertions.assertFalse(Bounds3D.from(p1).hasSize(high));
168         Assertions.assertFalse(Bounds3D.from(p1).hasSize(low));
169 
170         Assertions.assertTrue(Bounds3D.from(p1, p2).hasSize(high));
171         Assertions.assertFalse(Bounds3D.from(p1, p2).hasSize(low));
172 
173         Assertions.assertTrue(Bounds3D.from(p1, p3).hasSize(high));
174         Assertions.assertFalse(Bounds3D.from(p1, p3).hasSize(low));
175 
176         Assertions.assertTrue(Bounds3D.from(p1, p4).hasSize(high));
177         Assertions.assertFalse(Bounds3D.from(p1, p4).hasSize(low));
178 
179         Assertions.assertTrue(Bounds3D.from(p1, p5).hasSize(high));
180         Assertions.assertTrue(Bounds3D.from(p1, p5).hasSize(low));
181     }
182 
183     @Test
184     void testContains_strict() {
185         // arrange
186         final Bounds3D b = Bounds3D.from(
187                 Vector3D.of(0, 4, 8),
188                 Vector3D.of(2, 6, 10));
189 
190         // act/assert
191         assertContainsStrict(b, true,
192                 b.getCentroid(),
193                 Vector3D.of(0, 4, 8), Vector3D.of(2, 6, 10),
194                 Vector3D.of(1, 5, 9),
195                 Vector3D.of(0, 5, 9), Vector3D.of(2, 5, 9),
196                 Vector3D.of(1, 4, 9), Vector3D.of(1, 6, 9),
197                 Vector3D.of(1, 5, 8), Vector3D.of(1, 5, 10));
198 
199         assertContainsStrict(b, false,
200                 Vector3D.ZERO,
201                 Vector3D.of(-1, 5, 9), Vector3D.of(3, 5, 9),
202                 Vector3D.of(1, 3, 9), Vector3D.of(1, 7, 9),
203                 Vector3D.of(1, 5, 7), Vector3D.of(1, 5, 11),
204                 Vector3D.of(-1e-15, 4, 8), Vector3D.of(2, 6 + 1e-15, 10), Vector3D.of(0, 4, 10 + 1e-15));
205     }
206 
207     @Test
208     void testContains_precision() {
209         // arrange
210         final Bounds3D b = Bounds3D.from(
211                 Vector3D.of(0, 4, 8),
212                 Vector3D.of(2, 6, 10));
213 
214         // act/assert
215         assertContainsWithPrecision(b, true,
216                 b.getCentroid(),
217                 Vector3D.of(0, 4, 8), Vector3D.of(2, 6, 10),
218                 Vector3D.of(1, 5, 9),
219                 Vector3D.of(0, 5, 9), Vector3D.of(2, 5, 9),
220                 Vector3D.of(1, 4, 9), Vector3D.of(1, 6, 9),
221                 Vector3D.of(1, 5, 8), Vector3D.of(1, 5, 10),
222                 Vector3D.of(-1e-15, 4, 8), Vector3D.of(2, 6 + 1e-15, 10), Vector3D.of(0, 4, 10 + 1e-15));
223 
224         assertContainsWithPrecision(b, false,
225                 Vector3D.ZERO,
226                 Vector3D.of(-1, 5, 9), Vector3D.of(3, 5, 9),
227                 Vector3D.of(1, 3, 9), Vector3D.of(1, 7, 9),
228                 Vector3D.of(1, 5, 7), Vector3D.of(1, 5, 11));
229     }
230 
231     @Test
232     void testIntersects() {
233         // arrange
234         final Bounds3D b = Bounds3D.from(Vector3D.ZERO, Vector3D.of(1, 1, 1));
235 
236         // act/assert
237         checkIntersects(b, Vector3D::getX, (v, x) -> Vector3D.of(x, v.getY(), v.getZ()));
238         checkIntersects(b, Vector3D::getY, (v, y) -> Vector3D.of(v.getX(), y, v.getZ()));
239         checkIntersects(b, Vector3D::getZ, (v, z) -> Vector3D.of(v.getX(), v.getY(), z));
240     }
241 
242     private void checkIntersects(final Bounds3D b, final ToDoubleFunction<? super Vector3D> getter,
243                                  final BiFunction<? super Vector3D, Double, ? extends Vector3D> setter) {
244 
245         final Vector3D min = b.getMin();
246         final Vector3D max = b.getMax();
247 
248         final double minValue = getter.applyAsDouble(min);
249         final double maxValue = getter.applyAsDouble(max);
250         final double midValue = (0.5 * (maxValue - minValue)) + minValue;
251 
252         // check all possible interval relationships
253 
254         // start below minValue
255         Assertions.assertFalse(b.intersects(Bounds3D.from(
256                 setter.apply(min, minValue - 2), setter.apply(max, minValue - 1))));
257 
258         Assertions.assertTrue(b.intersects(Bounds3D.from(
259                 setter.apply(min, minValue - 2), setter.apply(max, minValue))));
260         Assertions.assertTrue(b.intersects(Bounds3D.from(
261                 setter.apply(min, minValue - 2), setter.apply(max, midValue))));
262         Assertions.assertTrue(b.intersects(Bounds3D.from(
263                 setter.apply(min, minValue - 2), setter.apply(max, maxValue))));
264         Assertions.assertTrue(b.intersects(Bounds3D.from(
265                 setter.apply(min, minValue - 2), setter.apply(max, maxValue + 1))));
266 
267         // start on minValue
268         Assertions.assertTrue(b.intersects(Bounds3D.from(
269                 setter.apply(min, minValue), setter.apply(max, minValue))));
270         Assertions.assertTrue(b.intersects(Bounds3D.from(
271                 setter.apply(min, minValue), setter.apply(max, midValue))));
272         Assertions.assertTrue(b.intersects(Bounds3D.from(
273                 setter.apply(min, minValue), setter.apply(max, maxValue))));
274         Assertions.assertTrue(b.intersects(Bounds3D.from(
275                 setter.apply(min, minValue), setter.apply(max, maxValue + 1))));
276 
277         // start on midValue
278         Assertions.assertTrue(b.intersects(Bounds3D.from(
279                 setter.apply(min, midValue), setter.apply(max, midValue))));
280         Assertions.assertTrue(b.intersects(Bounds3D.from(
281                 setter.apply(min, midValue), setter.apply(max, maxValue))));
282         Assertions.assertTrue(b.intersects(Bounds3D.from(
283                 setter.apply(min, midValue), setter.apply(max, maxValue + 1))));
284 
285         // start on maxValue
286         Assertions.assertTrue(b.intersects(Bounds3D.from(
287                 setter.apply(min, maxValue), setter.apply(max, maxValue))));
288         Assertions.assertTrue(b.intersects(Bounds3D.from(
289                 setter.apply(min, maxValue), setter.apply(max, maxValue + 1))));
290 
291         // start above maxValue
292         Assertions.assertFalse(b.intersects(Bounds3D.from(
293                 setter.apply(min, maxValue + 1), setter.apply(max, maxValue + 2))));
294     }
295 
296     @Test
297     void testIntersection() {
298         // -- arrange
299         final Bounds3D b = Bounds3D.from(Vector3D.ZERO, Vector3D.of(1, 1, 1));
300 
301         // -- act/assert
302 
303         // move along x-axis
304         Assertions.assertNull(b.intersection(Bounds3D.from(Vector3D.of(-2, 0, 0), Vector3D.of(-1, 1, 1))));
305         checkIntersection(b, Vector3D.of(-1, 0, 0), Vector3D.of(0, 1, 1),
306                 Vector3D.of(0, 0, 0), Vector3D.of(0, 1, 1));
307         checkIntersection(b, Vector3D.of(-1, 0, 0), Vector3D.of(0.5, 1, 1),
308                 Vector3D.of(0, 0, 0), Vector3D.of(0.5, 1, 1));
309         checkIntersection(b, Vector3D.of(-1, 0, 0), Vector3D.of(1, 1, 1),
310                 Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
311         checkIntersection(b, Vector3D.of(-1, 0, 0), Vector3D.of(2, 1, 1),
312                 Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
313         checkIntersection(b, Vector3D.of(0, 0, 0), Vector3D.of(2, 1, 1),
314                 Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
315         checkIntersection(b, Vector3D.of(0.5, 0, 0), Vector3D.of(2, 1, 1),
316                 Vector3D.of(0.5, 0, 0), Vector3D.of(1, 1, 1));
317         checkIntersection(b, Vector3D.of(1, 0, 0), Vector3D.of(2, 1, 1),
318                 Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 1));
319         Assertions.assertNull(b.intersection(Bounds3D.from(Vector3D.of(2, 0, 0), Vector3D.of(3, 1, 1))));
320 
321         // move along y-axis
322         Assertions.assertNull(b.intersection(Bounds3D.from(Vector3D.of(0, -2, 0), Vector3D.of(1, -1, 1))));
323         checkIntersection(b, Vector3D.of(0, -1, 0), Vector3D.of(1, 0, 1),
324                 Vector3D.of(0, 0, 0), Vector3D.of(1, 0, 1));
325         checkIntersection(b, Vector3D.of(0, -1, 0), Vector3D.of(1, 0.5, 1),
326                 Vector3D.of(0, 0, 0), Vector3D.of(1, 0.5, 1));
327         checkIntersection(b, Vector3D.of(0, -1, 0), Vector3D.of(1, 1, 1),
328                 Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
329         checkIntersection(b, Vector3D.of(0, -1, 0), Vector3D.of(1, 2, 1),
330                 Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
331         checkIntersection(b, Vector3D.of(0, 0, 0), Vector3D.of(1, 2, 1),
332                 Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
333         checkIntersection(b, Vector3D.of(0, 0.5, 0), Vector3D.of(1, 2, 1),
334                 Vector3D.of(0, 0.5, 0), Vector3D.of(1, 1, 1));
335         checkIntersection(b, Vector3D.of(0, 1, 0), Vector3D.of(1, 2, 1),
336                 Vector3D.of(0, 1, 0), Vector3D.of(1, 1, 1));
337         Assertions.assertNull(b.intersection(Bounds3D.from(Vector3D.of(0, 2, 0), Vector3D.of(1, 3, 1))));
338 
339         // move along z-axis
340         Assertions.assertNull(b.intersection(Bounds3D.from(Vector3D.of(0, 0, -2), Vector3D.of(1, 1, -1))));
341         checkIntersection(b, Vector3D.of(0, 0, -1), Vector3D.of(1, 1, 0),
342                 Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 0));
343         checkIntersection(b, Vector3D.of(0, 0, -1), Vector3D.of(1, 1, 0.5),
344                 Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 0.5));
345         checkIntersection(b, Vector3D.of(0, 0, -1), Vector3D.of(1, 1, 1),
346                 Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
347         checkIntersection(b, Vector3D.of(0, 0, -1), Vector3D.of(1, 1, 2),
348                 Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
349         checkIntersection(b, Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 2),
350                 Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
351         checkIntersection(b, Vector3D.of(0, 0, 0.5), Vector3D.of(1, 1, 2),
352                 Vector3D.of(0, 0, 0.5), Vector3D.of(1, 1, 1));
353         checkIntersection(b, Vector3D.of(0, 0, 1), Vector3D.of(1, 1, 2),
354                 Vector3D.of(0, 0, 1), Vector3D.of(1, 1, 1));
355         Assertions.assertNull(b.intersection(Bounds3D.from(Vector3D.of(0, 0, 2), Vector3D.of(1, 1, 3))));
356     }
357 
358     private void checkIntersection(final Bounds3D b, final Vector3D a1, final Vector3D a2, final Vector3D r1, final Vector3D r2) {
359         final Bounds3D a = Bounds3D.from(a1, a2);
360         final Bounds3D result = b.intersection(a);
361 
362         checkBounds(result, r1, r2);
363     }
364 
365     @Test
366     void toRegion() {
367         // arrange
368         final Bounds3D b = Bounds3D.from(
369                 Vector3D.of(0, 4, 8),
370                 Vector3D.of(2, 6, 10));
371 
372         // act
373         final Parallelepiped p = b.toRegion(TEST_PRECISION);
374 
375         // assert
376         Assertions.assertEquals(8, p.getSize(), TEST_EPS);
377         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 5, 9), p.getCentroid(), TEST_EPS);
378     }
379 
380     @Test
381     void toRegion_boundingBoxTooSmall() {
382         // act/assert
383         Assertions.assertThrows(IllegalArgumentException.class, () -> Bounds3D.from(Vector3D.ZERO, Vector3D.of(1e-12, 1e-12, 1e-12))
384                 .toRegion(TEST_PRECISION));
385     }
386 
387     @Test
388     void testEq() {
389         // arrange
390         final Precision.DoubleEquivalence low = Precision.doubleEquivalenceOfEpsilon(1e-2);
391         final Precision.DoubleEquivalence high = Precision.doubleEquivalenceOfEpsilon(1e-10);
392 
393         final Bounds3D b1 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
394 
395         final Bounds3D b2 = Bounds3D.from(Vector3D.of(1.1, 1, 1), Vector3D.of(2, 2, 2));
396         final Bounds3D b3 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(1.9, 2, 2));
397 
398         final Bounds3D b4 = Bounds3D.from(Vector3D.of(1.001, 1.001, 1.001), Vector3D.of(2.001, 2.001, 2.001));
399 
400         // act/assert
401         Assertions.assertTrue(b1.eq(b1, low));
402 
403         Assertions.assertFalse(b1.eq(b2, low));
404         Assertions.assertFalse(b1.eq(b3, low));
405 
406         Assertions.assertTrue(b1.eq(b4, low));
407         Assertions.assertTrue(b4.eq(b1, low));
408 
409         Assertions.assertFalse(b1.eq(b4, high));
410         Assertions.assertFalse(b4.eq(b1, high));
411     }
412 
413     @Test
414     void testHashCode() {
415         // arrange
416         final Bounds3D b1 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
417 
418         final Bounds3D b2 = Bounds3D.from(Vector3D.of(-2, 1, 1), Vector3D.of(2, 2, 2));
419         final Bounds3D b3 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(3, 2, 2));
420         final Bounds3D b4 = Bounds3D.from(Vector3D.of(1 + 1e-15, 1, 1), Vector3D.of(2, 2, 2));
421         final Bounds3D b5 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2 + 1e-15, 2, 2));
422 
423         final Bounds3D b6 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
424 
425         // act
426         final int hash = b1.hashCode();
427 
428         // assert
429         Assertions.assertEquals(hash, b1.hashCode());
430 
431         Assertions.assertNotEquals(hash, b2.hashCode());
432         Assertions.assertNotEquals(hash, b3.hashCode());
433         Assertions.assertNotEquals(hash, b4.hashCode());
434         Assertions.assertNotEquals(hash, b5.hashCode());
435 
436         Assertions.assertEquals(hash, b6.hashCode());
437     }
438 
439     @Test
440     void testEquals() {
441         // arrange
442         final Bounds3D b1 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
443 
444         final Bounds3D b2 = Bounds3D.from(Vector3D.of(-1, 1, 1), Vector3D.of(2, 2, 2));
445         final Bounds3D b3 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(3, 2, 2));
446         final Bounds3D b4 = Bounds3D.from(Vector3D.of(1 + 1e-15, 1, 1), Vector3D.of(2, 2, 2));
447         final Bounds3D b5 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2 + 1e-15, 2, 2));
448 
449         final Bounds3D b6 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
450 
451         // act/assert
452         GeometryTestUtils.assertSimpleEqualsCases(b1);
453 
454         Assertions.assertNotEquals(b1, b2);
455         Assertions.assertNotEquals(b1, b3);
456         Assertions.assertNotEquals(b1, b4);
457         Assertions.assertNotEquals(b1, b5);
458 
459         Assertions.assertEquals(b1, b6);
460     }
461 
462     @Test
463     void testToString() {
464         // arrange
465         final Bounds3D b = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
466 
467         // act
468         final String str = b.toString();
469 
470         // assert
471         GeometryTestUtils.assertContains("Bounds3D[min= (1", str);
472         GeometryTestUtils.assertContains(", max= (2", str);
473     }
474 
475     @Test
476     void testBuilder_addMethods() {
477         // arrange
478         final Vector3D p1 = Vector3D.of(1, 10, 11);
479         final Vector3D p2 = Vector3D.of(2, 9, 12);
480         final Vector3D p3 = Vector3D.of(3, 8, 13);
481         final Vector3D p4 = Vector3D.of(4, 7, 14);
482         final Vector3D p5 = Vector3D.of(5, 6, 15);
483 
484         // act
485         final Bounds3D b = Bounds3D.builder()
486                 .add(p1)
487                 .addAll(Arrays.asList(p2, p3))
488                 .add(Bounds3D.from(p4, p5))
489                 .build();
490 
491         // assert
492         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 6, 11), b.getMin(), TEST_EPS);
493         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(5, 10, 15), b.getMax(), TEST_EPS);
494         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 8, 13), b.getCentroid(), TEST_EPS);
495     }
496 
497     @Test
498     void testBuilder_hasBounds() {
499         // act/assert
500         Assertions.assertFalse(Bounds3D.builder().hasBounds());
501 
502         Assertions.assertFalse(Bounds3D.builder().add(Vector3D.of(Double.NaN, 1, 1)).hasBounds());
503         Assertions.assertFalse(Bounds3D.builder().add(Vector3D.of(1, Double.NaN, 1)).hasBounds());
504         Assertions.assertFalse(Bounds3D.builder().add(Vector3D.of(1, 1, Double.NaN)).hasBounds());
505 
506         Assertions.assertFalse(Bounds3D.builder().add(Vector3D.of(Double.POSITIVE_INFINITY, 1, 1)).hasBounds());
507         Assertions.assertFalse(Bounds3D.builder().add(Vector3D.of(1, Double.POSITIVE_INFINITY, 1)).hasBounds());
508         Assertions.assertFalse(Bounds3D.builder().add(Vector3D.of(1, 1, Double.POSITIVE_INFINITY)).hasBounds());
509 
510         Assertions.assertFalse(Bounds3D.builder().add(Vector3D.of(Double.NEGATIVE_INFINITY, 1, 1)).hasBounds());
511         Assertions.assertFalse(Bounds3D.builder().add(Vector3D.of(1, Double.NEGATIVE_INFINITY, 1)).hasBounds());
512         Assertions.assertFalse(Bounds3D.builder().add(Vector3D.of(1, 1, Double.NEGATIVE_INFINITY)).hasBounds());
513 
514         Assertions.assertTrue(Bounds3D.builder().add(Vector3D.ZERO).hasBounds());
515     }
516 
517     private static void checkBounds(final Bounds3D b, final Vector3D min, final Vector3D max) {
518         EuclideanTestUtils.assertCoordinatesEqual(min, b.getMin(), TEST_EPS);
519         EuclideanTestUtils.assertCoordinatesEqual(max, b.getMax(), TEST_EPS);
520     }
521 
522     private static void assertContainsStrict(final Bounds3D bounds, final boolean contains, final Vector3D... pts) {
523         for (final Vector3D pt : pts) {
524             Assertions.assertEquals(contains, bounds.contains(pt), "Unexpected location for point " + pt);
525         }
526     }
527 
528     private static void assertContainsWithPrecision(final Bounds3D bounds, final boolean contains, final Vector3D... pts) {
529         for (final Vector3D pt : pts) {
530             Assertions.assertEquals(contains, bounds.contains(pt, TEST_PRECISION), "Unexpected location for point " + pt);
531         }
532     }
533 }