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.stl;
18  
19  import java.io.Writer;
20  import java.util.List;
21  
22  import org.apache.commons.geometry.euclidean.internal.EuclideanUtils;
23  import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
24  import org.apache.commons.geometry.euclidean.threed.Triangle3D;
25  import org.apache.commons.geometry.euclidean.threed.Vector3D;
26  import org.apache.commons.geometry.io.core.utils.AbstractTextFormatWriter;
27  import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;
28  
29  /** Class for writing the text-based (i.e., "ASCII") STL format.
30   * @see <a href="https://en.wikipedia.org/wiki/STL_%28file_format%29#ASCII_STL">ASCII STL</a>
31   */
32  public class TextStlWriter extends AbstractTextFormatWriter {
33  
34      /** Space character. */
35      private static final char SPACE = ' ';
36  
37      /** Name of the current STL solid. */
38      private String name;
39  
40      /** True if an STL solid definition has been written. */
41      private boolean started;
42  
43      /** Construct a new instance for writing STL content to the given writer.
44       * @param writer writer to write to
45       */
46      public TextStlWriter(final Writer writer) {
47          super(writer);
48      }
49  
50      /** Write the start of an unnamed STL solid definition. This method is equivalent to calling
51       * {@code stlWriter.startSolid(null);}
52       * @throws java.io.UncheckedIOException if an I/O error occurs
53       */
54      public void startSolid() {
55          startSolid(null);
56      }
57  
58      /** Write the start of an STL solid definition with the given name.
59       * @param solidName the name of the solid; may be null
60       * @throws IllegalArgumentException if {@code solidName} contains new line characters
61       * @throws IllegalStateException if a solid definition has already been started
62       * @throws java.io.UncheckedIOException if an I/O error occurs
63       */
64      public void startSolid(final String solidName) {
65          if (started) {
66              throw new IllegalStateException("Cannot start solid definition: a solid is already being written");
67          }
68          if (solidName != null && (solidName.indexOf('\r') > -1 || solidName.indexOf('\n') > -1)) {
69              throw new IllegalArgumentException("Solid name cannot contain new line characters");
70          }
71  
72          name = solidName;
73          writeBeginOrEndLine(StlConstants.SOLID_START_KEYWORD);
74  
75          started = true;
76      }
77  
78      /** Write the end of the current STL solid definition. This method is called automatically on
79       * {@link #close()} if needed.
80       * @throws IllegalStateException if no solid definition has been started
81       * @throws java.io.UncheckedIOException if an I/O error occurs
82       */
83      public void endSolid() {
84          if (!started) {
85              throw new IllegalStateException("Cannot end solid definition: no solid has been started");
86          }
87  
88          writeBeginOrEndLine(StlConstants.SOLID_END_KEYWORD);
89          name = null;
90          started = false;
91      }
92  
93      /** Write the given boundary to the output as triangles.
94       * @param boundary boundary to write
95       * @throws IllegalStateException if no solid has been started yet
96       * @throws java.io.UncheckedIOException if an I/O error occurs
97       * @see PlaneConvexSubset#toTriangles()
98       */
99      public void writeTriangles(final PlaneConvexSubset boundary) {
100         for (final Triangle3D tri : boundary.toTriangles()) {
101             writeTriangles(tri.getVertices(), tri.getPlane().getNormal());
102         }
103     }
104 
105     /** Write the given facet definition to the output as triangles.
106      * @param facet facet definition to write
107      * @throws IllegalStateException if no solid has been started yet
108      * @throws java.io.UncheckedIOException if an I/O error occurs
109      * @see #writeTriangle(Vector3D, Vector3D, Vector3D, Vector3D)
110      */
111     public void writeTriangles(final FacetDefinition facet) {
112         writeTriangles(facet.getVertices(), facet.getNormal());
113     }
114 
115     /** Write the facet defined by the given vertices and normal to the output as triangles.
116      * If the the given list of vertices contains more than 3 vertices, it is converted to
117      * triangles using a triangle fan. Callers are responsible for ensuring that the given
118      * vertices represent a valid convex polygon.
119      *
120      * <p>If a non-zero normal is given, the vertices are ordered using the right-hand rule,
121      * meaning that they will be in a counter-clockwise orientation when looking down
122      * the normal. If no normal is given, or the given value cannot be normalized, a normal
123      * is computed from the triangle vertices, also using the right-hand rule. If this also
124      * fails (for example, if the triangle vertices do not define a plane), then the
125      * zero vector is used.</p>
126      * @param vertices vertices defining the facet
127      * @param normal facet normal; may be null
128      * @throws IllegalStateException if no solid has been started yet or fewer than 3 vertices
129      *      are given
130      * @throws java.io.UncheckedIOException if an I/O error occurs
131      */
132     public void writeTriangles(final List<Vector3D> vertices, final Vector3D normal) {
133         for (final List<Vector3D> triangle : EuclideanUtils.convexPolygonToTriangleFan(vertices, t -> t)) {
134             writeTriangle(
135                     triangle.get(0),
136                     triangle.get(1),
137                     triangle.get(2),
138                     normal);
139         }
140     }
141 
142     /** Write a triangle to the output.
143      *
144      * <p>If a non-zero normal is given, the vertices are ordered using the right-hand rule,
145      * meaning that they will be in a counter-clockwise orientation when looking down
146      * the normal. If no normal is given, or the given value cannot be normalized, a normal
147      * is computed from the triangle vertices, also using the right-hand rule. If this also
148      * fails (for example, if the triangle vertices do not define a plane), then the
149      * zero vector is used.</p>
150      * @param p1 first point
151      * @param p2 second point
152      * @param p3 third point
153      * @param normal facet normal; may be null
154      * @throws IllegalStateException if no solid has been started yet
155      * @throws java.io.UncheckedIOException if an I/O error occurs
156      */
157     public void writeTriangle(final Vector3D p1, final Vector3D p2, final Vector3D p3, final Vector3D normal) {
158         if (!started) {
159             throw new IllegalStateException("Cannot write triangle: no solid has been started");
160         }
161 
162         write(StlConstants.FACET_START_KEYWORD);
163         write(SPACE);
164         writeVector(StlUtils.determineNormal(p1, p2, p3, normal));
165         writeNewLine();
166 
167         write(StlConstants.OUTER_KEYWORD);
168         write(SPACE);
169         write(StlConstants.LOOP_START_KEYWORD);
170         writeNewLine();
171 
172         writeTriangleVertex(p1);
173 
174         if (StlUtils.pointsAreCounterClockwise(p1, p2, p3, normal)) {
175             writeTriangleVertex(p2);
176             writeTriangleVertex(p3);
177         } else {
178             writeTriangleVertex(p3);
179             writeTriangleVertex(p2);
180         }
181 
182         write(StlConstants.LOOP_END_KEYWORD);
183         writeNewLine();
184 
185         write(StlConstants.FACET_END_KEYWORD);
186         writeNewLine();
187     }
188 
189     /** {@inheritDoc} */
190     @Override
191     public void close() {
192         if (started) {
193             endSolid();
194         }
195 
196         super.close();
197     }
198 
199     /** Write a triangle vertex to the output.
200      * @param vertex triangle vertex
201      * @throws java.io.UncheckedIOException if an I/O error occurs
202      */
203     private void writeTriangleVertex(final Vector3D vertex) {
204         write(StlConstants.VERTEX_KEYWORD);
205         write(SPACE);
206         writeVector(vertex);
207         writeNewLine();
208     }
209 
210     /** Write a vector to the output.
211      * @param vec vector to write
212      * @throws java.io.UncheckedIOException if an I/O error occurs
213      */
214     private void writeVector(final Vector3D vec) {
215         write(vec.getX());
216         write(SPACE);
217         write(vec.getY());
218         write(SPACE);
219         write(vec.getZ());
220     }
221 
222     /** Write the beginning or ending line of the solid definition.
223      * @param keyword keyword at the start of the line
224      * @throws java.io.UncheckedIOException if an I/O error occurs
225      */
226     private void writeBeginOrEndLine(final String keyword) {
227         write(keyword);
228         write(SPACE);
229 
230         if (name != null) {
231             write(name);
232         }
233 
234         writeNewLine();
235     }
236 }