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.stl;
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.Collections;
24  import java.util.List;
25  import java.util.Locale;
26  
27  import org.apache.commons.geometry.core.GeometryTestUtils;
28  import org.apache.commons.geometry.euclidean.threed.ConvexPolygon3D;
29  import org.apache.commons.geometry.euclidean.threed.Planes;
30  import org.apache.commons.geometry.euclidean.threed.Vector3D;
31  import org.apache.commons.geometry.io.core.test.CloseCountWriter;
32  import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;
33  import org.apache.commons.geometry.io.euclidean.threed.SimpleFacetDefinition;
34  import org.apache.commons.numbers.core.Precision;
35  import org.junit.jupiter.api.Assertions;
36  import org.junit.jupiter.api.Test;
37  
38  class TextStlWriterTest {
39  
40      private static final double TEST_EPS = 1e-10;
41  
42      private static final Precision.DoubleEquivalence TEST_PRECISION =
43              Precision.doubleEquivalenceOfEpsilon(TEST_EPS);
44  
45      private final StringWriter out = new StringWriter();
46  
47      @Test
48      void testDefaultProperties() {
49          // act/assert
50          try (TextStlWriter writer = new TextStlWriter(out)) {
51              Assertions.assertNotNull(writer.getDoubleFormat());
52              Assertions.assertEquals("\n", writer.getLineSeparator());
53          }
54      }
55  
56      @Test
57      void testNoContent() {
58          // arrange
59          final CloseCountWriter countWriter = new CloseCountWriter(out);
60  
61          // act
62          try (TextStlWriter writer = new TextStlWriter(countWriter)) {
63              Assertions.assertEquals(0, countWriter.getCloseCount());
64          }
65  
66          // assert
67          Assertions.assertEquals(1, countWriter.getCloseCount());
68          Assertions.assertEquals("", out.toString());
69      }
70  
71      @Test
72      void testStartSolid_alreadyStarted() {
73          // arrange
74          try (TextStlWriter writer = new TextStlWriter(out)) {
75              writer.startSolid();
76  
77              // act/assert
78              GeometryTestUtils.assertThrowsWithMessage(
79                      () -> writer.startSolid(),
80                      IllegalStateException.class, "Cannot start solid definition: a solid is already being written");
81          }
82      }
83  
84      @Test
85      void testEndSolid_notStarted() {
86          // arrange
87          try (TextStlWriter writer = new TextStlWriter(out)) {
88              // act/assert
89              GeometryTestUtils.assertThrowsWithMessage(
90                      () -> writer.endSolid(),
91                      IllegalStateException.class, "Cannot end solid definition: no solid has been started");
92          }
93      }
94  
95      @Test
96      void testEmpty_noName() {
97          // arrange
98          final CloseCountWriter countWriter = new CloseCountWriter(out);
99  
100         // act
101         try (TextStlWriter writer = new TextStlWriter(countWriter)) {
102             writer.startSolid();
103             writer.endSolid();
104 
105             Assertions.assertEquals(0, countWriter.getCloseCount());
106         }
107 
108         // assert
109         Assertions.assertEquals(1, countWriter.getCloseCount());
110         Assertions.assertEquals(
111                 "solid \n" +
112                 "endsolid \n", out.toString());
113     }
114 
115     @Test
116     void testEmpty_withName() {
117         // arrange
118         final CloseCountWriter countWriter = new CloseCountWriter(out);
119 
120         // act
121         try (TextStlWriter writer = new TextStlWriter(countWriter)) {
122             writer.startSolid("Name of the solid");
123             writer.endSolid();
124 
125             Assertions.assertEquals(0, countWriter.getCloseCount());
126         }
127 
128         // assert
129         Assertions.assertEquals(1, countWriter.getCloseCount());
130         Assertions.assertEquals(
131                 "solid Name of the solid\n" +
132                 "endsolid Name of the solid\n", out.toString());
133     }
134 
135     @Test
136     void testClose_endsSolid() {
137         // arrange
138         final CloseCountWriter countWriter = new CloseCountWriter(out);
139 
140         // act
141         try (TextStlWriter writer = new TextStlWriter(countWriter)) {
142             writer.startSolid("name");
143 
144             Assertions.assertEquals(0, countWriter.getCloseCount());
145         }
146 
147         // assert
148         Assertions.assertEquals(1, countWriter.getCloseCount());
149         Assertions.assertEquals(
150                 "solid name\n" +
151                 "endsolid name\n", out.toString());
152     }
153 
154     @Test
155     void testStartSolid_containsNewLine() {
156         // arrange
157         try (TextStlWriter writer = new TextStlWriter(out)) {
158             final String err = "Solid name cannot contain new line characters";
159 
160             // act/assert
161             GeometryTestUtils.assertThrowsWithMessage(
162                     () -> writer.startSolid("Hi\nthere"),
163                     IllegalArgumentException.class, err);
164             GeometryTestUtils.assertThrowsWithMessage(
165                     () -> writer.startSolid("Hi\r\nthere"),
166                     IllegalArgumentException.class, err);
167             GeometryTestUtils.assertThrowsWithMessage(
168                     () -> writer.startSolid("Hi\rthere"),
169                     IllegalArgumentException.class, err);
170         }
171     }
172 
173     @Test
174     void testWriteTriangle_noNormal_computesNormal() {
175         // arrange
176         final Vector3D p1 = Vector3D.of(0, 4, 0);
177         final Vector3D p2 = Vector3D.of(1.0 / 3.0, 0, 0);
178         final Vector3D p3 = Vector3D.of(0, 0.5, 10);
179 
180         // act
181         try (TextStlWriter writer = new TextStlWriter(out)) {
182             writer.startSolid();
183             writer.writeTriangle(p1, p2, p3, null);
184         }
185 
186         // assert
187         Assertions.assertEquals(
188             "solid \n" +
189             "facet -0.9961250701090868 -0.08301042250909056 -0.029053647878181696\n" +
190             "outer loop\n" +
191             "vertex 0.0 4.0 0.0\n" +
192             "vertex 0.3333333333333333 0.0 0.0\n" +
193             "vertex 0.0 0.5 10.0\n" +
194             "endloop\n" +
195             "endfacet\n" +
196             "endsolid \n", out.toString());
197     }
198 
199     @Test
200     void testWriteTriangle_zeroNormal_computesNormal() {
201         // arrange
202         final Vector3D p1 = Vector3D.of(0, 4, 0);
203         final Vector3D p2 = Vector3D.of(1.0 / 3.0, 0, 0);
204         final Vector3D p3 = Vector3D.of(0, 0.5, 10);
205 
206         // act
207         try (TextStlWriter writer = new TextStlWriter(out)) {
208             writer.startSolid();
209             writer.writeTriangle(p1, p2, p3, Vector3D.ZERO);
210         }
211 
212         // assert
213         Assertions.assertEquals(
214             "solid \n" +
215             "facet -0.9961250701090868 -0.08301042250909056 -0.029053647878181696\n" +
216             "outer loop\n" +
217             "vertex 0.0 4.0 0.0\n" +
218             "vertex 0.3333333333333333 0.0 0.0\n" +
219             "vertex 0.0 0.5 10.0\n" +
220             "endloop\n" +
221             "endfacet\n" +
222             "endsolid \n", out.toString());
223     }
224 
225     @Test
226     void testWriteTriangle_noNormal_cannotComputeNormal() {
227         // arrange
228         final Vector3D p1 = Vector3D.ZERO;
229         final Vector3D p2 = Vector3D.of(1.0 / 3.0, 0, 0);
230         final Vector3D p3 = Vector3D.ZERO;
231 
232         // act
233         try (TextStlWriter writer = new TextStlWriter(out)) {
234             writer.startSolid();
235             writer.writeTriangle(p1, p2, p3, null);
236         }
237 
238         // assert
239         Assertions.assertEquals(
240             "solid \n" +
241             "facet 0.0 0.0 0.0\n" +
242             "outer loop\n" +
243             "vertex 0.0 0.0 0.0\n" +
244             "vertex 0.3333333333333333 0.0 0.0\n" +
245             "vertex 0.0 0.0 0.0\n" +
246             "endloop\n" +
247             "endfacet\n" +
248             "endsolid \n", out.toString());
249     }
250 
251     @Test
252     void testWriteTriangle_withNormal_correctOrientation() {
253         // arrange
254         final Vector3D p1 = Vector3D.of(0, 4, 0);
255         final Vector3D p2 = Vector3D.of(1.0 / 3.0, 0, 0);
256         final Vector3D p3 = Vector3D.of(0, 0.5, 10);
257 
258         final Vector3D normal = p1.vectorTo(p2).cross(p1.vectorTo(p3)).normalize();
259 
260         // act
261         try (TextStlWriter writer = new TextStlWriter(out)) {
262             writer.startSolid();
263             writer.writeTriangle(p1, p2, p3, normal);
264         }
265 
266         // assert
267         Assertions.assertEquals(
268             "solid \n" +
269             "facet -0.9961250701090868 -0.08301042250909056 -0.029053647878181696\n" +
270             "outer loop\n" +
271             "vertex 0.0 4.0 0.0\n" +
272             "vertex 0.3333333333333333 0.0 0.0\n" +
273             "vertex 0.0 0.5 10.0\n" +
274             "endloop\n" +
275             "endfacet\n" +
276             "endsolid \n", out.toString());
277     }
278 
279     @Test
280     void testWriteTriangle_withNormal_reversedOrientation() {
281         // arrange
282         final Vector3D p1 = Vector3D.of(0, 4, 0);
283         final Vector3D p2 = Vector3D.of(1.0 / 3.0, 0, 0);
284         final Vector3D p3 = Vector3D.of(0, 0.5, 10);
285 
286         final Vector3D normal = p1.vectorTo(p2).cross(p1.vectorTo(p3)).normalize();
287 
288         // act
289         try (TextStlWriter writer = new TextStlWriter(out)) {
290             writer.startSolid();
291             writer.writeTriangle(p1, p2, p3, normal.negate());
292         }
293 
294         // assert
295         Assertions.assertEquals(
296             "solid \n" +
297             "facet 0.9961250701090868 0.08301042250909056 0.029053647878181696\n" +
298             "outer loop\n" +
299             "vertex 0.0 4.0 0.0\n" +
300             "vertex 0.0 0.5 10.0\n" +
301             "vertex 0.3333333333333333 0.0 0.0\n" +
302             "endloop\n" +
303             "endfacet\n" +
304             "endsolid \n", out.toString());
305     }
306 
307     @Test
308     void testWrite_verticesAndNormal() {
309         // arrange
310         final List<Vector3D> vertices = Arrays.asList(
311                 Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0));
312         final Vector3D n1 = Vector3D.of(0, 0, 100);
313         final Vector3D n2 = Vector3D.Unit.MINUS_Z;
314 
315         // act
316         try (TextStlWriter writer = new TextStlWriter(out)) {
317             writer.startSolid();
318 
319             writer.writeTriangles(vertices, n1);
320             writer.writeTriangles(vertices, n2);
321             writer.writeTriangles(vertices, null);
322         }
323 
324         // assert
325         Assertions.assertEquals(
326             "solid \n" +
327             "facet 0.0 0.0 1.0\n" +
328             "outer loop\n" +
329             "vertex 0.0 0.0 0.0\n" +
330             "vertex 1.0 0.0 0.0\n" +
331             "vertex 0.0 1.0 0.0\n" +
332             "endloop\n" +
333             "endfacet\n" +
334             "facet 0.0 0.0 -1.0\n" +
335             "outer loop\n" +
336             "vertex 0.0 0.0 0.0\n" +
337             "vertex 0.0 1.0 0.0\n" +
338             "vertex 1.0 0.0 0.0\n" +
339             "endloop\n" +
340             "endfacet\n" +
341             "facet 0.0 0.0 1.0\n" +
342             "outer loop\n" +
343             "vertex 0.0 0.0 0.0\n" +
344             "vertex 1.0 0.0 0.0\n" +
345             "vertex 0.0 1.0 0.0\n" +
346             "endloop\n" +
347             "endfacet\n" +
348             "endsolid \n", out.toString());
349     }
350 
351     @Test
352     void testWrite_verticesAndNormal_moreThanThreeVertices() {
353         // arrange
354         final List<Vector3D> vertices = Arrays.asList(
355                 Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0), Vector3D.of(0, 1, 0));
356         final Vector3D normal = Vector3D.Unit.PLUS_Z;
357 
358         // act
359         try (TextStlWriter writer = new TextStlWriter(out)) {
360             writer.startSolid();
361 
362             writer.writeTriangles(vertices, normal);
363         }
364 
365         // assert
366         Assertions.assertEquals(
367             "solid \n" +
368             "facet 0.0 0.0 1.0\n" +
369             "outer loop\n" +
370             "vertex 0.0 0.0 0.0\n" +
371             "vertex 1.0 0.0 0.0\n" +
372             "vertex 1.0 1.0 0.0\n" +
373             "endloop\n" +
374             "endfacet\n" +
375             "facet 0.0 0.0 1.0\n" +
376             "outer loop\n" +
377             "vertex 0.0 0.0 0.0\n" +
378             "vertex 1.0 1.0 0.0\n" +
379             "vertex 0.0 1.0 0.0\n" +
380             "endloop\n" +
381             "endfacet\n" +
382             "endsolid \n", out.toString());
383     }
384 
385     @Test
386     void testWrite_verticesAndNormal_fewerThanThreeVertices() {
387         // arrange
388         try (TextStlWriter writer = new TextStlWriter(out)) {
389             writer.startSolid();
390 
391             final List<Vector3D> noElements = Collections.emptyList();
392             final List<Vector3D> singleElement = Collections.singletonList(Vector3D.ZERO);
393             final List<Vector3D> twoElements = Arrays.asList(Vector3D.ZERO, Vector3D.of(1, 1, 1));
394 
395             // act/assert
396             Assertions.assertThrows(IllegalArgumentException.class,
397                     () -> writer.writeTriangles(noElements, null));
398             Assertions.assertThrows(IllegalArgumentException.class,
399                     () -> writer.writeTriangles(singleElement, null));
400             Assertions.assertThrows(IllegalArgumentException.class,
401                     () -> writer.writeTriangles(twoElements, null));
402         }
403     }
404 
405     @Test
406     void testWrite_boundary() {
407         // arrange
408         final ConvexPolygon3D boundary = Planes.convexPolygonFromVertices(
409                 Arrays.asList(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 0, 1), Vector3D.of(0, 0, 1)),
410                 TEST_PRECISION);
411 
412         // act
413         try (TextStlWriter writer = new TextStlWriter(out)) {
414             writer.startSolid();
415 
416             writer.writeTriangles(boundary);
417         }
418 
419         // assert
420         Assertions.assertEquals(
421             "solid \n" +
422             "facet 0.0 -1.0 0.0\n" +
423             "outer loop\n" +
424             "vertex 0.0 0.0 0.0\n" +
425             "vertex 1.0 0.0 0.0\n" +
426             "vertex 1.0 0.0 1.0\n" +
427             "endloop\n" +
428             "endfacet\n" +
429             "facet 0.0 -1.0 0.0\n" +
430             "outer loop\n" +
431             "vertex 0.0 0.0 0.0\n" +
432             "vertex 1.0 0.0 1.0\n" +
433             "vertex 0.0 0.0 1.0\n" +
434             "endloop\n" +
435             "endfacet\n" +
436             "endsolid \n", out.toString());
437     }
438 
439     @Test
440     void testWrite_facetDefinition_noNormal() {
441         // arrange
442         final FacetDefinition facet = new SimpleFacetDefinition(Arrays.asList(
443                 Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0), Vector3D.of(0, 1, 0)));
444 
445         // act
446         try (TextStlWriter writer = new TextStlWriter(out)) {
447             writer.startSolid();
448 
449             writer.writeTriangles(facet);
450         }
451 
452         // assert
453         Assertions.assertEquals(
454             "solid \n" +
455             "facet 0.0 0.0 1.0\n" +
456             "outer loop\n" +
457             "vertex 0.0 0.0 0.0\n" +
458             "vertex 1.0 0.0 0.0\n" +
459             "vertex 1.0 1.0 0.0\n" +
460             "endloop\n" +
461             "endfacet\n" +
462             "facet 0.0 0.0 1.0\n" +
463             "outer loop\n" +
464             "vertex 0.0 0.0 0.0\n" +
465             "vertex 1.0 1.0 0.0\n" +
466             "vertex 0.0 1.0 0.0\n" +
467             "endloop\n" +
468             "endfacet\n" +
469             "endsolid \n", out.toString());
470     }
471 
472     @Test
473     void testWrite_facetDefinition_withNormal() {
474         // arrange
475         final Vector3D normal = Vector3D.Unit.PLUS_Z;
476         final FacetDefinition facet = new SimpleFacetDefinition(Arrays.asList(
477                 Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0), Vector3D.of(0, 1, 0)),
478                 normal);
479 
480         // act
481         try (TextStlWriter writer = new TextStlWriter(out)) {
482             writer.startSolid();
483 
484             writer.writeTriangles(facet);
485         }
486 
487         // assert
488         Assertions.assertEquals(
489             "solid \n" +
490             "facet 0.0 0.0 1.0\n" +
491             "outer loop\n" +
492             "vertex 0.0 0.0 0.0\n" +
493             "vertex 1.0 0.0 0.0\n" +
494             "vertex 1.0 1.0 0.0\n" +
495             "endloop\n" +
496             "endfacet\n" +
497             "facet 0.0 0.0 1.0\n" +
498             "outer loop\n" +
499             "vertex 0.0 0.0 0.0\n" +
500             "vertex 1.0 1.0 0.0\n" +
501             "vertex 0.0 1.0 0.0\n" +
502             "endloop\n" +
503             "endfacet\n" +
504             "endsolid \n", out.toString());
505     }
506 
507     @Test
508     void testWrite_noSolidStarted() {
509         // arrange
510         final List<Vector3D> vertices = Arrays.asList(
511                 Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0));
512         final Vector3D normal = Vector3D.Unit.PLUS_Z;
513 
514         final String msg = "Cannot write triangle: no solid has been started";
515 
516         try (TextStlWriter writer = new TextStlWriter(out)) {
517 
518             // act/assert
519             GeometryTestUtils.assertThrowsWithMessage(
520                     () -> writer.writeTriangle(vertices.get(0), vertices.get(1), vertices.get(2), normal),
521                     IllegalStateException.class, msg);
522 
523             GeometryTestUtils.assertThrowsWithMessage(
524                     () -> writer.writeTriangles(vertices, normal),
525                     IllegalStateException.class, msg);
526 
527             GeometryTestUtils.assertThrowsWithMessage(
528                     () -> writer.writeTriangles(new SimpleFacetDefinition(vertices, normal)),
529                     IllegalStateException.class, msg);
530 
531             GeometryTestUtils.assertThrowsWithMessage(
532                     () -> writer.writeTriangles(Planes.convexPolygonFromVertices(vertices, TEST_PRECISION)),
533                     IllegalStateException.class, msg);
534         }
535     }
536 
537     @Test
538     void testWrite_customFormat() {
539         // arrange
540         final List<Vector3D> vertices = Arrays.asList(
541                 Vector3D.ZERO, Vector3D.of(1.0 / 3.0, 0, 0), Vector3D.of(0, 1.0 / 3.0, 0));
542         final Vector3D normal = Vector3D.Unit.PLUS_Z;
543 
544         final DecimalFormat fmt =
545                 new DecimalFormat("0.0##", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
546 
547         try (TextStlWriter writer = new TextStlWriter(out)) {
548 
549             writer.setDoubleFormat(fmt::format);
550             writer.setLineSeparator("\r\n");
551 
552             // act
553             writer.startSolid();
554             writer.writeTriangles(vertices, normal);
555         }
556 
557         // assert
558         Assertions.assertEquals(
559             "solid \r\n" +
560             "facet 0.0 0.0 1.0\r\n" +
561             "outer loop\r\n" +
562             "vertex 0.0 0.0 0.0\r\n" +
563             "vertex 0.333 0.0 0.0\r\n" +
564             "vertex 0.0 0.333 0.0\r\n" +
565             "endloop\r\n" +
566             "endfacet\r\n" +
567             "endsolid \r\n", out.toString());
568     }
569 
570     @Test
571     void testWrite_badFacet_withNormal() {
572         // arrange
573         final List<Vector3D> vertices = Arrays.asList(
574                 Vector3D.ZERO, Vector3D.ZERO, Vector3D.ZERO);
575         final Vector3D normal = Vector3D.Unit.PLUS_Z;
576 
577         try (TextStlWriter writer = new TextStlWriter(out)) {
578             // act
579             writer.startSolid();
580             writer.writeTriangles(vertices, normal);
581         }
582 
583         // assert
584         Assertions.assertEquals(
585             "solid \n" +
586             "facet 0.0 0.0 1.0\n" +
587             "outer loop\n" +
588             "vertex 0.0 0.0 0.0\n" +
589             "vertex 0.0 0.0 0.0\n" +
590             "vertex 0.0 0.0 0.0\n" +
591             "endloop\n" +
592             "endfacet\n" +
593             "endsolid \n", out.toString());
594     }
595 
596     @Test
597     void testWrite_badFacet_noNormal() {
598         // arrange
599         final List<Vector3D> vertices = Arrays.asList(
600                 Vector3D.ZERO, Vector3D.ZERO, Vector3D.ZERO);
601 
602         try (TextStlWriter writer = new TextStlWriter(out)) {
603             // act
604             writer.startSolid();
605             writer.writeTriangles(vertices, null);
606         }
607 
608         // assert
609         Assertions.assertEquals(
610             "solid \n" +
611             "facet 0.0 0.0 0.0\n" +
612             "outer loop\n" +
613             "vertex 0.0 0.0 0.0\n" +
614             "vertex 0.0 0.0 0.0\n" +
615             "vertex 0.0 0.0 0.0\n" +
616             "endloop\n" +
617             "endfacet\n" +
618             "endsolid \n", out.toString());
619     }
620 }