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.Reader;
020import java.util.Arrays;
021
022import org.apache.commons.geometry.euclidean.threed.Vector3D;
023import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
024import org.apache.commons.geometry.io.core.internal.SimpleTextParser;
025import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;
026import org.apache.commons.geometry.io.euclidean.threed.FacetDefinitionReader;
027import org.apache.commons.geometry.io.euclidean.threed.SimpleFacetDefinition;
028
029/** {@link FacetDefinitionReader} for reading the text (i.e., "ASCII") version of the STL file format.
030 * @see <a href="https://en.wikipedia.org/wiki/STL_%28file_format%29#ASCII_STL">ASCII STL</a>
031 */
032public class TextStlFacetDefinitionReader implements FacetDefinitionReader {
033
034    /** Underlying reader instance. */
035    private Reader reader;
036
037    /** Text parser. */
038    private SimpleTextParser parser;
039
040    /** Flag indicating if the start of a solid definition was detected. */
041    private boolean foundSolidStart;
042
043    /** Flag indicating if the end of a solid definition was detected. */
044    private boolean foundSolidEnd;
045
046    /** The name of the solid being read. */
047    private String solidName;
048
049    /** Construct a new instance for reading text STL content from the given reader.
050     * @param reader reader to read characters from
051     */
052    public TextStlFacetDefinitionReader(final Reader reader) {
053        this.reader = reader;
054        this.parser = new SimpleTextParser(reader);
055    }
056
057    /** Get the name of the STL solid being read or null if no name was specified.
058     * @return the name of the STL solid being read or null if no name was specified
059     * @throws IllegalStateException if a data format error occurs
060     * @throws java.io.UncheckedIOException if an I/O error occurs
061     */
062    public String getSolidName() {
063        ensureSolidStarted();
064
065        return solidName;
066    }
067
068    /** {@inheritDoc} */
069    @Override
070    public FacetDefinition readFacet() {
071        if (!foundSolidEnd && parser.hasMoreCharacters()) {
072            ensureSolidStarted();
073
074            nextWord();
075
076            int choice = parser.chooseIgnoreCase(
077                    StlConstants.FACET_START_KEYWORD,
078                    StlConstants.SOLID_END_KEYWORD);
079
080            if (choice == 0) {
081                return readFacetInternal();
082            } else {
083                foundSolidEnd = true;
084            }
085        }
086
087        return null;
088    }
089
090    /** {@inheritDoc} */
091    @Override
092    public void close() {
093        GeometryIOUtils.closeUnchecked(reader);
094    }
095
096    /** Internal method to read a single facet from the STL content.
097     * @return next facet definition
098     * @throws IllegalStateException if a data format error occurs
099     * @throws java.io.UncheckedIOException if an I/O error occurs
100     */
101    private FacetDefinition readFacetInternal() {
102        matchKeyword(StlConstants.NORMAL_KEYWORD);
103        final Vector3D normal = readVector();
104
105        matchKeyword(StlConstants.OUTER_KEYWORD);
106        matchKeyword(StlConstants.LOOP_START_KEYWORD);
107
108        matchKeyword(StlConstants.VERTEX_KEYWORD);
109        final Vector3D p1 = readVector();
110
111        matchKeyword(StlConstants.VERTEX_KEYWORD);
112        final Vector3D p2 = readVector();
113
114        matchKeyword(StlConstants.VERTEX_KEYWORD);
115        final Vector3D p3 = readVector();
116
117        matchKeyword(StlConstants.LOOP_END_KEYWORD);
118        matchKeyword(StlConstants.FACET_END_KEYWORD);
119
120        return new SimpleFacetDefinition(Arrays.asList(p1, p2, p3), normal);
121    }
122
123    /** Ensure that an STL solid definition is in the process of being read. If not, the beginning
124     * of a the definition is attempted to be read from the input.
125     * @throws IllegalStateException if a data format error occurs
126     * @throws java.io.UncheckedIOException if an I/O error occurs
127     */
128    private void ensureSolidStarted() {
129        if (!foundSolidStart) {
130            beginSolid();
131
132            foundSolidStart = true;
133        }
134    }
135
136    /** Begin reading an STL solid definition. The "solid" keyword is read
137     * along with the name of the solid.
138     * @throws IllegalStateException if a data format error occurs
139     * @throws java.io.UncheckedIOException if an I/O error occurs
140     */
141    private void beginSolid() {
142        matchKeyword(StlConstants.SOLID_START_KEYWORD);
143
144        solidName = trimmedOrNull(parser.nextLine()
145                .getCurrentToken());
146    }
147
148    /** Read the next word from the content, discarding preceding whitespace.
149     * @throws IllegalStateException if a data format error occurs
150     * @throws java.io.UncheckedIOException if an I/O error occurs
151     */
152    private void nextWord() {
153        parser.discardWhitespace()
154            .nextAlphanumeric();
155    }
156
157    /** Read the next word from the content and match it against the given keyword.
158     * @param keyword keyword to match against
159     * @throws IllegalStateException if the read content does not match the given keyword
160     * @throws java.io.UncheckedIOException if an I/O error occurs or
161     */
162    private void matchKeyword(final String keyword) {
163        nextWord();
164        parser.matchIgnoreCase(keyword);
165    }
166
167    /** Read a vector from the input.
168     * @return the vector read from the input
169     * @throws IllegalStateException if a data format error occurs
170     * @throws java.io.UncheckedIOException if an I/O error occurs
171     */
172    private Vector3D readVector() {
173        final double x = readDouble();
174        final double y = readDouble();
175        final double z = readDouble();
176
177        return Vector3D.of(x, y, z);
178    }
179
180    /** Read a double value from the input.
181     * @return double value read from the input
182     * @throws IllegalStateException if a data format error occurs
183     * @throws java.io.UncheckedIOException if an I/O error occurs
184     */
185    private double readDouble() {
186        return parser
187                .discardWhitespace()
188                .next(SimpleTextParser::isDecimalPart)
189                .getCurrentTokenAsDouble();
190    }
191
192    /** Return a trimmed version of the given string or null if the string contains
193     * only whitespace.
194     * @param str input stream
195     * @return a trimmed version of the given string or null if the string contains only
196     *      whitespace
197     */
198    private static String trimmedOrNull(final String str) {
199        if (str != null) {
200            final String trimmed = str.trim();
201            if (!trimmed.isEmpty()) {
202                return trimmed;
203            }
204        }
205
206        return null;
207    }
208}