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.Arrays;
20  import java.util.List;
21  import java.util.regex.Pattern;
22  import java.util.stream.Collectors;
23  
24  import org.apache.commons.geometry.core.GeometryTestUtils;
25  import org.apache.commons.geometry.core.RegionLocation;
26  import org.apache.commons.geometry.core.Transform;
27  import org.apache.commons.geometry.core.partitioning.Split;
28  import org.apache.commons.geometry.core.partitioning.SplitLocation;
29  import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
30  import org.apache.commons.geometry.euclidean.threed.line.Lines3D;
31  import org.apache.commons.geometry.euclidean.twod.ConvexArea;
32  import org.apache.commons.numbers.core.Precision;
33  import org.junit.jupiter.api.Assertions;
34  import org.junit.jupiter.api.Test;
35  
36  class ConvexVolumeTest {
37  
38      private static final double TEST_EPS = 1e-10;
39  
40      private static final Precision.DoubleEquivalence TEST_PRECISION =
41              Precision.doubleEquivalenceOfEpsilon(TEST_EPS);
42  
43      @Test
44      void testFull() {
45          // act
46          final ConvexVolume vol = ConvexVolume.full();
47  
48          // assert
49          Assertions.assertTrue(vol.isFull());
50          Assertions.assertFalse(vol.isEmpty());
51  
52          GeometryTestUtils.assertPositiveInfinity(vol.getSize());
53          Assertions.assertNull(vol.getCentroid());
54  
55          Assertions.assertEquals(0, vol.getBoundaries().size());
56          Assertions.assertEquals(0, vol.getBoundarySize(), TEST_EPS);
57      }
58  
59      @Test
60      void testBoundaryStream() {
61          // arrange
62          final Plane plane = Planes.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION);
63          final ConvexVolume volume = ConvexVolume.fromBounds(plane);
64  
65          // act
66          final List<PlaneConvexSubset> boundaries = volume.boundaryStream().collect(Collectors.toList());
67  
68          // assert
69          Assertions.assertEquals(1, boundaries.size());
70  
71          final PlaneConvexSubset sp = boundaries.get(0);
72          Assertions.assertEquals(0, sp.getEmbedded().getSubspaceRegion().getBoundaries().size());
73          EuclideanTestUtils.assertCoordinatesEqual(plane.getOrigin(), sp.getPlane().getOrigin(), TEST_EPS);
74          EuclideanTestUtils.assertCoordinatesEqual(plane.getNormal(), sp.getPlane().getNormal(), TEST_EPS);
75      }
76  
77      @Test
78      void testBoundaryStream_noBoundaries() {
79          // arrange
80          final ConvexVolume volume = ConvexVolume.full();
81  
82          // act
83          final List<PlaneConvexSubset> boundaries = volume.boundaryStream().collect(Collectors.toList());
84  
85          // assert
86          Assertions.assertEquals(0, boundaries.size());
87      }
88  
89      @Test
90      void testTriangleStream_noBoundaries() {
91          // arrange
92          final ConvexVolume full = ConvexVolume.full();
93  
94          // act
95          final List<Triangle3D> tris = full.triangleStream().collect(Collectors.toList());
96  
97          // act/assert
98          Assertions.assertEquals(0, tris.size());
99      }
100 
101     @Test
102     void testTriangleStream_infinite() {
103         // arrange
104         final Pattern pattern = Pattern.compile("^Cannot convert infinite plane subset to triangles: .*");
105 
106         final ConvexVolume half = ConvexVolume.fromBounds(
107                 Planes.fromNormal(Vector3D.Unit.MINUS_X, TEST_PRECISION)
108             );
109 
110         final ConvexVolume quadrant = ConvexVolume.fromBounds(
111                     Planes.fromNormal(Vector3D.Unit.MINUS_X, TEST_PRECISION),
112                     Planes.fromNormal(Vector3D.Unit.MINUS_Y, TEST_PRECISION),
113                     Planes.fromNormal(Vector3D.Unit.MINUS_Z, TEST_PRECISION)
114                 );
115 
116         // act/assert
117         GeometryTestUtils.assertThrowsWithMessage(() -> {
118             half.triangleStream().collect(Collectors.toList());
119         }, IllegalStateException.class, pattern);
120 
121         GeometryTestUtils.assertThrowsWithMessage(() -> {
122             quadrant.triangleStream().collect(Collectors.toList());
123         }, IllegalStateException.class, pattern);
124     }
125 
126     @Test
127     void testTriangleStream_finite() {
128         // arrange
129         final Vector3D min = Vector3D.ZERO;
130         final Vector3D max = Vector3D.of(1, 1, 1);
131 
132         final ConvexVolume box = ConvexVolume.fromBounds(
133                     Planes.fromPointAndNormal(min, Vector3D.Unit.MINUS_X, TEST_PRECISION),
134                     Planes.fromPointAndNormal(min, Vector3D.Unit.MINUS_Y, TEST_PRECISION),
135                     Planes.fromPointAndNormal(min, Vector3D.Unit.MINUS_Z, TEST_PRECISION),
136 
137                     Planes.fromPointAndNormal(max, Vector3D.Unit.PLUS_X, TEST_PRECISION),
138                     Planes.fromPointAndNormal(max, Vector3D.Unit.PLUS_Y, TEST_PRECISION),
139                     Planes.fromPointAndNormal(max, Vector3D.Unit.PLUS_Z, TEST_PRECISION)
140                 );
141 
142         // act
143         final List<Triangle3D> tris = box.triangleStream().collect(Collectors.toList());
144 
145         // assert
146         Assertions.assertEquals(12, tris.size());
147 
148         final Bounds3D.Builder boundsBuilder = Bounds3D.builder();
149         tris.forEach(t -> boundsBuilder.addAll(t.getVertices()));
150 
151         final Bounds3D bounds = boundsBuilder.build();
152         EuclideanTestUtils.assertCoordinatesEqual(min, bounds.getMin(), TEST_EPS);
153         EuclideanTestUtils.assertCoordinatesEqual(max, bounds.getMax(), TEST_EPS);
154     }
155 
156     @Test
157     void testGetBounds_noBounds() {
158         // arrange
159         final ConvexVolume full = ConvexVolume.full();
160         final ConvexVolume halfFull = ConvexVolume.fromBounds(Planes.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION));
161 
162         // act/assert
163         Assertions.assertNull(full.getBounds());
164         Assertions.assertNull(halfFull.getBounds());
165     }
166 
167     @Test
168     void testGetBounds_hasBounds() {
169         // arrange
170         final ConvexVolume vol = rect(Vector3D.of(1, 1, 1), 0.5, 1, 2);
171 
172         // act
173         final Bounds3D bounds = vol.getBounds();
174 
175         // assert
176         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0, -1), bounds.getMin(), TEST_EPS);
177         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.5, 2, 3), bounds.getMax(), TEST_EPS);
178     }
179 
180     @Test
181     void testToList_full() {
182         // arrange
183         final ConvexVolume volume = ConvexVolume.full();
184 
185         // act
186         final BoundaryList3D list = volume.toList();
187 
188         // assert
189         Assertions.assertEquals(0, list.count());
190     }
191 
192     @Test
193     void testToList() {
194         // arrange
195         final ConvexVolume volume = ConvexVolume.fromBounds(
196                     Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.MINUS_X, TEST_PRECISION),
197                     Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.MINUS_Y, TEST_PRECISION),
198                     Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.MINUS_Z, TEST_PRECISION),
199 
200                     Planes.fromPointAndNormal(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X, TEST_PRECISION),
201                     Planes.fromPointAndNormal(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Y, TEST_PRECISION),
202                     Planes.fromPointAndNormal(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Z, TEST_PRECISION)
203                 );
204 
205         // act
206         final BoundaryList3D list = volume.toList();
207 
208         // assert
209         Assertions.assertEquals(6, list.count());
210         Assertions.assertEquals(1, list.toTree().getSize(), TEST_EPS);
211     }
212 
213     @Test
214     void testToTree_full() {
215         // arrange
216         final ConvexVolume volume = ConvexVolume.full();
217 
218         // act
219         final RegionBSPTree3D tree = volume.toTree();
220 
221         // assert
222         Assertions.assertTrue(tree.isFull());
223         Assertions.assertFalse(tree.isEmpty());
224     }
225 
226     @Test
227     void testToTree() {
228         // arrange
229         final ConvexVolume volume = ConvexVolume.fromBounds(
230                     Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.MINUS_X, TEST_PRECISION),
231                     Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.MINUS_Y, TEST_PRECISION),
232                     Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.MINUS_Z, TEST_PRECISION),
233 
234                     Planes.fromPointAndNormal(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X, TEST_PRECISION),
235                     Planes.fromPointAndNormal(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Y, TEST_PRECISION),
236                     Planes.fromPointAndNormal(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Z, TEST_PRECISION)
237                 );
238 
239         // act
240         final RegionBSPTree3D tree = volume.toTree();
241 
242         // assert
243         Assertions.assertEquals(1, tree.getSize(), TEST_EPS);
244         Assertions.assertEquals(6, tree.getBoundarySize(), TEST_EPS);
245         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), tree.getCentroid(), TEST_EPS);
246 
247         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.OUTSIDE,
248                 Vector3D.of(-1, 0.5, 0.5), Vector3D.of(2, 0.5, 0.5),
249                 Vector3D.of(0.5, -1, 0.5), Vector3D.of(0.5, 2, 0.5),
250                 Vector3D.of(0.5, 0.5, -1), Vector3D.of(0.5, 0.5, 2));
251         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.BOUNDARY, Vector3D.ZERO);
252         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.INSIDE, Vector3D.of(0.5, 0.5, 0.5));
253     }
254 
255     @Test
256     void testFromBounds_noPlanes() {
257         // act
258         final ConvexVolume vol = ConvexVolume.fromBounds();
259 
260         // assert
261         Assertions.assertSame(ConvexVolume.full(), vol);
262     }
263 
264     @Test
265     void testFromBounds_halfspace() {
266         // act
267         final ConvexVolume vol = ConvexVolume.fromBounds(Planes.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION));
268 
269         // assert
270         Assertions.assertFalse(vol.isFull());
271         Assertions.assertFalse(vol.isEmpty());
272 
273         GeometryTestUtils.assertPositiveInfinity(vol.getSize());
274         Assertions.assertNull(vol.getCentroid());
275 
276         Assertions.assertEquals(1, vol.getBoundaries().size());
277         GeometryTestUtils.assertPositiveInfinity(vol.getBoundarySize());
278 
279         EuclideanTestUtils.assertRegionLocation(vol, RegionLocation.OUTSIDE, Vector3D.of(0, 0, 1));
280         EuclideanTestUtils.assertRegionLocation(vol, RegionLocation.BOUNDARY, Vector3D.of(0, 0, 0));
281         EuclideanTestUtils.assertRegionLocation(vol, RegionLocation.INSIDE, Vector3D.of(0, 0, -1));
282     }
283 
284     @Test
285     void testFromBounds_cube() {
286         // act
287         final ConvexVolume vol = rect(Vector3D.of(1, 1, 1), 0.5, 1, 2);
288 
289         // assert
290         Assertions.assertFalse(vol.isFull());
291         Assertions.assertFalse(vol.isEmpty());
292 
293         Assertions.assertEquals(8, vol.getSize(), TEST_EPS);
294         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, 1), vol.getCentroid(), TEST_EPS);
295 
296         Assertions.assertEquals(6, vol.getBoundaries().size());
297         Assertions.assertEquals(28, vol.getBoundarySize(), TEST_EPS);
298 
299         EuclideanTestUtils.assertRegionLocation(vol, RegionLocation.INSIDE, Vector3D.of(1, 1, 1));
300 
301         EuclideanTestUtils.assertRegionLocation(vol, RegionLocation.BOUNDARY,
302                 Vector3D.of(0.5, 0, -1), Vector3D.of(1.5, 2, 3));
303 
304         EuclideanTestUtils.assertRegionLocation(vol, RegionLocation.OUTSIDE,
305                 Vector3D.of(0, 1, 1), Vector3D.of(2, 1, 1),
306                 Vector3D.of(1, -1, 1), Vector3D.of(1, 3, 1),
307                 Vector3D.of(1, 1, -2), Vector3D.of(1, 1, 4));
308     }
309 
310     @Test
311     void testTrim() {
312         // arrange
313         final ConvexVolume vol = rect(Vector3D.ZERO, 0.5, 0.5, 0.5);
314 
315         final PlaneConvexSubset subplane = Planes.subsetFromConvexArea(
316                 Planes.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION).getEmbedding(), ConvexArea.full());
317 
318         // act
319         final PlaneConvexSubset trimmed = vol.trim(subplane);
320 
321         // assert
322         Assertions.assertEquals(1, trimmed.getSize(), TEST_EPS);
323 
324         final List<Vector3D> vertices = trimmed.getVertices();
325 
326         Assertions.assertEquals(4, vertices.size());
327         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0.5, -0.5), vertices.get(0), TEST_EPS);
328         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0.5, 0.5), vertices.get(1), TEST_EPS);
329         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, -0.5, 0.5), vertices.get(2), TEST_EPS);
330         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, -0.5, -0.5), vertices.get(3), TEST_EPS);
331     }
332 
333     @Test
334     void testSplit() {
335         // arrange
336         final ConvexVolume vol = rect(Vector3D.ZERO, 0.5, 0.5, 0.5);
337 
338         final Plane splitter = Planes.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION);
339 
340         // act
341         final Split<ConvexVolume> split = vol.split(splitter);
342 
343         // assert
344         Assertions.assertEquals(SplitLocation.BOTH, split.getLocation());
345 
346         final ConvexVolume minus = split.getMinus();
347         Assertions.assertEquals(0.5, minus.getSize(), TEST_EPS);
348         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-0.25, 0, 0), minus.getCentroid(), TEST_EPS);
349 
350         final ConvexVolume plus = split.getPlus();
351         Assertions.assertEquals(0.5, plus.getSize(), TEST_EPS);
352         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.25, 0, 0), plus.getCentroid(), TEST_EPS);
353     }
354 
355     @Test
356     void testLinecast_full() {
357         // arrange
358         final ConvexVolume volume = ConvexVolume.full();
359 
360         // act/assert
361         LinecastChecker3D.with(volume)
362             .expectNothing()
363             .whenGiven(Lines3D.fromPoints(Vector3D.ZERO, Vector3D.Unit.PLUS_X, TEST_PRECISION));
364 
365         LinecastChecker3D.with(volume)
366             .expectNothing()
367             .whenGiven(Lines3D.segmentFromPoints(Vector3D.Unit.MINUS_X, Vector3D.Unit.PLUS_X, TEST_PRECISION));
368     }
369 
370     @Test
371     void testLinecast() {
372         // arrange
373         final ConvexVolume volume = rect(Vector3D.of(0.5, 0.5, 0.5), 0.5, 0.5, 0.5);
374 
375         // act/assert
376         LinecastChecker3D.with(volume)
377             .expectNothing()
378             .whenGiven(Lines3D.fromPoints(Vector3D.of(0, 5, 5), Vector3D.of(1, 5, 5), TEST_PRECISION));
379 
380         LinecastChecker3D.with(volume)
381             .expect(Vector3D.ZERO, Vector3D.Unit.MINUS_X)
382             .and(Vector3D.ZERO, Vector3D.Unit.MINUS_Y)
383             .and(Vector3D.ZERO, Vector3D.Unit.MINUS_Z)
384             .and(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Z)
385             .and(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Y)
386             .and(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X)
387             .whenGiven(Lines3D.fromPoints(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION));
388 
389         LinecastChecker3D.with(volume)
390             .expect(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Z)
391             .and(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Y)
392             .and(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X)
393             .whenGiven(Lines3D.segmentFromPoints(Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(1, 1, 1), TEST_PRECISION));
394     }
395 
396     @Test
397     void testTransform() {
398         // arrange
399         final ConvexVolume vol = rect(Vector3D.ZERO, 0.5, 0.5, 0.5);
400 
401         final Transform<Vector3D> transform = AffineTransformMatrix3D.identity()
402                 .translate(Vector3D.of(1, 2, 3))
403                 .scale(Vector3D.of(2, 1, 1));
404 
405         // act
406         final ConvexVolume transformed = vol.transform(transform);
407 
408         // assert
409         Assertions.assertEquals(2, transformed.getSize(), TEST_EPS);
410         Assertions.assertEquals(10, transformed.getBoundarySize(), TEST_EPS);
411 
412         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 2, 3), transformed.getCentroid(), TEST_EPS);
413     }
414 
415     private static ConvexVolume rect(final Vector3D center, final double xDelta, final double yDelta, final double zDelta) {
416         final List<Plane> planes = Arrays.asList(
417                     Planes.fromPointAndNormal(center.add(Vector3D.of(xDelta, 0, 0)), Vector3D.Unit.PLUS_X, TEST_PRECISION),
418                     Planes.fromPointAndNormal(center.add(Vector3D.of(-xDelta, 0, 0)), Vector3D.Unit.MINUS_X, TEST_PRECISION),
419 
420                     Planes.fromPointAndNormal(center.add(Vector3D.of(0, yDelta, 0)), Vector3D.Unit.PLUS_Y, TEST_PRECISION),
421                     Planes.fromPointAndNormal(center.add(Vector3D.of(0, -yDelta, 0)), Vector3D.Unit.MINUS_Y, TEST_PRECISION),
422 
423                     Planes.fromPointAndNormal(center.add(Vector3D.of(0, 0, zDelta)), Vector3D.Unit.PLUS_Z, TEST_PRECISION),
424                     Planes.fromPointAndNormal(center.add(Vector3D.of(0, 0, -zDelta)), Vector3D.Unit.MINUS_Z, TEST_PRECISION)
425                 );
426 
427         return ConvexVolume.fromBounds(planes);
428     }
429 }