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();
    }
}