1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.geometry.io.euclidean.threed;
18
19 import java.io.BufferedInputStream;
20 import java.io.BufferedOutputStream;
21 import java.io.IOException;
22 import java.net.URL;
23 import java.nio.file.Files;
24 import java.nio.file.Path;
25 import java.nio.file.Paths;
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.stream.Collectors;
31 import java.util.stream.Stream;
32
33 import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
34 import org.apache.commons.geometry.euclidean.threed.AffineTransformMatrix3D;
35 import org.apache.commons.geometry.euclidean.threed.BoundaryList3D;
36 import org.apache.commons.geometry.euclidean.threed.BoundarySource3D;
37 import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
38 import org.apache.commons.geometry.euclidean.threed.RegionBSPTree3D;
39 import org.apache.commons.geometry.euclidean.threed.Triangle3D;
40 import org.apache.commons.geometry.euclidean.threed.Vector3D;
41 import org.apache.commons.geometry.euclidean.threed.shape.Parallelepiped;
42 import org.apache.commons.geometry.io.core.GeometryFormat;
43 import org.apache.commons.geometry.io.core.input.GeometryInput;
44 import org.apache.commons.geometry.io.core.input.StreamGeometryInput;
45 import org.apache.commons.geometry.io.core.output.GeometryOutput;
46 import org.apache.commons.geometry.io.core.output.StreamGeometryOutput;
47 import org.apache.commons.geometry.io.core.test.CloseCountInputStream;
48 import org.apache.commons.geometry.io.core.test.CloseCountOutputStream;
49 import org.apache.commons.geometry.io.euclidean.EuclideanIOTestUtils;
50 import org.apache.commons.numbers.core.Precision;
51 import org.junit.jupiter.api.Assertions;
52 import org.junit.jupiter.api.Test;
53 import org.junit.jupiter.api.io.TempDir;
54
55 class IO3DTest {
56
57 private static final double TEST_EPS = 1e-4;
58
59
60
61
62 private static final double BOUNDARY_TEST_EPS = 0.03;
63
64 private static final double MODEL_EPS = 1e-8;
65
66 private static final Precision.DoubleEquivalence MODEL_PRECISION = Precision.doubleEquivalenceOfEpsilon(MODEL_EPS);
67
68 @TempDir
69 public Path tempDir;
70
71 @Test
72 void testStreamExample() {
73 final Path origFile = tempDir.resolve("orig.obj");
74 final Path scaledFile = tempDir.resolve("scaled.csv");
75
76 final Precision.DoubleEquivalence precision = Precision.doubleEquivalenceOfEpsilon(1e-10);
77 final BoundarySource3D src = Parallelepiped.unitCube(precision);
78
79 IO3D.write(src, origFile);
80
81 final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(2);
82
83 try (Stream<Triangle3D> stream = IO3D.triangles(origFile, precision)) {
84 IO3D.write(stream.map(t -> t.transform(transform)), scaledFile);
85 }
86
87 final RegionBSPTree3D result = IO3D.read(scaledFile, precision).toTree();
88
89
90 Assertions.assertEquals(8, result.getSize(), TEST_EPS);
91 EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, result.getCentroid(), TEST_EPS);
92 }
93
94 @Test
95 void testReadWriteFacets_facetDefinitionReader() throws Exception {
96
97 testReadWriteWithPath(
98 (fmt, path) -> readerToBoundaryList(IO3D.facetDefinitionReader(path)),
99 (src, fmt, path) -> IO3D.writeFacets(boundarySourceToFacets(src), path));
100 testReadWriteWithUrl(
101 (fmt, url) -> readerToBoundaryList(IO3D.facetDefinitionReader(url)),
102 (src, fmt, path) -> IO3D.writeFacets(boundarySourceToFacets(src), path));
103 testReadWriteWithInputOutputStreams(
104 (fmt, in) -> readerToBoundaryList(IO3D.facetDefinitionReader(in, fmt)),
105 (src, fmt, out) -> IO3D.writeFacets(boundarySourceToFacets(src), out, fmt));
106 }
107
108 @Test
109 void testReadWriteFacets_facetStream() throws Exception {
110
111 testReadWriteWithPath(
112 (fmt, path) -> facetsToBoundaryList(IO3D.facets(path)),
113 (src, fmt, path) -> IO3D.writeFacets(boundarySourceToFacets(src), path));
114 testReadWriteWithUrl(
115 (fmt, url) -> facetsToBoundaryList(IO3D.facets(url)),
116 (src, fmt, path) -> IO3D.writeFacets(boundarySourceToFacets(src), path));
117 testReadWriteWithInputOutputStreams(
118 (fmt, in) -> facetsToBoundaryList(IO3D.facets(in, fmt)),
119 (src, fmt, out) -> IO3D.writeFacets(boundarySourceToFacets(src), out, fmt));
120 }
121
122 @Test
123 void testReadWriteBoundarySource() throws Exception {
124
125 testReadWriteWithPath(
126 (fmt, path) -> IO3D.read(path, MODEL_PRECISION),
127 (src, fmt, path) -> IO3D.write(src, path));
128 testReadWriteWithUrl(
129 (fmt, url) -> IO3D.read(url, MODEL_PRECISION),
130 (src, fmt, path) -> IO3D.write(src, path));
131 testReadWriteWithInputOutputStreams(
132 (fmt, in) -> IO3D.read(in, fmt, MODEL_PRECISION),
133 (src, fmt, out) -> IO3D.write(src, out, fmt));
134 }
135
136 @Test
137 void testReadWriteBoundarySource_triangleMesh() throws Exception {
138
139 testReadWriteWithPath(
140 (fmt, path) -> IO3D.readTriangleMesh(path, MODEL_PRECISION),
141 (src, fmt, path) -> IO3D.write(src.toTriangleMesh(MODEL_PRECISION), path));
142 testReadWriteWithUrl(
143 (fmt, url) -> IO3D.readTriangleMesh(url, MODEL_PRECISION),
144 (src, fmt, path) -> IO3D.write(src.toTriangleMesh(MODEL_PRECISION), path));
145 testReadWriteWithInputOutputStreams(
146 (fmt, in) -> IO3D.readTriangleMesh(in, fmt, MODEL_PRECISION),
147 (src, fmt, out) -> IO3D.write(src.toTriangleMesh(MODEL_PRECISION), out, fmt));
148 }
149
150 @Test
151 void testReadWriteBoundarySource_boundaryStream() throws Exception {
152
153 testReadWriteWithPath(
154 (fmt, path) -> boundariesToBoundaryList(IO3D.boundaries(path, MODEL_PRECISION)),
155 (src, fmt, path) -> IO3D.write(src, path));
156 testReadWriteWithUrl(
157 (fmt, url) -> boundariesToBoundaryList(IO3D.boundaries(url, MODEL_PRECISION)),
158 (src, fmt, path) -> IO3D.write(src, path));
159 testReadWriteWithInputOutputStreams(
160 (fmt, in) -> boundariesToBoundaryList(IO3D.boundaries(in, fmt, MODEL_PRECISION)),
161 (src, fmt, out) -> IO3D.write(src, out, fmt));
162 }
163
164 @Test
165 void testReadWriteBoundarySource_triangleStream() throws Exception {
166
167 testReadWriteWithPath(
168 (fmt, path) -> boundariesToBoundaryList(IO3D.triangles(path, MODEL_PRECISION)),
169 (src, fmt, path) -> IO3D.write(src, path));
170 testReadWriteWithUrl(
171 (fmt, url) -> boundariesToBoundaryList(IO3D.triangles(url, MODEL_PRECISION)),
172 (src, fmt, path) -> IO3D.write(src, path));
173 testReadWriteWithInputOutputStreams(
174 (fmt, in) -> boundariesToBoundaryList(IO3D.triangles(in, fmt, MODEL_PRECISION)),
175 (src, fmt, out) -> IO3D.write(src, out, fmt));
176 }
177
178 @Test
179 void testWriteBoundaryStream() throws Exception {
180
181 testReadWriteWithPath(
182 (fmt, path) -> boundariesToBoundaryList(IO3D.triangles(path, MODEL_PRECISION)),
183 (src, fmt, path) -> IO3D.write(src.boundaryStream(), path));
184 testReadWriteWithUrl(
185 (fmt, url) -> boundariesToBoundaryList(IO3D.triangles(url, MODEL_PRECISION)),
186 (src, fmt, path) -> IO3D.write(src.boundaryStream(), path));
187 testReadWriteWithInputOutputStreams(
188 (fmt, in) -> boundariesToBoundaryList(IO3D.triangles(in, fmt, MODEL_PRECISION)),
189 (src, fmt, out) -> IO3D.write(src.boundaryStream(), out, fmt));
190 }
191
192 @Test
193 void testWriteFacetStream() throws Exception {
194
195 testReadWriteWithPath(
196 (fmt, path) -> boundariesToBoundaryList(IO3D.triangles(path, MODEL_PRECISION)),
197 (src, fmt, path) -> IO3D.writeFacets(boundarySourceToFacets(src).stream(), path));
198 testReadWriteWithUrl(
199 (fmt, url) -> boundariesToBoundaryList(IO3D.triangles(url, MODEL_PRECISION)),
200 (src, fmt, path) -> IO3D.writeFacets(boundarySourceToFacets(src).stream(), path));
201 testReadWriteWithInputOutputStreams(
202 (fmt, in) -> boundariesToBoundaryList(IO3D.triangles(in, fmt, MODEL_PRECISION)),
203 (src, fmt, out) -> IO3D.writeFacets(boundarySourceToFacets(src).stream(), out, fmt));
204 }
205
206 private void testReadWriteWithPath(final ReadFn<Path> readFn, final WriteFn<Path> writeFn)
207 throws Exception {
208 String baseName;
209 RegionBSPTree3D expected;
210 String location;
211 Path path;
212 for (final Map.Entry<String, RegionBSPTree3D> entry : getTestInputs().entrySet()) {
213 baseName = entry.getKey();
214 expected = entry.getValue();
215
216 for (final GeometryFormat fmt : GeometryFormat3D.values()) {
217 location = getModelLocation(baseName, fmt);
218 path = Paths.get(EuclideanIOTestUtils.resource(location).toURI());
219
220 testReadWriteWithPath(fmt, path, readFn, writeFn, expected);
221 }
222 }
223 }
224
225 private void testReadWriteWithPath(final GeometryFormat fmt, final Path path,
226 final ReadFn<Path> readFn, final WriteFn<Path> writeFn,
227 final RegionBSPTree3D expected) throws IOException {
228
229 final Path tmp = Files.createTempFile("tmp", "." + fmt.getDefaultFileExtension());
230
231 final BoundarySource3D orig = readFn.read(fmt, path);
232 assertRegion(expected, orig);
233
234 writeFn.write(orig, fmt, tmp);
235
236 final BoundarySource3D result = readFn.read(fmt, tmp);
237 assertRegion(expected, result);
238 }
239
240 private void testReadWriteWithUrl(final ReadFn<URL> readFn, final WriteFn<Path> writeFn) throws Exception {
241 String baseName;
242 RegionBSPTree3D expected;
243 String location;
244 URL url;
245 for (final Map.Entry<String, RegionBSPTree3D> entry : getTestInputs().entrySet()) {
246 baseName = entry.getKey();
247 expected = entry.getValue();
248
249 for (final GeometryFormat fmt : GeometryFormat3D.values()) {
250 location = getModelLocation(baseName, fmt);
251 url = EuclideanIOTestUtils.resource(location);
252
253 testReadWriteWithUrl(fmt, url, readFn, writeFn, expected);
254 }
255 }
256 }
257
258 private void testReadWriteWithUrl(final GeometryFormat fmt, final URL url,
259 final ReadFn<URL> readFn, final WriteFn<Path> writeFn,
260 final RegionBSPTree3D expected) throws IOException {
261
262 final Path tmp = Files.createTempFile("tmp", "." + fmt);
263
264 final BoundarySource3D orig = readFn.read(fmt, url);
265 assertRegion(expected, orig);
266
267 writeFn.write(orig, fmt, tmp);
268
269 final BoundarySource3D result = readFn.read(fmt, tmp.toUri().toURL());
270 assertRegion(expected, result);
271 }
272
273 private void testReadWriteWithInputOutputStreams(final ReadFn<GeometryInput> readFn,
274 final WriteFn<GeometryOutput> writeFn) throws Exception {
275 String baseName;
276 RegionBSPTree3D expected;
277 String location;
278 Path path;
279 for (final Map.Entry<String, RegionBSPTree3D> entry : getTestInputs().entrySet()) {
280 baseName = entry.getKey();
281 expected = entry.getValue();
282
283 for (final GeometryFormat fmt : GeometryFormat3D.values()) {
284 location = getModelLocation(baseName, fmt);
285 path = Paths.get(EuclideanIOTestUtils.resource(location).toURI());
286
287 testReadWriteWithStreams(fmt, path, readFn, writeFn, expected);
288 }
289 }
290 }
291
292 private void testReadWriteWithStreams(final GeometryFormat fmt, final Path path,
293 final ReadFn<GeometryInput> readFn, final WriteFn<GeometryOutput> writeFn,
294 final RegionBSPTree3D expected) throws IOException {
295
296 final Path tmp = Files.createTempFile("tmp", "." + fmt.getDefaultFileExtension());
297
298 final BoundarySource3D orig;
299 try (CloseCountInputStream in =
300 new CloseCountInputStream(new BufferedInputStream(Files.newInputStream(path)))) {
301 orig = readFn.read(fmt, new StreamGeometryInput(in));
302
303 Assertions.assertEquals(1, in.getCloseCount());
304 }
305 assertRegion(expected, orig);
306
307 try (CloseCountOutputStream out =
308 new CloseCountOutputStream(new BufferedOutputStream(Files.newOutputStream(tmp)))) {
309 writeFn.write(orig, fmt, new StreamGeometryOutput(out));
310
311 Assertions.assertEquals(1, out.getCloseCount());
312 }
313
314 final BoundarySource3D result;
315 try (CloseCountInputStream in =
316 new CloseCountInputStream(new BufferedInputStream(Files.newInputStream(tmp)))) {
317 result = readFn.read(fmt, new StreamGeometryInput(in));
318 }
319 assertRegion(expected, result);
320 }
321
322 private static void assertRegion(final RegionBSPTree3D expected, final BoundarySource3D actual) {
323 final RegionBSPTree3D actualRegion = actual.toTree();
324
325 Assertions.assertEquals(expected.getSize(), actualRegion.getSize(), TEST_EPS);
326 Assertions.assertEquals(expected.getBoundarySize(), actualRegion.getBoundarySize(), BOUNDARY_TEST_EPS);
327
328 if (expected.isEmpty()) {
329 Assertions.assertTrue(actualRegion.isEmpty());
330 } else {
331 EuclideanTestUtils.assertCoordinatesEqual(expected.getCentroid(), actualRegion.getCentroid(), TEST_EPS);
332 }
333
334 final RegionBSPTree3D diff = RegionBSPTree3D.empty();
335 diff.difference(expected, actualRegion);
336
337 Assertions.assertEquals(0, diff.getSize(), BOUNDARY_TEST_EPS);
338 }
339
340 private static String getModelLocation(final String baseName, final GeometryFormat fmt) {
341 return "/models/" + baseName + "." + fmt.getDefaultFileExtension();
342 }
343
344 private static Map<String, RegionBSPTree3D> getTestInputs() {
345 final Map<String, RegionBSPTree3D> inputs = new HashMap<>();
346
347 inputs.put("empty", RegionBSPTree3D.empty());
348 inputs.put("cube", EuclideanIOTestUtils.cube(MODEL_PRECISION).toTree());
349 inputs.put("cube-minus-sphere", EuclideanIOTestUtils.cubeMinusSphere(MODEL_PRECISION).toTree());
350
351 return inputs;
352 }
353
354 private static BoundaryList3D readerToBoundaryList(final FacetDefinitionReader reader) {
355 try (FacetDefinitionReader toClose = reader) {
356 final List<PlaneConvexSubset> list = new ArrayList<>();
357 FacetDefinition f;
358 while ((f = reader.readFacet()) != null) {
359 list.add(FacetDefinitions.toPolygon(f, MODEL_PRECISION));
360 }
361
362 return new BoundaryList3D(list);
363 }
364 }
365
366 private static BoundaryList3D facetsToBoundaryList(final Stream<FacetDefinition> stream) {
367 try (Stream<FacetDefinition> facetStream = stream) {
368 final List<PlaneConvexSubset> list = facetStream
369 .map(f -> FacetDefinitions.toPolygon(f, MODEL_PRECISION))
370 .collect(Collectors.toList());
371
372 return new BoundaryList3D(list);
373 }
374 }
375
376 private static <T extends PlaneConvexSubset> BoundaryList3D boundariesToBoundaryList(final Stream<T> stream) {
377 try (Stream<T> boundaryStream = stream) {
378 final List<PlaneConvexSubset> list = boundaryStream.collect(Collectors.toList());
379
380 return new BoundaryList3D(list);
381 }
382 }
383
384 private static List<FacetDefinition> boundarySourceToFacets(final BoundarySource3D src) {
385 return src.boundaryStream()
386 .map(b -> new SimpleFacetDefinition(b.getVertices()))
387 .collect(Collectors.toList());
388 }
389
390 @FunctionalInterface
391 interface ReadFn<T> {
392 BoundarySource3D read(GeometryFormat fmt, T t) throws IOException;
393 }
394
395 @FunctionalInterface
396 interface WriteFn<D> {
397 void write(BoundarySource3D src, GeometryFormat fmt, D dst) throws IOException;
398 }
399 }