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