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.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
46 final StringWriter writer = new StringWriter();
47
48
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
60 final StringWriter writer = new StringWriter();
61
62
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
73 final StringWriter writer = new StringWriter();
74
75
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
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
94 final StringWriter writer = new StringWriter();
95 final DecimalFormat fmt =
96 new DecimalFormat("0.0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
97
98
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
106 Assertions.assertEquals("v 1.1 2.0 3.1\n", writer.getBuffer().toString());
107 }
108
109 @Test
110 void testWriteComment() {
111
112 final StringWriter writer = new StringWriter();
113
114
115 try (ObjWriter objWriter = new ObjWriter(writer)) {
116 objWriter.writeComment("test");
117 objWriter.writeComment(" a\r\n multi-line\ncomment");
118 }
119
120
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
131 final StringWriter writer = new StringWriter();
132
133
134 try (ObjWriter objWriter = new ObjWriter(writer)) {
135 objWriter.writeObjectName("test-object");
136 }
137
138
139 Assertions.assertEquals("o test-object\n", writer.getBuffer().toString());
140 }
141
142 @Test
143 void testWriteGroupName() {
144
145 final StringWriter writer = new StringWriter();
146
147
148 try (ObjWriter objWriter = new ObjWriter(writer)) {
149 objWriter.writeGroupName("test-group");
150 }
151
152
153 Assertions.assertEquals("g test-group\n", writer.getBuffer().toString());
154 }
155
156 @Test
157 void testWriteVertex() {
158
159 final StringWriter writer = new StringWriter();
160
161
162 final DecimalFormat fmt =
163 new DecimalFormat("0.0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
164
165
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
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
190 final StringWriter writer = new StringWriter();
191 final DecimalFormat fmt =
192 new DecimalFormat("0.0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
193
194
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
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
219 final StringWriter writer = new StringWriter();
220
221
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
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
245 final StringWriter writer = new StringWriter();
246
247
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
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
276 final StringWriter writer = new StringWriter();
277
278
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
289 final StringWriter writer = new StringWriter();
290
291
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
314 final StringWriter writer = new StringWriter();
315
316
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
345 final StringWriter writer = new StringWriter();
346
347
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
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
366 try (ObjWriter objWriter = new ObjWriter(writer)) {
367 objWriter.writeMesh(mesh);
368 }
369
370
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
383 final StringWriter writer = new StringWriter();
384
385 try (ObjWriter objWriter = new ObjWriter(writer)) {
386 ObjWriter.MeshBuffer buf = objWriter.meshBuffer();
387
388
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
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
416 final StringWriter writer = new StringWriter();
417
418 try (ObjWriter objWriter = new ObjWriter(writer)) {
419 ObjWriter.MeshBuffer buf = objWriter.meshBuffer(2);
420
421
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
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
451 final StringWriter writer = new StringWriter();
452
453 try (ObjWriter objWriter = new ObjWriter(writer)) {
454 ObjWriter.MeshBuffer buf = objWriter.meshBuffer(2);
455
456
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
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
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
508 try (ObjWriter objWriter = new ObjWriter(writer)) {
509 objWriter.writeBoundaries(mesh);
510 }
511
512
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
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
533 try (ObjWriter objWriter = new ObjWriter(writer)) {
534 objWriter.writeBoundaries(src);
535 }
536
537
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
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
558 try (ObjWriter objWriter = new ObjWriter(writer)) {
559 objWriter.writeBoundaries(src, 1);
560 }
561
562
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
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
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 }