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