1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
39 final PolygonObjParser p = parser("");
40
41
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
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
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
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
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
115 final PolygonObjParser p = parser(lines(
116 "",
117 "curv2 abc"
118 ));
119 p.setFailOnNonPolygonKeywords(true);
120
121
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
132 final PolygonObjParser p = parser("");
133
134
135 assertNextKeyword(null, p);
136 }
137
138 @Test
139 void testNextKeyword_unexpectedContent() {
140
141 final PolygonObjParser p = parser(lines(
142 " f",
143 "-- bad comment attempt"
144 ));
145
146
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
161 final PolygonObjParser p = parser(lines(
162 " line\t",
163 "",
164 " \\",
165 "a \\",
166 "b\\",
167 "cd\\",
168 ".\\"
169 ));
170
171
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
181 final PolygonObjParser p = parser(lines(
182 " line\t",
183 "",
184 " \\",
185 "a \\",
186 "b\\",
187 "cd\\",
188 ".\\"
189 ));
190
191
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
212 final PolygonObjParser p = parser(lines(
213 "1.01 3e-02 123.999 extra"
214 ));
215
216
217 EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.01, 0.03, 123.999), p.readVector(), EPS);
218 }
219
220 @Test
221 void testReadVector_parseFailures() {
222
223 final PolygonObjParser p = parser(lines(
224 "0.1 0.2 a",
225 "1",
226 ""
227 ));
228
229
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
244 final PolygonObjParser p = parser(lines(
245 "0.1 0.2 3e2 4e2 500.01",
246 " 12.001 ",
247 " ",
248 ""
249 ));
250
251
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
273 final PolygonObjParser p = parser(lines(
274 "0.1 0.2 a",
275 "b"
276 ));
277
278
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
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
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
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
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
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
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
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
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
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
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
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 "\\",
552 " \\",
553 "",
554 "v 0 0 0",
555 "v 1\\", ".0 0 0",
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"
567 ));
568
569
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
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
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
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
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
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
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
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
767 nextMatchingKeyword("f", p);
768 Assertions.assertEquals(vertices.subList(1, 4), p.readFace().getVertices(vertexFn));
769 }
770
771 @Test
772 void testFace_getVerticesCounterClockwise() {
773
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
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
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
866 Assertions.assertArrayEquals(expectedVertexIndices, face.getVertexIndices());
867 Assertions.assertArrayEquals(expectedTextureIndices, face.getTextureIndices());
868 Assertions.assertArrayEquals(expectedNormalIndices, face.getNormalIndices());
869 }
870 }