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 }