View Javadoc

    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.obj;
    18
    19  import java.io.StringReader;
    20  import java.util.ArrayList;
    21  import java.util.Arrays;
    22  import java.util.Collections;
    23  import java.util.List;
    24  import java.util.function.IntFunction;
    25
    26  import org.apache.commons.geometry.core.GeometryTestUtils;
    27  import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
    28  import org.apache.commons.geometry.euclidean.threed.Vector3D;
    29  import org.junit.jupiter.api.Assertions;
    30  import org.junit.jupiter.api.Test;
    31
    32  class PolygonObjParserTest {
    33
    34      private static final double EPS = 1e-10;
    35
    36      @Test
    37      void testInitialState() {
    38          // act
    39          final PolygonObjParser p = parser("");
    40
    41          // assert
    42          Assertions.assertNull(p.getCurrentKeyword());
    43          Assertions.assertEquals(0, p.getVertexCount());
    44          Assertions.assertEquals(0, p.getVertexNormalCount());
    45          Assertions.assertEquals(0, p.getTextureCoordinateCount());
    46          Assertions.assertFalse(p.isFailOnNonPolygonKeywords());
    47      }
    48
    49      @Test
    50      void testNextKeyword() {
    51          // arrange
    52          final PolygonObjParser p = parser(lines(
    53                  "#comment",
    54                  "",
    55                  "  \t",
    56                  "o test",
    57                  "v",
    58                  "v 1 0 0 1",
    59                  "v 0 1 0",
    60                  "# comment",
    61                  " ",
    62                  "g triangle-\\",
    63                  "group",
    64                  "f 1 2 3",
    65                  "",
    66                  "curv2",
    67                  "# end"
    68          ));
    69
    70          // act/assert
    71          assertNextKeyword("o", p);
    72          assertNextKeyword("v", p);
    73          assertNextKeyword("v", p);
    74          assertNextKeyword("v", p);
    75          assertNextKeyword("g", p);
    76          assertNextKeyword("f", p);
    77          assertNextKeyword("curv2", p);
    78
    79          assertNextKeyword(null, p);
    80      }
    81
    82      @Test
    83      void testNextKeyword_polygonKeywordsOnly_valid() {
    84          // arrange
    85          final PolygonObjParser p = parser(lines(
    86                  "v",
    87                  "vn",
    88                  "vt",
    89                  "f",
    90                  "o",
    91                  "s",
    92                  "g",
    93                  "mtllib",
    94                  "usemtl"
    95          ));
    96          p.setFailOnNonPolygonKeywords(true);
    97
    98          // act/assert
    99          assertNextKeyword("v", p);
    100         assertNextKeyword("vn", p);
    101         assertNextKeyword("vt", p);
    102         assertNextKeyword("f", p);
    103         assertNextKeyword("o", p);
    104         assertNextKeyword("s", p);
    105         assertNextKeyword("g", p);
    106         assertNextKeyword("mtllib", p);
    107         assertNextKeyword("usemtl", p);
    108
    109         assertNextKeyword(null, p);
    110     }
    111
    112     @Test
    113     void testNextKeyword_polygonKeywordsOnly_invalid() {
    114         // arrange
    115         final PolygonObjParser p = parser(lines(
    116                 "",
    117                 "curv2 abc"
    118         ));
    119         p.setFailOnNonPolygonKeywords(true);
    120
    121         // act/assert
    122         GeometryTestUtils.assertThrowsWithMessage(() -> {
    123             p.nextKeyword();
    124         }, IllegalStateException.class,
    125                 "Parsing failed at line 2, column 1: expected keyword to be one of " +
    126                 "[f, g, mtllib, o, s, usemtl, v, vn, vt] but was [curv2]");
    127     }
    128
    129     @Test
    130     void testNextKeyword_emptyContent() {
    131         // arrange
    132         final PolygonObjParser p = parser("");
    133
    134         // act/assert
    135         assertNextKeyword(null, p);
    136     }
    137
    138     @Test
    139     void testNextKeyword_unexpectedContent() {
    140         // arrange
    141         final PolygonObjParser p = parser(lines(
    142                     " f",
    143                     "-- bad comment attempt"
    144                 ));
    145
    146         // act/assert
    147         GeometryTestUtils.assertThrowsWithMessage(() -> {
    148             p.nextKeyword();
    149         }, IllegalStateException.class, "Parsing failed at line 1, column 2: " +
    150             "non-blank lines must begin with an OBJ keyword or comment character");
    151
    152         GeometryTestUtils.assertThrowsWithMessage(() -> {
    153             p.nextKeyword();
    154         }, IllegalStateException.class, "Parsing failed at line 2, column 1: " +
    155             "expected OBJ keyword but found empty token followed by [-]");
    156     }
    157
    158     @Test
    159     void testReadDataLine() {
    160         // arrange
    161         final PolygonObjParser p = parser(lines(
    162                 "  line\t",
    163                 "",
    164                 " \\",
    165                 "a \\",
    166                 "b\\",
    167                 "cd\\",
    168                 ".\\"
    169         ));
    170
    171         // act/assert
    172         Assertions.assertEquals("  line\t", p.readDataLine());
    173         Assertions.assertEquals("", p.readDataLine());
    174         Assertions.assertEquals(" a bcd.", p.readDataLine());
    175         Assertions.assertNull(p.readDataLine());
    176     }
    177
    178     @Test
    179     void testDiscardDataLine() {
    180         // arrange
    181         final PolygonObjParser p = parser(lines(
    182                 "  line\t",
    183                 "",
    184                 " \\",
    185                 "a \\",
    186                 "b\\",
    187                 "cd\\",
    188                 ".\\"
    189         ));
    190
    191         // act/assert
    192         p.discardDataLine();
    193         Assertions.assertEquals(2, p.getTextParser().getLineNumber());
    194         Assertions.assertEquals(1, p.getTextParser().getColumnNumber());
    195
    196         p.discardDataLine();
    197         Assertions.assertEquals(3, p.getTextParser().getLineNumber());
    198         Assertions.assertEquals(1, p.getTextParser().getColumnNumber());
    199
    200         p.discardDataLine();
    201         Assertions.assertEquals(8, p.getTextParser().getLineNumber());
    202         Assertions.assertEquals(1, p.getTextParser().getColumnNumber());
    203
    204         p.discardDataLine();
    205         Assertions.assertEquals(8, p.getTextParser().getLineNumber());
    206         Assertions.assertEquals(1, p.getTextParser().getColumnNumber());
    207     }
    208
    209     @Test
    210     void testReadVector() {
    211         // arrange
    212         final PolygonObjParser p = parser(lines(
    213                 "1.01 3e-02 123.999 extra"
    214         ));
    215
    216         // act/assert
    217         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.01, 0.03, 123.999), p.readVector(), EPS);
    218     }
    219
    220     @Test
    221     void testReadVector_parseFailures() {
    222         // arrange
    223         final PolygonObjParser p = parser(lines(
    224                 "0.1 0.2 a",
    225                 "1",
    226                 ""
    227         ));
    228
    229         // act/assert
    230         GeometryTestUtils.assertThrowsWithMessage(() -> {
    231             p.readVector();
    232         }, IllegalStateException.class, "Parsing failed at line 1, column 9: expected double but found [a]");
    233
    234         p.readDataLine();
    235
    236         GeometryTestUtils.assertThrowsWithMessage(() -> {
    237             p.readVector();
    238         }, IllegalStateException.class, "Parsing failed at line 2, column 2: expected double but found end of line");
    239     }
    240
    241     @Test
    242     void testReadDoubles() {
    243         // arrange
    244         final PolygonObjParser p = parser(lines(
    245                 "0.1 0.2 3e2 4e2 500.01",
    246                 "  12.001  ",
    247                 "  ",
    248                 ""
    249         ));
    250
    251         // act/assert
    252         Assertions.assertArrayEquals(new double[] {
    253             0.1, 0.2, 3e2, 4e2, 500.01
    254         }, p.readDoubles(), EPS);
    255         Assertions.assertArrayEquals(new double[0], p.readDoubles(), EPS);
    256
    257         p.readDataLine();
    258
    259         Assertions.assertArrayEquals(new double[] {12.001}, p.readDoubles(), EPS);
    260
    261         p.readDataLine();
    262
    263         Assertions.assertArrayEquals(new double[0], p.readDoubles(), EPS);
    264
    265         p.readDataLine();
    266
    267         Assertions.assertArrayEquals(new double[0], p.readDoubles(), EPS);
    268     }
    269
    270     @Test
    271     void testReadDoubles_parseFailures() {
    272         // arrange
    273         final PolygonObjParser p = parser(lines(
    274                 "0.1 0.2 a",
    275                 "b"
    276         ));
    277
    278         // act/assert
    279         GeometryTestUtils.assertThrowsWithMessage(() -> {
    280             p.readDoubles();
    281         }, IllegalStateException.class, "Parsing failed at line 1, column 9: expected double but found [a]");
    282
    283         p.readDataLine();
    284
    285         GeometryTestUtils.assertThrowsWithMessage(() -> {
    286             p.readDoubles();
    287         }, IllegalStateException.class, "Parsing failed at line 2, column 1: expected double but found [b]");
    288     }
    289
    290     @Test
    291     void testReadFace() {
    292         // arrange
    293         final PolygonObjParser p = parser(lines(
    294                 "# test content",
    295                 "o test",
    296                 "v 0 0 0",
    297                 "v 1 0 0",
    298                 "v 1 1 0",
    299                 "v 0 1 0",
    300                 "vt 1 2",
    301                 "vt 3 4",
    302                 "vt 5 6",
    303                 "vt 7 8",
    304                 "vt 9 10",
    305                 "vn 0 0 1",
    306                 "vn 0 0 -1",
    307
    308                 "f 1 2 3 4",
    309                 "f -4// -3// -2// -1//",
    310
    311                 "f 1//1 2//2 3//1 4//2",
    312                 "f -4//-2 -3//-1 -2//-2 -1//-1",
    313
    314                 "f 1/4/1 2/3/2 3/2/1 4/1/2",
    315                 "f -4/-1/-2 -3/-2/-1 -2/-3/-2 -1/-4/-1",
    316
    317                 "f 1/4 2/3 3/2 4/1",
    318                 "f -4/-1 -3/-2 -2/-3 -1/-4"
    319         ));
    320
    321         nextFace(p);
    322
    323         // act/assert
    324         assertFace(new int[][] {
    325             {0, -1, -1},
    326             {1, -1, -1},
    327             {2, -1, -1},
    328             {3, -1, -1},
    329         }, p.readFace());
    330
    331         nextFace(p);
    332
    333         assertFace(new int[][] {
    334             {0, -1, -1},
    335             {1, -1, -1},
    336             {2, -1, -1},
    337             {3, -1, -1},
    338         }, p.readFace());
    339
    340         nextFace(p);
    341
    342         assertFace(new int[][] {
    343             {0, -1, 0},
    344             {1, -1, 1},
    345             {2, -1, 0},
    346             {3, -1, 1},
    347         }, p.readFace());
    348
    349         nextFace(p);
    350
    351         assertFace(new int[][] {
    352             {0, -1, 0},
    353             {1, -1, 1},
    354             {2, -1, 0},
    355             {3, -1, 1},
    356         }, p.readFace());
    357
    358         nextFace(p);
    359
    360         assertFace(new int[][] {
    361             {0, 3, 0},
    362             {1, 2, 1},
    363             {2, 1, 0},
    364             {3, 0, 1},
    365         }, p.readFace());
    366
    367         nextFace(p);
    368
    369         assertFace(new int[][] {
    370             {0, 4, 0},
    371             {1, 3, 1},
    372             {2, 2, 0},
    373             {3, 1, 1},
    374         }, p.readFace());
    375
    376         nextFace(p);
    377
    378         assertFace(new int[][] {
    379             {0, 3, -1},
    380             {1, 2, -1},
    381             {2, 1, -1},
    382             {3, 0, -1},
    383         }, p.readFace());
    384
    385         nextFace(p);
    386
    387         assertFace(new int[][] {
    388             {0, 4, -1},
    389             {1, 3, -1},
    390             {2, 2, -1},
    391             {3, 1, -1},
    392         }, p.readFace());
    393     }
    394
    395     @Test
    396     void testReadFace_notEnoughVertices() {
    397         // arrange
    398         final PolygonObjParser p = parser(lines(
    399                 "# test content",
    400                 "v 0 0 0",
    401                 "v 1 0 0",
    402                 "v 1 1 0",
    403                 "f 1 2"
    404         ));
    405
    406         // act/assert
    407         nextFace(p);
    408         GeometryTestUtils.assertThrowsWithMessage(() -> {
    409             p.readFace();
    410         }, IllegalStateException.class, "Parsing failed at line 5, column 6: " +
    411             "face must contain at least 3 vertices but found only 2");
    412     }
    413
    414     @Test
    415     void testReadFace_invalidVertexIndex() {
    416         // arrange
    417         final PolygonObjParser p = parser(lines(
    418                 "# test content",
    419                 "f 1 2 3",
    420                 "v 0 0 0",
    421                 "v 1 0 0",
    422                 "v 1 1 0",
    423                 "f 1 2 -4",
    424                 "f 1 0 3",
    425                 "f 4 2 3"
    426         ));
    427
    428         // act/assert
    429         nextFace(p);
    430         GeometryTestUtils.assertThrowsWithMessage(() -> {
    431             p.readFace();
    432         }, IllegalStateException.class, "Parsing failed at line 2, column 3: " +
    433             "vertex index cannot be used because no values of that type have been defined");
    434
    435         nextFace(p);
    436         GeometryTestUtils.assertThrowsWithMessage(() -> {
    437             p.readFace();
    438         }, IllegalStateException.class, "Parsing failed at line 6, column 7: " +
    439             "vertex index must evaluate to be within the range [1, 3] but was -4");
    440
    441         nextFace(p);
    442         GeometryTestUtils.assertThrowsWithMessage(() -> {
    443             p.readFace();
    444         }, IllegalStateException.class, "Parsing failed at line 7, column 5: " +
    445             "vertex index must evaluate to be within the range [1, 3] but was 0");
    446
    447         nextFace(p);
    448         GeometryTestUtils.assertThrowsWithMessage(() -> {
    449             p.readFace();
    450         }, IllegalStateException.class, "Parsing failed at line 8, column 3: " +
    451             "vertex index must evaluate to be within the range [1, 3] but was 4");
    452     }
    453
    454     @Test
    455     void testReadFace_invalidTextureIndex() {
    456         // arrange
    457         final PolygonObjParser p = parser(lines(
    458                 "# test content",
    459                 "v 0 0 0",
    460                 "v 1 0 0",
    461                 "v 1 1 0",
    462                 "f 1/1 2/2 3/3",
    463                 "vt 1 2",
    464                 "vt 3 4",
    465                 "vt 5 6",
    466                 "f 1/1 2/2 3/-4",
    467                 "f 1/1 1/0 3/3",
    468                 "f 1/4 2/2 3/3"
    469         ));
    470
    471         // act/assert
    472         nextFace(p);
    473         GeometryTestUtils.assertThrowsWithMessage(() -> {
    474             p.readFace();
    475         }, IllegalStateException.class, "Parsing failed at line 5, column 5: " +
    476             "texture index cannot be used because no values of that type have been defined");
    477
    478         nextFace(p);
    479         GeometryTestUtils.assertThrowsWithMessage(() -> {
    480             p.readFace();
    481         }, IllegalStateException.class, "Parsing failed at line 9, column 13: " +
    482             "texture index must evaluate to be within the range [1, 3] but was -4");
    483
    484         nextFace(p);
    485         GeometryTestUtils.assertThrowsWithMessage(() -> {
    486             p.readFace();
    487         }, IllegalStateException.class, "Parsing failed at line 10, column 9: " +
    488             "texture index must evaluate to be within the range [1, 3] but was 0");
    489
    490         nextFace(p);
    491         GeometryTestUtils.assertThrowsWithMessage(() -> {
    492             p.readFace();
    493         }, IllegalStateException.class, "Parsing failed at line 11, column 5: " +
    494             "texture index must evaluate to be within the range [1, 3] but was 4");
    495     }
    496
    497     @Test
    498     void testReadFace_invalidNormalIndex() {
    499         // arrange
    500         final PolygonObjParser p = parser(lines(
    501                 "# test content",
    502                 "v 0 0 0",
    503                 "v 1 0 0",
    504                 "v 1 1 0",
    505                 "f 1//1 2//2 3//3",
    506                 "vn 1 0 0",
    507                 "vn 0 1 0",
    508                 "vn 0 0 1",
    509                 "f 1//1 2//2 3//-4",
    510                 "f 1//1 1//0 3//3",
    511                 "f 1//4 2//2 3//3"
    512         ));
    513
    514         // act/assert
    515         nextFace(p);
    516         GeometryTestUtils.assertThrowsWithMessage(() -> {
    517             p.readFace();
    518         }, IllegalStateException.class, "Parsing failed at line 5, column 6: " +
    519             "normal index cannot be used because no values of that type have been defined");
    520
    521         nextFace(p);
    522         GeometryTestUtils.assertThrowsWithMessage(() -> {
    523             p.readFace();
    524         }, IllegalStateException.class, "Parsing failed at line 9, column 16: " +
    525             "normal index must evaluate to be within the range [1, 3] but was -4");
    526
    527         nextFace(p);
    528         GeometryTestUtils.assertThrowsWithMessage(() -> {
    529             p.readFace();
    530         }, IllegalStateException.class, "Parsing failed at line 10, column 11: " +
    531             "normal index must evaluate to be within the range [1, 3] but was 0");
    532
    533         nextFace(p);
    534         GeometryTestUtils.assertThrowsWithMessage(() -> {
    535             p.readFace();
    536         }, IllegalStateException.class, "Parsing failed at line 11, column 6: " +
    537             "normal index must evaluate to be within the range [1, 3] but was 4");
    538     }
    539
    540     @Test
    541     void testParse() {
    542         // arrange
    543         final PolygonObjParser p = parser(lines(
    544                 "# test content",
    545                 "o test",
    546                 "g test",
    547                 "s test",
    548                 "mtllib mylib.mtl",
    549                 "usemtl mymaterial",
    550                 "",
    551                 "\\", // line continuation
    552                 " \\", // line continuation
    553                 "",
    554                 "v 0 0 0",
    555                 "v 1\\", ".0 0 0", // line continuation
    556                 "v 1 1 0",
    557                 "v 0 1 0",
    558                 "",
    559                 "vt 0 0",
    560                 "vt 1 0",
    561                 "vt 1 1",
    562                 "",
    563                 "vn 0 0 1",
    564                 "",
    565                 "f 1 2 4",
    566                 "f 1/1/1 2/2/1 3\\", "/3/1" // line continuation
    567         ));
    568
    569         // act/assert
    570         assertNextKeyword("o", p);
    571         Assertions.assertEquals("test", p.readDataLine());
    572
    573         assertNextKeyword("g", p);
    574         Assertions.assertEquals("test", p.readDataLine());
    575
    576         assertNextKeyword("s", p);
    577         Assertions.assertEquals("test", p.readDataLine());
    578
    579         assertNextKeyword("mtllib", p);
    580         Assertions.assertEquals("mylib.mtl", p.readDataLine());
    581
    582         assertNextKeyword("usemtl", p);
    583         Assertions.assertEquals("mymaterial", p.readDataLine());
    584
    585         assertNextKeyword("v", p);
    586         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, p.readVector(), EPS);
    587
    588         assertNextKeyword("v", p);
    589         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_X, p.readVector(), EPS);
    590
    591         assertNextKeyword("v", p);
    592         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, 0), p.readVector(), EPS);
    593
    594         assertNextKeyword("v", p);
    595         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_Y, p.readVector(), EPS);
    596
    597         assertNextKeyword("vt", p);
    598         Assertions.assertArrayEquals(new double[] {0, 0}, p.readDoubles(),  EPS);
    599
    600         assertNextKeyword("vt", p);
    601         Assertions.assertArrayEquals(new double[] {1, 0}, p.readDoubles(),  EPS);
    602
    603         assertNextKeyword("vt", p);
    604         Assertions.assertArrayEquals(new double[] {1, 1}, p.readDoubles(),  EPS);
    605
    606         assertNextKeyword("vn", p);
    607         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_Z, p.readVector(), EPS);
    608
    609         assertNextKeyword("f", p);
    610         assertFace(new int[][] {
    611             {0, -1, -1},
    612             {1, -1, -1},
    613             {3, -1, -1},
    614         }, p.readFace());
    615
    616         assertNextKeyword("f", p);
    617         assertFace(new int[][] {
    618             {0, 0, 0},
    619             {1, 1, 0},
    620             {2, 2, 0},
    621         }, p.readFace());
    622
    623         Assertions.assertEquals(4, p.getVertexCount());
    624         Assertions.assertEquals(3, p.getTextureCoordinateCount());
    625         Assertions.assertEquals(1, p.getVertexNormalCount());
    626     }
    627
    628     @Test
    629     void testFace_getDefinedCompositeNormal() {
    630         // arrange
    631         final PolygonObjParser p = parser(lines(
    632                 "v 0 0 0",
    633                 "v 1 0 0",
    634                 "v 1 1 0",
    635                 "v 0 1 0",
    636                 "",
    637                 "vn 0 0 1",
    638                 "vn 0 0 -1",
    639                 "vn 2 2 2",
    640                 "vn -2 2 2",
    641                 "",
    642                 "f 1 2 3 4",
    643                 "f 1//1 2 3",
    644                 "f 1//1 2//1 3//1 4//1",
    645                 "f 1//1 2//2 3//1 4//2",
    646                 "f 1//-2 2//-1 3//3 4//4"
    647         ));
    648
    649         final List<Vector3D> normals = Arrays.asList(
    650                 Vector3D.Unit.PLUS_Z,
    651                 Vector3D.Unit.MINUS_Z,
    652                 Vector3D.of(1, 1, 1),
    653                 Vector3D.of(-1, 1, 1));
    654         final IntFunction<Vector3D> normalFn = normals::get;
    655
    656         // act/assert
    657         nextMatchingKeyword("f", p);
    658         Assertions.assertNull(p.readFace().getDefinedCompositeNormal(normalFn));
    659
    660         nextMatchingKeyword("f", p);
    661         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_Z,
    662                 p.readFace().getDefinedCompositeNormal(normalFn), EPS);
    663
    664         nextMatchingKeyword("f", p);
    665         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_Z,
    666                 p.readFace().getDefinedCompositeNormal(normalFn), EPS);
    667
    668         nextMatchingKeyword("f", p);
    669         Assertions.assertNull(p.readFace().getDefinedCompositeNormal(normalFn));
    670
    671         nextMatchingKeyword("f", p);
    672         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 1, 1).normalize(),
    673                 p.readFace().getDefinedCompositeNormal(normalFn), EPS);
    674     }
    675
    676     @Test
    677     void testFace_computeNormalFromVertices() {
    678         // arrange
    679         final PolygonObjParser p = parser(lines(
    680                 "v 0 0 0",
    681                 "v 1 0 0",
    682                 "v 2 0 0",
    683                 "v 0 1 0",
    684                 "",
    685                 "vn 0 0 1",
    686                 "",
    687                 "f 1 2 4",
    688                 "f 1//1 2//1 3//1"
    689         ));
    690
    691         final List<Vector3D> vertices = Arrays.asList(
    692                 Vector3D.ZERO,
    693                 Vector3D.Unit.PLUS_X,
    694                 Vector3D.of(2, 0, 0),
    695                 Vector3D.of(0, 1, 0));
    696         final IntFunction<Vector3D> vertexFn = vertices::get;
    697
    698         // act/assert
    699         nextMatchingKeyword("f", p);
    700         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_Z,
    701                 p.readFace().computeNormalFromVertices(vertexFn), EPS);
    702
    703         nextMatchingKeyword("f", p);
    704         Assertions.assertNull(p.readFace().computeNormalFromVertices(vertexFn));
    705     }
    706
    707     @Test
    708     void testFace_getVertexAttributesCounterClockwise() {
    709         // arrange
    710         final PolygonObjParser p = parser(lines(
    711                 "v 0 0 0",
    712                 "v 1 0 0",
    713                 "v 0 1 0",
    714                 "f 1 2 3"
    715         ));
    716
    717         final List<Vector3D> vertices = Arrays.asList(
    718                 Vector3D.ZERO,
    719                 Vector3D.Unit.PLUS_X,
    720                 Vector3D.Unit.PLUS_Y,
    721                 Vector3D.of(2, 0, 0));
    722         final IntFunction<Vector3D> vertexFn = vertices::get;
    723
    724         nextMatchingKeyword("f", p);
    725         final PolygonObjParser.Face f = p.readFace();
    726
    727         final List<PolygonObjParser.VertexAttributes> attrs = f.getVertexAttributes();
    728
    729         final List<PolygonObjParser.VertexAttributes> reverseAttrs = new ArrayList<>(attrs);
    730         Collections.reverse(reverseAttrs);
    731
    732         // act/assert
    733         Assertions.assertEquals(attrs, f.getVertexAttributesCounterClockwise(null, vertexFn));
    734
    735         Assertions.assertEquals(attrs, f.getVertexAttributesCounterClockwise(Vector3D.Unit.PLUS_Z, vertexFn));
    736         Assertions.assertEquals(attrs, f.getVertexAttributesCounterClockwise(Vector3D.of(1, 0, 0.1), vertexFn));
    737         Assertions.assertEquals(attrs, f.getVertexAttributesCounterClockwise(Vector3D.Unit.PLUS_X, vertexFn));
    738
    739         Assertions.assertEquals(reverseAttrs, f.getVertexAttributesCounterClockwise(Vector3D.Unit.MINUS_Z, vertexFn));
    740         Assertions.assertEquals(reverseAttrs, f.getVertexAttributesCounterClockwise(Vector3D.of(1, 0, -0.1), vertexFn));
    741     }
    742
    743     @Test
    744     void testFace_getVertices() {
    745         // arrange
    746         final PolygonObjParser p = parser(lines(
    747                 "v 0 0 0",
    748                 "v 1 0 0",
    749                 "v 1 1 0",
    750                 "v 0 1 0",
    751                 "v 0 0 1",
    752                 "v 0 0 -1",
    753                 "",
    754                 "f 2 3 4"
    755         ));
    756
    757         final List<Vector3D> vertices = Arrays.asList(
    758                 Vector3D.ZERO,
    759                 Vector3D.Unit.PLUS_X,
    760                 Vector3D.of(1, 1, 0),
    761                 Vector3D.Unit.PLUS_Y,
    762                 Vector3D.of(0, 0, 1),
    763                 Vector3D.of(0, 0, -1));
    764         final IntFunction<Vector3D> vertexFn = vertices::get;
    765
    766         // act/assert
    767         nextMatchingKeyword("f", p);
    768         Assertions.assertEquals(vertices.subList(1, 4), p.readFace().getVertices(vertexFn));
    769     }
    770
    771     @Test
    772     void testFace_getVerticesCounterClockwise() {
    773         // arrange
    774         final PolygonObjParser p = parser(lines(
    775                 "v 0 0 0",
    776                 "v 1 0 0",
    777                 "v 0 1 0",
    778                 "v 0 0 -1",
    779                 "f 1 2 3"
    780         ));
    781
    782         final List<Vector3D> vertices = Arrays.asList(
    783                 Vector3D.ZERO,
    784                 Vector3D.Unit.PLUS_X,
    785                 Vector3D.Unit.PLUS_Y,
    786                 Vector3D.of(0, 0, -1));
    787         final IntFunction<Vector3D> vertexFn = vertices::get;
    788
    789         final List<Vector3D> faceVertices = vertices.subList(0, 3);
    790         final List<Vector3D> reverseFaceVertices = new ArrayList<>(faceVertices);
    791         Collections.reverse(reverseFaceVertices);
    792
    793         nextMatchingKeyword("f", p);
    794         final PolygonObjParser.Face f = p.readFace();
    795
    796         // act/assert
    797         Assertions.assertEquals(faceVertices, f.getVerticesCounterClockwise(null, vertexFn));
    798
    799         Assertions.assertEquals(faceVertices, f.getVerticesCounterClockwise(Vector3D.Unit.PLUS_Z, vertexFn));
    800         Assertions.assertEquals(faceVertices, f.getVerticesCounterClockwise(Vector3D.of(1, 0, 0.1), vertexFn));
    801         Assertions.assertEquals(faceVertices, f.getVerticesCounterClockwise(Vector3D.Unit.PLUS_X, vertexFn));
    802
    803         Assertions.assertEquals(reverseFaceVertices, f.getVerticesCounterClockwise(Vector3D.Unit.MINUS_Z, vertexFn));
    804         Assertions.assertEquals(reverseFaceVertices, f.getVerticesCounterClockwise(Vector3D.of(1, 0, -0.1), vertexFn));
    805     }
    806
    807     private static PolygonObjParser parser(final String content) {
    808         return new PolygonObjParser(new StringReader(content));
    809     }
    810
    811     private static String lines(final String... lines) {
    812         final String[] newlineOptions = {"\n", "\r", "\r\n"};
    813
    814         final StringBuilder sb = new StringBuilder();
    815         for (int i = 0; i < lines.length; ++i) {
    816             sb.append(lines[i])
    817                 .append(newlineOptions[i % newlineOptions.length]);
    818         }
    819
    820         return sb.toString();
    821     }
    822
    823     private static void nextFace(final PolygonObjParser parser) {
    824         nextMatchingKeyword(ObjConstants.FACE_KEYWORD, parser);
    825     }
    826
    827     private static void nextMatchingKeyword(final String keyword, final PolygonObjParser parser) {
    828         while (parser.nextKeyword()) {
    829             if (keyword.equals(parser.getCurrentKeyword())) {
    830                 return;
    831             }
    832         }
    833     }
    834
    835     private static void assertNextKeyword(final String expected, final PolygonObjParser parser) {
    836         Assertions.assertEquals(expected != null, parser.nextKeyword());
    837         Assertions.assertEquals(expected, parser.getCurrentKeyword());
    838     }
    839
    840     private static void assertFace(final int[][] vertexAttributes, final PolygonObjParser.Face face) {
    841         Assertions.assertEquals(vertexAttributes.length, face.getVertexAttributes().size());
    842
    843         final int[] expectedVertexIndices = new int[vertexAttributes.length];
    844         final int[] expectedTextureIndices = new int[vertexAttributes.length];
    845         final int[] expectedNormalIndices = new int[vertexAttributes.length];
    846
    847         // check the indices directly on the vertex attributes
    848         PolygonObjParser.VertexAttributes attrs;
    849         String msg;
    850         for (int i = 0; i < vertexAttributes.length; ++i) {
    851             attrs = face.getVertexAttributes().get(i);
    852
    853             msg = "Unexpected face vertex attributes at index " + i;
    854             Assertions.assertArrayEquals(vertexAttributes[i], new int[] {
    855                     attrs.getVertexIndex(),
    856                     attrs.getTextureIndex(),
    857                     attrs.getNormalIndex()
    858             }, msg);
    859
    860             expectedVertexIndices[i] = attrs.getVertexIndex();
    861             expectedTextureIndices[i] = attrs.getTextureIndex();
    862             expectedNormalIndices[i] = attrs.getNormalIndex();
    863         }
    864
    865         // check the individual index arrays from the face
    866         Assertions.assertArrayEquals(expectedVertexIndices, face.getVertexIndices());
    867         Assertions.assertArrayEquals(expectedTextureIndices, face.getTextureIndices());
    868         Assertions.assertArrayEquals(expectedNormalIndices, face.getNormalIndices());
    869     }
    870 }