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.StringWriter;
20  import java.text.DecimalFormat;
21  import java.text.DecimalFormatSymbols;
22  import java.util.Arrays;
23  import java.util.Locale;
24  import java.util.regex.Pattern;
25  
26  import org.apache.commons.geometry.core.GeometryTestUtils;
27  import org.apache.commons.geometry.euclidean.threed.BoundarySource3D;
28  import org.apache.commons.geometry.euclidean.threed.Planes;
29  import org.apache.commons.geometry.euclidean.threed.Vector3D;
30  import org.apache.commons.geometry.euclidean.threed.mesh.SimpleTriangleMesh;
31  import org.apache.commons.geometry.io.euclidean.threed.SimpleFacetDefinition;
32  import org.apache.commons.numbers.core.Precision;
33  import org.junit.jupiter.api.Assertions;
34  import org.junit.jupiter.api.Test;
35  
36  class ObjWriterTest {
37  
38      private static final double TEST_EPS = 1e-10;
39  
40      private static final Precision.DoubleEquivalence TEST_PRECISION =
41              Precision.doubleEquivalenceOfEpsilon(TEST_EPS);
42  
43      @Test
44      void testPropertyDefaults() {
45          // arrange
46          final StringWriter writer = new StringWriter();
47  
48          // act/assert
49          try (ObjWriter objWriter = new ObjWriter(writer)) {
50              Assertions.assertEquals("\n", objWriter.getLineSeparator());
51              Assertions.assertNotNull(objWriter.getDoubleFormat());
52              Assertions.assertEquals(0, objWriter.getVertexCount());
53              Assertions.assertEquals(0, objWriter.getVertexNormalCount());
54          }
55      }
56  
57      @Test
58      void testClose_calledMultipleTimes() {
59          // arrange
60          final StringWriter writer = new StringWriter();
61  
62          // act/assert
63          try (ObjWriter objWriter = new ObjWriter(writer)) {
64              objWriter.close();
65          }
66  
67          Assertions.assertEquals("", writer.toString());
68      }
69  
70      @Test
71      void testSetLineSeparator() {
72          // arrange
73          final StringWriter writer = new StringWriter();
74  
75          // act
76          try (ObjWriter objWriter = new ObjWriter(writer)) {
77              objWriter.setLineSeparator("\r\n");
78  
79              objWriter.writeComment("line 1");
80              objWriter.writeComment("line 2");
81              objWriter.writeVertex(Vector3D.ZERO);
82          }
83  
84          // assert
85          Assertions.assertEquals(
86              "# line 1\r\n" +
87              "# line 2\r\n" +
88              "v 0.0 0.0 0.0\r\n", writer.getBuffer().toString());
89      }
90  
91      @Test
92      void testSetDecimalFormat() {
93          // arrange
94          final StringWriter writer = new StringWriter();
95          final DecimalFormat fmt =
96                  new DecimalFormat("0.0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
97  
98          // act
99          try (ObjWriter objWriter = new ObjWriter(writer)) {
100             objWriter.setDoubleFormat(fmt::format);
101 
102             objWriter.writeVertex(Vector3D.of(1.09, 2.05, 3.06));
103         }
104 
105         // assert
106         Assertions.assertEquals("v 1.1 2.0 3.1\n", writer.getBuffer().toString());
107     }
108 
109     @Test
110     void testWriteComment() {
111         // arrange
112         final StringWriter writer = new StringWriter();
113 
114         // act
115         try (ObjWriter objWriter = new ObjWriter(writer)) {
116             objWriter.writeComment("test");
117             objWriter.writeComment(" a\r\n multi-line\ncomment");
118         }
119 
120         // assert
121         Assertions.assertEquals(
122             "# test\n" +
123             "#  a\n" +
124             "#  multi-line\n" +
125             "# comment\n", writer.getBuffer().toString());
126     }
127 
128     @Test
129     void testWriteObjectName() {
130         // arrange
131         final StringWriter writer = new StringWriter();
132 
133         // act
134         try (ObjWriter objWriter = new ObjWriter(writer)) {
135             objWriter.writeObjectName("test-object");
136         }
137 
138         // assert
139         Assertions.assertEquals("o test-object\n", writer.getBuffer().toString());
140     }
141 
142     @Test
143     void testWriteGroupName() {
144         // arrange
145         final StringWriter writer = new StringWriter();
146 
147         // act
148         try (ObjWriter objWriter = new ObjWriter(writer)) {
149             objWriter.writeGroupName("test-group");
150         }
151 
152         // assert
153         Assertions.assertEquals("g test-group\n", writer.getBuffer().toString());
154     }
155 
156     @Test
157     void testWriteVertex() {
158         // arrange
159         final StringWriter writer = new StringWriter();
160 
161         // arrange
162         final DecimalFormat fmt =
163                 new DecimalFormat("0.0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
164 
165         // act
166         final int index1;
167         final int index2;
168         final int count;
169         try (ObjWriter objWriter = new ObjWriter(writer)) {
170             objWriter.setDoubleFormat(fmt::format);
171 
172             index1 = objWriter.writeVertex(Vector3D.of(1.09, 2.1, 3.005));
173             index2 = objWriter.writeVertex(Vector3D.of(0.06, 10, 12));
174 
175             count = objWriter.getVertexCount();
176         }
177 
178         // assert
179         Assertions.assertEquals(0, index1);
180         Assertions.assertEquals(1, index2);
181         Assertions.assertEquals(2, count);
182         Assertions.assertEquals(
183             "v 1.1 2.1 3.0\n" +
184             "v 0.1 10.0 12.0\n", writer.getBuffer().toString());
185     }
186 
187     @Test
188     void testWriteNormal() {
189         // arrange
190         final StringWriter writer = new StringWriter();
191         final DecimalFormat fmt =
192                 new DecimalFormat("0.0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
193 
194         // act
195         final int index1;
196         final int index2;
197         final int count;
198         try (ObjWriter objWriter = new ObjWriter(writer)) {
199             objWriter.setDoubleFormat(fmt::format);
200 
201             index1 = objWriter.writeVertexNormal(Vector3D.of(1.09, 2.1, 3.005));
202             index2 = objWriter.writeVertexNormal(Vector3D.of(0.06, 10, 12));
203 
204             count = objWriter.getVertexNormalCount();
205         }
206 
207         // assert
208         Assertions.assertEquals(0, index1);
209         Assertions.assertEquals(1, index2);
210         Assertions.assertEquals(2, count);
211         Assertions.assertEquals(
212             "vn 1.1 2.1 3.0\n" +
213             "vn 0.1 10.0 12.0\n", writer.getBuffer().toString());
214     }
215 
216     @Test
217     void testWriteFace() {
218         // arrange
219         final StringWriter writer = new StringWriter();
220 
221         // act
222         try (ObjWriter objWriter = new ObjWriter(writer)) {
223             objWriter.writeVertex(Vector3D.ZERO);
224             objWriter.writeVertex(Vector3D.of(1, 0, 0));
225             objWriter.writeVertex(Vector3D.of(1, 1, 0));
226             objWriter.writeVertex(Vector3D.of(0, 1, 0));
227 
228             objWriter.writeFace(0, 1, 2);
229             objWriter.writeFace(0, 1, 2, 3);
230         }
231 
232         // assert
233         Assertions.assertEquals(
234             "v 0.0 0.0 0.0\n" +
235             "v 1.0 0.0 0.0\n" +
236             "v 1.0 1.0 0.0\n" +
237             "v 0.0 1.0 0.0\n" +
238             "f 1 2 3\n" +
239             "f 1 2 3 4\n", writer.getBuffer().toString());
240     }
241 
242     @Test
243     void testWriteFace_withNormals() {
244         // arrange
245         final StringWriter writer = new StringWriter();
246 
247         // act
248         try (ObjWriter objWriter = new ObjWriter(writer)) {
249             objWriter.writeVertex(Vector3D.ZERO);
250             objWriter.writeVertex(Vector3D.of(1, 0, 0));
251             objWriter.writeVertex(Vector3D.of(1, 1, 0));
252             objWriter.writeVertex(Vector3D.of(0, 1, 0));
253 
254             objWriter.writeVertexNormal(Vector3D.Unit.PLUS_Z);
255             objWriter.writeVertexNormal(Vector3D.Unit.MINUS_Z);
256 
257             objWriter.writeFace(new int[] {0, 1, 2}, 0);
258             objWriter.writeFace(new int[] {0, 1, 2, 3}, new int[] {1, 1, 1, 1});
259         }
260 
261         // assert
262         Assertions.assertEquals(
263             "v 0.0 0.0 0.0\n" +
264             "v 1.0 0.0 0.0\n" +
265             "v 1.0 1.0 0.0\n" +
266             "v 0.0 1.0 0.0\n" +
267             "vn 0.0 0.0 1.0\n" +
268             "vn 0.0 0.0 -1.0\n" +
269             "f 1//1 2//1 3//1\n" +
270             "f 1//2 2//2 3//2 4//2\n", writer.getBuffer().toString());
271     }
272 
273     @Test
274     void testWriteFace_invalidVertexNumber() {
275         // arrange
276         final StringWriter writer = new StringWriter();
277 
278         // act
279         GeometryTestUtils.assertThrowsWithMessage(() -> {
280             try (ObjWriter objWriter = new ObjWriter(writer)) {
281                 objWriter.writeFace(1, 2);
282             }
283         }, IllegalArgumentException.class, "Face must have more than 3 vertices; found 2");
284     }
285 
286     @Test
287     void testWriteFace_vertexIndexOutOfBounds() {
288         // arrange
289         final StringWriter writer = new StringWriter();
290 
291         // act/assert
292         GeometryTestUtils.assertThrowsWithMessage(() -> {
293             try (ObjWriter objWriter = new ObjWriter(writer)) {
294                 objWriter.writeVertex(Vector3D.ZERO);
295                 objWriter.writeVertex(Vector3D.of(1, 1, 1));
296 
297                 objWriter.writeFace(0, 1, 2);
298             }
299         }, IndexOutOfBoundsException.class, "Vertex index out of bounds: 2");
300 
301         GeometryTestUtils.assertThrowsWithMessage(() -> {
302             try (ObjWriter objWriter = new ObjWriter(writer)) {
303                 objWriter.writeVertex(Vector3D.ZERO);
304                 objWriter.writeVertex(Vector3D.of(1, 1, 1));
305 
306                 objWriter.writeFace(0, -1, 1);
307             }
308         }, IndexOutOfBoundsException.class, "Vertex index out of bounds: -1");
309     }
310 
311     @Test
312     void testWriteFace_normalIndexOutOfBounds() {
313         // arrange
314         final StringWriter writer = new StringWriter();
315 
316         // act/assert
317         GeometryTestUtils.assertThrowsWithMessage(() -> {
318             try (ObjWriter objWriter = new ObjWriter(writer)) {
319                 objWriter.writeVertex(Vector3D.ZERO);
320                 objWriter.writeVertex(Vector3D.of(1, 1, 1));
321                 objWriter.writeVertex(Vector3D.of(0, 2, 0));
322 
323                 objWriter.writeVertexNormal(Vector3D.Unit.PLUS_Z);
324 
325                 objWriter.writeFace(new int[] {0, 1, 2}, 1);
326             }
327         }, IndexOutOfBoundsException.class, "Normal index out of bounds: 1");
328 
329         GeometryTestUtils.assertThrowsWithMessage(() -> {
330             try (ObjWriter objWriter = new ObjWriter(writer)) {
331                 objWriter.writeVertex(Vector3D.ZERO);
332                 objWriter.writeVertex(Vector3D.of(1, 1, 1));
333                 objWriter.writeVertex(Vector3D.of(0, 2, 0));
334 
335                 objWriter.writeVertexNormal(Vector3D.Unit.PLUS_Z);
336 
337                 objWriter.writeFace(new int[] {0, 1, 2}, -1);
338             }
339         }, IndexOutOfBoundsException.class, "Normal index out of bounds: -1");
340     }
341 
342     @Test
343     void testWriteFace_invalidVertexAndNormalCountMismatch() {
344         // arrange
345         final StringWriter writer = new StringWriter();
346 
347         // act
348         GeometryTestUtils.assertThrowsWithMessage(() -> {
349             try (ObjWriter objWriter = new ObjWriter(writer)) {
350                 objWriter.writeFace(new int[] {0, 1, 2, 3}, new int[] {0, 1, 2});
351             }
352         }, IllegalArgumentException.class, "Face normal index count must equal vertex index count; expected 4 but was 3");
353     }
354 
355     @Test
356     void testWriteMesh() {
357         // arrange
358         final SimpleTriangleMesh mesh = SimpleTriangleMesh.builder(TEST_PRECISION)
359                 .addFaceUsingVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0))
360                 .addFaceUsingVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 0, 1))
361                 .build();
362 
363         final StringWriter writer = new StringWriter();
364 
365         // act
366         try (ObjWriter objWriter = new ObjWriter(writer)) {
367             objWriter.writeMesh(mesh);
368         }
369 
370         // assert
371         Assertions.assertEquals(
372             "v 0.0 0.0 0.0\n" +
373             "v 1.0 0.0 0.0\n" +
374             "v 0.0 1.0 0.0\n" +
375             "v 0.0 0.0 1.0\n" +
376             "f 1 2 3\n" +
377             "f 1 2 4\n", writer.getBuffer().toString());
378     }
379 
380     @Test
381     void testMeshBuffer() {
382         // arrange
383         final StringWriter writer = new StringWriter();
384 
385         try (ObjWriter objWriter = new ObjWriter(writer)) {
386             ObjWriter.MeshBuffer buf = objWriter.meshBuffer();
387 
388             // act
389             buf.add(new SimpleFacetDefinition(Arrays.asList(
390                     Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0)), Vector3D.Unit.MINUS_Z));
391             buf.add(Planes.convexPolygonFromVertices(Arrays.asList(
392                     Vector3D.ZERO, Vector3D.of(1, 1, 0), Vector3D.of(0, 1.5, 0)), TEST_PRECISION));
393             buf.add(new SimpleFacetDefinition(Arrays.asList(
394                     Vector3D.of(0, 1.5, 0), Vector3D.of(1, 1, 0), Vector3D.of(0, 2, 0)), Vector3D.Unit.PLUS_Z));
395 
396             buf.flush();
397         }
398 
399         // assert
400         Assertions.assertEquals(
401             "v 0.0 0.0 0.0\n" +
402             "v 1.0 0.0 0.0\n" +
403             "v 1.0 1.0 0.0\n" +
404             "v 0.0 1.5 0.0\n" +
405             "v 0.0 2.0 0.0\n" +
406             "vn 0.0 0.0 -1.0\n" +
407             "vn 0.0 0.0 1.0\n" +
408             "f 1//1 2//1 3//1\n" +
409             "f 1 3 4\n" +
410             "f 4//2 3//2 5//2\n", writer.getBuffer().toString());
411     }
412 
413     @Test
414     void testMeshBuffer_givenBatchSize() {
415         // arrange
416         final StringWriter writer = new StringWriter();
417 
418         try (ObjWriter objWriter = new ObjWriter(writer)) {
419             ObjWriter.MeshBuffer buf = objWriter.meshBuffer(2);
420 
421             // act
422             buf.add(new SimpleFacetDefinition(Arrays.asList(
423                     Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0)), Vector3D.Unit.MINUS_Z));
424             buf.add(Planes.convexPolygonFromVertices(Arrays.asList(
425                     Vector3D.ZERO, Vector3D.of(1, 1, 0), Vector3D.of(0, 1.5, 0)), TEST_PRECISION));
426             buf.add(new SimpleFacetDefinition(Arrays.asList(
427                     Vector3D.of(0, 1.5, 0), Vector3D.of(1, 1, 0), Vector3D.of(0, 2, 0)), Vector3D.Unit.PLUS_Z));
428 
429             buf.flush();
430         }
431 
432         // assert
433         Assertions.assertEquals(
434             "v 0.0 0.0 0.0\n" +
435             "v 1.0 0.0 0.0\n" +
436             "v 1.0 1.0 0.0\n" +
437             "v 0.0 1.5 0.0\n" +
438             "vn 0.0 0.0 -1.0\n" +
439             "f 1//1 2//1 3//1\n" +
440             "f 1 3 4\n" +
441             "v 0.0 1.5 0.0\n" +
442             "v 1.0 1.0 0.0\n" +
443             "v 0.0 2.0 0.0\n" +
444             "vn 0.0 0.0 1.0\n" +
445             "f 5//2 6//2 7//2\n", writer.getBuffer().toString());
446     }
447 
448     @Test
449     void testMeshBuffer_mixedWithDirectlyAddedFace() {
450         // arrange
451         final StringWriter writer = new StringWriter();
452 
453         try (ObjWriter objWriter = new ObjWriter(writer)) {
454             ObjWriter.MeshBuffer buf = objWriter.meshBuffer(2);
455 
456             // act
457             objWriter.writeVertex(Vector3D.ZERO);
458             objWriter.writeVertex(Vector3D.Unit.MINUS_Y);
459             objWriter.writeVertex(Vector3D.Unit.MINUS_X);
460             objWriter.writeVertexNormal(Vector3D.Unit.PLUS_Z);
461             objWriter.writeFace(new int[] {0, 1, 2}, 0);
462 
463             buf.add(new SimpleFacetDefinition(Arrays.asList(
464                     Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0)), Vector3D.Unit.MINUS_Z));
465             buf.add(Planes.convexPolygonFromVertices(Arrays.asList(
466                     Vector3D.ZERO, Vector3D.of(1, 1, 0), Vector3D.of(0, 1.5, 0)), TEST_PRECISION));
467             buf.add(new SimpleFacetDefinition(Arrays.asList(
468                     Vector3D.of(0, 1.5, 0), Vector3D.of(1, 1, 0), Vector3D.of(0, 2, 0)), Vector3D.Unit.PLUS_Z));
469 
470             buf.flush();
471 
472             objWriter.writeFace(objWriter.getVertexCount() - 1, 2, 1, 0);
473         }
474 
475         // assert
476         Assertions.assertEquals(
477             "v 0.0 0.0 0.0\n" +
478             "v 0.0 -1.0 0.0\n" +
479             "v -1.0 0.0 0.0\n" +
480             "vn 0.0 0.0 1.0\n" +
481             "f 1//1 2//1 3//1\n" +
482             "v 0.0 0.0 0.0\n" +
483             "v 1.0 0.0 0.0\n" +
484             "v 1.0 1.0 0.0\n" +
485             "v 0.0 1.5 0.0\n" +
486             "vn 0.0 0.0 -1.0\n" +
487             "f 4//2 5//2 6//2\n" +
488             "f 4 6 7\n" +
489             "v 0.0 1.5 0.0\n" +
490             "v 1.0 1.0 0.0\n" +
491             "v 0.0 2.0 0.0\n" +
492             "vn 0.0 0.0 1.0\n" +
493             "f 8//3 9//3 10//3\n" +
494             "f 10 3 2 1\n", writer.getBuffer().toString());
495     }
496 
497     @Test
498     void testWriteBoundaries_meshArgument() {
499         // arrange
500         final SimpleTriangleMesh mesh = SimpleTriangleMesh.builder(TEST_PRECISION)
501                 .addFaceUsingVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0))
502                 .addFaceUsingVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 0, 1))
503                 .build();
504 
505         final StringWriter writer = new StringWriter();
506 
507         // act
508         try (ObjWriter objWriter = new ObjWriter(writer)) {
509             objWriter.writeBoundaries(mesh);
510         }
511 
512         // assert
513         Assertions.assertEquals(
514             "v 0.0 0.0 0.0\n" +
515             "v 1.0 0.0 0.0\n" +
516             "v 0.0 1.0 0.0\n" +
517             "v 0.0 0.0 1.0\n" +
518             "f 1 2 3\n" +
519             "f 1 2 4\n", writer.getBuffer().toString());
520     }
521 
522     @Test
523     void testWriteBoundaries_nonMeshArgument() {
524         // arrange
525         final BoundarySource3D src = BoundarySource3D.of(
526                     Planes.triangleFromVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0), TEST_PRECISION),
527                     Planes.triangleFromVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 0, 1), TEST_PRECISION)
528                 );
529 
530         final StringWriter writer = new StringWriter();
531 
532         // act
533         try (ObjWriter objWriter = new ObjWriter(writer)) {
534             objWriter.writeBoundaries(src);
535         }
536 
537         // assert
538         Assertions.assertEquals(
539             "v 0.0 0.0 0.0\n" +
540             "v 1.0 0.0 0.0\n" +
541             "v 0.0 1.0 0.0\n" +
542             "v 0.0 0.0 1.0\n" +
543             "f 1 2 3\n" +
544             "f 1 2 4\n", writer.getBuffer().toString());
545     }
546 
547     @Test
548     void testWriteBoundaries_nonMeshArgument_smallBatchSize() {
549         // arrange
550         final BoundarySource3D src = BoundarySource3D.of(
551                     Planes.triangleFromVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0), TEST_PRECISION),
552                     Planes.triangleFromVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 0, 1), TEST_PRECISION)
553                 );
554 
555         final StringWriter writer = new StringWriter();
556 
557         // act
558         try (ObjWriter objWriter = new ObjWriter(writer)) {
559             objWriter.writeBoundaries(src, 1);
560         }
561 
562         // assert
563         Assertions.assertEquals(
564             "v 0.0 0.0 0.0\n" +
565             "v 1.0 0.0 0.0\n" +
566             "v 0.0 1.0 0.0\n" +
567             "f 1 2 3\n" +
568             "v 0.0 0.0 0.0\n" +
569             "v 1.0 0.0 0.0\n" +
570             "v 0.0 0.0 1.0\n" +
571             "f 4 5 6\n", writer.getBuffer().toString());
572     }
573 
574     @Test
575     void testWriteBoundaries_infiniteBoundary() {
576         // arrange
577         final BoundarySource3D src = BoundarySource3D.of(
578                     Planes.triangleFromVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0), TEST_PRECISION),
579                     Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION).span()
580                 );
581 
582         final StringWriter writer = new StringWriter();
583 
584         // act/assert
585         GeometryTestUtils.assertThrowsWithMessage(() -> {
586             try (ObjWriter objWriter = new ObjWriter(writer)) {
587                 objWriter.writeBoundaries(src);
588             }
589         }, IllegalArgumentException.class, Pattern.compile("^OBJ input geometry cannot be infinite: .*"));
590     }
591 }