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.obj;
018
019import java.io.Reader;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collections;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Set;
026import java.util.function.IntFunction;
027import java.util.function.ToIntFunction;
028import java.util.stream.Collectors;
029
030import org.apache.commons.geometry.euclidean.internal.EuclideanUtils;
031import org.apache.commons.geometry.euclidean.threed.Vector3D;
032import org.apache.commons.geometry.io.core.internal.SimpleTextParser;
033
034/** Low-level parser class for reading 3D polygon (face) data in the OBJ file format.
035 * This class provides access to OBJ data structures but does not retain any of the
036 * parsed data. For example, it is up to callers to store vertices as they are parsed
037 * for later reference. This allows callers to determine what values are stored and in
038 * what format.
039 */
040public class PolygonObjParser extends AbstractObjParser {
041
042    /** Set containing OBJ keywords commonly used with files containing only polygon content. */
043    private static final Set<String> STANDARD_POLYGON_KEYWORDS =
044            Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
045                        ObjConstants.VERTEX_KEYWORD,
046                        ObjConstants.VERTEX_NORMAL_KEYWORD,
047                        ObjConstants.TEXTURE_COORDINATE_KEYWORD,
048                        ObjConstants.FACE_KEYWORD,
049
050                        ObjConstants.OBJECT_KEYWORD,
051                        ObjConstants.GROUP_KEYWORD,
052                        ObjConstants.SMOOTHING_GROUP_KEYWORD,
053
054                        ObjConstants.MATERIAL_LIBRARY_KEYWORD,
055                        ObjConstants.USE_MATERIAL_KEYWORD
056                    )));
057
058    /** Number of vertex keywords encountered in the file so far. */
059    private int vertexCount;
060
061    /** Number of vertex normal keywords encountered in the file so far. */
062    private int vertexNormalCount;
063
064    /** Number of texture coordinate keywords encountered in the file so far. */
065    private int textureCoordinateCount;
066
067    /** If true, parsing will fail when non-polygon keywords are encountered in the OBJ content. */
068    private boolean failOnNonPolygonKeywords;
069
070    /** Construct a new instance for parsing OBJ content from the given reader.
071     * @param reader reader to parser content from
072     */
073    public PolygonObjParser(final Reader reader) {
074        this(new SimpleTextParser(reader));
075    }
076
077    /** Construct a new instance for parsing OBJ content from the given text parser.
078     * @param parser text parser to read content from
079     */
080    public PolygonObjParser(final SimpleTextParser parser) {
081        super(parser);
082    }
083
084    /** Get the number of {@link ObjConstants#VERTEX_KEYWORD vertex keywords} parsed
085     * so far.
086     * @return the number of vertex keywords parsed so far
087     */
088    public int getVertexCount() {
089        return vertexCount;
090    }
091
092    /** Get the number of {@link ObjConstants#VERTEX_NORMAL_KEYWORD vertex normal keywords} parsed
093     * so far.
094     * @return the number of vertex normal keywords parsed so far
095     */
096    public int getVertexNormalCount() {
097        return vertexNormalCount;
098    }
099
100    /** Get the number of {@link ObjConstants#TEXTURE_COORDINATE_KEYWORD texture coordinate keywords} parsed
101     * so far.
102     * @return the number of texture coordinate keywords parsed so far
103     */
104    public int getTextureCoordinateCount() {
105        return textureCoordinateCount;
106    }
107
108    /** Return true if the instance is configured to throw an {@link IllegalStateException} when OBJ keywords
109     * not commonly used with files containing only polygon data are encountered. The default value is {@code false},
110     * meaning that no keyword validation is performed. When set to true, only the following keywords are
111     * accepted:
112     * <ul>
113     *  <li>{@code v}</li>
114     *  <li>{@code vn}</li>
115     *  <li>{@code vt}</li>
116     *  <li>{@code f}</li>
117     *  <li>{@code o}</li>
118     *  <li>{@code g}</li>
119     *  <li>{@code s}</li>
120     *  <li>{@code mtllib}</li>
121     *  <li>{@code usemtl}</li>
122     * </ul>
123     * @return true if the instance is configured to fail when a non-polygon keyword is encountered
124     */
125    public boolean isFailOnNonPolygonKeywords() {
126        return failOnNonPolygonKeywords;
127    }
128
129    /** Set the flag determining if the instance should throw an {@link IllegalStateException} when encountering
130     * keywords not commonly used with OBJ files containing only polygon data. If true, only the following keywords
131     * are accepted:
132     * <ul>
133     *  <li>{@code v}</li>
134     *  <li>{@code vn}</li>
135     *  <li>{@code vt}</li>
136     *  <li>{@code f}</li>
137     *  <li>{@code o}</li>
138     *  <li>{@code g}</li>
139     *  <li>{@code s}</li>
140     *  <li>{@code mtllib}</li>
141     *  <li>{@code usemtl}</li>
142     * </ul>
143     * If false, all keywords are accepted.
144     * @param failOnNonPolygonKeywords new flag value
145     */
146    public void setFailOnNonPolygonKeywords(final boolean failOnNonPolygonKeywords) {
147        this.failOnNonPolygonKeywords = failOnNonPolygonKeywords;
148    }
149
150    /** {@inheritDoc} */
151    @Override
152    protected void handleKeyword(final String keywordValue) {
153        if (failOnNonPolygonKeywords && !STANDARD_POLYGON_KEYWORDS.contains(keywordValue)) {
154            final String allowedKeywords = STANDARD_POLYGON_KEYWORDS.stream()
155                    .sorted()
156                    .collect(Collectors.joining(", "));
157
158            throw getTextParser().tokenError("expected keyword to be one of [" + allowedKeywords +
159                    "] but was [" + keywordValue + "]");
160        }
161
162        // update counts in order to validate face vertex attributes
163        switch (keywordValue) {
164        case ObjConstants.VERTEX_KEYWORD:
165            ++vertexCount;
166            break;
167        case ObjConstants.VERTEX_NORMAL_KEYWORD:
168            ++vertexNormalCount;
169            break;
170        case ObjConstants.TEXTURE_COORDINATE_KEYWORD:
171            ++textureCoordinateCount;
172            break;
173        default:
174            break;
175        }
176    }
177
178    /** Read an OBJ face definition from the current line.
179     * @return OBJ face definition read from the current line
180     * @throws IllegalStateException if a face definition is not able to be parsed
181     * @throws java.io.UncheckedIOException if an I/O error occurs
182     */
183    public Face readFace() {
184        final List<VertexAttributes> vertices = new ArrayList<>();
185
186        while (nextDataLineContent()) {
187            vertices.add(readFaceVertex());
188        }
189
190        if (vertices.size() < EuclideanUtils.TRIANGLE_VERTEX_COUNT) {
191            throw getTextParser().parseError(
192                    "face must contain at least " + EuclideanUtils.TRIANGLE_VERTEX_COUNT +
193                    " vertices but found only " + vertices.size());
194        }
195
196        discardDataLine();
197
198        return new Face(vertices);
199    }
200
201    /** Read an OBJ face vertex definition from the current parser position.
202     * @return OBJ face vertex definition
203     * @throws IllegalStateException if a vertex definition is not able to be parsed
204     * @throws java.io.UncheckedIOException if an I/O error occurs
205     */
206    private VertexAttributes readFaceVertex() {
207        final SimpleTextParser parser = getTextParser();
208
209        discardDataLineWhitespace();
210
211        final int vertexIndex = readNormalizedVertexAttributeIndex("vertex", vertexCount);
212
213        int textureIndex = -1;
214        if (parser.peekChar() == ObjConstants.FACE_VERTEX_ATTRIBUTE_SEP_CHAR) {
215            parser.discard(1);
216
217            if (parser.peekChar() != ObjConstants.FACE_VERTEX_ATTRIBUTE_SEP_CHAR) {
218                textureIndex = readNormalizedVertexAttributeIndex("texture", textureCoordinateCount);
219            }
220        }
221
222        int normalIndex = -1;
223        if (parser.peekChar() == ObjConstants.FACE_VERTEX_ATTRIBUTE_SEP_CHAR) {
224            parser.discard(1);
225
226            if (SimpleTextParser.isIntegerPart(parser.peekChar())) {
227                normalIndex = readNormalizedVertexAttributeIndex("normal", vertexNormalCount);
228            }
229        }
230
231        return new VertexAttributes(vertexIndex, textureIndex, normalIndex);
232    }
233
234    /** Read a vertex attribute index from the current parser position and normalize it to
235     * be 0-based and positive.
236     * @param type type of attribute being read; this value is used in error messages
237     * @param available number of available values of the given type parsed from the content
238     *      so far
239     * @return 0-based positive attribute index
240     * @throws IllegalStateException if the integer index cannot be parsed or the index is
241     *      out of range for the number of parsed elements of the given type
242     * @throws java.io.UncheckedIOException if an I/O error occurs
243     */
244    private int readNormalizedVertexAttributeIndex(final String type, final int available) {
245        final SimpleTextParser parser = getTextParser();
246
247        final int objIndex = parser
248                .nextWithLineContinuation(ObjConstants.LINE_CONTINUATION_CHAR, SimpleTextParser::isIntegerPart)
249                .getCurrentTokenAsInt();
250
251        final int normalizedIndex = objIndex < 0 ?
252                available + objIndex :
253                objIndex - 1;
254
255        if (normalizedIndex < 0 || normalizedIndex >= available) {
256            final StringBuilder err = new StringBuilder();
257            err.append(type)
258                .append(" index ");
259
260            if (available < 1) {
261                err.append("cannot be used because no values of that type have been defined");
262            } else {
263                err.append("must evaluate to be within the range [1, ")
264                    .append(available)
265                    .append("] but was ")
266                    .append(objIndex);
267            }
268
269            throw parser.tokenError(err.toString());
270        }
271
272        return normalizedIndex;
273    }
274
275    /** Class representing an OBJ face definition. Faces are defined with the format
276     * <p>
277     *  <code>
278     *      f v<sub>1</sub>/vt<sub>1</sub>/vn<sub>1</sub> v<sub>2</sub>/vt<sub>2</sub>/vn<sub>2</sub> v<sub>3</sub>/vt<sub>3</sub>/vn<sub>3</sub> ...
279     *  </code>
280     * </p>
281     * <p>where the {@code v} elements are indices into the model vertices, the {@code vt}
282     * elements are indices into the model texture coordinates, and the {@code vn} elements
283     * are indices into the model normal coordinates. Only the vertex indices are required.</p>
284     *
285     * <p>All vertex attribute indices are normalized to be 0-based and positive and all
286     * faces are assumed to define geometrically valid convex polygons.</p>
287     */
288    public static final class Face {
289
290        /** List of vertex attributes for the face. */
291        private final List<VertexAttributes> vertexAttributes;
292
293        /** Construct a new instance with the given vertex attributes.
294         * @param vertexAttributes face vertex attributes
295         */
296        Face(final List<VertexAttributes> vertexAttributes) {
297            this.vertexAttributes = Collections.unmodifiableList(vertexAttributes);
298        }
299
300        /** Get the list of vertex attributes for the instance.
301         * @return list of vertex attribute
302         */
303        public List<VertexAttributes> getVertexAttributes() {
304            return vertexAttributes;
305        }
306
307        /** Get a composite normal for the face by computing the sum of all defined vertex
308         * normals and normalizing the result. Null is returned if no vertex normals are
309         * defined or the defined normals sum to zero.
310         * @param modelNormalFn function used to access normals parsed earlier in the model;
311         *      callers are responsible for storing these values as they are parsed
312         * @return composite face normal or null if no composite normal can be determined from the
313         *      normals defined for the face
314         */
315        public Vector3D getDefinedCompositeNormal(final IntFunction<Vector3D> modelNormalFn) {
316            Vector3D sum = Vector3D.ZERO;
317
318            int normalIdx;
319            for (final VertexAttributes vertex : vertexAttributes) {
320                normalIdx = vertex.getNormalIndex();
321                if (normalIdx > -1) {
322                    sum = sum.add(modelNormalFn.apply(normalIdx));
323                }
324            }
325
326            return sum.normalizeOrNull();
327        }
328
329        /** Compute a normal for the face using its first three vertices. The vertices will wind in a
330         * counter-clockwise direction when viewed looking down the returned normal. Null is returned
331         * if the normal could not be determined, which would be the case if the vertices lie in the
332         * same line or two or more are equal.
333         * @param modelVertexFn function used to access model vertices parsed earlier in the content;
334         *      callers are responsible for storing these values as they are passed
335         * @return a face normal computed from the first 3 vertices or null if a normal cannot
336         *      be determined
337         */
338        public Vector3D computeNormalFromVertices(final IntFunction<Vector3D> modelVertexFn) {
339            final Vector3D p0 = modelVertexFn.apply(vertexAttributes.get(0).getVertexIndex());
340            final Vector3D p1 = modelVertexFn.apply(vertexAttributes.get(1).getVertexIndex());
341            final Vector3D p2 = modelVertexFn.apply(vertexAttributes.get(2).getVertexIndex());
342
343            return p0.vectorTo(p1).cross(p0.vectorTo(p2)).normalizeOrNull();
344        }
345
346        /** Get the vertex attributes for the face listed in the order that produces a counter-clockwise
347         * winding of vertices when viewed looking down the given normal direction. If {@code normal}
348         * is null, the original vertex sequence is used.
349         * @param normal requested face normal; may be null
350         * @param modelVertexFn function used to access model vertices parsed earlier in the content;
351         *      callers are responsible for storing these values as they are passed
352         * @return list of vertex attributes for the face, oriented to correspond with the given
353         *      face normal
354         */
355        public List<VertexAttributes> getVertexAttributesCounterClockwise(final Vector3D normal,
356                final IntFunction<Vector3D> modelVertexFn) {
357            List<VertexAttributes> result = vertexAttributes;
358
359            if (normal != null) {
360                final Vector3D computedNormal = computeNormalFromVertices(modelVertexFn);
361                if (computedNormal != null && normal.dot(computedNormal) < 0) {
362                    // face is oriented the opposite way; reverse the order of the vertices
363                    result = new ArrayList<>(vertexAttributes);
364                    Collections.reverse(result);
365                }
366            }
367
368            return result;
369        }
370
371        /** Get the face vertices in the order defined in the face definition.
372         * @param modelVertexFn function used to access model vertices parsed earlier in the content;
373         *      callers are responsible for storing these values as they are passed
374         * @return face vertices in their defined ordering
375         */
376        public List<Vector3D> getVertices(final IntFunction<Vector3D> modelVertexFn) {
377            return vertexAttributes.stream()
378                    .map(v -> modelVertexFn.apply(v.getVertexIndex()))
379                    .collect(Collectors.toList());
380        }
381
382        /** Get the face vertices in the order that produces a counter-clockwise winding when viewed
383         * looking down the given normal.
384         * @param normal requested face normal
385         * @param modelVertexFn function used to access model vertices parsed earlier in the content;
386         *      callers are responsible for storing these values as they are passed
387         * @return face vertices in the order that produces a counter-clockwise winding when viewed
388         *      looking down the given normal
389         * @see #getVertexAttributesCounterClockwise(Vector3D, IntFunction)
390         */
391        public List<Vector3D> getVerticesCounterClockwise(final Vector3D normal,
392                final IntFunction<Vector3D> modelVertexFn) {
393            return getVertexAttributesCounterClockwise(normal, modelVertexFn).stream()
394                    .map(v -> modelVertexFn.apply(v.getVertexIndex()))
395                    .collect(Collectors.toList());
396        }
397
398        /** Get the vertex indices for the face.
399         * @return vertex indices for the face
400         */
401        public int[] getVertexIndices() {
402            return getIndices(VertexAttributes::getVertexIndex);
403        }
404
405        /** Get the texture indices for the face. The value {@code -1} is used if a texture index
406         * is not set.
407         * @return texture indices
408         */
409        public int[] getTextureIndices() {
410            return getIndices(VertexAttributes::getTextureIndex);
411        }
412
413        /** Get the normal indices for the face. The value {@code -1} is used if a texture index
414         * is not set.
415         * @return normal indices
416         */
417        public int[] getNormalIndices() {
418            return getIndices(VertexAttributes::getNormalIndex);
419        }
420
421        /** Get indices for the face, using the given function to extract the value from
422         * the vertex attributes.
423         * @param fn function used to extract the required value from each vertex attribute
424         * @return extracted indices
425         */
426        private int[] getIndices(final ToIntFunction<VertexAttributes> fn) {
427            final int len = vertexAttributes.size();
428            final int[] indices = new int[len];
429
430            for (int i = 0; i < len; ++i) {
431                indices[i] = fn.applyAsInt(vertexAttributes.get(i));
432            }
433
434            return indices;
435        }
436    }
437
438    /** Class representing a set of attributes for a face vertex. All index values are 0-based
439     * and positive, in contrast with OBJ indices which are 1-based and support negative
440     * values. If an index value is not given in the OBJ content, it is set to {@code -1}.
441     */
442    public static final class VertexAttributes {
443
444        /** Vertex index. */
445        private final int vertexIndex;
446
447        /** Texture coordinate index. */
448        private final int textureIndex;
449
450        /** Vertex normal index. */
451        private final int normalIndex;
452
453        /** Construct a new instance with the given vertices.
454         * @param vertexIndex vertex index
455         * @param textureIndex texture index
456         * @param normalIndex vertex normal index
457         */
458        VertexAttributes(final int vertexIndex, final int textureIndex, final int normalIndex) {
459            this.vertexIndex = vertexIndex;
460            this.textureIndex = textureIndex;
461            this.normalIndex = normalIndex;
462        }
463
464        /** Get the vertex position index for this instance. This value is required and is guaranteed to
465         * be a valid index into the list of vertex positions parsed so far in the OBJ content.
466         * @return vertex index
467         */
468        public int getVertexIndex() {
469            return vertexIndex;
470        }
471
472        /** Get the texture index for this instance or {@code -1} if not specified in the
473         * OBJ content.
474         * @return texture index or {@code -1} if not specified in the OBJ content.
475         */
476        public int getTextureIndex() {
477            return textureIndex;
478        }
479
480        /** Get the normal index for this instance or {@code -1} if not specified in the
481         * OBJ content.
482         * @return normal index or {@code -1} if not specified in the OBJ content.
483         */
484        public int getNormalIndex() {
485            return normalIndex;
486        }
487    }
488}