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 }