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;
    18
    19  import java.io.File;
    20  import java.util.Arrays;
    21  import java.util.List;
    22  import java.util.stream.Collectors;
    23
    24  import org.apache.commons.geometry.core.RegionLocation;
    25  import org.apache.commons.geometry.core.partitioning.Split;
    26  import org.apache.commons.geometry.core.partitioning.bsp.RegionCutRule;
    27  import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
    28  import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
    29  import org.apache.commons.geometry.euclidean.oned.Interval;
    30  import org.apache.commons.geometry.euclidean.oned.RegionBSPTree1D;
    31  import org.apache.commons.geometry.euclidean.oned.Vector1D;
    32  import org.apache.commons.geometry.euclidean.testio.TestOBJWriter;
    33  import org.apache.commons.geometry.euclidean.threed.AffineTransformMatrix3D;
    34  import org.apache.commons.geometry.euclidean.threed.ConvexPolygon3D;
    35  import org.apache.commons.geometry.euclidean.threed.Plane;
    36  import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
    37  import org.apache.commons.geometry.euclidean.threed.Planes;
    38  import org.apache.commons.geometry.euclidean.threed.RegionBSPTree3D;
    39  import org.apache.commons.geometry.euclidean.threed.Vector3D;
    40  import org.apache.commons.geometry.euclidean.threed.line.Line3D;
    41  import org.apache.commons.geometry.euclidean.threed.line.LinecastPoint3D;
    42  import org.apache.commons.geometry.euclidean.threed.line.Lines3D;
    43  import org.apache.commons.geometry.euclidean.threed.line.Ray3D;
    44  import org.apache.commons.geometry.euclidean.threed.mesh.TriangleMesh;
    45  import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
    46  import org.apache.commons.geometry.euclidean.threed.shape.Parallelepiped;
    47  import org.apache.commons.geometry.euclidean.threed.shape.Sphere;
    48  import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
    49  import org.apache.commons.geometry.euclidean.twod.Line;
    50  import org.apache.commons.geometry.euclidean.twod.LinecastPoint2D;
    51  import org.apache.commons.geometry.euclidean.twod.Lines;
    52  import org.apache.commons.geometry.euclidean.twod.Ray;
    53  import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
    54  import org.apache.commons.geometry.euclidean.twod.Segment;
    55  import org.apache.commons.geometry.euclidean.twod.Vector2D;
    56  import org.apache.commons.geometry.euclidean.twod.path.LinePath;
    57  import org.apache.commons.geometry.euclidean.twod.shape.Parallelogram;
    58  import org.apache.commons.numbers.angle.PlaneAngleRadians;
    59  import org.junit.Assert;
    60  import org.junit.Test;
    61
    62  /** This class contains code listed as examples in the user guide and other documentation.
    63   * If any portion of this code changes, the corresponding examples in the documentation <em>must</em> be updated.
    64   */
    65  public class DocumentationExamplesTest {
    66
    67      private static final double TEST_EPS = 1e-12;
    68
    69      @Test
    70      public void testIndexPageExample() {
    71          // construct a precision context to handle floating-point comparisons
    72          final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
    73
    74          // create a BSP tree representing the unit cube
    75          final RegionBSPTree3D tree = Parallelepiped.unitCube(precision).toTree();
    76
    77          // create a sphere centered on the origin
    78          final Sphere sphere = Sphere.from(Vector3D.ZERO, 0.65, precision);
    79
    80          // subtract a BSP tree approximation of the sphere containing 512 facets
    81          // from the cube, modifying the cube tree in place
    82          tree.difference(sphere.toTree(3));
    83
    84          // compute some properties of the resulting region
    85          final double size = tree.getSize(); // 0.11509505362599505
    86          final Vector3D centroid = tree.getCentroid(); // (0, 0, 0)
    87
    88          // convert to a triangle mesh for output to other programs
    89          final TriangleMesh mesh = tree.toTriangleMesh(precision);
    90
    91          // -----------
    92          Assert.assertEquals(0.11509505362599505, size, TEST_EPS);
    93          EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, centroid, TEST_EPS);
    94
    95          TestOBJWriter.write(mesh, new File("target/index-page-example.obj"));
    96      }
    97
    98      @Test
    99      public void testPrecisionContextExample() {
    100         // create a precision context with an epsilon (aka, tolerance) value of 1e-3
    101         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
    102
    103         // test for equality using the eq() method
    104         precision.eq(1.0009, 1.0); // true; difference is less than epsilon
    105         precision.eq(1.002, 1.0); // false; difference is greater than epsilon
    106
    107         // compare
    108         precision.compare(1.0009, 1.0); // 0
    109         precision.compare(1.002, 1.0); // 1
    110
    111         // ------------------
    112         Assert.assertTrue(precision.eq(1.0009, 1.0));
    113         Assert.assertFalse(precision.eq(1.002, 1.0));
    114
    115         Assert.assertEquals(0, precision.compare(1.0009, 1.0));
    116         Assert.assertEquals(1, precision.compare(1.002, 1.0));
    117     }
    118
    119     @Test
    120     public void testEqualsVsEqExample() {
    121         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
    122
    123         final Vector2D v1 = Vector2D.of(1, 1); // (1.0, 1.0)
    124         final Vector2D v2 = Vector2D.parse("(1, 1)"); // (1.0, 1.0)
    125
    126         final Vector2D v3 = Vector2D.of(Math.sqrt(2), 0).transform(
    127                 AffineTransformMatrix2D.createRotation(0.25 * Math.PI)); // (1.0000000000000002, 1.0)
    128
    129         v1.equals(v2); // true - exactly equal
    130         v1.equals(v3); // false - not exactly equal
    131
    132         v1.eq(v3, precision); // true - approximately equal according to the given precision context
    133
    134         // ---------------------
    135         Assert.assertEquals(v1, v2);
    136         Assert.assertNotEquals(v1, v3);
    137         Assert.assertTrue(v1.eq(v3, precision));
    138     }
    139
    140     @Test
    141     public void testManualBSPTreeExample() {
    142         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
    143
    144         // create a tree representing an empty space (nothing "inside")
    145         final RegionBSPTree2D tree = RegionBSPTree2D.empty();
    146
    147         // insert a "structural" cut, meaning a cut whose children have the same inside/outside
    148         // status as the parent; this will help keep our tree balanced and limit its overall height
    149         tree.getRoot().insertCut(Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(1, 1), precision),
    150                 RegionCutRule.INHERIT);
    151
    152         RegionBSPTree2D.RegionNode2D currentNode;
    153
    154         // insert on the plus side of the structural diagonal cut
    155         currentNode = tree.getRoot().getPlus();
    156
    157         currentNode.insertCut(Lines.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, precision));
    158         currentNode = currentNode.getMinus();
    159
    160         currentNode.insertCut(Lines.fromPointAndDirection(Vector2D.of(1, 0), Vector2D.Unit.PLUS_Y, precision));
    161
    162         // insert on the plus side of the structural diagonal cut
    163         currentNode = tree.getRoot().getMinus();
    164
    165         currentNode.insertCut(Lines.fromPointAndDirection(Vector2D.of(1, 1), Vector2D.Unit.MINUS_X, precision));
    166         currentNode = currentNode.getMinus();
    167
    168         currentNode.insertCut(Lines.fromPointAndDirection(Vector2D.of(0, 1), Vector2D.Unit.MINUS_Y, precision));
    169
    170         // compute some tree properties
    171         final int count = tree.count(); // number of nodes in the tree = 11
    172         final int height = tree.height(); // height of the tree = 3
    173         final double size = tree.getSize(); // size of the region = 1
    174         final Vector2D centroid = tree.getCentroid(); // region centroid = (0.5, 0.5)
    175
    176         // ---------
    177         Assert.assertEquals(1, size, TEST_EPS);
    178         Assert.assertEquals(11, count);
    179         Assert.assertEquals(3, height);
    180         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), centroid, TEST_EPS);
    181     }
    182
    183     @Test
    184     public void testHyperplaneSubsetBSPTreeExample() {
    185         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
    186
    187         // create a tree representing an empty space (nothing "inside")
    188         final RegionBSPTree2D tree = RegionBSPTree2D.empty();
    189
    190         // insert the hyperplane subsets
    191         tree.insert(Arrays.asList(
    192                     Lines.segmentFromPoints(Vector2D.ZERO, Vector2D.of(1, 0), precision),
    193                     Lines.segmentFromPoints(Vector2D.of(1, 0), Vector2D.of(1, 1), precision),
    194                     Lines.segmentFromPoints(Vector2D.of(1, 1), Vector2D.of(0, 1), precision),
    195                     Lines.segmentFromPoints(Vector2D.of(0, 1), Vector2D.ZERO, precision)
    196                 ));
    197
    198         // compute some tree properties
    199         final int count = tree.count(); // number of nodes in the tree = 9
    200         final int height = tree.height(); // height of the tree = 4
    201         final double size = tree.getSize(); // size of the region = 1
    202         final Vector2D centroid = tree.getCentroid(); // region centroid = (0.5, 0.5)
    203
    204         // ---------
    205         Assert.assertEquals(1, size, TEST_EPS);
    206         Assert.assertEquals(9, count);
    207         Assert.assertEquals(4, height);
    208         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), centroid, TEST_EPS);
    209     }
    210
    211     @Test
    212     public void testIntervalExample() {
    213         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
    214
    215         // create a closed interval and a half-open interval with a min but no max
    216         final Interval closed = Interval.of(1, 2, precision);
    217         final Interval halfOpen = Interval.min(1, precision);
    218
    219         // classify some points against the intervals
    220         closed.contains(0.0); // false
    221         halfOpen.contains(Vector1D.ZERO); // false
    222
    223         final RegionLocation closedOneLoc = closed.classify(Vector1D.of(1)); // RegionLocation.BOUNDARY
    224         final RegionLocation halfOpenOneLoc = halfOpen.classify(Vector1D.of(1)); // RegionLocation.BOUNDARY
    225
    226         final RegionLocation closedThreeLoc = closed.classify(3.0); // RegionLocation.OUTSIDE
    227         final RegionLocation halfOpenThreeLoc = halfOpen.classify(3.0); // RegionLocation.INSIDE
    228
    229         // --------------------
    230         Assert.assertFalse(closed.contains(0));
    231         Assert.assertFalse(halfOpen.contains(0));
    232
    233         Assert.assertEquals(RegionLocation.BOUNDARY, closedOneLoc);
    234         Assert.assertEquals(RegionLocation.BOUNDARY, halfOpenOneLoc);
    235
    236         Assert.assertEquals(RegionLocation.OUTSIDE, closedThreeLoc);
    237         Assert.assertEquals(RegionLocation.INSIDE, halfOpenThreeLoc);
    238     }
    239
    240     @Test
    241     public void testRegionBSPTree1DExample() {
    242         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
    243
    244         // build a bsp tree from the union of several intervals
    245         final RegionBSPTree1D tree = RegionBSPTree1D.empty();
    246
    247         tree.add(Interval.of(1, 2, precision));
    248         tree.add(Interval.of(1.5, 3, precision));
    249         tree.add(Interval.of(-1, -2, precision));
    250
    251         // compute the size;
    252         final double size = tree.getSize(); // 3
    253
    254         // convert back to intervals
    255         final List<Interval> intervals = tree.toIntervals(); // size = 2
    256
    257         // ----------------------
    258         Assert.assertEquals(3, size, TEST_EPS);
    259         Assert.assertEquals(2, intervals.size());
    260     }
    261
    262     @Test
    263     public void testLineIntersectionExample() {
    264         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
    265
    266         // create some lines
    267         final Line a = Lines.fromPoints(Vector2D.ZERO, Vector2D.of(2, 2), precision);
    268         final Line b = Lines.fromPointAndDirection(Vector2D.of(1, -1), Vector2D.Unit.PLUS_Y, precision);
    269
    270         // compute the intersection and angles
    271         final Vector2D intersection = a.intersection(b); // (1, 1)
    272         final double angleAtoB = a.angle(b); // pi/4
    273         final double angleBtoA = b.angle(a); // -pi/4
    274
    275         // ----------------------------
    276         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), intersection, TEST_EPS);
    277         Assert.assertEquals(0.25 * Math.PI, angleAtoB, TEST_EPS);
    278         Assert.assertEquals(-0.25 * Math.PI, angleBtoA, TEST_EPS);
    279     }
    280
    281     @Test
    282     public void testLineSegmentIntersectionExample() {
    283         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
    284
    285         // create some line segments
    286         final Segment segmentA = Lines.segmentFromPoints(Vector2D.of(3, -1), Vector2D.of(3, 1), precision);
    287         final Segment segmentB = Lines.segmentFromPoints(Vector2D.of(-3, -1), Vector2D.of(-3, 1), precision);
    288
    289         // create a ray to intersect against the segments
    290         final Ray ray = Lines.rayFromPointAndDirection(Vector2D.of(2, 0), Vector2D.Unit.PLUS_X, precision);
    291
    292         // compute some intersections
    293         final Vector2D aIntersection = segmentA.intersection(ray); // (3, 0)
    294         final Vector2D bIntersection = segmentB.intersection(ray); // null - no intersection
    295
    296         // ----------------------------
    297         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 0), aIntersection, TEST_EPS);
    298         Assert.assertNull(bIntersection);
    299     }
    300
    301     @Test
    302     public void testRegionBSPTree2DExample() {
    303         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
    304
    305         // create a connected sequence of line segments forming the unit square
    306         final LinePath path = LinePath.builder(precision)
    307                 .append(Vector2D.ZERO)
    308                 .append(Vector2D.Unit.PLUS_X)
    309                 .append(Vector2D.of(1, 1))
    310                 .append(Vector2D.Unit.PLUS_Y)
    311                 .build(true); // build the path, ending it with the starting point
    312
    313         // convert to a tree
    314         final RegionBSPTree2D tree = path.toTree();
    315
    316         // copy the tree
    317         final RegionBSPTree2D copy = tree.copy();
    318
    319         // translate the copy
    320         copy.transform(AffineTransformMatrix2D.createTranslation(Vector2D.of(0.5, 0.5)));
    321
    322         // compute the union of the regions, storing the result back into the
    323         // first tree
    324         tree.union(copy);
    325
    326         // compute some properties
    327         final double size = tree.getSize(); // 1.75
    328         final Vector2D centroid = tree.getCentroid(); // (0.75, 0.75)
    329
    330         // get a line path representing the boundary; a list is returned since trees
    331         // can represent disjoint regions
    332         final List<LinePath> boundaries = tree.getBoundaryPaths(); // size = 1
    333
    334         // ----------------
    335         Assert.assertEquals(1.75, size, TEST_EPS);
    336         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.75, 0.75), centroid, TEST_EPS);
    337         Assert.assertEquals(1, boundaries.size());
    338     }
    339
    340     @Test
    341     public void testLinecast2DExample() {
    342         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
    343
    344         final Parallelogram box = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(2, 1), precision);
    345
    346         final LinecastPoint2D pt = box.linecastFirst(
    347                 Lines.segmentFromPoints(Vector2D.of(1, 0.5), Vector2D.of(4, 0.5), precision));
    348
    349         final Vector2D intersection = pt.getPoint(); // (2.0, 0.5)
    350         final Vector2D normal = pt.getNormal(); // (1.0, 0.0)
    351
    352         // ----------------
    353         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 0.5), intersection, TEST_EPS);
    354         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 0), normal, TEST_EPS);
    355     }
    356
    357     @Test
    358     public void testPlaneIntersectionExample() {
    359         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
    360
    361         // create two planes
    362         final Plane a = Planes.fromPointAndNormal(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Z, precision);
    363         final Plane b = Planes.fromPointAndPlaneVectors(Vector3D.of(1, 1, 1),
    364                 Vector3D.Unit.PLUS_Z, Vector3D.Unit.MINUS_Y, precision);
    365
    366         // compute the intersection
    367         final Line3D line = a.intersection(b);
    368
    369         final Vector3D dir = line.getDirection(); // (0, 1, 0)
    370
    371         // ----------------------
    372         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_Y, dir, TEST_EPS);
    373     }
    374
    375     @Test
    376     public void testTransform3DExample() {
    377         final List<Vector3D> inputPts = Arrays.asList(
    378                 Vector3D.ZERO,
    379                 Vector3D.Unit.PLUS_X,
    380                 Vector3D.Unit.PLUS_Y,
    381                 Vector3D.Unit.PLUS_Z);
    382
    383         // create a 4x4 transform matrix and quaternion rotation
    384         final AffineTransformMatrix3D mat = AffineTransformMatrix3D.createScale(2)
    385                 .translate(Vector3D.of(1, 2, 3));
    386
    387         final QuaternionRotation rot = QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Z,
    388                 PlaneAngleRadians.PI_OVER_TWO);
    389
    390         // transform the input points
    391         final List<Vector3D> matOutput = inputPts.stream()
    392                 .map(mat)
    393                 .collect(Collectors.toList()); // [(1, 2, 3), (3, 2, 3), (1, 4, 3), (1, 2, 5)]
    394
    395         final List<Vector3D> rotOutput = inputPts.stream()
    396                 .map(rot)
    397                 .collect(Collectors.toList()); // [(0, 0, 0), (0, 1, 0), (-1, 0, 0), (0, 0, 1)]
    398
    399         // ----------------
    400         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 3), matOutput.get(0), TEST_EPS);
    401         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 2, 3), matOutput.get(1), TEST_EPS);
    402         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 4, 3), matOutput.get(2), TEST_EPS);
    403         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 5), matOutput.get(3), TEST_EPS);
    404
    405         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, 0), rotOutput.get(0), TEST_EPS);
    406         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 1, 0), rotOutput.get(1), TEST_EPS);
    407         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 0), rotOutput.get(2), TEST_EPS);
    408         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, 1), rotOutput.get(3), TEST_EPS);
    409     }
    410
    411     @Test
    412     public void testRegionBSPTree3DExample() {
    413         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
    414
    415         // create the faces of a pyramid with a square base and its apex pointing along the
    416         // positive z axis
    417         final Vector3D[] vertices = {
    418             Vector3D.Unit.PLUS_Z,
    419             Vector3D.of(0.5, 0.5, 0.0),
    420             Vector3D.of(0.5, -0.5, 0.0),
    421             Vector3D.of(-0.5, -0.5, 0.0),
    422             Vector3D.of(-0.5, 0.5, 0.0)
    423         };
    424
    425         final int[][] faceIndices = {
    426             {1, 0, 2},
    427             {2, 0, 3},
    428             {3, 0, 4},
    429             {4, 0, 1},
    430             {1, 2, 3, 4}
    431         };
    432
    433         // convert the vertices and faces to convex polygons and use to construct a BSP tree
    434         final List<ConvexPolygon3D> faces = Planes.indexedConvexPolygons(vertices, faceIndices, precision);
    435         final RegionBSPTree3D tree = RegionBSPTree3D.from(faces);
    436
    437         // split the region through its centroid along a diagonal of the base
    438         final Plane cutter = Planes.fromPointAndNormal(tree.getCentroid(), Vector3D.Unit.from(1, 1, 0), precision);
    439         final Split<RegionBSPTree3D> split = tree.split(cutter);
    440
    441         // compute some properties for the minus side of the split and convert back to hyperplane subsets
    442         // (ie, boundary facets)
    443         final RegionBSPTree3D minus = split.getMinus();
    444
    445         final double minusSize = minus.getSize(); // 1/6
    446         final List<PlaneConvexSubset> minusBoundaries = minus.getBoundaries(); // size = 4
    447
    448         // ---------------------
    449         Assert.assertEquals(1.0 / 6.0, minusSize, TEST_EPS);
    450         Assert.assertEquals(4, minusBoundaries.size());
    451     }
    452
    453     @Test
    454     public void testLinecast3DExample() {
    455         final DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
    456
    457         // create a BSP tree representing an axis-aligned cube with corners at (0, 0, 0) and (1, 1, 1)
    458         final RegionBSPTree3D tree = Parallelepiped.axisAligned(Vector3D.ZERO, Vector3D.of(1, 1, 1), precision)
    459                 .toTree();
    460
    461         // create a ray starting on one side of the cube and pointing through its center
    462         final Ray3D ray = Lines3D.rayFromPointAndDirection(Vector3D.of(0.5, 0.5, -1), Vector3D.Unit.PLUS_Z, precision);
    463
    464         // perform the linecast
    465         final List<LinecastPoint3D> pts = tree.linecast(ray);
    466
    467         // check the results
    468         final int intersectionCount = pts.size(); // intersectionCount = 2
    469         final Vector3D intersection = pts.get(0).getPoint(); // (0.5, 0.5, 0.0)
    470         final Vector3D normal = pts.get(0).getNormal(); // (0.0, 0.0, -1.0)
    471
    472         // ----------------
    473         Assert.assertEquals(2, intersectionCount);
    474
    475         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0), intersection, TEST_EPS);
    476         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, -1), normal, TEST_EPS);
    477     }
    478 }