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 }