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.euclidean.threed.mesh;
18  
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.Collections;
22  import java.util.Iterator;
23  import java.util.List;
24  import java.util.NoSuchElementException;
25  import java.util.regex.Pattern;
26  import java.util.stream.Collectors;
27  
28  import org.apache.commons.geometry.core.GeometryTestUtils;
29  import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
30  import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
31  import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
32  import org.apache.commons.geometry.euclidean.threed.AffineTransformMatrix3D;
33  import org.apache.commons.geometry.euclidean.threed.BoundarySource3D;
34  import org.apache.commons.geometry.euclidean.threed.Bounds3D;
35  import org.apache.commons.geometry.euclidean.threed.Planes;
36  import org.apache.commons.geometry.euclidean.threed.RegionBSPTree3D;
37  import org.apache.commons.geometry.euclidean.threed.Triangle3D;
38  import org.apache.commons.geometry.euclidean.threed.Vector3D;
39  import org.apache.commons.geometry.euclidean.threed.shape.Parallelepiped;
40  import org.junit.Assert;
41  import org.junit.Test;
42  
43  public class SimpleTriangleMeshTest {
44  
45      private static final double TEST_EPS = 1e-10;
46  
47      private static final DoublePrecisionContext TEST_PRECISION =
48              new EpsilonDoublePrecisionContext(TEST_EPS);
49  
50      @Test
51      public void testFrom_verticesAndFaces() {
52          // arrange
53          final Vector3D[] vertices = {
54              Vector3D.ZERO,
55              Vector3D.of(1, 1, 0),
56              Vector3D.of(1, 1, 1),
57              Vector3D.of(0, 0, 1)
58          };
59  
60          final int[][] faceIndices = new int[][] {
61              {0, 1, 2},
62              {0, 2, 3}
63          };
64  
65          // act
66          final SimpleTriangleMesh mesh = SimpleTriangleMesh.from(vertices, faceIndices, TEST_PRECISION);
67  
68          // assert
69          Assert.assertEquals(4, mesh.getVertexCount());
70          Assert.assertEquals(Arrays.asList(vertices), mesh.getVertices());
71  
72          Assert.assertEquals(2, mesh.getFaceCount());
73  
74          final List<TriangleMesh.Face> faces = mesh.getFaces();
75          Assert.assertEquals(2, faces.size());
76  
77          final TriangleMesh.Face f1 = faces.get(0);
78          Assert.assertEquals(0, f1.getIndex());
79          Assert.assertArrayEquals(new int[] {0, 1, 2}, f1.getVertexIndices());
80          Assert.assertSame(vertices[0], f1.getPoint1());
81          Assert.assertSame(vertices[1], f1.getPoint2());
82          Assert.assertSame(vertices[2], f1.getPoint3());
83          Assert.assertEquals(Arrays.asList(vertices[0], vertices[1], vertices[2]), f1.getVertices());
84          Assert.assertTrue(f1.definesPolygon());
85  
86          final Triangle3D t1 = f1.getPolygon();
87          Assert.assertEquals(Arrays.asList(vertices[0], vertices[1], vertices[2]), t1.getVertices());
88  
89          final TriangleMesh.Face f2 = faces.get(1);
90          Assert.assertEquals(1, f2.getIndex());
91          Assert.assertArrayEquals(new int[] {0, 2, 3}, f2.getVertexIndices());
92          Assert.assertSame(vertices[0], f2.getPoint1());
93          Assert.assertSame(vertices[2], f2.getPoint2());
94          Assert.assertSame(vertices[3], f2.getPoint3());
95          Assert.assertEquals(Arrays.asList(vertices[0], vertices[2], vertices[3]), f2.getVertices());
96          Assert.assertTrue(f2.definesPolygon());
97  
98          final Triangle3D t2 = f2.getPolygon();
99          Assert.assertEquals(Arrays.asList(vertices[0], vertices[2], vertices[3]), t2.getVertices());
100 
101         final Bounds3D bounds = mesh.getBounds();
102         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, bounds.getMin(), TEST_EPS);
103         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, 1), bounds.getMax(), TEST_EPS);
104 
105         Assert.assertSame(TEST_PRECISION, mesh.getPrecision());
106     }
107 
108     @Test
109     public void testFrom_verticesAndFaces_empty() {
110         // arrange
111         final Vector3D[] vertices = {};
112 
113         final int[][] faceIndices = new int[][] {};
114 
115         // act
116         final SimpleTriangleMesh mesh = SimpleTriangleMesh.from(vertices, faceIndices, TEST_PRECISION);
117 
118         // assert
119         Assert.assertEquals(0, mesh.getVertexCount());
120         Assert.assertEquals(0, mesh.getVertices().size());
121 
122         Assert.assertEquals(0, mesh.getFaceCount());
123         Assert.assertEquals(0, mesh.getFaces().size());
124 
125         Assert.assertNull(mesh.getBounds());
126 
127         Assert.assertTrue(mesh.toTree().isEmpty());
128     }
129 
130     @Test
131     public void testFrom_boundarySource() {
132         // arrange
133         final BoundarySource3D src = Parallelepiped.axisAligned(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION);
134 
135         // act
136         final SimpleTriangleMesh mesh = SimpleTriangleMesh.from(src, TEST_PRECISION);
137 
138         // assert
139         Assert.assertEquals(8, mesh.getVertexCount());
140 
141         final Vector3D p1 = Vector3D.of(0, 0, 0);
142         final Vector3D p2 = Vector3D.of(0, 0, 1);
143         final Vector3D p3 = Vector3D.of(0, 1, 0);
144         final Vector3D p4 = Vector3D.of(0, 1, 1);
145 
146         final Vector3D p5 = Vector3D.of(1, 0, 0);
147         final Vector3D p6 = Vector3D.of(1, 0, 1);
148         final Vector3D p7 = Vector3D.of(1, 1, 0);
149         final Vector3D p8 = Vector3D.of(1, 1, 1);
150 
151         final List<Vector3D> vertices = mesh.getVertices();
152         Assert.assertEquals(8, vertices.size());
153 
154         Assert.assertTrue(vertices.contains(p1));
155         Assert.assertTrue(vertices.contains(p2));
156         Assert.assertTrue(vertices.contains(p3));
157         Assert.assertTrue(vertices.contains(p4));
158         Assert.assertTrue(vertices.contains(p5));
159         Assert.assertTrue(vertices.contains(p6));
160         Assert.assertTrue(vertices.contains(p7));
161         Assert.assertTrue(vertices.contains(p8));
162 
163         Assert.assertEquals(12, mesh.getFaceCount());
164 
165         final RegionBSPTree3D tree = mesh.toTree();
166 
167         Assert.assertEquals(1, tree.getSize(), TEST_EPS);
168         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), tree.getCentroid(), TEST_EPS);
169 
170         Assert.assertSame(TEST_PRECISION, mesh.getPrecision());
171     }
172 
173     @Test
174     public void testFrom_boundarySource_empty() {
175         // act
176         final SimpleTriangleMesh mesh = SimpleTriangleMesh.from(BoundarySource3D.from(Collections.emptyList()),
177                 TEST_PRECISION);
178 
179         // assert
180         Assert.assertEquals(0, mesh.getVertexCount());
181         Assert.assertEquals(0, mesh.getVertices().size());
182 
183         Assert.assertEquals(0, mesh.getFaceCount());
184         Assert.assertEquals(0, mesh.getFaces().size());
185 
186         Assert.assertNull(mesh.getBounds());
187 
188         Assert.assertTrue(mesh.toTree().isEmpty());
189     }
190 
191     @Test
192     public void testVertices_iterable() {
193         // arrange
194         final List<Vector3D> vertices = Arrays.asList(
195             Vector3D.ZERO,
196             Vector3D.of(1, 0, 0),
197             Vector3D.of(0, 1, 0)
198         );
199 
200         final List<int[]> faceIndices = Collections.singletonList(new int[]{0, 1, 2});
201 
202         final SimpleTriangleMesh mesh = SimpleTriangleMesh.from(vertices, faceIndices, TEST_PRECISION);
203 
204         // act
205         final List<Vector3D> result = new ArrayList<>();
206         mesh.vertices().forEach(result::add);
207 
208         // assert
209         Assert.assertEquals(vertices, result);
210     }
211 
212     @Test
213     public void testFaces_iterable() {
214         // arrange
215         final List<Vector3D> vertices = Arrays.asList(
216             Vector3D.ZERO,
217             Vector3D.of(1, 0, 0),
218             Vector3D.of(0, 1, 0),
219             Vector3D.of(0, 0, 1)
220         );
221 
222         final List<int[]> faceIndices = Arrays.asList(
223             new int[] {0, 1, 2},
224             new int[] {0, 2, 3}
225         );
226 
227         final SimpleTriangleMesh mesh = SimpleTriangleMesh.from(vertices, faceIndices, TEST_PRECISION);
228 
229         // act
230         final List<TriangleMesh.Face> result = new ArrayList<>();
231         mesh.faces().forEach(result::add);
232 
233         // assert
234         Assert.assertEquals(2, result.size());
235 
236         final TriangleMesh.Face f1 = result.get(0);
237         Assert.assertEquals(0, f1.getIndex());
238         Assert.assertArrayEquals(new int[] {0, 1, 2}, f1.getVertexIndices());
239         Assert.assertSame(vertices.get(0), f1.getPoint1());
240         Assert.assertSame(vertices.get(1), f1.getPoint2());
241         Assert.assertSame(vertices.get(2), f1.getPoint3());
242         Assert.assertEquals(Arrays.asList(vertices.get(0), vertices.get(1), vertices.get(2)), f1.getVertices());
243         Assert.assertTrue(f1.definesPolygon());
244 
245         final TriangleMesh.Face f2 = result.get(1);
246         Assert.assertEquals(1, f2.getIndex());
247         Assert.assertArrayEquals(new int[] {0, 2, 3}, f2.getVertexIndices());
248         Assert.assertSame(vertices.get(0), f2.getPoint1());
249         Assert.assertSame(vertices.get(2), f2.getPoint2());
250         Assert.assertSame(vertices.get(3), f2.getPoint3());
251         Assert.assertEquals(Arrays.asList(vertices.get(0), vertices.get(2), vertices.get(3)), f2.getVertices());
252         Assert.assertTrue(f2.definesPolygon());
253     }
254 
255     @Test
256     public void testFaces_iterator() {
257         // arrange
258         final List<Vector3D> vertices = Arrays.asList(
259             Vector3D.ZERO,
260             Vector3D.of(1, 0, 0),
261             Vector3D.of(0, 1, 0)
262         );
263 
264         final List<int[]> faceIndices = Arrays.asList(
265             new int[] {0, 1, 2}
266         );
267 
268         final SimpleTriangleMesh mesh = SimpleTriangleMesh.from(vertices, faceIndices, TEST_PRECISION);
269 
270         // act/assert
271         final Iterator<TriangleMesh.Face> it = mesh.faces().iterator();
272 
273         Assert.assertTrue(it.hasNext());
274         Assert.assertEquals(0, it.next().getIndex());
275         Assert.assertFalse(it.hasNext());
276 
277         GeometryTestUtils.assertThrows(() -> it.next(), NoSuchElementException.class);
278     }
279 
280     @Test
281     public void testTriangleStream() {
282         // arrange
283         final List<Vector3D> vertices = Arrays.asList(
284             Vector3D.ZERO,
285             Vector3D.of(1, 0, 0),
286             Vector3D.of(0, 1, 0),
287             Vector3D.of(0, 0, 1)
288         );
289 
290         final List<int[]> faceIndices = Arrays.asList(
291             new int[] {0, 1, 2},
292             new int[] {0, 2, 3}
293         );
294 
295         final SimpleTriangleMesh mesh = SimpleTriangleMesh.from(vertices, faceIndices, TEST_PRECISION);
296 
297         // act
298         final List<Triangle3D> tris = mesh.triangleStream().collect(Collectors.toList());
299 
300         // assert
301         Assert.assertEquals(2, tris.size());
302 
303         final Triangle3D t1 = tris.get(0);
304         Assert.assertSame(vertices.get(0), t1.getPoint1());
305         Assert.assertSame(vertices.get(1), t1.getPoint2());
306         Assert.assertSame(vertices.get(2), t1.getPoint3());
307 
308         final Triangle3D t2 = tris.get(1);
309         Assert.assertSame(vertices.get(0), t2.getPoint1());
310         Assert.assertSame(vertices.get(2), t2.getPoint2());
311         Assert.assertSame(vertices.get(3), t2.getPoint3());
312     }
313 
314     @Test
315     public void testToTriangleMesh() {
316         // arrange
317         final DoublePrecisionContext precision1 = new EpsilonDoublePrecisionContext(1e-1);
318         final DoublePrecisionContext precision2 = new EpsilonDoublePrecisionContext(1e-2);
319         final DoublePrecisionContext precision3 = new EpsilonDoublePrecisionContext(1e-1);
320 
321         final SimpleTriangleMesh mesh = SimpleTriangleMesh.from(Parallelepiped.unitCube(TEST_PRECISION), precision1);
322 
323         // act/assert
324         Assert.assertSame(mesh, mesh.toTriangleMesh(precision1));
325 
326         final SimpleTriangleMesh other = mesh.toTriangleMesh(precision2);
327         Assert.assertSame(precision2, other.getPrecision());
328         Assert.assertEquals(mesh.getVertices(), other.getVertices());
329         Assert.assertEquals(12, other.getFaceCount());
330         for (int i = 0; i < 12; ++i) {
331             Assert.assertArrayEquals(mesh.getFace(i).getVertexIndices(), other.getFace(i).getVertexIndices());
332         }
333 
334         Assert.assertSame(mesh, mesh.toTriangleMesh(precision3));
335     }
336 
337     @Test
338     public void testFace_doesNotDefineTriangle() {
339         // arrange
340         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-1);
341         final Vector3D[] vertices = new Vector3D[] {
342             Vector3D.ZERO,
343             Vector3D.of(0.01, -0.01, 0.01),
344             Vector3D.of(0.01, 0.01, 0.01),
345             Vector3D.of(1, 0, 0),
346             Vector3D.of(2, 0.01, 0)
347         };
348         final int[][] faces = new int[][] {
349             {0, 1, 2},
350             {0, 3, 4}
351         };
352         final SimpleTriangleMesh mesh = SimpleTriangleMesh.from(vertices, faces, precision);
353 
354         // act/assert
355         final Pattern msgPattern = Pattern.compile("^Points do not define a plane: .*");
356 
357         Assert.assertFalse(mesh.getFace(0).definesPolygon());
358         GeometryTestUtils.assertThrows(() -> {
359             mesh.getFace(0).getPolygon();
360         }, IllegalArgumentException.class, msgPattern);
361 
362         Assert.assertFalse(mesh.getFace(1).definesPolygon());
363         GeometryTestUtils.assertThrows(() -> {
364             mesh.getFace(1).getPolygon();
365         }, IllegalArgumentException.class, msgPattern);
366     }
367 
368     @Test
369     public void testToTree_smallNumberOfFaces() {
370         // arrange
371         final SimpleTriangleMesh mesh = SimpleTriangleMesh.from(Parallelepiped.unitCube(TEST_PRECISION), TEST_PRECISION);
372 
373         // act
374         final RegionBSPTree3D tree = mesh.toTree();
375 
376         // assert
377         Assert.assertFalse(tree.isFull());
378         Assert.assertFalse(tree.isEmpty());
379         Assert.assertFalse(tree.isInfinite());
380         Assert.assertTrue(tree.isFinite());
381 
382         Assert.assertEquals(1, tree.getSize(), 1);
383         Assert.assertEquals(6, tree.getBoundarySize(), 1);
384 
385         Assert.assertEquals(6, tree.getRoot().height());
386     }
387 
388     @Test
389     public void testTransform() {
390         // arrange
391         final SimpleTriangleMesh mesh = SimpleTriangleMesh.from(Parallelepiped.unitCube(TEST_PRECISION), TEST_PRECISION);
392 
393         final AffineTransformMatrix3D t = AffineTransformMatrix3D.createScale(1, 2, 3)
394                 .translate(0.5, 1, 1.5);
395 
396         // act
397         final SimpleTriangleMesh result = mesh.transform(t);
398 
399         // assert
400         Assert.assertNotSame(mesh, result);
401 
402         Assert.assertEquals(8, result.getVertexCount());
403         Assert.assertEquals(12, result.getFaceCount());
404 
405         final Bounds3D resultBounds = result.getBounds();
406         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, resultBounds.getMin(), TEST_EPS);
407         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 3), resultBounds.getMax(), TEST_EPS);
408 
409         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 1, 1.5), result.toTree().getCentroid(), TEST_EPS);
410         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, mesh.toTree().getCentroid(), TEST_EPS);
411     }
412 
413     @Test
414     public void testTransform_empty() {
415         // arrange
416         final SimpleTriangleMesh mesh = SimpleTriangleMesh.builder(TEST_PRECISION).build();
417 
418         final AffineTransformMatrix3D t = AffineTransformMatrix3D.createScale(1, 2, 3);
419 
420         // act
421         final SimpleTriangleMesh result = mesh.transform(t);
422 
423         // assert
424         Assert.assertEquals(0, result.getVertexCount());
425         Assert.assertEquals(0, result.getFaceCount());
426 
427         Assert.assertNull(result.getBounds());
428     }
429 
430     @Test
431     public void testToString() {
432         // arrange
433         final Triangle3D tri = Planes.triangleFromVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0),
434                 TEST_PRECISION);
435         final SimpleTriangleMesh mesh = SimpleTriangleMesh.from(BoundarySource3D.from(tri), TEST_PRECISION);
436 
437         // act
438         final String str = mesh.toString();
439 
440         // assert
441         GeometryTestUtils.assertContains("SimpleTriangleMesh[vertexCount= 3, faceCount= 1, bounds= Bounds3D[", str);
442     }
443 
444     @Test
445     public void testFaceToString() {
446         // arrange
447         final Triangle3D tri = Planes.triangleFromVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0),
448                 TEST_PRECISION);
449         final SimpleTriangleMesh mesh = SimpleTriangleMesh.from(BoundarySource3D.from(tri), TEST_PRECISION);
450 
451         // act
452         final String str = mesh.getFace(0).toString();
453 
454         // assert
455         GeometryTestUtils.assertContains("SimpleTriangleFace[index= 0, vertexIndices= [0, 1, 2], vertices= [(0", str);
456     }
457 
458     @Test
459     public void testBuilder_mixedBuildMethods() {
460         // arrange
461         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-1);
462         final SimpleTriangleMesh.Builder builder = SimpleTriangleMesh.builder(precision);
463 
464         // act
465         builder.addVertices(Arrays.asList(Vector3D.ZERO, Vector3D.of(1, 0, 0)));
466         builder.useVertex(Vector3D.of(0, 0, 1));
467         builder.addVertex(Vector3D.of(0, 1, 0));
468         builder.useVertex(Vector3D.of(1, 1, 1));
469 
470         builder.addFace(0, 2, 1);
471         builder.addFaceUsingVertices(Vector3D.of(0.5, 0, 0), Vector3D.of(1.01, 0, 0), Vector3D.of(1, 1, 0.95));
472 
473         final SimpleTriangleMesh mesh = builder.build();
474 
475         // assert
476         Assert.assertEquals(6, mesh.getVertexCount());
477         Assert.assertEquals(2, mesh.getFaceCount());
478 
479         final List<TriangleMesh.Face> faces = mesh.getFaces();
480         Assert.assertEquals(2, faces.size());
481 
482         Assert.assertArrayEquals(new int[] {0, 2, 1},  faces.get(0).getVertexIndices());
483         Assert.assertArrayEquals(new int[] {5, 1, 4},  faces.get(1).getVertexIndices());
484     }
485 
486     @Test
487     public void testBuilder_addVerticesAndFaces() {
488         // act
489         final SimpleTriangleMesh mesh = SimpleTriangleMesh.builder(TEST_PRECISION)
490             .addVertices(new Vector3D[] {
491                 Vector3D.ZERO,
492                 Vector3D.of(1, 1, 0),
493                 Vector3D.of(1, 1, 1),
494                 Vector3D.of(0, 0, 1)
495             })
496             .addFaces(new int[][] {
497                 {0, 1, 2},
498                 {0, 2, 3}
499             })
500             .build();
501 
502         // assert
503         Assert.assertEquals(4, mesh.getVertexCount());
504         Assert.assertEquals(2, mesh.getFaceCount());
505     }
506 
507     @Test
508     public void testBuilder_invalidFaceIndices() {
509         // arrange
510         final SimpleTriangleMesh.Builder builder = SimpleTriangleMesh.builder(TEST_PRECISION);
511         builder.useVertex(Vector3D.ZERO);
512         builder.useVertex(Vector3D.of(1, 0, 0));
513         builder.useVertex(Vector3D.of(0, 1, 0));
514 
515         final String msgBase = "Invalid vertex index: ";
516 
517         // act/assert
518         GeometryTestUtils.assertThrows(() -> {
519             builder.addFace(-1, 1, 2);
520         }, IllegalArgumentException.class, msgBase + "-1");
521 
522         GeometryTestUtils.assertThrows(() -> {
523             builder.addFace(0, 3, 2);
524         }, IllegalArgumentException.class, msgBase + "3");
525 
526         GeometryTestUtils.assertThrows(() -> {
527             builder.addFace(0, 1, 4);
528         }, IllegalArgumentException.class, msgBase + "4");
529 
530         GeometryTestUtils.assertThrows(() -> {
531             builder.addFaces(new int[][] {{-1, 1, 2}});
532         }, IllegalArgumentException.class, msgBase + "-1");
533 
534         GeometryTestUtils.assertThrows(() -> {
535             builder.addFaces(new int[][] {{0, 3, 2}});
536         }, IllegalArgumentException.class, msgBase + "3");
537 
538         GeometryTestUtils.assertThrows(() -> {
539             builder.addFaces(new int[][] {{0, 1, 4}});
540         }, IllegalArgumentException.class, msgBase + "4");
541     }
542 
543     @Test
544     public void testBuilder_invalidFaceIndexCount() {
545         // arrange
546         final SimpleTriangleMesh.Builder builder = SimpleTriangleMesh.builder(TEST_PRECISION);
547         builder.useVertex(Vector3D.ZERO);
548         builder.useVertex(Vector3D.of(1, 0, 0));
549         builder.useVertex(Vector3D.of(0, 1, 0));
550         builder.useVertex(Vector3D.of(0, 0, 1));
551 
552         final String msgBase = "Face must contain 3 vertex indices; found ";
553 
554         // act/assert
555         GeometryTestUtils.assertThrows(() -> {
556             builder.addFaces(new int[][] {{}});
557         }, IllegalArgumentException.class, msgBase + "0");
558 
559         GeometryTestUtils.assertThrows(() -> {
560             builder.addFaces(new int[][] {{0}});
561         }, IllegalArgumentException.class, msgBase + "1");
562 
563         GeometryTestUtils.assertThrows(() -> {
564             builder.addFaces(new int[][] {{0, 1}});
565         }, IllegalArgumentException.class, msgBase + "2");
566 
567         GeometryTestUtils.assertThrows(() -> {
568             builder.addFaces(new int[][] {{0, 1, 2, 3}});
569         }, IllegalArgumentException.class, msgBase + "4");
570     }
571 
572     @Test
573     public void testBuilder_cannotModifyOnceBuilt() {
574         // arrange
575         final SimpleTriangleMesh.Builder builder = SimpleTriangleMesh.builder(TEST_PRECISION)
576             .addVertices(new Vector3D[] {
577                 Vector3D.ZERO,
578                 Vector3D.of(1, 1, 0),
579                 Vector3D.of(1, 1, 1),
580             })
581             .addFaces(new int[][] {
582                 {0, 1, 2}
583             });
584         builder.build();
585 
586         final String msg = "Builder instance cannot be modified: mesh construction is complete";
587 
588         // act/assert
589         GeometryTestUtils.assertThrows(() -> {
590             builder.useVertex(Vector3D.ZERO);
591         }, IllegalStateException.class, msg);
592 
593         GeometryTestUtils.assertThrows(() -> {
594             builder.addVertex(Vector3D.ZERO);
595         }, IllegalStateException.class, msg);
596 
597         GeometryTestUtils.assertThrows(() -> {
598             builder.addVertices(Collections.singletonList(Vector3D.ZERO));
599         }, IllegalStateException.class, msg);
600 
601         GeometryTestUtils.assertThrows(() -> {
602             builder.addVertices(new Vector3D[] {Vector3D.ZERO});
603         }, IllegalStateException.class, msg);
604 
605         GeometryTestUtils.assertThrows(() -> {
606             builder.addFaceUsingVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0));
607         }, IllegalStateException.class, msg);
608 
609         GeometryTestUtils.assertThrows(() -> {
610             builder.addFace(0, 1, 2);
611         }, IllegalStateException.class, msg);
612 
613         GeometryTestUtils.assertThrows(() -> {
614             builder.addFaces(Collections.singletonList(new int[]{0, 1, 2}));
615         }, IllegalStateException.class, msg);
616 
617         GeometryTestUtils.assertThrows(() -> {
618             builder.addFaces(new int[][] {{0, 1, 2}});
619         }, IllegalStateException.class, msg);
620     }
621 
622     @Test
623     public void testBuilder_addFaceAndVertices_vs_addFaceUsingVertices() {
624         // arrange
625         final SimpleTriangleMesh.Builder builder = SimpleTriangleMesh.builder(TEST_PRECISION);
626         final Vector3D p1 = Vector3D.ZERO;
627         final Vector3D p2 = Vector3D.of(1, 0, 0);
628         final Vector3D p3 = Vector3D.of(0, 1, 0);
629 
630         // act
631         builder.addFaceUsingVertices(p1, p2, p3);
632         builder.addFaceAndVertices(p1, p2, p3);
633         builder.addFaceUsingVertices(p1, p2, p3);
634 
635         // assert
636         Assert.assertEquals(6, builder.getVertexCount());
637         Assert.assertEquals(3, builder.getFaceCount());
638 
639         final SimpleTriangleMesh mesh = builder.build();
640 
641         Assert.assertEquals(6, mesh.getVertexCount());
642         Assert.assertEquals(3, mesh.getFaceCount());
643 
644         final TriangleMesh.Face f1 = mesh.getFace(0);
645         Assert.assertArrayEquals(new int[] {0, 1, 2}, f1.getVertexIndices());
646 
647         final TriangleMesh.Face f2 = mesh.getFace(1);
648         Assert.assertArrayEquals(new int[] {3, 4, 5}, f2.getVertexIndices());
649 
650         final TriangleMesh.Face f3 = mesh.getFace(2);
651         Assert.assertArrayEquals(new int[] {0, 1, 2}, f3.getVertexIndices());
652     }
653 }