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.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
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
59 final CloseCountWriter countWriter = new CloseCountWriter(out);
60
61
62 try (TextStlWriter writer = new TextStlWriter(countWriter)) {
63 Assertions.assertEquals(0, countWriter.getCloseCount());
64 }
65
66
67 Assertions.assertEquals(1, countWriter.getCloseCount());
68 Assertions.assertEquals("", out.toString());
69 }
70
71 @Test
72 void testStartSolid_alreadyStarted() {
73
74 try (TextStlWriter writer = new TextStlWriter(out)) {
75 writer.startSolid();
76
77
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
87 try (TextStlWriter writer = new TextStlWriter(out)) {
88
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
98 final CloseCountWriter countWriter = new CloseCountWriter(out);
99
100
101 try (TextStlWriter writer = new TextStlWriter(countWriter)) {
102 writer.startSolid();
103 writer.endSolid();
104
105 Assertions.assertEquals(0, countWriter.getCloseCount());
106 }
107
108
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
118 final CloseCountWriter countWriter = new CloseCountWriter(out);
119
120
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
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
138 final CloseCountWriter countWriter = new CloseCountWriter(out);
139
140
141 try (TextStlWriter writer = new TextStlWriter(countWriter)) {
142 writer.startSolid("name");
143
144 Assertions.assertEquals(0, countWriter.getCloseCount());
145 }
146
147
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
157 try (TextStlWriter writer = new TextStlWriter(out)) {
158 final String err = "Solid name cannot contain new line characters";
159
160
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
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
181 try (TextStlWriter writer = new TextStlWriter(out)) {
182 writer.startSolid();
183 writer.writeTriangle(p1, p2, p3, null);
184 }
185
186
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
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
207 try (TextStlWriter writer = new TextStlWriter(out)) {
208 writer.startSolid();
209 writer.writeTriangle(p1, p2, p3, Vector3D.ZERO);
210 }
211
212
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
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
233 try (TextStlWriter writer = new TextStlWriter(out)) {
234 writer.startSolid();
235 writer.writeTriangle(p1, p2, p3, null);
236 }
237
238
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
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
261 try (TextStlWriter writer = new TextStlWriter(out)) {
262 writer.startSolid();
263 writer.writeTriangle(p1, p2, p3, normal);
264 }
265
266
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
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
289 try (TextStlWriter writer = new TextStlWriter(out)) {
290 writer.startSolid();
291 writer.writeTriangle(p1, p2, p3, normal.negate());
292 }
293
294
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
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
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
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
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
359 try (TextStlWriter writer = new TextStlWriter(out)) {
360 writer.startSolid();
361
362 writer.writeTriangles(vertices, normal);
363 }
364
365
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
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
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
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
413 try (TextStlWriter writer = new TextStlWriter(out)) {
414 writer.startSolid();
415
416 writer.writeTriangles(boundary);
417 }
418
419
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
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
446 try (TextStlWriter writer = new TextStlWriter(out)) {
447 writer.startSolid();
448
449 writer.writeTriangles(facet);
450 }
451
452
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
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
481 try (TextStlWriter writer = new TextStlWriter(out)) {
482 writer.startSolid();
483
484 writer.writeTriangles(facet);
485 }
486
487
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
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
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
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
553 writer.startSolid();
554 writer.writeTriangles(vertices, normal);
555 }
556
557
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
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
579 writer.startSolid();
580 writer.writeTriangles(vertices, normal);
581 }
582
583
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
599 final List<Vector3D> vertices = Arrays.asList(
600 Vector3D.ZERO, Vector3D.ZERO, Vector3D.ZERO);
601
602 try (TextStlWriter writer = new TextStlWriter(out)) {
603
604 writer.startSolid();
605 writer.writeTriangles(vertices, null);
606 }
607
608
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 }