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}