TextStlWriter.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.geometry.io.euclidean.threed.stl;
import java.io.Writer;
import java.util.List;
import org.apache.commons.geometry.euclidean.internal.EuclideanUtils;
import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
import org.apache.commons.geometry.euclidean.threed.Triangle3D;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
import org.apache.commons.geometry.io.core.utils.AbstractTextFormatWriter;
import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;
/** Class for writing the text-based (i.e., "ASCII") STL format.
* @see <a href="https://en.wikipedia.org/wiki/STL_%28file_format%29#ASCII_STL">ASCII STL</a>
*/
public class TextStlWriter extends AbstractTextFormatWriter {
/** Space character. */
private static final char SPACE = ' ';
/** Name of the current STL solid. */
private String name;
/** True if an STL solid definition has been written. */
private boolean started;
/** Construct a new instance for writing STL content to the given writer.
* @param writer writer to write to
*/
public TextStlWriter(final Writer writer) {
super(writer);
}
/** Write the start of an unnamed STL solid definition. This method is equivalent to calling
* {@code stlWriter.startSolid(null);}
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
public void startSolid() {
startSolid(null);
}
/** Write the start of an STL solid definition with the given name.
* @param solidName the name of the solid; may be null
* @throws IllegalArgumentException if {@code solidName} contains new line characters
* @throws IllegalStateException if a solid definition has already been started
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
public void startSolid(final String solidName) {
if (started) {
throw new IllegalStateException("Cannot start solid definition: a solid is already being written");
}
if (solidName != null && (solidName.indexOf('\r') > -1 || solidName.indexOf('\n') > -1)) {
throw new IllegalArgumentException("Solid name cannot contain new line characters");
}
name = solidName;
writeBeginOrEndLine(StlConstants.SOLID_START_KEYWORD);
started = true;
}
/** Write the end of the current STL solid definition. This method is called automatically on
* {@link #close()} if needed.
* @throws IllegalStateException if no solid definition has been started
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
public void endSolid() {
if (!started) {
throw new IllegalStateException("Cannot end solid definition: no solid has been started");
}
writeBeginOrEndLine(StlConstants.SOLID_END_KEYWORD);
name = null;
started = false;
}
/** Write the given boundary to the output as triangles.
* @param boundary boundary to write
* @throws IllegalStateException if no solid has been started yet
* @throws java.io.UncheckedIOException if an I/O error occurs
* @see PlaneConvexSubset#toTriangles()
*/
public void writeTriangles(final PlaneConvexSubset boundary) {
for (final Triangle3D tri : boundary.toTriangles()) {
writeTriangles(tri.getVertices(), tri.getPlane().getNormal());
}
}
/** Write the given facet definition to the output as triangles.
* @param facet facet definition to write
* @throws IllegalStateException if no solid has been started yet
* @throws java.io.UncheckedIOException if an I/O error occurs
* @see #writeTriangle(Vector3D, Vector3D, Vector3D, Vector3D)
*/
public void writeTriangles(final FacetDefinition facet) {
writeTriangles(facet.getVertices(), facet.getNormal());
}
/** Write the facet defined by the given vertices and normal to the output as triangles.
* If the the given list of vertices contains more than 3 vertices, it is converted to
* triangles using a triangle fan. Callers are responsible for ensuring that the given
* vertices represent a valid convex polygon.
*
* <p>If a non-zero normal is given, the vertices are ordered using the right-hand rule,
* meaning that they will be in a counter-clockwise orientation when looking down
* the normal. If no normal is given, or the given value cannot be normalized, a normal
* is computed from the triangle vertices, also using the right-hand rule. If this also
* fails (for example, if the triangle vertices do not define a plane), then the
* zero vector is used.</p>
* @param vertices vertices defining the facet
* @param normal facet normal; may be null
* @throws IllegalStateException if no solid has been started yet or fewer than 3 vertices
* are given
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
public void writeTriangles(final List<Vector3D> vertices, final Vector3D normal) {
for (final List<Vector3D> triangle : EuclideanUtils.convexPolygonToTriangleFan(vertices, t -> t)) {
writeTriangle(
triangle.get(0),
triangle.get(1),
triangle.get(2),
normal);
}
}
/** Write a triangle to the output.
*
* <p>If a non-zero normal is given, the vertices are ordered using the right-hand rule,
* meaning that they will be in a counter-clockwise orientation when looking down
* the normal. If no normal is given, or the given value cannot be normalized, a normal
* is computed from the triangle vertices, also using the right-hand rule. If this also
* fails (for example, if the triangle vertices do not define a plane), then the
* zero vector is used.</p>
* @param p1 first point
* @param p2 second point
* @param p3 third point
* @param normal facet normal; may be null
* @throws IllegalStateException if no solid has been started yet
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
public void writeTriangle(final Vector3D p1, final Vector3D p2, final Vector3D p3, final Vector3D normal) {
if (!started) {
throw new IllegalStateException("Cannot write triangle: no solid has been started");
}
write(StlConstants.FACET_START_KEYWORD);
write(SPACE);
writeVector(StlUtils.determineNormal(p1, p2, p3, normal));
writeNewLine();
write(StlConstants.OUTER_KEYWORD);
write(SPACE);
write(StlConstants.LOOP_START_KEYWORD);
writeNewLine();
writeTriangleVertex(p1);
if (StlUtils.pointsAreCounterClockwise(p1, p2, p3, normal)) {
writeTriangleVertex(p2);
writeTriangleVertex(p3);
} else {
writeTriangleVertex(p3);
writeTriangleVertex(p2);
}
write(StlConstants.LOOP_END_KEYWORD);
writeNewLine();
write(StlConstants.FACET_END_KEYWORD);
writeNewLine();
}
/** {@inheritDoc} */
@Override
public void close() {
if (started) {
endSolid();
}
super.close();
}
/** Write a triangle vertex to the output.
* @param vertex triangle vertex
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
private void writeTriangleVertex(final Vector3D vertex) {
write(StlConstants.VERTEX_KEYWORD);
write(SPACE);
writeVector(vertex);
writeNewLine();
}
/** Write a vector to the output.
* @param vec vector to write
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
private void writeVector(final Vector3D vec) {
write(vec.getX());
write(SPACE);
write(vec.getY());
write(SPACE);
write(vec.getZ());
}
/** Write the beginning or ending line of the solid definition.
* @param keyword keyword at the start of the line
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
private void writeBeginOrEndLine(final String keyword) {
write(keyword);
write(SPACE);
if (name != null) {
write(name);
}
writeNewLine();
}
}