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.io.euclidean.threed.obj;
    18
    19  import java.io.StringWriter;
    20  import java.text.DecimalFormat;
    21  import java.text.DecimalFormatSymbols;
    22  import java.util.Arrays;
    23  import java.util.Locale;
    24  import java.util.regex.Pattern;
    25
    26  import org.apache.commons.geometry.core.GeometryTestUtils;
    27  import org.apache.commons.geometry.euclidean.threed.BoundarySource3D;
    28  import org.apache.commons.geometry.euclidean.threed.Planes;
    29  import org.apache.commons.geometry.euclidean.threed.Vector3D;
    30  import org.apache.commons.geometry.euclidean.threed.mesh.SimpleTriangleMesh;
    31  import org.apache.commons.geometry.io.euclidean.threed.SimpleFacetDefinition;
    32  import org.apache.commons.numbers.core.Precision;
    33  import org.junit.jupiter.api.Assertions;
    34  import org.junit.jupiter.api.Test;
    35
    36  class ObjWriterTest {
    37
    38      private static final double TEST_EPS = 1e-10;
    39
    40      private static final Precision.DoubleEquivalence TEST_PRECISION =
    41              Precision.doubleEquivalenceOfEpsilon(TEST_EPS);
    42
    43      @Test
    44      void testPropertyDefaults() {
    45          // arrange
    46          final StringWriter writer = new StringWriter();
    47
    48          // act/assert
    49          try (ObjWriter objWriter = new ObjWriter(writer)) {
    50              Assertions.assertEquals("\n", objWriter.getLineSeparator());
    51              Assertions.assertNotNull(objWriter.getDoubleFormat());
    52              Assertions.assertEquals(0, objWriter.getVertexCount());
    53              Assertions.assertEquals(0, objWriter.getVertexNormalCount());
    54          }
    55      }
    56
    57      @Test
    58      void testClose_calledMultipleTimes() {
    59          // arrange
    60          final StringWriter writer = new StringWriter();
    61
    62          // act/assert
    63          try (ObjWriter objWriter = new ObjWriter(writer)) {
    64              objWriter.close();
    65          }
    66
    67          Assertions.assertEquals("", writer.toString());
    68      }
    69
    70      @Test
    71      void testSetLineSeparator() {
    72          // arrange
    73          final StringWriter writer = new StringWriter();
    74
    75          // act
    76          try (ObjWriter objWriter = new ObjWriter(writer)) {
    77              objWriter.setLineSeparator("\r\n");
    78
    79              objWriter.writeComment("line 1");
    80              objWriter.writeComment("line 2");
    81              objWriter.writeVertex(Vector3D.ZERO);
    82          }
    83
    84          // assert
    85          Assertions.assertEquals(
    86              "# line 1\r\n" +
    87              "# line 2\r\n" +
    88              "v 0.0 0.0 0.0\r\n", writer.getBuffer().toString());
    89      }
    90
    91      @Test
    92      void testSetDecimalFormat() {
    93          // arrange
    94          final StringWriter writer = new StringWriter();
    95          final DecimalFormat fmt =
    96                  new DecimalFormat("0.0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
    97
    98          // act
    99          try (ObjWriter objWriter = new ObjWriter(writer)) {
    100             objWriter.setDoubleFormat(fmt::format);
    101
    102             objWriter.writeVertex(Vector3D.of(1.09, 2.05, 3.06));
    103         }
    104
    105         // assert
    106         Assertions.assertEquals("v 1.1 2.0 3.1\n", writer.getBuffer().toString());
    107     }
    108
    109     @Test
    110     void testWriteComment() {
    111         // arrange
    112         final StringWriter writer = new StringWriter();
    113
    114         // act
    115         try (ObjWriter objWriter = new ObjWriter(writer)) {
    116             objWriter.writeComment("test");
    117             objWriter.writeComment(" a\r\n multi-line\ncomment");
    118         }
    119
    120         // assert
    121         Assertions.assertEquals(
    122             "# test\n" +
    123             "#  a\n" +
    124             "#  multi-line\n" +
    125             "# comment\n", writer.getBuffer().toString());
    126     }
    127
    128     @Test
    129     void testWriteObjectName() {
    130         // arrange
    131         final StringWriter writer = new StringWriter();
    132
    133         // act
    134         try (ObjWriter objWriter = new ObjWriter(writer)) {
    135             objWriter.writeObjectName("test-object");
    136         }
    137
    138         // assert
    139         Assertions.assertEquals("o test-object\n", writer.getBuffer().toString());
    140     }
    141
    142     @Test
    143     void testWriteGroupName() {
    144         // arrange
    145         final StringWriter writer = new StringWriter();
    146
    147         // act
    148         try (ObjWriter objWriter = new ObjWriter(writer)) {
    149             objWriter.writeGroupName("test-group");
    150         }
    151
    152         // assert
    153         Assertions.assertEquals("g test-group\n", writer.getBuffer().toString());
    154     }
    155
    156     @Test
    157     void testWriteVertex() {
    158         // arrange
    159         final StringWriter writer = new StringWriter();
    160
    161         // arrange
    162         final DecimalFormat fmt =
    163                 new DecimalFormat("0.0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
    164
    165         // act
    166         final int index1;
    167         final int index2;
    168         final int count;
    169         try (ObjWriter objWriter = new ObjWriter(writer)) {
    170             objWriter.setDoubleFormat(fmt::format);
    171
    172             index1 = objWriter.writeVertex(Vector3D.of(1.09, 2.1, 3.005));
    173             index2 = objWriter.writeVertex(Vector3D.of(0.06, 10, 12));
    174
    175             count = objWriter.getVertexCount();
    176         }
    177
    178         // assert
    179         Assertions.assertEquals(0, index1);
    180         Assertions.assertEquals(1, index2);
    181         Assertions.assertEquals(2, count);
    182         Assertions.assertEquals(
    183             "v 1.1 2.1 3.0\n" +
    184             "v 0.1 10.0 12.0\n", writer.getBuffer().toString());
    185     }
    186
    187     @Test
    188     void testWriteNormal() {
    189         // arrange
    190         final StringWriter writer = new StringWriter();
    191         final DecimalFormat fmt =
    192                 new DecimalFormat("0.0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
    193
    194         // act
    195         final int index1;
    196         final int index2;
    197         final int count;
    198         try (ObjWriter objWriter = new ObjWriter(writer)) {
    199             objWriter.setDoubleFormat(fmt::format);
    200
    201             index1 = objWriter.writeVertexNormal(Vector3D.of(1.09, 2.1, 3.005));
    202             index2 = objWriter.writeVertexNormal(Vector3D.of(0.06, 10, 12));
    203
    204             count = objWriter.getVertexNormalCount();
    205         }
    206
    207         // assert
    208         Assertions.assertEquals(0, index1);
    209         Assertions.assertEquals(1, index2);
    210         Assertions.assertEquals(2, count);
    211         Assertions.assertEquals(
    212             "vn 1.1 2.1 3.0\n" +
    213             "vn 0.1 10.0 12.0\n", writer.getBuffer().toString());
    214     }
    215
    216     @Test
    217     void testWriteFace() {
    218         // arrange
    219         final StringWriter writer = new StringWriter();
    220
    221         // act
    222         try (ObjWriter objWriter = new ObjWriter(writer)) {
    223             objWriter.writeVertex(Vector3D.ZERO);
    224             objWriter.writeVertex(Vector3D.of(1, 0, 0));
    225             objWriter.writeVertex(Vector3D.of(1, 1, 0));
    226             objWriter.writeVertex(Vector3D.of(0, 1, 0));
    227
    228             objWriter.writeFace(0, 1, 2);
    229             objWriter.writeFace(0, 1, 2, 3);
    230         }
    231
    232         // assert
    233         Assertions.assertEquals(
    234             "v 0.0 0.0 0.0\n" +
    235             "v 1.0 0.0 0.0\n" +
    236             "v 1.0 1.0 0.0\n" +
    237             "v 0.0 1.0 0.0\n" +
    238             "f 1 2 3\n" +
    239             "f 1 2 3 4\n", writer.getBuffer().toString());
    240     }
    241
    242     @Test
    243     void testWriteFace_withNormals() {
    244         // arrange
    245         final StringWriter writer = new StringWriter();
    246
    247         // act
    248         try (ObjWriter objWriter = new ObjWriter(writer)) {
    249             objWriter.writeVertex(Vector3D.ZERO);
    250             objWriter.writeVertex(Vector3D.of(1, 0, 0));
    251             objWriter.writeVertex(Vector3D.of(1, 1, 0));
    252             objWriter.writeVertex(Vector3D.of(0, 1, 0));
    253
    254             objWriter.writeVertexNormal(Vector3D.Unit.PLUS_Z);
    255             objWriter.writeVertexNormal(Vector3D.Unit.MINUS_Z);
    256
    257             objWriter.writeFace(new int[] {0, 1, 2}, 0);
    258             objWriter.writeFace(new int[] {0, 1, 2, 3}, new int[] {1, 1, 1, 1});
    259         }
    260
    261         // assert
    262         Assertions.assertEquals(
    263             "v 0.0 0.0 0.0\n" +
    264             "v 1.0 0.0 0.0\n" +
    265             "v 1.0 1.0 0.0\n" +
    266             "v 0.0 1.0 0.0\n" +
    267             "vn 0.0 0.0 1.0\n" +
    268             "vn 0.0 0.0 -1.0\n" +
    269             "f 1//1 2//1 3//1\n" +
    270             "f 1//2 2//2 3//2 4//2\n", writer.getBuffer().toString());
    271     }
    272
    273     @Test
    274     void testWriteFace_invalidVertexNumber() {
    275         // arrange
    276         final StringWriter writer = new StringWriter();
    277
    278         // act
    279         GeometryTestUtils.assertThrowsWithMessage(() -> {
    280             try (ObjWriter objWriter = new ObjWriter(writer)) {
    281                 objWriter.writeFace(1, 2);
    282             }
    283         }, IllegalArgumentException.class, "Face must have more than 3 vertices; found 2");
    284     }
    285
    286     @Test
    287     void testWriteFace_vertexIndexOutOfBounds() {
    288         // arrange
    289         final StringWriter writer = new StringWriter();
    290
    291         // act/assert
    292         GeometryTestUtils.assertThrowsWithMessage(() -> {
    293             try (ObjWriter objWriter = new ObjWriter(writer)) {
    294                 objWriter.writeVertex(Vector3D.ZERO);
    295                 objWriter.writeVertex(Vector3D.of(1, 1, 1));
    296
    297                 objWriter.writeFace(0, 1, 2);
    298             }
    299         }, IndexOutOfBoundsException.class, "Vertex index out of bounds: 2");
    300
    301         GeometryTestUtils.assertThrowsWithMessage(() -> {
    302             try (ObjWriter objWriter = new ObjWriter(writer)) {
    303                 objWriter.writeVertex(Vector3D.ZERO);
    304                 objWriter.writeVertex(Vector3D.of(1, 1, 1));
    305
    306                 objWriter.writeFace(0, -1, 1);
    307             }
    308         }, IndexOutOfBoundsException.class, "Vertex index out of bounds: -1");
    309     }
    310
    311     @Test
    312     void testWriteFace_normalIndexOutOfBounds() {
    313         // arrange
    314         final StringWriter writer = new StringWriter();
    315
    316         // act/assert
    317         GeometryTestUtils.assertThrowsWithMessage(() -> {
    318             try (ObjWriter objWriter = new ObjWriter(writer)) {
    319                 objWriter.writeVertex(Vector3D.ZERO);
    320                 objWriter.writeVertex(Vector3D.of(1, 1, 1));
    321                 objWriter.writeVertex(Vector3D.of(0, 2, 0));
    322
    323                 objWriter.writeVertexNormal(Vector3D.Unit.PLUS_Z);
    324
    325                 objWriter.writeFace(new int[] {0, 1, 2}, 1);
    326             }
    327         }, IndexOutOfBoundsException.class, "Normal index out of bounds: 1");
    328
    329         GeometryTestUtils.assertThrowsWithMessage(() -> {
    330             try (ObjWriter objWriter = new ObjWriter(writer)) {
    331                 objWriter.writeVertex(Vector3D.ZERO);
    332                 objWriter.writeVertex(Vector3D.of(1, 1, 1));
    333                 objWriter.writeVertex(Vector3D.of(0, 2, 0));
    334
    335                 objWriter.writeVertexNormal(Vector3D.Unit.PLUS_Z);
    336
    337                 objWriter.writeFace(new int[] {0, 1, 2}, -1);
    338             }
    339         }, IndexOutOfBoundsException.class, "Normal index out of bounds: -1");
    340     }
    341
    342     @Test
    343     void testWriteFace_invalidVertexAndNormalCountMismatch() {
    344         // arrange
    345         final StringWriter writer = new StringWriter();
    346
    347         // act
    348         GeometryTestUtils.assertThrowsWithMessage(() -> {
    349             try (ObjWriter objWriter = new ObjWriter(writer)) {
    350                 objWriter.writeFace(new int[] {0, 1, 2, 3}, new int[] {0, 1, 2});
    351             }
    352         }, IllegalArgumentException.class, "Face normal index count must equal vertex index count; expected 4 but was 3");
    353     }
    354
    355     @Test
    356     void testWriteMesh() {
    357         // arrange
    358         final SimpleTriangleMesh mesh = SimpleTriangleMesh.builder(TEST_PRECISION)
    359                 .addFaceUsingVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0))
    360                 .addFaceUsingVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 0, 1))
    361                 .build();
    362
    363         final StringWriter writer = new StringWriter();
    364
    365         // act
    366         try (ObjWriter objWriter = new ObjWriter(writer)) {
    367             objWriter.writeMesh(mesh);
    368         }
    369
    370         // assert
    371         Assertions.assertEquals(
    372             "v 0.0 0.0 0.0\n" +
    373             "v 1.0 0.0 0.0\n" +
    374             "v 0.0 1.0 0.0\n" +
    375             "v 0.0 0.0 1.0\n" +
    376             "f 1 2 3\n" +
    377             "f 1 2 4\n", writer.getBuffer().toString());
    378     }
    379
    380     @Test
    381     void testMeshBuffer() {
    382         // arrange
    383         final StringWriter writer = new StringWriter();
    384
    385         try (ObjWriter objWriter = new ObjWriter(writer)) {
    386             ObjWriter.MeshBuffer buf = objWriter.meshBuffer();
    387
    388             // act
    389             buf.add(new SimpleFacetDefinition(Arrays.asList(
    390                     Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0)), Vector3D.Unit.MINUS_Z));
    391             buf.add(Planes.convexPolygonFromVertices(Arrays.asList(
    392                     Vector3D.ZERO, Vector3D.of(1, 1, 0), Vector3D.of(0, 1.5, 0)), TEST_PRECISION));
    393             buf.add(new SimpleFacetDefinition(Arrays.asList(
    394                     Vector3D.of(0, 1.5, 0), Vector3D.of(1, 1, 0), Vector3D.of(0, 2, 0)), Vector3D.Unit.PLUS_Z));
    395
    396             buf.flush();
    397         }
    398
    399         // assert
    400         Assertions.assertEquals(
    401             "v 0.0 0.0 0.0\n" +
    402             "v 1.0 0.0 0.0\n" +
    403             "v 1.0 1.0 0.0\n" +
    404             "v 0.0 1.5 0.0\n" +
    405             "v 0.0 2.0 0.0\n" +
    406             "vn 0.0 0.0 -1.0\n" +
    407             "vn 0.0 0.0 1.0\n" +
    408             "f 1//1 2//1 3//1\n" +
    409             "f 1 3 4\n" +
    410             "f 4//2 3//2 5//2\n", writer.getBuffer().toString());
    411     }
    412
    413     @Test
    414     void testMeshBuffer_givenBatchSize() {
    415         // arrange
    416         final StringWriter writer = new StringWriter();
    417
    418         try (ObjWriter objWriter = new ObjWriter(writer)) {
    419             ObjWriter.MeshBuffer buf = objWriter.meshBuffer(2);
    420
    421             // act
    422             buf.add(new SimpleFacetDefinition(Arrays.asList(
    423                     Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0)), Vector3D.Unit.MINUS_Z));
    424             buf.add(Planes.convexPolygonFromVertices(Arrays.asList(
    425                     Vector3D.ZERO, Vector3D.of(1, 1, 0), Vector3D.of(0, 1.5, 0)), TEST_PRECISION));
    426             buf.add(new SimpleFacetDefinition(Arrays.asList(
    427                     Vector3D.of(0, 1.5, 0), Vector3D.of(1, 1, 0), Vector3D.of(0, 2, 0)), Vector3D.Unit.PLUS_Z));
    428
    429             buf.flush();
    430         }
    431
    432         // assert
    433         Assertions.assertEquals(
    434             "v 0.0 0.0 0.0\n" +
    435             "v 1.0 0.0 0.0\n" +
    436             "v 1.0 1.0 0.0\n" +
    437             "v 0.0 1.5 0.0\n" +
    438             "vn 0.0 0.0 -1.0\n" +
    439             "f 1//1 2//1 3//1\n" +
    440             "f 1 3 4\n" +
    441             "v 0.0 1.5 0.0\n" +
    442             "v 1.0 1.0 0.0\n" +
    443             "v 0.0 2.0 0.0\n" +
    444             "vn 0.0 0.0 1.0\n" +
    445             "f 5//2 6//2 7//2\n", writer.getBuffer().toString());
    446     }
    447
    448     @Test
    449     void testMeshBuffer_mixedWithDirectlyAddedFace() {
    450         // arrange
    451         final StringWriter writer = new StringWriter();
    452
    453         try (ObjWriter objWriter = new ObjWriter(writer)) {
    454             ObjWriter.MeshBuffer buf = objWriter.meshBuffer(2);
    455
    456             // act
    457             objWriter.writeVertex(Vector3D.ZERO);
    458             objWriter.writeVertex(Vector3D.Unit.MINUS_Y);
    459             objWriter.writeVertex(Vector3D.Unit.MINUS_X);
    460             objWriter.writeVertexNormal(Vector3D.Unit.PLUS_Z);
    461             objWriter.writeFace(new int[] {0, 1, 2}, 0);
    462
    463             buf.add(new SimpleFacetDefinition(Arrays.asList(
    464                     Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 0)), Vector3D.Unit.MINUS_Z));
    465             buf.add(Planes.convexPolygonFromVertices(Arrays.asList(
    466                     Vector3D.ZERO, Vector3D.of(1, 1, 0), Vector3D.of(0, 1.5, 0)), TEST_PRECISION));
    467             buf.add(new SimpleFacetDefinition(Arrays.asList(
    468                     Vector3D.of(0, 1.5, 0), Vector3D.of(1, 1, 0), Vector3D.of(0, 2, 0)), Vector3D.Unit.PLUS_Z));
    469
    470             buf.flush();
    471
    472             objWriter.writeFace(objWriter.getVertexCount() - 1, 2, 1, 0);
    473         }
    474
    475         // assert
    476         Assertions.assertEquals(
    477             "v 0.0 0.0 0.0\n" +
    478             "v 0.0 -1.0 0.0\n" +
    479             "v -1.0 0.0 0.0\n" +
    480             "vn 0.0 0.0 1.0\n" +
    481             "f 1//1 2//1 3//1\n" +
    482             "v 0.0 0.0 0.0\n" +
    483             "v 1.0 0.0 0.0\n" +
    484             "v 1.0 1.0 0.0\n" +
    485             "v 0.0 1.5 0.0\n" +
    486             "vn 0.0 0.0 -1.0\n" +
    487             "f 4//2 5//2 6//2\n" +
    488             "f 4 6 7\n" +
    489             "v 0.0 1.5 0.0\n" +
    490             "v 1.0 1.0 0.0\n" +
    491             "v 0.0 2.0 0.0\n" +
    492             "vn 0.0 0.0 1.0\n" +
    493             "f 8//3 9//3 10//3\n" +
    494             "f 10 3 2 1\n", writer.getBuffer().toString());
    495     }
    496
    497     @Test
    498     void testWriteBoundaries_meshArgument() {
    499         // arrange
    500         final SimpleTriangleMesh mesh = SimpleTriangleMesh.builder(TEST_PRECISION)
    501                 .addFaceUsingVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0))
    502                 .addFaceUsingVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 0, 1))
    503                 .build();
    504
    505         final StringWriter writer = new StringWriter();
    506
    507         // act
    508         try (ObjWriter objWriter = new ObjWriter(writer)) {
    509             objWriter.writeBoundaries(mesh);
    510         }
    511
    512         // assert
    513         Assertions.assertEquals(
    514             "v 0.0 0.0 0.0\n" +
    515             "v 1.0 0.0 0.0\n" +
    516             "v 0.0 1.0 0.0\n" +
    517             "v 0.0 0.0 1.0\n" +
    518             "f 1 2 3\n" +
    519             "f 1 2 4\n", writer.getBuffer().toString());
    520     }
    521
    522     @Test
    523     void testWriteBoundaries_nonMeshArgument() {
    524         // arrange
    525         final BoundarySource3D src = BoundarySource3D.of(
    526                     Planes.triangleFromVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0), TEST_PRECISION),
    527                     Planes.triangleFromVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 0, 1), TEST_PRECISION)
    528                 );
    529
    530         final StringWriter writer = new StringWriter();
    531
    532         // act
    533         try (ObjWriter objWriter = new ObjWriter(writer)) {
    534             objWriter.writeBoundaries(src);
    535         }
    536
    537         // assert
    538         Assertions.assertEquals(
    539             "v 0.0 0.0 0.0\n" +
    540             "v 1.0 0.0 0.0\n" +
    541             "v 0.0 1.0 0.0\n" +
    542             "v 0.0 0.0 1.0\n" +
    543             "f 1 2 3\n" +
    544             "f 1 2 4\n", writer.getBuffer().toString());
    545     }
    546
    547     @Test
    548     void testWriteBoundaries_nonMeshArgument_smallBatchSize() {
    549         // arrange
    550         final BoundarySource3D src = BoundarySource3D.of(
    551                     Planes.triangleFromVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0), TEST_PRECISION),
    552                     Planes.triangleFromVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 0, 1), TEST_PRECISION)
    553                 );
    554
    555         final StringWriter writer = new StringWriter();
    556
    557         // act
    558         try (ObjWriter objWriter = new ObjWriter(writer)) {
    559             objWriter.writeBoundaries(src, 1);
    560         }
    561
    562         // assert
    563         Assertions.assertEquals(
    564             "v 0.0 0.0 0.0\n" +
    565             "v 1.0 0.0 0.0\n" +
    566             "v 0.0 1.0 0.0\n" +
    567             "f 1 2 3\n" +
    568             "v 0.0 0.0 0.0\n" +
    569             "v 1.0 0.0 0.0\n" +
    570             "v 0.0 0.0 1.0\n" +
    571             "f 4 5 6\n", writer.getBuffer().toString());
    572     }
    573
    574     @Test
    575     void testWriteBoundaries_infiniteBoundary() {
    576         // arrange
    577         final BoundarySource3D src = BoundarySource3D.of(
    578                     Planes.triangleFromVertices(Vector3D.ZERO, Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0), TEST_PRECISION),
    579                     Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION).span()
    580                 );
    581
    582         final StringWriter writer = new StringWriter();
    583
    584         // act/assert
    585         GeometryTestUtils.assertThrowsWithMessage(() -> {
    586             try (ObjWriter objWriter = new ObjWriter(writer)) {
    587                 objWriter.writeBoundaries(src);
    588             }
    589         }, IllegalArgumentException.class, Pattern.compile("^OBJ input geometry cannot be infinite: .*"));
    590     }
    591 }