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 }