TextStlWriter.java

  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. import java.io.Writer;
  19. import java.util.List;

  20. import org.apache.commons.geometry.euclidean.internal.EuclideanUtils;
  21. import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
  22. import org.apache.commons.geometry.euclidean.threed.Triangle3D;
  23. import org.apache.commons.geometry.euclidean.threed.Vector3D;
  24. import org.apache.commons.geometry.io.core.utils.AbstractTextFormatWriter;
  25. import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;

  26. /** Class for writing the text-based (i.e., "ASCII") STL format.
  27.  * @see <a href="https://en.wikipedia.org/wiki/STL_%28file_format%29#ASCII_STL">ASCII STL</a>
  28.  */
  29. public class TextStlWriter extends AbstractTextFormatWriter {

  30.     /** Space character. */
  31.     private static final char SPACE = ' ';

  32.     /** Name of the current STL solid. */
  33.     private String name;

  34.     /** True if an STL solid definition has been written. */
  35.     private boolean started;

  36.     /** Construct a new instance for writing STL content to the given writer.
  37.      * @param writer writer to write to
  38.      */
  39.     public TextStlWriter(final Writer writer) {
  40.         super(writer);
  41.     }

  42.     /** Write the start of an unnamed STL solid definition. This method is equivalent to calling
  43.      * {@code stlWriter.startSolid(null);}
  44.      * @throws java.io.UncheckedIOException if an I/O error occurs
  45.      */
  46.     public void startSolid() {
  47.         startSolid(null);
  48.     }

  49.     /** Write the start of an STL solid definition with the given name.
  50.      * @param solidName the name of the solid; may be null
  51.      * @throws IllegalArgumentException if {@code solidName} contains new line characters
  52.      * @throws IllegalStateException if a solid definition has already been started
  53.      * @throws java.io.UncheckedIOException if an I/O error occurs
  54.      */
  55.     public void startSolid(final String solidName) {
  56.         if (started) {
  57.             throw new IllegalStateException("Cannot start solid definition: a solid is already being written");
  58.         }
  59.         if (solidName != null && (solidName.indexOf('\r') > -1 || solidName.indexOf('\n') > -1)) {
  60.             throw new IllegalArgumentException("Solid name cannot contain new line characters");
  61.         }

  62.         name = solidName;
  63.         writeBeginOrEndLine(StlConstants.SOLID_START_KEYWORD);

  64.         started = true;
  65.     }

  66.     /** Write the end of the current STL solid definition. This method is called automatically on
  67.      * {@link #close()} if needed.
  68.      * @throws IllegalStateException if no solid definition has been started
  69.      * @throws java.io.UncheckedIOException if an I/O error occurs
  70.      */
  71.     public void endSolid() {
  72.         if (!started) {
  73.             throw new IllegalStateException("Cannot end solid definition: no solid has been started");
  74.         }

  75.         writeBeginOrEndLine(StlConstants.SOLID_END_KEYWORD);
  76.         name = null;
  77.         started = false;
  78.     }

  79.     /** Write the given boundary to the output as triangles.
  80.      * @param boundary boundary to write
  81.      * @throws IllegalStateException if no solid has been started yet
  82.      * @throws java.io.UncheckedIOException if an I/O error occurs
  83.      * @see PlaneConvexSubset#toTriangles()
  84.      */
  85.     public void writeTriangles(final PlaneConvexSubset boundary) {
  86.         for (final Triangle3D tri : boundary.toTriangles()) {
  87.             writeTriangles(tri.getVertices(), tri.getPlane().getNormal());
  88.         }
  89.     }

  90.     /** Write the given facet definition to the output as triangles.
  91.      * @param facet facet definition to write
  92.      * @throws IllegalStateException if no solid has been started yet
  93.      * @throws java.io.UncheckedIOException if an I/O error occurs
  94.      * @see #writeTriangle(Vector3D, Vector3D, Vector3D, Vector3D)
  95.      */
  96.     public void writeTriangles(final FacetDefinition facet) {
  97.         writeTriangles(facet.getVertices(), facet.getNormal());
  98.     }

  99.     /** Write the facet defined by the given vertices and normal to the output as triangles.
  100.      * If the the given list of vertices contains more than 3 vertices, it is converted to
  101.      * triangles using a triangle fan. Callers are responsible for ensuring that the given
  102.      * vertices represent a valid convex polygon.
  103.      *
  104.      * <p>If a non-zero normal is given, the vertices are ordered using the right-hand rule,
  105.      * meaning that they will be in a counter-clockwise orientation when looking down
  106.      * the normal. If no normal is given, or the given value cannot be normalized, a normal
  107.      * is computed from the triangle vertices, also using the right-hand rule. If this also
  108.      * fails (for example, if the triangle vertices do not define a plane), then the
  109.      * zero vector is used.</p>
  110.      * @param vertices vertices defining the facet
  111.      * @param normal facet normal; may be null
  112.      * @throws IllegalStateException if no solid has been started yet or fewer than 3 vertices
  113.      *      are given
  114.      * @throws java.io.UncheckedIOException if an I/O error occurs
  115.      */
  116.     public void writeTriangles(final List<Vector3D> vertices, final Vector3D normal) {
  117.         for (final List<Vector3D> triangle : EuclideanUtils.convexPolygonToTriangleFan(vertices, t -> t)) {
  118.             writeTriangle(
  119.                     triangle.get(0),
  120.                     triangle.get(1),
  121.                     triangle.get(2),
  122.                     normal);
  123.         }
  124.     }

  125.     /** Write a triangle to the output.
  126.      *
  127.      * <p>If a non-zero normal is given, the vertices are ordered using the right-hand rule,
  128.      * meaning that they will be in a counter-clockwise orientation when looking down
  129.      * the normal. If no normal is given, or the given value cannot be normalized, a normal
  130.      * is computed from the triangle vertices, also using the right-hand rule. If this also
  131.      * fails (for example, if the triangle vertices do not define a plane), then the
  132.      * zero vector is used.</p>
  133.      * @param p1 first point
  134.      * @param p2 second point
  135.      * @param p3 third point
  136.      * @param normal facet normal; may be null
  137.      * @throws IllegalStateException if no solid has been started yet
  138.      * @throws java.io.UncheckedIOException if an I/O error occurs
  139.      */
  140.     public void writeTriangle(final Vector3D p1, final Vector3D p2, final Vector3D p3, final Vector3D normal) {
  141.         if (!started) {
  142.             throw new IllegalStateException("Cannot write triangle: no solid has been started");
  143.         }

  144.         write(StlConstants.FACET_START_KEYWORD);
  145.         write(SPACE);
  146.         writeVector(StlUtils.determineNormal(p1, p2, p3, normal));
  147.         writeNewLine();

  148.         write(StlConstants.OUTER_KEYWORD);
  149.         write(SPACE);
  150.         write(StlConstants.LOOP_START_KEYWORD);
  151.         writeNewLine();

  152.         writeTriangleVertex(p1);

  153.         if (StlUtils.pointsAreCounterClockwise(p1, p2, p3, normal)) {
  154.             writeTriangleVertex(p2);
  155.             writeTriangleVertex(p3);
  156.         } else {
  157.             writeTriangleVertex(p3);
  158.             writeTriangleVertex(p2);
  159.         }

  160.         write(StlConstants.LOOP_END_KEYWORD);
  161.         writeNewLine();

  162.         write(StlConstants.FACET_END_KEYWORD);
  163.         writeNewLine();
  164.     }

  165.     /** {@inheritDoc} */
  166.     @Override
  167.     public void close() {
  168.         if (started) {
  169.             endSolid();
  170.         }

  171.         super.close();
  172.     }

  173.     /** Write a triangle vertex to the output.
  174.      * @param vertex triangle vertex
  175.      * @throws java.io.UncheckedIOException if an I/O error occurs
  176.      */
  177.     private void writeTriangleVertex(final Vector3D vertex) {
  178.         write(StlConstants.VERTEX_KEYWORD);
  179.         write(SPACE);
  180.         writeVector(vertex);
  181.         writeNewLine();
  182.     }

  183.     /** Write a vector to the output.
  184.      * @param vec vector to write
  185.      * @throws java.io.UncheckedIOException if an I/O error occurs
  186.      */
  187.     private void writeVector(final Vector3D vec) {
  188.         write(vec.getX());
  189.         write(SPACE);
  190.         write(vec.getY());
  191.         write(SPACE);
  192.         write(vec.getZ());
  193.     }

  194.     /** Write the beginning or ending line of the solid definition.
  195.      * @param keyword keyword at the start of the line
  196.      * @throws java.io.UncheckedIOException if an I/O error occurs
  197.      */
  198.     private void writeBeginOrEndLine(final String keyword) {
  199.         write(keyword);
  200.         write(SPACE);

  201.         if (name != null) {
  202.             write(name);
  203.         }

  204.         writeNewLine();
  205.     }
  206. }