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.testio;
    18
    19  import java.io.File;
    20  import java.io.IOException;
    21  import java.io.UncheckedIOException;
    22  import java.io.Writer;
    23  import java.nio.charset.StandardCharsets;
    24  import java.nio.file.Files;
    25  import java.text.DecimalFormat;
    26  import java.util.Iterator;
    27  import java.util.List;
    28  import java.util.stream.Stream;
    29
    30  import org.apache.commons.geometry.euclidean.threed.BoundarySource3D;
    31  import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
    32  import org.apache.commons.geometry.euclidean.threed.Vector3D;
    33  import org.apache.commons.geometry.euclidean.threed.mesh.Mesh;
    34
    35  /** Class for writing OBJ files containing 3D mesh data. This class is primarily intended for use in
    36   * debugging unit tests.
    37   */
    38  public final class TestOBJWriter implements AutoCloseable {
    39
    40      /** Space character. */
    41      private static final char SPACE = ' ';
    42
    43      /** The default maximum number of fraction digits in formatted numbers. */
    44      private static final int DEFAULT_MAXIMUM_FRACTION_DIGITS = 6;
    45
    46      /** The default line separator value. This is not directly specified by the OBJ format
    47       * but the value used here matches that
    48       * <a href="https://docs.blender.org/manual/en/2.80/addons/io_scene_obj.html">used by Blender</a>.
    49       */
    50      private static final String DEFAULT_LINE_SEPARATOR = "\n";
    51
    52      /** Underlying writer instance. */
    53      private Writer writer;
    54
    55      /** Line separator string. */
    56      private String lineSeparator = DEFAULT_LINE_SEPARATOR;
    57
    58      /** Decimal formatter. */
    59      private DecimalFormat decimalFormat;
    60
    61      /** Number of vertices written to the output. */
    62      private int vertexCount = 0;
    63
    64      /** Create a new instance for writing to the given file.
    65       * @param file file to write to
    66       * @throws IOException if an IO operation fails
    67       */
    68      public TestOBJWriter(final File file) throws IOException {
    69          this(Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8));
    70      }
    71
    72      /** Create a new instance that writes output with the given writer.
    73       * @param writer writer used to write output
    74       */
    75      public TestOBJWriter(final Writer writer) {
    76          this.writer = writer;
    77
    78          this.decimalFormat = new DecimalFormat();
    79          this.decimalFormat.setMaximumFractionDigits(DEFAULT_MAXIMUM_FRACTION_DIGITS);
    80      }
    81
    82      /** Get the current line separator. This value defaults to {@value #DEFAULT_LINE_SEPARATOR}.
    83       * @return the current line separator
    84       */
    85      public String getLineSeparator() {
    86          return lineSeparator;
    87      }
    88
    89      /** Set the line separator.
    90       * @param lineSeparator the line separator to use
    91       */
    92      public void setLineSeparator(final String lineSeparator) {
    93          this.lineSeparator = lineSeparator;
    94      }
    95
    96      /** Get the {@link DecimalFormat} instance used to format floating point output.
    97       * @return the decimal format instance
    98       */
    99      public DecimalFormat getDecimalFormat() {
    100         return decimalFormat;
    101     }
    102
    103     /** Set the {@link DecimalFormat} instance used to format floatin point output.
    104      * @param decimalFormat decimal format instance
    105      */
    106     public void setDecimalFormat(final DecimalFormat decimalFormat) {
    107         this.decimalFormat = decimalFormat;
    108     }
    109
    110     /** Write an OBJ comment with the given value.
    111      * @param comment comment to write
    112      * @throws IOException if an IO operation fails
    113      */
    114     public void writeComment(final String comment) throws IOException {
    115         for (final String line : comment.split("\r?\n")) {
    116             writer.write('#');
    117             writer.write(SPACE);
    118             writer.write(line);
    119             writer.write(lineSeparator);
    120         }
    121     }
    122
    123     /** Write an object name to the output. This is metadata for the file and
    124      * does not affect the geometry, although it may affect how the file content
    125      * is read by other programs.
    126      * @param objectName the name to write
    127      */
    128     public void writeObjectName(final String objectName) throws IOException {
    129         writer.write('o');
    130         writer.write(SPACE);
    131         writer.write(objectName);
    132         writer.write(lineSeparator);
    133     }
    134
    135     /** Write a group name to the output. This is metadata for the file and
    136      * does not affect the geometry, although it may affect how the file content
    137      * is read by other programs.
    138      * @param groupName the name to write
    139      */
    140     public void writeGroupName(final String groupName) throws IOException {
    141         writer.write('g');
    142         writer.write(SPACE);
    143         writer.write(groupName);
    144         writer.write(lineSeparator);
    145     }
    146
    147     /** Write a vertex to the output. The OBJ 1-based index of the vertex is returned. This
    148      * index can be used to reference the vertex in faces via {@link #writeFace(int...)}.
    149      * @param vertex vertex to write
    150      * @throws IOException if an IO operation fails
    151      * @return the index of the written vertex in the OBJ 1-based convention
    152      */
    153     public int writeVertex(final Vector3D vertex) throws IOException {
    154         writer.write('v');
    155         writer.write(SPACE);
    156         writer.write(decimalFormat.format(vertex.getX()));
    157         writer.write(SPACE);
    158         writer.write(decimalFormat.format(vertex.getY()));
    159         writer.write(SPACE);
    160         writer.write(decimalFormat.format(vertex.getZ()));
    161         writer.write(lineSeparator);
    162
    163         return ++vertexCount;
    164     }
    165
    166     /** Write a face with the given vertex indices, specified in the OBJ 1-based
    167      * convention. Callers are responsible for ensuring that the indices are valid.
    168      * @param vertexIndices vertex indices for the face, in the 1-based OBJ convention
    169      */
    170     public void writeFace(final int... vertexIndices) throws IOException {
    171         writeFaceWithVertexOffset(0, vertexIndices);
    172     }
    173
    174     /** Write the boundaries present in the given boundary source. If the argument is a {@link Mesh},
    175      * it is written using {@link #writeMesh(Mesh)}. Otherwise, each boundary is written to the output
    176      * separately.
    177      * @param boundarySource boundary source containing the boundaries to write to the output
    178      * @throws IllegalArgumentException if any boundary in the argument is infinite
    179      */
    180     public void writeBoundaries(final BoundarySource3D boundarySource) throws IOException {
    181         if (boundarySource instanceof Mesh) {
    182             writeMesh((Mesh<?>) boundarySource);
    183         } else {
    184             try (Stream<PlaneConvexSubset> stream = boundarySource.boundaryStream()) {
    185                 writeBoundaries(stream.iterator());
    186             }
    187         }
    188     }
    189
    190     /** Write the boundaries in the argument to the output. Each boundary is written separately.
    191      * @param it boundary iterator
    192      * @throws IllegalArgumentException if any boundary in the argument is infinite
    193      */
    194     private void writeBoundaries(final Iterator<PlaneConvexSubset> it) throws IOException {
    195         PlaneConvexSubset boundary;
    196         List<Vector3D> vertices;
    197         int[] vertexIndices;
    198
    199         while (it.hasNext()) {
    200             boundary = it.next();
    201             if (boundary.isInfinite()) {
    202                 throw new IllegalArgumentException("OBJ input geometry cannot be infinite: " + boundary);
    203             }
    204
    205             vertices = boundary.getVertices();
    206             vertexIndices = new int[vertices.size()];
    207
    208             for (int i = 0; i < vertexIndices.length; ++i) {
    209                 vertexIndices[i] = writeVertex(vertices.get(i));
    210             }
    211
    212             writeFace(vertexIndices);
    213         }
    214     }
    215
    216     /** Write a mesh to the output.
    217      * @param mesh the mesh to write
    218      * @throws IOException if an IO operation fails
    219      */
    220     public void writeMesh(final Mesh<?> mesh) throws IOException {
    221         final int vertexOffset = vertexCount + 1;
    222
    223         for (final Vector3D vertex : mesh.vertices()) {
    224             writeVertex(vertex);
    225         }
    226
    227         for (final Mesh.Face face : mesh.faces()) {
    228             writeFaceWithVertexOffset(vertexOffset, face.getVertexIndices());
    229         }
    230     }
    231
    232     /** Write a face with the given vertex offset value and indices. The offset is added to each
    233      * index before being written.
    234      * @param vertexOffset vertex offset value
    235      * @param vertexIndices vertex indices for the face
    236      * @throws IOException if an IO operation fails
    237      */
    238     private void writeFaceWithVertexOffset(final int vertexOffset, final int... vertexIndices)
    239             throws IOException {
    240         if (vertexIndices.length < 3) {
    241             throw new IllegalArgumentException("Face must have more than 3 vertices; found " + vertexIndices.length);
    242         }
    243
    244         writer.write('f');
    245
    246         for (final int vertexIndex : vertexIndices) {
    247             writer.write(SPACE);
    248             writer.write(String.valueOf(vertexIndex + vertexOffset));
    249         }
    250
    251         writer.write(lineSeparator);
    252     }
    253
    254     /** {@inheritDoc} */
    255     @Override
    256     public void close() throws IOException {
    257         if (writer != null) {
    258             writer.close();
    259         }
    260         writer = null;
    261     }
    262
    263     /** Convenience method for writing a boundary source to a file.
    264      * @param src boundary source to write
    265      * @param file file to write to
    266      */
    267     public static void write(final BoundarySource3D src, final File file) {
    268         try (TestOBJWriter writer = new TestOBJWriter(file)) {
    269             writer.writeBoundaries(src);
    270         } catch (final IOException exc) {
    271             throw new UncheckedIOException(exc);
    272         }
    273     }
    274
    275     /** Convenience method for writing a boundary source to a file.
    276      * @param src boundary source to write
    277      * @param file file path to write to
    278      */
    279     public static void write(final BoundarySource3D src, final String file) {
    280         write(src, new File(file));
    281     }
    282 }