001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.io.euclidean.threed.stl;
018
019import java.io.Writer;
020import java.util.List;
021
022import org.apache.commons.geometry.euclidean.internal.EuclideanUtils;
023import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
024import org.apache.commons.geometry.euclidean.threed.Triangle3D;
025import org.apache.commons.geometry.euclidean.threed.Vector3D;
026import org.apache.commons.geometry.io.core.utils.AbstractTextFormatWriter;
027import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;
028
029/** Class for writing the text-based (i.e., "ASCII") STL format.
030 * @see <a href="https://en.wikipedia.org/wiki/STL_%28file_format%29#ASCII_STL">ASCII STL</a>
031 */
032public class TextStlWriter extends AbstractTextFormatWriter {
033
034    /** Space character. */
035    private static final char SPACE = ' ';
036
037    /** Name of the current STL solid. */
038    private String name;
039
040    /** True if an STL solid definition has been written. */
041    private boolean started;
042
043    /** Construct a new instance for writing STL content to the given writer.
044     * @param writer writer to write to
045     */
046    public TextStlWriter(final Writer writer) {
047        super(writer);
048    }
049
050    /** Write the start of an unnamed STL solid definition. This method is equivalent to calling
051     * {@code stlWriter.startSolid(null);}
052     * @throws java.io.UncheckedIOException if an I/O error occurs
053     */
054    public void startSolid() {
055        startSolid(null);
056    }
057
058    /** Write the start of an STL solid definition with the given name.
059     * @param solidName the name of the solid; may be null
060     * @throws IllegalArgumentException if {@code solidName} contains new line characters
061     * @throws IllegalStateException if a solid definition has already been started
062     * @throws java.io.UncheckedIOException if an I/O error occurs
063     */
064    public void startSolid(final String solidName) {
065        if (started) {
066            throw new IllegalStateException("Cannot start solid definition: a solid is already being written");
067        }
068        if (solidName != null && (solidName.indexOf('\r') > -1 || solidName.indexOf('\n') > -1)) {
069            throw new IllegalArgumentException("Solid name cannot contain new line characters");
070        }
071
072        name = solidName;
073        writeBeginOrEndLine(StlConstants.SOLID_START_KEYWORD);
074
075        started = true;
076    }
077
078    /** Write the end of the current STL solid definition. This method is called automatically on
079     * {@link #close()} if needed.
080     * @throws IllegalStateException if no solid definition has been started
081     * @throws java.io.UncheckedIOException if an I/O error occurs
082     */
083    public void endSolid() {
084        if (!started) {
085            throw new IllegalStateException("Cannot end solid definition: no solid has been started");
086        }
087
088        writeBeginOrEndLine(StlConstants.SOLID_END_KEYWORD);
089        name = null;
090        started = false;
091    }
092
093    /** Write the given boundary to the output as triangles.
094     * @param boundary boundary to write
095     * @throws IllegalStateException if no solid has been started yet
096     * @throws java.io.UncheckedIOException if an I/O error occurs
097     * @see PlaneConvexSubset#toTriangles()
098     */
099    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}