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 }