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 }