1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
46 final ConvexVolume vol = ConvexVolume.full();
47
48
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
62 final Plane plane = Planes.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION);
63 final ConvexVolume volume = ConvexVolume.fromBounds(plane);
64
65
66 final List<PlaneConvexSubset> boundaries = volume.boundaryStream().collect(Collectors.toList());
67
68
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
80 final ConvexVolume volume = ConvexVolume.full();
81
82
83 final List<PlaneConvexSubset> boundaries = volume.boundaryStream().collect(Collectors.toList());
84
85
86 Assertions.assertEquals(0, boundaries.size());
87 }
88
89 @Test
90 void testTriangleStream_noBoundaries() {
91
92 final ConvexVolume full = ConvexVolume.full();
93
94
95 final List<Triangle3D> tris = full.triangleStream().collect(Collectors.toList());
96
97
98 Assertions.assertEquals(0, tris.size());
99 }
100
101 @Test
102 void testTriangleStream_infinite() {
103
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
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
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
143 final List<Triangle3D> tris = box.triangleStream().collect(Collectors.toList());
144
145
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
159 final ConvexVolume full = ConvexVolume.full();
160 final ConvexVolume halfFull = ConvexVolume.fromBounds(Planes.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION));
161
162
163 Assertions.assertNull(full.getBounds());
164 Assertions.assertNull(halfFull.getBounds());
165 }
166
167 @Test
168 void testGetBounds_hasBounds() {
169
170 final ConvexVolume vol = rect(Vector3D.of(1, 1, 1), 0.5, 1, 2);
171
172
173 final Bounds3D bounds = vol.getBounds();
174
175
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
183 final ConvexVolume volume = ConvexVolume.full();
184
185
186 final BoundaryList3D list = volume.toList();
187
188
189 Assertions.assertEquals(0, list.count());
190 }
191
192 @Test
193 void testToList() {
194
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
206 final BoundaryList3D list = volume.toList();
207
208
209 Assertions.assertEquals(6, list.count());
210 Assertions.assertEquals(1, list.toTree().getSize(), TEST_EPS);
211 }
212
213 @Test
214 void testToTree_full() {
215
216 final ConvexVolume volume = ConvexVolume.full();
217
218
219 final RegionBSPTree3D tree = volume.toTree();
220
221
222 Assertions.assertTrue(tree.isFull());
223 Assertions.assertFalse(tree.isEmpty());
224 }
225
226 @Test
227 void testToTree() {
228
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
240 final RegionBSPTree3D tree = volume.toTree();
241
242
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
258 final ConvexVolume vol = ConvexVolume.fromBounds();
259
260
261 Assertions.assertSame(ConvexVolume.full(), vol);
262 }
263
264 @Test
265 void testFromBounds_halfspace() {
266
267 final ConvexVolume vol = ConvexVolume.fromBounds(Planes.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION));
268
269
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
287 final ConvexVolume vol = rect(Vector3D.of(1, 1, 1), 0.5, 1, 2);
288
289
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
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
319 final PlaneConvexSubset trimmed = vol.trim(subplane);
320
321
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
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
341 final Split<ConvexVolume> split = vol.split(splitter);
342
343
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
358 final ConvexVolume volume = ConvexVolume.full();
359
360
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
373 final ConvexVolume volume = rect(Vector3D.of(0.5, 0.5, 0.5), 0.5, 0.5, 0.5);
374
375
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
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
406 final ConvexVolume transformed = vol.transform(transform);
407
408
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 }