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.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      /** Less strict epsilon value for testing values related to the region boundary,
60       * since these can vary more than other metrics.
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          // assert
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          // act/assert
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         // act/assert
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         // act/assert
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         // act/assert
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         // act/assert
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         // act/assert
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         // act/assert
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         // act/assert
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 }