You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by er...@apache.org on 2020/01/07 15:14:34 UTC

[commons-geometry] branch master updated (b14888f -> 200933a)

This is an automated email from the ASF dual-hosted git repository.

erans pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git.


    from b14888f  Add "NOTICE.txt".
     new 6c10297  GEOMETRY-68: adding Linecast2D/3D API
     new b59e0b0  GEOMETRY-68: adding linecast examples to user guide
     new 6808e05  GEOMETRY-68: fixing issue where duplicate (equivalent) points are returned in some linecast operations
     new 2ee4af0  moving Equivalency interface out of internal package since it is part of the public API
     new 66740f4  GEOMETRY-68: removing Equivalency interface in favor of defining eq() methods that accept DoublePrecisionContext objects as arguments
     new 0454598  GEOMETRY-83: adding shape-generation packages with Parallelogram and Parallelepiped classes; adding back RegionBSPTreeXX.from(Iterable) factory methods
     new 6ab4eaf  adding entry to userguide explaining equals() vs eq() methods
     new 200933a  Merge branch 'GEOMETRY-68__Matt'

The 8 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../geometry/core/internal/Equivalency.java        |  33 --
 .../geometry/euclidean/AbstractLinecastPoint.java  | 130 ++++++
 .../geometry/euclidean/oned/OrientedPoint.java     |  32 +-
 .../euclidean/threed/BoundarySource3D.java         |  20 +
 .../threed/BoundarySourceLinecastWrapper3D.java    |  89 ++++
 .../geometry/euclidean/threed/ConvexSubPlane.java  |  27 ++
 .../geometry/euclidean/threed/ConvexVolume.java    |  14 +-
 .../commons/geometry/euclidean/threed/Line3D.java  |  13 +
 .../geometry/euclidean/threed/LinecastPoint3D.java | 116 ++++++
 .../geometry/euclidean/threed/Linecastable3D.java  |  71 ++++
 .../commons/geometry/euclidean/threed/Plane.java   |  34 +-
 .../geometry/euclidean/threed/RegionBSPTree3D.java | 166 ++++++--
 .../geometry/euclidean/threed/SubPlane.java        |   2 +-
 .../Parallelepiped.java}                           |  34 +-
 .../threed/{ => shapes}/package-info.java          |   8 +-
 .../geometry/euclidean/twod/BoundarySource2D.java  |  20 +
 .../twod/BoundarySourceLinecastWrapper2D.java      |  89 ++++
 .../geometry/euclidean/twod/ConvexArea.java        |  14 +-
 .../commons/geometry/euclidean/twod/Line.java      |  34 +-
 .../geometry/euclidean/twod/LinecastPoint2D.java   | 115 ++++++
 .../geometry/euclidean/twod/Linecastable2D.java    |  72 ++++
 .../geometry/euclidean/twod/PolarCoordinates.java  |   2 +-
 .../commons/geometry/euclidean/twod/Polyline.java  |  14 +-
 .../geometry/euclidean/twod/RegionBSPTree2D.java   | 164 +++++++-
 .../commons/geometry/euclidean/twod/SubLine.java   |   2 +-
 .../Parallelogram.java}                            |  37 +-
 .../euclidean/twod/{ => shapes}/package-info.java  |   8 +-
 .../euclidean/DocumentationExamplesTest.java       |  72 +++-
 .../geometry/euclidean/oned/OrientedPointTest.java |  17 +-
 .../euclidean/threed/BoundarySource3DTest.java     |  98 +++++
 .../BoundarySourceLinecastWrapper3DTest.java       | 246 +++++++++++
 .../euclidean/threed/ConvexSubPlaneTest.java       |  42 ++
 .../euclidean/threed/ConvexVolumeTest.java         |  41 ++
 .../geometry/euclidean/threed/Line3DTest.java      |  32 ++
 .../euclidean/threed/LinecastChecker3D.java        | 191 +++++++++
 .../euclidean/threed/LinecastPoint3DTest.java      | 203 +++++++++
 .../geometry/euclidean/threed/PlaneTest.java       |  14 +-
 .../euclidean/threed/RegionBSPTree3DTest.java      | 456 ++++++++++++++-------
 .../geometry/euclidean/threed/SubPlaneTest.java    |  20 +-
 .../ParallelepipedTest.java}                       |  36 +-
 .../euclidean/twod/BoundarySource2DTest.java       |  93 +++++
 .../twod/BoundarySourceLinecastWrapper2DTest.java  | 235 +++++++++++
 .../geometry/euclidean/twod/ConvexAreaTest.java    |  41 ++
 .../commons/geometry/euclidean/twod/LineTest.java  |  35 +-
 .../geometry/euclidean/twod/LinecastChecker2D.java | 191 +++++++++
 .../euclidean/twod/LinecastPoint2DTest.java        | 203 +++++++++
 .../geometry/euclidean/twod/PolylineTest.java      |  41 ++
 .../euclidean/twod/RegionBSPTree2DTest.java        | 203 ++++++++-
 .../ParallelogramTest.java}                        |  44 +-
 .../commons/geometry/spherical/oned/CutAngle.java  |  30 +-
 .../geometry/spherical/twod/GreatCircle.java       |  24 +-
 .../geometry/spherical/twod/RegionBSPTree2S.java   |  12 +
 .../geometry/spherical/twod/SubGreatCircle.java    |   2 +-
 .../geometry/spherical/oned/CutAngleTest.java      |  16 +-
 .../geometry/spherical/twod/GreatCircleTest.java   |  20 +-
 .../spherical/twod/RegionBSPTree2STest.java        |  31 +-
 src/site/xdoc/index.xml                            |   8 +-
 src/site/xdoc/userguide/index.xml                  |  65 ++-
 58 files changed, 3610 insertions(+), 512 deletions(-)
 delete mode 100644 commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/Equivalency.java
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractLinecastPoint.java
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3D.java
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3D.java
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Linecastable3D.java
 rename commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/{Boundaries3D.java => shapes/Parallelepiped.java} (71%)
 copy commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/{ => shapes}/package-info.java (86%)
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2D.java
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2D.java
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Linecastable2D.java
 rename commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/{Boundaries2D.java => shapes/Parallelogram.java} (67%)
 copy commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/{ => shapes}/package-info.java (86%)
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3DTest.java
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3DTest.java
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LinecastChecker3D.java
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3DTest.java
 rename commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/{Boundaries3DTest.java => shapes/ParallelepipedTest.java} (75%)
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2DTest.java
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2DTest.java
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LinecastChecker2D.java
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2DTest.java
 rename commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/{Boundaries2DTest.java => shapes/ParallelogramTest.java} (65%)


[commons-geometry] 07/08: adding entry to userguide explaining equals() vs eq() methods

Posted by er...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

erans pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git

commit 6ab4eaf83d8401e7b543aa10e4572bd41a391daf
Author: Matt Juntunen <ma...@hotmail.com>
AuthorDate: Sun Jan 5 11:20:24 2020 -0500

    adding entry to userguide explaining equals() vs eq() methods
---
 .../euclidean/DocumentationExamplesTest.java       | 24 +++++++++++++-
 src/site/xdoc/userguide/index.xml                  | 37 +++++++++++++++++++++-
 2 files changed, 59 insertions(+), 2 deletions(-)

diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
index 29cdf78..c04f37d 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
@@ -37,6 +37,7 @@ import org.apache.commons.geometry.euclidean.threed.Transform3D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
 import org.apache.commons.geometry.euclidean.threed.shapes.Parallelepiped;
+import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
 import org.apache.commons.geometry.euclidean.twod.Line;
 import org.apache.commons.geometry.euclidean.twod.LinecastPoint2D;
 import org.apache.commons.geometry.euclidean.twod.Polyline;
@@ -90,7 +91,7 @@ public class DocumentationExamplesTest {
         // create a precision context with an epsilon (aka, tolerance) value of 1e-3
         DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
 
-        // test for equality
+        // test for equality using the eq() method
         precision.eq(1.0009, 1.0); // true; difference is less than epsilon
         precision.eq(1.002, 1.0); // false; difference is greater than epsilon
 
@@ -107,6 +108,27 @@ public class DocumentationExamplesTest {
     }
 
     @Test
+    public void testEqualsVsEqExample() {
+        DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
+
+        Vector2D v1 = Vector2D.of(1, 1); // (1.0, 1.0)
+        Vector2D v2 = Vector2D.parse("(1, 1)"); // (1.0, 1.0)
+
+        Vector2D v3 = Vector2D.of(Math.sqrt(2), 0).transform(
+                AffineTransformMatrix2D.createRotation(0.25 * Math.PI)); // (1.0000000000000002, 1.0)
+
+        v1.equals(v2); // true - exactly equal
+        v1.equals(v3); // false - not exactly equal
+
+        v1.eq(v3, precision); // true - approximately equal according to the given precision context
+
+        // ---------------------
+        Assert.assertTrue(v1.equals(v2));
+        Assert.assertFalse(v1.equals(v3));
+        Assert.assertTrue(v1.eq(v3, precision));
+    }
+
+    @Test
     public void testManualBSPTreeExample() {
         DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
 
diff --git a/src/site/xdoc/userguide/index.xml b/src/site/xdoc/userguide/index.xml
index 4c54a05..bdeb6fc 100644
--- a/src/site/xdoc/userguide/index.xml
+++ b/src/site/xdoc/userguide/index.xml
@@ -134,7 +134,7 @@
 // create a precision context with an epsilon (aka, tolerance) value of 1e-3
 DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
 
-// test for equality
+// test for equality using the eq() method
 precision.eq(1.0009, 1.0); // true; difference is less than epsilon
 precision.eq(1.002, 1.0); // false; difference is greater than epsilon
 
@@ -142,6 +142,41 @@ precision.eq(1.002, 1.0); // false; difference is greater than epsilon
 precision.compare(1.0009, 1.0); // 0
 precision.compare(1.002, 1.0); // 1
         </source>
+
+        <h4><span class="code">equals()</span> vs <span class="code">eq()</span></h4>
+        <p>
+          Many objects in <em>Commons Geometry</em> provide both a standard Java <span class="code">equals()</span>
+          method as well as an <span class="code">eq()</span> method. The <span class="code">equals()</span> method
+          always tests for <em>strict</em> equality between objects. In general, any floating point values in the two
+          objects must be exactly equal in order for the <span class="code">equals()</span> method to return true (see
+          the documentation on individual classes for details). This strictness is enforced so that
+          <span class="code">equals()</span> can behave as expected by the JDK, fulfilling properties such as
+          transitivity and the relationship to <span class="code">hashCode()</span>.
+        </p>
+        <p>
+          In contrast, the <span class="code">eq()</span> method is used to test for <em>approximate</em> equality
+          between objects, with floating point values being evaluated by a provided
+          <a class="code" href="../commons-geometry-core/apidocs/org/apache/commons/geometry/core/precision/DoublePrecisionContext.html"
+          >DoublePrecisionContext</a>. Because of this approximate nature, this method cannot be guaranteed to
+          be transitive or have any meaningful relationship to <span class="code">hashCode</span>. The
+          <span class="code">eq()</span> should be used to test for object equality in cases where floating-point
+          errors in a computation may have introduced small discrepancies in values. The example below demonstrates
+          the differences between <span class="code">equals()</span> and <span class="code">eq()</span>.
+        </p>
+        <source>
+DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
+
+Vector2D v1 = Vector2D.of(1, 1); // (1.0, 1.0)
+Vector2D v2 = Vector2D.parse("(1, 1)"); // (1.0, 1.0)
+
+Vector2D v3 = Vector2D.of(Math.sqrt(2), 0).transform(
+        AffineTransformMatrix2D.createRotation(0.25 * Math.PI)); // (1.0000000000000002, 1.0)
+
+v1.equals(v2); // true - exactly equal
+v1.equals(v3); // false - not exactly equal
+
+v1.eq(v3, precision); // true - approximately equal according to the given precision context
+        </source>
       </subsection>
 
       <subsection name="Transforms" id="transforms">


[commons-geometry] 02/08: GEOMETRY-68: adding linecast examples to user guide

Posted by er...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

erans pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git

commit b59e0b02232b38f75a2f9cd85cc476aa8300e9a8
Author: Matt Juntunen <ma...@hotmail.com>
AuthorDate: Sun Dec 29 01:17:30 2019 -0500

    GEOMETRY-68: adding linecast examples to user guide
---
 .../euclidean/DocumentationExamplesTest.java       | 40 ++++++++++++++++++++++
 src/site/xdoc/userguide/index.xml                  | 27 +++++++++++++++
 2 files changed, 67 insertions(+)

diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
index d234e56..221820e 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
@@ -31,12 +31,15 @@ import org.apache.commons.geometry.euclidean.threed.AffineTransformMatrix3D;
 import org.apache.commons.geometry.euclidean.threed.Boundaries3D;
 import org.apache.commons.geometry.euclidean.threed.ConvexSubPlane;
 import org.apache.commons.geometry.euclidean.threed.Line3D;
+import org.apache.commons.geometry.euclidean.threed.LinecastPoint3D;
 import org.apache.commons.geometry.euclidean.threed.Plane;
 import org.apache.commons.geometry.euclidean.threed.RegionBSPTree3D;
 import org.apache.commons.geometry.euclidean.threed.Transform3D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
+import org.apache.commons.geometry.euclidean.twod.Boundaries2D;
 import org.apache.commons.geometry.euclidean.twod.Line;
+import org.apache.commons.geometry.euclidean.twod.LinecastPoint2D;
 import org.apache.commons.geometry.euclidean.twod.Polyline;
 import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
 import org.apache.commons.geometry.euclidean.twod.Segment;
@@ -288,6 +291,23 @@ public class DocumentationExamplesTest {
     }
 
     @Test
+    public void testLinecast2DExample() {
+        DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
+
+        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(2, 1), precision).toTree();
+
+        LinecastPoint2D pt = tree.linecastFirst(
+                Segment.fromPoints(Vector2D.of(1, 0.5), Vector2D.of(4, 0.5), precision));
+
+        Vector2D intersection = pt.getPoint(); // (2.0, 0.5)
+        Vector2D normal = pt.getNormal(); // (1.0, 0.0)
+
+        // ----------------
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 0.5), intersection, TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 0), normal, TEST_EPS);
+    }
+
+    @Test
     public void testPlaneIntersectionExample() {
         DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
 
@@ -382,4 +402,24 @@ public class DocumentationExamplesTest {
         Assert.assertEquals(1.0 / 6.0, minusSize, TEST_EPS);
         Assert.assertEquals(4, minusFacets.size());
     }
+
+    @Test
+    public void testLinecast3DExample() {
+        DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
+
+        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 2, 3), precision).toTree();
+
+        List<LinecastPoint3D> pts = tree.linecast(
+                Line3D.fromPoints(Vector3D.of(0.5, 0.5, -10), Vector3D.of(0.5, 0.5, 10), precision));
+
+        int intersectionCount = pts.size(); // intersectionCount = 2
+        Vector3D intersection = pts.get(0).getPoint(); // (0.5, 0.5, 0.0)
+        Vector3D normal = pts.get(0).getNormal(); // (0.0, 0.0, -1.0)
+
+        // ----------------
+        Assert.assertEquals(2, intersectionCount);
+
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0), intersection, TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, -1), normal, TEST_EPS);
+    }
 }
diff --git a/src/site/xdoc/userguide/index.xml b/src/site/xdoc/userguide/index.xml
index 98442d1..4868ecc 100644
--- a/src/site/xdoc/userguide/index.xml
+++ b/src/site/xdoc/userguide/index.xml
@@ -606,6 +606,19 @@ Vector2D center = tree.getBarycenter(); // (0.75, 0.75)
 // can represent disjoint regions
 List&lt;Polyline&gt; boundaries = tree.getBoundaryPaths(); // size = 1
         </source>
+
+        <h5>Linecast</h5>
+        <source>
+DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
+
+RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(2, 1), precision).toTree();
+
+LinecastPoint2D pt = tree.linecastFirst(
+        Segment.fromPoints(Vector2D.of(1, 0.5), Vector2D.of(4, 0.5), precision));
+
+Vector2D intersection = pt.getPoint(); // (2.0, 0.5)
+Vector2D normal = pt.getNormal(); // (1.0, 0.0)
+        </source>
       </subsection>
 
       <subsection name="Euclidean 3D" id="euclidean_3d">
@@ -757,6 +770,20 @@ RegionBSPTree3D minus = split.getMinus();
 double minusSize = minus.getSize(); // 1/6
 List&lt;ConvexSubPlane&gt; minusFacets = minus.getBoundaries(); // size = 4
         </source>
+
+        <h5>Linecast</h5>
+        <source>
+DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
+
+RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 2, 3), precision).toTree();
+
+List&lt;LinecastPoint3D&gt; pts = tree.linecast(
+        Line3D.fromPoints(Vector3D.of(0.5, 0.5, -10), Vector3D.of(0.5, 0.5, 10), precision));
+
+int intersectionCount = pts.size(); // intersectionCount = 2
+Vector3D intersection = pts.get(0).getPoint(); // (0.5, 0.5, 0.0)
+Vector3D normal = pts.get(0).getNormal(); // (0.0, 0.0, -1.0)
+        </source>
       </subsection>
 
     </section>


[commons-geometry] 03/08: GEOMETRY-68: fixing issue where duplicate (equivalent) points are returned in some linecast operations

Posted by er...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

erans pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git

commit 6808e0599b6b935fd2425bfe21478cc3d3783e1c
Author: Matt Juntunen <ma...@hotmail.com>
AuthorDate: Wed Jan 1 16:55:13 2020 -0500

    GEOMETRY-68: fixing issue where duplicate (equivalent) points are returned in some linecast operations
---
 .../geometry/euclidean/threed/Boundaries3D.java    |  26 ++-
 .../threed/BoundarySourceLinecastWrapper3D.java    |  10 +-
 .../commons/geometry/euclidean/threed/Line3D.java  |  27 ++-
 .../geometry/euclidean/threed/LinecastPoint3D.java |  74 +++++++-
 .../geometry/euclidean/threed/RegionBSPTree3D.java | 197 ++++++++-------------
 .../geometry/euclidean/twod/Boundaries2D.java      |  28 ++-
 .../twod/BoundarySourceLinecastWrapper2D.java      |  10 +-
 .../commons/geometry/euclidean/twod/Line.java      |   2 +-
 .../geometry/euclidean/twod/LinecastPoint2D.java   |  74 +++++++-
 .../geometry/euclidean/twod/PolarCoordinates.java  |   2 +-
 .../geometry/euclidean/twod/RegionBSPTree2D.java   | 196 ++++++++------------
 .../euclidean/threed/Boundaries3DTest.java         |  65 +++++++
 .../BoundarySourceLinecastWrapper3DTest.java       |  32 ++++
 .../geometry/euclidean/threed/Line3DTest.java      |  35 ++++
 .../euclidean/threed/LinecastPoint3DTest.java      |  77 ++++++++
 .../euclidean/threed/RegionBSPTree3DTest.java      |  17 ++
 .../geometry/euclidean/twod/Boundaries2DTest.java  |  60 +++++++
 .../twod/BoundarySourceLinecastWrapper2DTest.java  |  30 ++++
 .../euclidean/twod/LinecastPoint2DTest.java        |  77 ++++++++
 .../euclidean/twod/RegionBSPTree2DTest.java        |  19 +-
 20 files changed, 796 insertions(+), 262 deletions(-)

diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Boundaries3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Boundaries3D.java
index 6e5a2ae..5312e03 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Boundaries3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Boundaries3D.java
@@ -17,12 +17,11 @@
 package org.apache.commons.geometry.euclidean.threed;
 
 import java.util.Arrays;
-import java.util.List;
+import java.util.Collection;
 
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 
-/** Utility class for constructing {@link BoundarySource3D} objects to produce common
- * shapes.
+/** Utility class for constructing {@link BoundarySource3D} instances.
  */
 public final class Boundaries3D {
 
@@ -30,6 +29,23 @@ public final class Boundaries3D {
     private Boundaries3D() {
     }
 
+    /** Return a {@link BoundarySource3D} instance containing the given convex subplanes.
+     * @param boundaries convex subplanes to include in the boundary source
+     * @return a boundary source containing the given boundaries
+     */
+    public static BoundarySource3D from(final ConvexSubPlane... boundaries) {
+        return from(Arrays.asList(boundaries));
+    }
+
+    /** Return a {@link BoundarySource3D} instance containing the given convex subplanes. The given
+     * collection is used directly as the source of the subplanes; no copy is made.
+     * @param boundaries convex subplanes to include in the boundary source
+     * @return a boundary source containing the given boundaries
+     */
+    public static BoundarySource3D from(final Collection<ConvexSubPlane> boundaries) {
+        return () -> boundaries.stream();
+    }
+
     /** Return a {@link BoundarySource3D} instance defining an axis-aligned rectangular prism. The points {@code a}
      * and {@code b} are taken to represent opposite corner points in the prism and may be specified in
      * any order.
@@ -67,7 +83,7 @@ public final class Boundaries3D {
             Vector3D.of(minX, maxY, maxZ)
         };
 
-        List<ConvexSubPlane> facets = Arrays.asList(
+        return from(
             // -z and +z sides
             ConvexSubPlane.fromVertexLoop(Arrays.asList(vertices[0], vertices[3], vertices[2], vertices[1]), precision),
             ConvexSubPlane.fromVertexLoop(Arrays.asList(vertices[4], vertices[5], vertices[6], vertices[7]), precision),
@@ -80,7 +96,5 @@ public final class Boundaries3D {
             ConvexSubPlane.fromVertexLoop(Arrays.asList(vertices[0], vertices[1], vertices[5], vertices[4]), precision),
             ConvexSubPlane.fromVertexLoop(Arrays.asList(vertices[3], vertices[7], vertices[6], vertices[2]), precision)
         );
-
-        return () -> facets.stream();
     }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3D.java
index 30c33bb..05b0b13 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3D.java
@@ -16,6 +16,7 @@
  */
 package org.apache.commons.geometry.euclidean.threed;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -40,9 +41,12 @@ final class BoundarySourceLinecastWrapper3D implements Linecastable3D {
     /** {@inheritDoc} */
     @Override
     public List<LinecastPoint3D> linecast(final Segment3D segment) {
-        return getIntersectionStream(segment)
-                .sorted(LinecastPoint3D.ABSCISSA_ORDER)
-                .collect(Collectors.toList());
+        final List<LinecastPoint3D> results =  getIntersectionStream(segment)
+                .collect(Collectors.toCollection(ArrayList::new));
+
+        LinecastPoint3D.sortAndFilter(results);
+
+        return results;
     }
 
     /** {@inheritDoc} */
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line3D.java
index 36cf00a..8ee21cc 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line3D.java
@@ -20,6 +20,7 @@ import java.util.Objects;
 
 import org.apache.commons.geometry.core.Embedding;
 import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.internal.Equivalency;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.oned.AffineTransformMatrix1D;
 import org.apache.commons.geometry.euclidean.oned.Interval;
@@ -29,7 +30,7 @@ import org.apache.commons.geometry.euclidean.oned.Vector1D;
  *
  * <p>Instances of this class are guaranteed to be immutable.</p>
  */
-public final class Line3D implements Embedding<Vector3D, Vector1D> {
+public final class Line3D implements Embedding<Vector3D, Vector1D>, Equivalency<Line3D> {
     /** Line point closest to the origin. */
     private final Vector3D origin;
 
@@ -319,6 +320,30 @@ public final class Line3D implements Embedding<Vector3D, Vector1D> {
         return new SubLine3D(this);
     }
 
+    /**{@inheritDoc}
+     *
+     * <p>Instances are considered equivalent if they</p>
+     * <ul>
+     *   <li>contain equal {@link DoublePrecisionContext precision contexts},</li>
+     *   <li>have equivalent origin locations (as evaluated by the precision context), and</li>
+     *   <li>point in the same direction (as evaluated by the precision context)</li>
+     * </ul>
+     * @param other the point to compare with
+     * @return true if this instance should be considered equivalent to the argument
+     */
+    @Override
+    public boolean eq(final Line3D other) {
+        if (this == other) {
+            return true;
+        }
+
+        final DoublePrecisionContext testPrecision = getPrecision();
+
+        return testPrecision.equals(other.getPrecision()) &&
+                getOrigin().eq(other.getOrigin(), precision) &&
+                getDirection().eq(other.getDirection(), precision);
+    }
+
     /** {@inheritDoc} */
     @Override
     public int hashCode() {
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3D.java
index 0a2431d..c9ccda0 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3D.java
@@ -16,8 +16,14 @@
  */
 package org.apache.commons.geometry.euclidean.threed;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
 
+import org.apache.commons.geometry.core.internal.Equivalency;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.AbstractLinecastPoint;
 
 /** Class representing intersections resulting from linecast operations in Euclidean
@@ -25,7 +31,8 @@ import org.apache.commons.geometry.euclidean.AbstractLinecastPoint;
  * of the target at the point of intersection.
  * @see Linecastable3D
  */
-public class LinecastPoint3D extends AbstractLinecastPoint<Vector3D, Vector3D.Unit, Line3D> {
+public class LinecastPoint3D extends AbstractLinecastPoint<Vector3D, Vector3D.Unit, Line3D>
+    implements Equivalency<LinecastPoint3D> {
 
     /** Comparator that sorts intersection instances by increasing abscissa order. If two abscissa
      * values are equal, the comparison uses {@link Vector3D#COORDINATE_ASCENDING_ORDER} with the
@@ -47,4 +54,69 @@ public class LinecastPoint3D extends AbstractLinecastPoint<Vector3D, Vector3D.Un
     public LinecastPoint3D(final Vector3D point, final Vector3D normal, final Line3D line) {
         super(point, normal.normalize(), line);
     }
+
+    /** {@inheritDoc}
+     *
+     * <p>
+     * Instances are considered equivalent if they have equivalent points, normals, and lines.
+     * </p>
+     */
+    @Override
+    public boolean eq(final LinecastPoint3D other) {
+        if (this == other) {
+            return true;
+        }
+
+        final DoublePrecisionContext precision = getLine().getPrecision();
+
+        return getLine().eq(other.getLine()) &&
+                getPoint().eq(other.getPoint(), precision) &&
+                getNormal().eq(other.getNormal(), precision);
+    }
+
+    /** Sort the given list of linecast points by increasing abscissa value and filter to remove
+     * duplicate entries (as determined by the {@link #eq(LinecastPoint3D)} method). The argument
+     * is modified.
+     * @param pts list of points to sort and filter
+     */
+    public static void sortAndFilter(final List<LinecastPoint3D> pts) {
+        Collections.sort(pts, ABSCISSA_ORDER);
+
+        double currentAbscissa = Double.POSITIVE_INFINITY;
+        final List<LinecastPoint3D> abscissaList = new ArrayList<>();
+
+        final ListIterator<LinecastPoint3D> it = pts.listIterator();
+        LinecastPoint3D pt;
+        while (it.hasNext()) {
+            pt = it.next();
+            if (!pt.getLine().getPrecision().eq(currentAbscissa, pt.getAbscissa())) {
+                // new abscissa value
+                currentAbscissa = pt.getAbscissa();
+                abscissaList.clear();
+
+                abscissaList.add(pt);
+            } else if (containsEq(pt, abscissaList)) {
+                // duplicate found for this abscissa value
+                it.remove();
+            } else {
+                // not a duplicate
+                abscissaList.add(pt);
+            }
+        }
+    }
+
+    /** Return true if the given linecast point is equivalent to any of those in the given list.
+     * @param pt point to test
+     * @param list list to test against
+     * @return true if the given linecast point is equivalent to any of those in the given list
+     */
+    private static boolean containsEq(final LinecastPoint3D pt, final List<LinecastPoint3D> list) {
+        for (LinecastPoint3D listPt : list) {
+            if (listPt.eq(pt)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java
index 54a5b30..867dd27 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java
@@ -17,7 +17,6 @@
 package org.apache.commons.geometry.euclidean.threed;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
@@ -140,7 +139,7 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
     /** {@inheritDoc} */
     @Override
     public List<LinecastPoint3D> linecast(final Segment3D segment) {
-        final LinecastVisitor visitor = new LinecastVisitor(segment);
+        final LinecastVisitor visitor = new LinecastVisitor(segment, false);
         accept(visitor);
 
         return visitor.getResults();
@@ -149,10 +148,10 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
     /** {@inheritDoc} */
     @Override
     public LinecastPoint3D linecastFirst(final Segment3D segment) {
-        final LinecastFirstVisitor visitor = new LinecastFirstVisitor(segment);
+        final LinecastVisitor visitor = new LinecastVisitor(segment, true);
         accept(visitor);
 
-        return visitor.getResult();
+        return visitor.getFirstResult();
     }
 
     /** {@inheritDoc} */
@@ -362,137 +361,60 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
         }
     }
 
-    /** Base class for BSP tree visitors that perform linecast operations.
+    /** BSP tree visitor that performs a linecast operation against the boundaries of the visited tree.
      */
-    private abstract static class AbstractLinecastVisitor implements BSPTreeVisitor<Vector3D, RegionNode3D> {
+    private static final class LinecastVisitor implements BSPTreeVisitor<Vector3D, RegionNode3D> {
 
         /** The line segment to intersect with the boundaries of the BSP tree. */
         private final Segment3D linecastSegment;
 
-        /** Create a new instance with the given intersecting line segment.
-         * @param linecastSegment segment to intersect with the BSP tree region boundary
+        /** If true, the visitor will stop visiting the tree once the first linecast
+         * point is determined.
          */
-        AbstractLinecastVisitor(final Segment3D linecastSegment) {
-            this.linecastSegment = linecastSegment;
-        }
+        private final boolean firstOnly;
 
-        /** Get the intersecting segment for the linecast operation.
-         * @return the intersecting segment for the linecast operation
-         */
-        protected Segment3D getLinecastSegment() {
-            return linecastSegment;
-        }
+        /** The minimum abscissa found during the search. */
+        private double minAbscissa = Double.POSITIVE_INFINITY;
 
-        /** Compute the linecast point for the given intersection point and tree node, returning null
-         * if the point does not actually lie on the region boundary.
-         * @param pt intersection point
-         * @param node node containing the cut subhyperplane that the linecast line
-         *      intersected with
-         * @return a new linecast point instance or null if the intersection point does not lie
-         *      on the region boundary
-         */
-        protected LinecastPoint3D computeLinecastPoint(final Vector3D pt, final RegionNode3D node) {
-            final Plane cut = (Plane) node.getCutHyperplane();
-            final RegionCutBoundary<Vector3D> boundary = node.getCutBoundary();
-
-            boolean onBoundary = false;
-            boolean negateNormal = false;
-
-            if (boundary.getInsideFacing() != null && boundary.getInsideFacing().contains(pt)) {
-                // on inside-facing boundary
-                onBoundary = true;
-                negateNormal = true;
-            } else  if (boundary.getOutsideFacing() != null && boundary.getOutsideFacing().contains(pt)) {
-                // on outside-facing boundary
-                onBoundary = true;
-            }
-
-            if (onBoundary) {
-                Vector3D normal = cut.getNormal();
-                if (negateNormal) {
-                    normal = normal.negate();
-                }
-
-                return new LinecastPoint3D(pt, normal, getLinecastSegment().getLine());
-            }
-
-            return null;
-        }
-    }
-
-    /** BSP tree visitor that performs a linecast operation against the boundaries of the visited tree, returning
-     * all computed boundary intersections, in order of their abscissa position along the intersecting line.
-     */
-    private static final class LinecastVisitor extends AbstractLinecastVisitor {
-
-        /** Results of the linecast operation. */
+        /** List of results from the linecast operation. */
         private final List<LinecastPoint3D> results = new ArrayList<>();
 
         /** Create a new instance with the given intersecting line segment.
          * @param linecastSegment segment to intersect with the BSP tree region boundary
+         * @param firstOnly if true, the visitor will stop visiting the tree once the first
+         *      linecast point is determined
          */
-        LinecastVisitor(final Segment3D linecastSegment) {
-            super(linecastSegment);
+        LinecastVisitor(final Segment3D linecastSegment, final boolean firstOnly) {
+            this.linecastSegment = linecastSegment;
+            this.firstOnly = firstOnly;
         }
 
-        /** Get the ordered results of the linecast operation.
-         * @return the ordered results of the linecast operation
+        /** Get the first {@link LinecastPoint2D} resulting from the linecast operation.
+         * @return the first linecast result point
          */
-        public List<LinecastPoint3D> getResults() {
-            // sort the results before returning
-            Collections.sort(results, LinecastPoint3D.ABSCISSA_ORDER);
+        public LinecastPoint3D getFirstResult() {
+            final List<LinecastPoint3D> sortedResults = getResults();
 
-            return results;
+            return sortedResults.isEmpty() ?
+                    null :
+                    sortedResults.get(0);
         }
 
-        /** {@inheritDoc} */
-        @Override
-        public Result visit(final RegionNode3D node) {
-            if (node.isInternal()) {
-                // check if the line segment intersects the cut subhyperplane
-                final Segment3D segment = getLinecastSegment();
-                final Line3D line = segment.getLine();
-                final Vector3D pt = ((Plane) node.getCutHyperplane()).intersection(line);
-
-                if (pt != null && segment.contains(pt)) {
-                    final LinecastPoint3D resultPoint = computeLinecastPoint(pt, node);
-                    if (resultPoint != null) {
-                        results.add(resultPoint);
-                    }
-                }
-            }
-
-            return Result.CONTINUE;
-        }
-    }
-
-    /** BSP tree visitor that performs a linecast operation against the boundaries of the visited tree, returning
-     * only the intersection closest to the start of the line segment.
-     */
-    private static final class LinecastFirstVisitor extends AbstractLinecastVisitor {
-
-        /** The result of the linecast operation. */
-        private LinecastPoint3D result;
-
-        /** Create a new instance with the given intersecting line segment.
-         * @param linecastSegment segment to intersect with the BSP tree region boundary
+        /** Get a list containing the results of the linecast operation. The list is
+         * sorted and filtered.
+         * @return list of sorted and filtered results from the linecast operation
          */
-        LinecastFirstVisitor(final Segment3D linecastSegment) {
-            super(linecastSegment);
-        }
+        public List<LinecastPoint3D> getResults() {
+            LinecastPoint3D.sortAndFilter(results);
 
-        /** Get the {@link LinecastPoint2D} resulting from the linecast operation.
-         * @return the linecast result point
-         */
-        public LinecastPoint3D getResult() {
-            return result;
+            return results;
         }
 
         /** {@inheritDoc} */
         @Override
         public Order visitOrder(final RegionNode3D internalNode) {
             final Plane cut = (Plane) internalNode.getCutHyperplane();
-            final Line3D line = getLinecastSegment().getLine();
+            final Line3D line = linecastSegment.getLine();
 
             final boolean plusIsNear = line.getDirection().dot(cut.getNormal()) < 0;
 
@@ -506,23 +428,24 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
         public Result visit(final RegionNode3D node) {
             if (node.isInternal()) {
                 // check if the line segment intersects the cut subhyperplane
-                final Segment3D segment = getLinecastSegment();
-                final Line3D line = segment.getLine();
+                final Line3D line = linecastSegment.getLine();
                 final Vector3D pt = ((Plane) node.getCutHyperplane()).intersection(line);
 
                 if (pt != null) {
-                    if (result != null && line.getPrecision()
-                        .compare(result.getAbscissa(), line.abscissa(pt)) < 0) {
-                        // we have a result and we are now sure that no other intersection points will be
+                    if (firstOnly && !results.isEmpty() &&
+                            line.getPrecision().compare(minAbscissa, line.abscissa(pt)) < 0) {
+                        // we have results and we are now sure that no other intersection points will be
                         // found that are closer or at the same position on the intersecting line.
                         return Result.TERMINATE;
-                    } else if (segment.contains(pt)) {
-                        // we've potentially found a new linecast point; it just needs to lie on
-                        // the boundary and be closer than any current result
+                    } else if (linecastSegment.contains(pt)) {
+                        // we've potentially found a new linecast point; add it to the list of potential
+                        // results
                         LinecastPoint3D potentialResult = computeLinecastPoint(pt, node);
-                        if (potentialResult != null && (result == null ||
-                                LinecastPoint3D.ABSCISSA_ORDER.compare(potentialResult, result) < 0)) {
-                            result = potentialResult;
+                        if (potentialResult != null) {
+                            results.add(potentialResult);
+
+                            // update the min abscissa
+                            minAbscissa = Math.min(minAbscissa, potentialResult.getAbscissa());
                         }
                     }
                 }
@@ -530,5 +453,41 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
 
             return Result.CONTINUE;
         }
+
+        /** Compute the linecast point for the given intersection point and tree node, returning null
+         * if the point does not actually lie on the region boundary.
+         * @param pt intersection point
+         * @param node node containing the cut subhyperplane that the linecast line
+         *      intersected with
+         * @return a new linecast point instance or null if the intersection point does not lie
+         *      on the region boundary
+         */
+        private LinecastPoint3D computeLinecastPoint(final Vector3D pt, final RegionNode3D node) {
+            final Plane cut = (Plane) node.getCutHyperplane();
+            final RegionCutBoundary<Vector3D> boundary = node.getCutBoundary();
+
+            boolean onBoundary = false;
+            boolean negateNormal = false;
+
+            if (boundary.getInsideFacing() != null && boundary.getInsideFacing().contains(pt)) {
+                // on inside-facing boundary
+                onBoundary = true;
+                negateNormal = true;
+            } else  if (boundary.getOutsideFacing() != null && boundary.getOutsideFacing().contains(pt)) {
+                // on outside-facing boundary
+                onBoundary = true;
+            }
+
+            if (onBoundary) {
+                Vector3D normal = cut.getNormal();
+                if (negateNormal) {
+                    normal = normal.negate();
+                }
+
+                return new LinecastPoint3D(pt, normal, linecastSegment.getLine());
+            }
+
+            return null;
+        }
     }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Boundaries2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Boundaries2D.java
index 37de050..ca8d6fc 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Boundaries2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Boundaries2D.java
@@ -17,12 +17,11 @@
 package org.apache.commons.geometry.euclidean.twod;
 
 import java.util.Arrays;
-import java.util.List;
+import java.util.Collection;
 
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 
-/** Utility class for creating {@link BoundarySource2D} instances for generating common
- * shapes.
+/** Utility class for constructing {@link BoundarySource2D} instances.
  */
 public final class Boundaries2D {
 
@@ -30,7 +29,24 @@ public final class Boundaries2D {
     private Boundaries2D() {
     }
 
-    /** Create a {@link BoundarySource2D} defining an axis-aligned rectangular region. The points {@code a}
+    /** Return a {@link BoundarySource2D} instance containing the given segments.
+     * @param boundaries segments to include in the boundary source
+     * @return a boundary source containing the given boundaries
+     */
+    public static BoundarySource2D from(final Segment... boundaries) {
+        return from(Arrays.asList(boundaries));
+    }
+
+    /** Return a {@link BoundarySource2D} instance containing the given segments. The given
+     * collection is used directly as the source of the segments; no copy is made.
+     * @param boundaries segments to include in the boundary source
+     * @return a boundary source containing the given boundaries
+     */
+    public static BoundarySource2D from(final Collection<Segment> boundaries) {
+        return () -> boundaries.stream();
+    }
+
+    /** Return a {@link BoundarySource2D} instance defining an axis-aligned rectangular region. The points {@code a}
      * and {@code b} are taken to represent opposite corner points in the rectangle and may be specified in
      * any order.
      * @param a first corner point in the rectangle (opposite of {@code b})
@@ -59,13 +75,11 @@ public final class Boundaries2D {
         final Vector2D upperRight = Vector2D.of(maxX, maxY);
         final Vector2D lowerRight = Vector2D.of(maxX, minY);
 
-        List<Segment> segments = Arrays.asList(
+        return from(
                 Segment.fromPoints(lowerLeft, lowerRight, precision),
                 Segment.fromPoints(upperRight, upperLeft, precision),
                 Segment.fromPoints(lowerRight, upperRight, precision),
                 Segment.fromPoints(upperLeft, lowerLeft, precision)
             );
-
-        return () -> segments.stream();
     }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2D.java
index 836aee5..0c0b57b 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2D.java
@@ -16,6 +16,7 @@
  */
 package org.apache.commons.geometry.euclidean.twod;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -40,9 +41,12 @@ final class BoundarySourceLinecastWrapper2D implements Linecastable2D {
     /** {@inheritDoc} */
     @Override
     public List<LinecastPoint2D> linecast(final Segment segment) {
-        return getIntersectionStream(segment)
-                .sorted(LinecastPoint2D.ABSCISSA_ORDER)
-                .collect(Collectors.toList());
+        final List<LinecastPoint2D> results = getIntersectionStream(segment)
+                .collect(Collectors.toCollection(ArrayList::new));
+
+        LinecastPoint2D.sortAndFilter(results);
+
+        return results;
     }
 
     /** {@inheritDoc} */
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java
index 517968b..89173ae 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java
@@ -439,7 +439,7 @@ public final class Line extends AbstractHyperplane<Vector2D>
     * <p>Instances are considered equivalent if they
     * <ul>
     *   <li>contain equal {@link DoublePrecisionContext precision contexts},</li>
-    *   <li>have equivalent locations (as evaluated by the precision context), and</li>
+    *   <li>have equivalent origin locations (as evaluated by the precision context), and</li>
     *   <li>point in the same direction (as evaluated by the precision context)</li>
     * </ul>
     * @param other the point to compare with
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2D.java
index 8bfc55a..12c1811 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2D.java
@@ -16,8 +16,14 @@
  */
 package org.apache.commons.geometry.euclidean.twod;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Comparator;
+import java.util.List;
+import java.util.ListIterator;
 
+import org.apache.commons.geometry.core.internal.Equivalency;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.AbstractLinecastPoint;
 
 /** Class representing intersections resulting from linecast operations in Euclidean
@@ -25,7 +31,8 @@ import org.apache.commons.geometry.euclidean.AbstractLinecastPoint;
  * of the target at the point of intersection.
  * @see Linecastable2D
  */
-public class LinecastPoint2D extends AbstractLinecastPoint<Vector2D, Vector2D.Unit, Line> {
+public class LinecastPoint2D extends AbstractLinecastPoint<Vector2D, Vector2D.Unit, Line>
+    implements Equivalency<LinecastPoint2D> {
 
     /** Comparator that sorts intersection instances by increasing abscissa order. If two abscissa
      * values are equal, the comparison uses {@link Vector2D#COORDINATE_ASCENDING_ORDER} with the
@@ -47,4 +54,69 @@ public class LinecastPoint2D extends AbstractLinecastPoint<Vector2D, Vector2D.Un
     public LinecastPoint2D(final Vector2D point, final Vector2D normal, final Line line) {
         super(point, normal.normalize(), line);
     }
+
+    /** {@inheritDoc}
+     *
+     * <p>
+     * Instances are considered equivalent if they have equivalent points, normals, and lines.
+     * </p>
+     */
+    @Override
+    public boolean eq(final LinecastPoint2D other) {
+        if (this == other) {
+            return true;
+        }
+
+        final DoublePrecisionContext precision = getLine().getPrecision();
+
+        return getLine().eq(other.getLine()) &&
+                getPoint().eq(other.getPoint(), precision) &&
+                getNormal().eq(other.getNormal(), precision);
+    }
+
+    /** Sort the given list of linecast points by increasing abscissa value and filter
+     * to remove duplicate entries (as determined by the {@link #eq(LinecastPoint2D)} method).
+     * The argument is modified.
+     * @param pts list of points to sort and filter
+     */
+    public static void sortAndFilter(final List<LinecastPoint2D> pts) {
+        Collections.sort(pts, ABSCISSA_ORDER);
+
+        double currentAbscissa = Double.POSITIVE_INFINITY;
+        final List<LinecastPoint2D> abscissaList = new ArrayList<>();
+
+        final ListIterator<LinecastPoint2D> it = pts.listIterator();
+        LinecastPoint2D pt;
+        while (it.hasNext()) {
+            pt = it.next();
+            if (!pt.getLine().getPrecision().eq(currentAbscissa, pt.getAbscissa())) {
+                // new abscissa value
+                currentAbscissa = pt.getAbscissa();
+                abscissaList.clear();
+
+                abscissaList.add(pt);
+            } else if (containsEq(pt, abscissaList)) {
+                // duplicate found for this abscissa value
+                it.remove();
+            } else {
+                // not a duplicate
+                abscissaList.add(pt);
+            }
+        }
+    }
+
+    /** Return true if the given linecast point is equivalent to any of those in the given list.
+     * @param pt point to test
+     * @param list list to test against
+     * @return true if the given linecast point is equivalent to any of those in the given list
+     */
+    private static boolean containsEq(final LinecastPoint2D pt, final List<LinecastPoint2D> list) {
+        for (LinecastPoint2D listPt : list) {
+            if (listPt.eq(pt)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinates.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinates.java
index 6857fcd..0bd77b3 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinates.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolarCoordinates.java
@@ -16,9 +16,9 @@
  */
 package org.apache.commons.geometry.euclidean.twod;
 
-import org.apache.commons.numbers.angle.PlaneAngleRadians;
 import org.apache.commons.geometry.core.Spatial;
 import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
+import org.apache.commons.numbers.angle.PlaneAngleRadians;
 
 /** Class representing <a href="https://en.wikipedia.org/wiki/Polar_coordinate_system">polar coordinates</a>
  * in 2 dimensional Euclidean space.
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java
index cf171a0..c659164 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java
@@ -160,7 +160,7 @@ public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, Regio
     /** {@inheritDoc} */
     @Override
     public List<LinecastPoint2D> linecast(final Segment segment) {
-        final LinecastVisitor visitor = new LinecastVisitor(segment);
+        final LinecastVisitor visitor = new LinecastVisitor(segment, false);
         accept(visitor);
 
         return visitor.getResults();
@@ -169,10 +169,10 @@ public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, Regio
     /** {@inheritDoc} */
     @Override
     public LinecastPoint2D linecastFirst(final Segment segment) {
-        final LinecastFirstVisitor visitor = new LinecastFirstVisitor(segment);
+        final LinecastVisitor visitor = new LinecastVisitor(segment, true);
         accept(visitor);
 
-        return visitor.getResult();
+        return visitor.getFirstResult();
     }
 
     /** Compute the line segment paths comprising the region boundary.
@@ -345,137 +345,60 @@ public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, Regio
         }
     }
 
-    /** Base class for BSP tree visitors that perform linecast operations.
+    /** BSP tree visitor that performs a linecast operation against the boundaries of the visited tree.
      */
-    private abstract static class AbstractLinecastVisitor implements BSPTreeVisitor<Vector2D, RegionNode2D> {
+    private static final class LinecastVisitor implements BSPTreeVisitor<Vector2D, RegionNode2D> {
 
         /** The line segment to intersect with the boundaries of the BSP tree. */
         private final Segment linecastSegment;
 
-        /** Create a new instance with the given intersecting line segment.
-         * @param linecastSegment segment to intersect with the BSP tree region boundary
+        /** If true, the visitor will stop visiting the tree once the first linecast
+         * point is determined.
          */
-        AbstractLinecastVisitor(final Segment linecastSegment) {
-            this.linecastSegment = linecastSegment;
-        }
+        private final boolean firstOnly;
 
-        /** Get the intersecting segment for the linecast operation.
-         * @return the intersecting segment for the linecast operation
-         */
-        protected Segment getLinecastSegment() {
-            return linecastSegment;
-        }
+        /** The minimum abscissa found during the search. */
+        private double minAbscissa = Double.POSITIVE_INFINITY;
 
-        /** Compute the linecast point for the given intersection point and tree node, returning null
-         * if the point does not actually lie on the region boundary.
-         * @param pt intersection point
-         * @param node node containing the cut subhyperplane that the linecast line
-         *      intersected with
-         * @return a new linecast point instance or null if the intersection point does not lie
-         *      on the region boundary
-         */
-        protected LinecastPoint2D computeLinecastPoint(final Vector2D pt, final RegionNode2D node) {
-            final Line cut = (Line) node.getCutHyperplane();
-            final RegionCutBoundary<Vector2D> boundary = node.getCutBoundary();
-
-            boolean onBoundary = false;
-            boolean negateNormal = false;
-
-            if (boundary.getInsideFacing() != null && boundary.getInsideFacing().contains(pt)) {
-                // on inside-facing boundary
-                onBoundary = true;
-                negateNormal = true;
-            } else  if (boundary.getOutsideFacing() != null && boundary.getOutsideFacing().contains(pt)) {
-                // on outside-facing boundary
-                onBoundary = true;
-            }
-
-            if (onBoundary) {
-                Vector2D normal = cut.getOffsetDirection();
-                if (negateNormal) {
-                    normal = normal.negate();
-                }
-
-                return new LinecastPoint2D(pt, normal, getLinecastSegment().getLine());
-            }
-
-            return null;
-        }
-    }
-
-    /** BSP tree visitor that performs a linecast operation against the boundaries of the visited tree, returning
-     * all computed boundary intersections, in order of their abscissa position along the intersecting line.
-     */
-    private static final class LinecastVisitor extends AbstractLinecastVisitor {
-
-        /** Results of the linecast operation. */
+        /** List of results from the linecast operation. */
         private final List<LinecastPoint2D> results = new ArrayList<>();
 
         /** Create a new instance with the given intersecting line segment.
          * @param linecastSegment segment to intersect with the BSP tree region boundary
+         * @param firstOnly if true, the visitor will stop visiting the tree once the first
+         *      linecast point is determined
          */
-        LinecastVisitor(final Segment linecastSegment) {
-            super(linecastSegment);
+        LinecastVisitor(final Segment linecastSegment, final boolean firstOnly) {
+            this.linecastSegment = linecastSegment;
+            this.firstOnly = firstOnly;
         }
 
-        /** Get the ordered results of the linecast operation.
-         * @return the ordered results of the linecast operation
+        /** Get the first {@link LinecastPoint2D} resulting from the linecast operation.
+         * @return the first linecast result point
          */
-        public List<LinecastPoint2D> getResults() {
-            // sort the results before returning
-            Collections.sort(results, LinecastPoint2D.ABSCISSA_ORDER);
+        public LinecastPoint2D getFirstResult() {
+            final List<LinecastPoint2D> sortedResults = getResults();
 
-            return results;
+            return sortedResults.isEmpty() ?
+                    null :
+                    sortedResults.get(0);
         }
 
-        /** {@inheritDoc} */
-        @Override
-        public Result visit(final RegionNode2D node) {
-            if (node.isInternal()) {
-                // check if the line segment intersects the cut subhyperplane
-                final Segment segment = getLinecastSegment();
-                final Line line = segment.getLine();
-                final Vector2D pt = ((Line) node.getCutHyperplane()).intersection(line);
-
-                if (pt != null && segment.contains(pt)) {
-                    final LinecastPoint2D resultPoint = computeLinecastPoint(pt, node);
-                    if (resultPoint != null) {
-                        results.add(resultPoint);
-                    }
-                }
-            }
-
-            return Result.CONTINUE;
-        }
-    }
-
-    /** BSP tree visitor that performs a linecast operation against the boundaries of the visited tree, returning
-     * only the intersection closest to the start of the line segment.
-     */
-    private static final class LinecastFirstVisitor extends AbstractLinecastVisitor {
-
-        /** The result of the linecast operation. */
-        private LinecastPoint2D result;
-
-        /** Create a new instance with the given intersecting line segment.
-         * @param linecastSegment segment to intersect with the BSP tree region boundary
+        /** Get a list containing the results of the linecast operation. The list is
+         * sorted and filtered.
+         * @return list of sorted and filtered results from the linecast operation
          */
-        LinecastFirstVisitor(final Segment linecastSegment) {
-            super(linecastSegment);
-        }
+        public List<LinecastPoint2D> getResults() {
+            LinecastPoint2D.sortAndFilter(results);
 
-        /** Get the {@link LinecastPoint2D} resulting from the linecast operation.
-         * @return the linecast result point
-         */
-        public LinecastPoint2D getResult() {
-            return result;
+            return results;
         }
 
         /** {@inheritDoc} */
         @Override
         public Order visitOrder(final RegionNode2D internalNode) {
             final Line cut = (Line) internalNode.getCutHyperplane();
-            final Line line = getLinecastSegment().getLine();
+            final Line line = linecastSegment.getLine();
 
             final boolean plusIsNear = line.getDirection().dot(cut.getOffsetDirection()) < 0;
 
@@ -489,23 +412,24 @@ public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, Regio
         public Result visit(final RegionNode2D node) {
             if (node.isInternal()) {
                 // check if the line segment intersects the cut subhyperplane
-                final Segment segment = getLinecastSegment();
-                final Line line = segment.getLine();
+                final Line line = linecastSegment.getLine();
                 final Vector2D pt = ((Line) node.getCutHyperplane()).intersection(line);
 
                 if (pt != null) {
-                    if (result != null && line.getPrecision()
-                        .compare(result.getAbscissa(), line.abscissa(pt)) < 0) {
-                        // we have a result and we are now sure that no other intersection points will be
+                    if (firstOnly && !results.isEmpty() &&
+                            line.getPrecision().compare(minAbscissa, line.abscissa(pt)) < 0) {
+                        // we have results and we are now sure that no other intersection points will be
                         // found that are closer or at the same position on the intersecting line.
                         return Result.TERMINATE;
-                    } else if (segment.contains(pt)) {
-                        // we've potentially found a new linecast point; it just needs to lie on
-                        // the boundary and be closer than any current result
+                    } else if (linecastSegment.contains(pt)) {
+                        // we've potentially found a new linecast point; add it to the list of potential
+                        // results
                         LinecastPoint2D potentialResult = computeLinecastPoint(pt, node);
-                        if (potentialResult != null && (result == null ||
-                                LinecastPoint2D.ABSCISSA_ORDER.compare(potentialResult, result) < 0)) {
-                            result = potentialResult;
+                        if (potentialResult != null) {
+                            results.add(potentialResult);
+
+                            // update the min abscissa
+                            minAbscissa = Math.min(minAbscissa, potentialResult.getAbscissa());
                         }
                     }
                 }
@@ -513,5 +437,41 @@ public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, Regio
 
             return Result.CONTINUE;
         }
+
+        /** Compute the linecast point for the given intersection point and tree node, returning null
+         * if the point does not actually lie on the region boundary.
+         * @param pt intersection point
+         * @param node node containing the cut subhyperplane that the linecast line
+         *      intersected with
+         * @return a new linecast point instance or null if the intersection point does not lie
+         *      on the region boundary
+         */
+        private LinecastPoint2D computeLinecastPoint(final Vector2D pt, final RegionNode2D node) {
+            final Line cut = (Line) node.getCutHyperplane();
+            final RegionCutBoundary<Vector2D> boundary = node.getCutBoundary();
+
+            boolean onBoundary = false;
+            boolean negateNormal = false;
+
+            if (boundary.getInsideFacing() != null && boundary.getInsideFacing().contains(pt)) {
+                // on inside-facing boundary
+                onBoundary = true;
+                negateNormal = true;
+            } else  if (boundary.getOutsideFacing() != null && boundary.getOutsideFacing().contains(pt)) {
+                // on outside-facing boundary
+                onBoundary = true;
+            }
+
+            if (onBoundary) {
+                Vector2D normal = cut.getOffsetDirection();
+                if (negateNormal) {
+                    normal = normal.negate();
+                }
+
+                return new LinecastPoint2D(pt, normal, linecastSegment.getLine());
+            }
+
+            return null;
+        }
     }
 }
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Boundaries3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Boundaries3DTest.java
index 469913a..d44c728 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Boundaries3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Boundaries3DTest.java
@@ -16,6 +16,8 @@
  */
 package org.apache.commons.geometry.euclidean.threed;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -34,6 +36,69 @@ public class Boundaries3DTest {
             new EpsilonDoublePrecisionContext(TEST_EPS);
 
     @Test
+    public void testFrom_varargs_empty() {
+        // act
+        BoundarySource3D src = Boundaries3D.from();
+
+        // assert
+        List<ConvexSubPlane> segments = src.boundaryStream().collect(Collectors.toList());
+        Assert.assertEquals(0, segments.size());
+    }
+
+    @Test
+    public void testFrom_varargs() {
+        // act
+        ConvexSubPlane a = ConvexSubPlane.fromVertexLoop(
+                Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION);
+        ConvexSubPlane b = ConvexSubPlane.fromVertexLoop(
+                Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_Z), TEST_PRECISION);
+
+        BoundarySource3D src = Boundaries3D.from(a, b);
+
+        // assert
+        List<ConvexSubPlane> boundaries = src.boundaryStream().collect(Collectors.toList());
+        Assert.assertEquals(2, boundaries.size());
+
+        Assert.assertSame(a, boundaries.get(0));
+        Assert.assertSame(b, boundaries.get(1));
+    }
+
+    @Test
+    public void testFrom_list_empty() {
+        // arrange
+        List<ConvexSubPlane> input = new ArrayList<>();
+
+        // act
+        BoundarySource3D src = Boundaries3D.from(input);
+
+        // assert
+        List<ConvexSubPlane> segments = src.boundaryStream().collect(Collectors.toList());
+        Assert.assertEquals(0, segments.size());
+    }
+
+    @Test
+    public void testFrom_list() {
+        // act
+        ConvexSubPlane a = ConvexSubPlane.fromVertexLoop(
+                Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION);
+        ConvexSubPlane b = ConvexSubPlane.fromVertexLoop(
+                Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_Z), TEST_PRECISION);
+
+        List<ConvexSubPlane> input = new ArrayList<>();
+        input.add(a);
+        input.add(b);
+
+        BoundarySource3D src = Boundaries3D.from(input);
+
+        // assert
+        List<ConvexSubPlane> segments = src.boundaryStream().collect(Collectors.toList());
+        Assert.assertEquals(2, segments.size());
+
+        Assert.assertSame(a, segments.get(0));
+        Assert.assertSame(b, segments.get(1));
+    }
+
+    @Test
     public void testRect_minFirst() {
         // act
         List<ConvexSubPlane> boundaries = Boundaries3D.rect(Vector3D.of(1, 2, 3), Vector3D.of(4, 5, 6), TEST_PRECISION)
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3DTest.java
index 2301645..021fe46 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3DTest.java
@@ -16,6 +16,8 @@
  */
 package org.apache.commons.geometry.euclidean.threed;
 
+import java.util.Arrays;
+
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
 import org.junit.Test;
@@ -94,6 +96,21 @@ public class BoundarySourceLinecastWrapper3DTest {
     }
 
     @Test
+    public void testLinecast_line_removesDuplicatePoints() {
+        // arrange
+        BoundarySource3D src = Boundaries3D.from(
+                    ConvexSubPlane.fromVertexLoop(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION),
+                    ConvexSubPlane.fromVertexLoop(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X), TEST_PRECISION)
+                );
+        BoundarySourceLinecastWrapper3D wrapper = new BoundarySourceLinecastWrapper3D(src);
+
+        // act/assert
+        LinecastChecker3D.with(wrapper)
+            .returns(Vector3D.of(0, 0.5, 0), Vector3D.Unit.PLUS_Z)
+            .whenGiven(Line3D.fromPointAndDirection(Vector3D.of(-1, 0.5, 1), Vector3D.of(1, 0, -1), TEST_PRECISION));
+    }
+
+    @Test
     public void testLinecast_segment_simple() {
         // arrange
         BoundarySourceLinecastWrapper3D wrapper = new BoundarySourceLinecastWrapper3D(UNIT_CUBE);
@@ -210,4 +227,19 @@ public class BoundarySourceLinecastWrapper3DTest {
             .and(corner, Vector3D.Unit.PLUS_X)
             .whenGiven(Segment3D.fromPoints(Vector3D.of(0, 2, 2), corner, TEST_PRECISION));
     }
+
+    @Test
+    public void testLinecast_segment_removesDuplicatePoints() {
+        // arrange
+        BoundarySource3D src = Boundaries3D.from(
+                    ConvexSubPlane.fromVertexLoop(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION),
+                    ConvexSubPlane.fromVertexLoop(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X), TEST_PRECISION)
+                );
+        BoundarySourceLinecastWrapper3D wrapper = new BoundarySourceLinecastWrapper3D(src);
+
+        // act/assert
+        LinecastChecker3D.with(wrapper)
+            .returns(Vector3D.of(0, 0.5, 0), Vector3D.Unit.PLUS_Z)
+            .whenGiven(Segment3D.fromPoints(Vector3D.of(-1, 0.5, 1), Vector3D.of(1, 0.5, -1), TEST_PRECISION));
+    }
 }
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Line3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Line3DTest.java
index 4d0ada1..88c2d48 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Line3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Line3DTest.java
@@ -397,6 +397,41 @@ public class Line3DTest {
     }
 
     @Test
+    public void testEq() {
+        // arrange
+        DoublePrecisionContext precision1 = new EpsilonDoublePrecisionContext(1e-3);
+        DoublePrecisionContext precision2 = new EpsilonDoublePrecisionContext(1e-2);
+
+        Vector3D p = Vector3D.of(1, 2, 3);
+        Vector3D dir = Vector3D.of(1, 0, 0);
+
+        Line3D a = Line3D.fromPointAndDirection(p, dir, precision1);
+        Line3D b = Line3D.fromPointAndDirection(Vector3D.ZERO, dir, precision1);
+        Line3D c = Line3D.fromPointAndDirection(p, Vector3D.of(1, 1, 0), precision1);
+        Line3D d = Line3D.fromPointAndDirection(p, dir, precision2);
+
+        Line3D e = Line3D.fromPointAndDirection(p, dir, precision1);
+        Line3D f = Line3D.fromPointAndDirection(p.add(Vector3D.of(1e-4, 1e-4, 1e-4)), dir, precision1);
+        Line3D g = Line3D.fromPointAndDirection(p, Vector3D.of(1 + 1e-4, 1e-4, 1e-4), precision1);
+
+        // act/assert
+        Assert.assertTrue(a.eq(a));
+
+        Assert.assertTrue(a.eq(e));
+        Assert.assertTrue(e.eq(a));
+
+        Assert.assertTrue(a.eq(f));
+        Assert.assertTrue(f.eq(a));
+
+        Assert.assertTrue(a.eq(g));
+        Assert.assertTrue(g.eq(a));
+
+        Assert.assertFalse(a.eq(b));
+        Assert.assertFalse(a.eq(c));
+        Assert.assertFalse(a.eq(d));
+    }
+
+    @Test
     public void testHashCode() {
         // arrange
         Line3D a = Line3D.fromPointAndDirection(Vector3D.of(1, 2, 3), Vector3D.of(4, 5, 6), TEST_PRECISION);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3DTest.java
index fa4aea6..aa6784d 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3DTest.java
@@ -16,6 +16,10 @@
  */
 package org.apache.commons.geometry.euclidean.threed;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 import org.apache.commons.geometry.core.GeometryTestUtils;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
@@ -113,6 +117,33 @@ public class LinecastPoint3DTest {
     }
 
     @Test
+    public void testEq() {
+        // arrange
+        DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+
+        Line3D line = Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.Unit.PLUS_X, precision);
+        Line3D otherLine = Line3D.fromPointAndDirection(Vector3D.of(1e-4, 1e-4, 1e-4), Vector3D.Unit.PLUS_X, precision);
+
+        LinecastPoint3D a = new LinecastPoint3D(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X, line);
+
+        LinecastPoint3D b = new LinecastPoint3D(Vector3D.of(2, 2, 2), Vector3D.Unit.PLUS_X, line);
+        LinecastPoint3D c = new LinecastPoint3D(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Y, line);
+
+        LinecastPoint3D d = new LinecastPoint3D(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X, line);
+        LinecastPoint3D e = new LinecastPoint3D(
+                Vector3D.of(1 + 1e-3, 1 + 1e-3, 1 + 1e-3), Vector3D.Unit.from(1 + 1e-3, 1e-3, 1e-3), otherLine);
+
+        // act/assert
+        Assert.assertTrue(a.eq(a));
+
+        Assert.assertFalse(a.eq(b));
+        Assert.assertFalse(a.eq(c));
+
+        Assert.assertTrue(a.eq(d));
+        Assert.assertTrue(a.eq(e));
+    }
+
+    @Test
     public void testToString() {
         // arrange
         LinecastPoint3D it = new LinecastPoint3D(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X, X_AXIS);
@@ -123,4 +154,50 @@ public class LinecastPoint3DTest {
         // assert
         GeometryTestUtils.assertContains("LinecastPoint3D[point= (1.0, 1.0, 1.0), normal= (1.0, 0.0, 0.0)", str);
     }
+
+    @Test
+    public void testSortAndFilter_empty() {
+        // arrange
+        List<LinecastPoint3D> pts = new ArrayList<>();
+
+        // act
+        LinecastPoint3D.sortAndFilter(pts);
+
+        // assert
+        Assert.assertEquals(0, pts.size());
+    }
+
+    @Test
+    public void testSortAndFilter() {
+        // arrange
+        DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+
+        Line3D line = Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.Unit.PLUS_X, precision);
+        Line3D eqLine = Line3D.fromPointAndDirection(Vector3D.of(1e-3, 1e-3, 1e-3), Vector3D.Unit.PLUS_X, precision);
+        Line3D diffLine = Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, precision);
+
+        LinecastPoint3D a = new LinecastPoint3D(Vector3D.ZERO, Vector3D.Unit.MINUS_Y, line);
+        LinecastPoint3D aDup1 = new LinecastPoint3D(Vector3D.of(1e-3, 0, 0), Vector3D.Unit.MINUS_Y, line);
+        LinecastPoint3D aDup2 = new LinecastPoint3D(Vector3D.of(1e-3, 1e-3, 1e-3), Vector3D.of(1e-3, -1, 0), eqLine);
+
+        LinecastPoint3D b = new LinecastPoint3D(Vector3D.ZERO, Vector3D.Unit.MINUS_X, diffLine);
+        LinecastPoint3D bDup = new LinecastPoint3D(Vector3D.of(-1e-3, 1e-4, 1e-4), Vector3D.Unit.MINUS_X, diffLine);
+
+        LinecastPoint3D c = new LinecastPoint3D(Vector3D.of(0.5, 0, 0), Vector3D.Unit.MINUS_Y, line);
+
+        LinecastPoint3D d = new LinecastPoint3D(Vector3D.of(1, 0, 0), Vector3D.Unit.MINUS_Y, line);
+
+        List<LinecastPoint3D> list = new ArrayList<>(Arrays.asList(d, aDup1, bDup, b, c, a, aDup2));
+
+        // act
+        LinecastPoint3D.sortAndFilter(list);
+
+        // assert
+        Assert.assertEquals(4, list.size());
+
+        Assert.assertSame(b, list.get(0));
+        Assert.assertSame(a, list.get(1));
+        Assert.assertSame(c, list.get(2));
+        Assert.assertSame(d, list.get(3));
+    }
 }
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java
index 4baeed8..4b3e8ec 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java
@@ -393,6 +393,23 @@ public class RegionBSPTree3DTest {
     }
 
     @Test
+    public void testLinecast_removesDuplicatePoints() {
+        // arrange
+        RegionBSPTree3D tree = RegionBSPTree3D.empty();
+        tree.insert(Plane.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION).span());
+        tree.insert(Plane.fromNormal(Vector3D.Unit.PLUS_Y, TEST_PRECISION).span());
+
+        // act/assert
+        LinecastChecker3D.with(tree)
+            .returns(Vector3D.ZERO, Vector3D.Unit.PLUS_Y)
+            .whenGiven(Line3D.fromPoints(Vector3D.of(1, 1, 1), Vector3D.of(-1, -1, -1), TEST_PRECISION));
+
+        LinecastChecker3D.with(tree)
+        .returns(Vector3D.ZERO, Vector3D.Unit.PLUS_Y)
+            .whenGiven(Segment3D.fromPoints(Vector3D.of(1, 1, 1), Vector3D.of(-1, -1, -1), TEST_PRECISION));
+    }
+
+    @Test
     public void testLinecastFirst_multipleDirections() {
         // arrange
         RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.of(-1, -1, -1), Vector3D.of(1, 1, 1), TEST_PRECISION)
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Boundaries2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Boundaries2DTest.java
index 793497b..7c4bdff 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Boundaries2DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Boundaries2DTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.commons.geometry.euclidean.twod;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -34,6 +35,65 @@ public class Boundaries2DTest {
             new EpsilonDoublePrecisionContext(TEST_EPS);
 
     @Test
+    public void testFrom_varargs_empty() {
+        // act
+        BoundarySource2D src = Boundaries2D.from();
+
+        // assert
+        List<Segment> segments = src.boundaryStream().collect(Collectors.toList());
+        Assert.assertEquals(0, segments.size());
+    }
+
+    @Test
+    public void testFrom_varargs() {
+        // act
+        Segment a = Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+        Segment b = Segment.fromPoints(Vector2D.Unit.PLUS_X, Vector2D.of(1, 1), TEST_PRECISION);
+
+        BoundarySource2D src = Boundaries2D.from(a, b);
+
+        // assert
+        List<Segment> segments = src.boundaryStream().collect(Collectors.toList());
+        Assert.assertEquals(2, segments.size());
+
+        Assert.assertSame(a, segments.get(0));
+        Assert.assertSame(b, segments.get(1));
+    }
+
+    @Test
+    public void testFrom_list_empty() {
+        // arrange
+        List<Segment> input = new ArrayList<>();
+
+        // act
+        BoundarySource2D src = Boundaries2D.from(input);
+
+        // assert
+        List<Segment> segments = src.boundaryStream().collect(Collectors.toList());
+        Assert.assertEquals(0, segments.size());
+    }
+
+    @Test
+    public void testFrom_list() {
+        // act
+        Segment a = Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+        Segment b = Segment.fromPoints(Vector2D.Unit.PLUS_X, Vector2D.of(1, 1), TEST_PRECISION);
+
+        List<Segment> input = new ArrayList<>();
+        input.add(a);
+        input.add(b);
+
+        BoundarySource2D src = Boundaries2D.from(input);
+
+        // assert
+        List<Segment> segments = src.boundaryStream().collect(Collectors.toList());
+        Assert.assertEquals(2, segments.size());
+
+        Assert.assertSame(a, segments.get(0));
+        Assert.assertSame(b, segments.get(1));
+    }
+
+    @Test
     public void testRect_minFirst() {
         // act
         List<Segment> segments = Boundaries2D.rect(Vector2D.of(1, 2), Vector2D.of(3, 4), TEST_PRECISION)
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2DTest.java
index b3d31d3..b729dea 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2DTest.java
@@ -89,6 +89,21 @@ public class BoundarySourceLinecastWrapper2DTest {
     }
 
     @Test
+    public void testLinecast_line_removesDuplicatePoints() {
+        // arrange
+        BoundarySource2D src = Boundaries2D.from(
+                    Segment.fromPoints(Vector2D.of(-1, -1), Vector2D.ZERO, TEST_PRECISION),
+                    Segment.fromPoints(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
+                );
+        BoundarySourceLinecastWrapper2D wrapper = new BoundarySourceLinecastWrapper2D(src);
+
+        // act/assert
+        LinecastChecker2D.with(wrapper)
+            .returns(Vector2D.ZERO, Vector2D.Unit.from(1, -1))
+            .whenGiven(Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION));
+    }
+
+    @Test
     public void testLinecast_segment_simple() {
         // arrange
         BoundarySourceLinecastWrapper2D wrapper = new BoundarySourceLinecastWrapper2D(UNIT_SQUARE);
@@ -200,5 +215,20 @@ public class BoundarySourceLinecastWrapper2DTest {
             .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X)
             .whenGiven(Segment.fromPoints(Vector2D.of(0, 2), Vector2D.of(1, 1), TEST_PRECISION));
     }
+
+    @Test
+    public void testLinecast_segment_removesDuplicatePoints() {
+        // arrange
+        BoundarySource2D src = Boundaries2D.from(
+                    Segment.fromPoints(Vector2D.of(-1, -1), Vector2D.ZERO, TEST_PRECISION),
+                    Segment.fromPoints(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
+                );
+        BoundarySourceLinecastWrapper2D wrapper = new BoundarySourceLinecastWrapper2D(src);
+
+        // act/assert
+        LinecastChecker2D.with(wrapper)
+            .returns(Vector2D.ZERO, Vector2D.Unit.from(1, -1))
+            .whenGiven(Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION));
+    }
 }
 
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2DTest.java
index 29a0d6d..e000b4f 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2DTest.java
@@ -16,6 +16,10 @@
  */
 package org.apache.commons.geometry.euclidean.twod;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 import org.apache.commons.geometry.core.GeometryTestUtils;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
@@ -113,6 +117,33 @@ public class LinecastPoint2DTest {
     }
 
     @Test
+    public void testEq() {
+        // arrange
+        DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+
+        Line line = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, precision);
+        Line otherLine = Line.fromPointAndDirection(Vector2D.of(1e-4, 1e-4), Vector2D.Unit.PLUS_X, precision);
+
+        LinecastPoint2D a = new LinecastPoint2D(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X, line);
+
+        LinecastPoint2D b = new LinecastPoint2D(Vector2D.of(2, 2), Vector2D.Unit.PLUS_X, line);
+        LinecastPoint2D c = new LinecastPoint2D(Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y, line);
+
+        LinecastPoint2D d = new LinecastPoint2D(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X, line);
+        LinecastPoint2D e = new LinecastPoint2D(
+                Vector2D.of(1 + 1e-3, 1 + 1e-3), Vector2D.Unit.from(1 + 1e-3, 1e-3), otherLine);
+
+        // act/assert
+        Assert.assertTrue(a.eq(a));
+
+        Assert.assertFalse(a.eq(b));
+        Assert.assertFalse(a.eq(c));
+
+        Assert.assertTrue(a.eq(d));
+        Assert.assertTrue(a.eq(e));
+    }
+
+    @Test
     public void testToString() {
         // arrange
         LinecastPoint2D it = new LinecastPoint2D(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X, X_AXIS);
@@ -123,4 +154,50 @@ public class LinecastPoint2DTest {
         // assert
         GeometryTestUtils.assertContains("LinecastPoint2D[point= (1.0, 1.0), normal= (1.0, 0.0)", str);
     }
+
+    @Test
+    public void testSortAndFilter_empty() {
+        // arrange
+        List<LinecastPoint2D> pts = new ArrayList<>();
+
+        // act
+        LinecastPoint2D.sortAndFilter(pts);
+
+        // assert
+        Assert.assertEquals(0, pts.size());
+    }
+
+    @Test
+    public void testSortAndFilter() {
+        // arrange
+        DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-2);
+
+        Line line = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, precision);
+        Line eqLine = Line.fromPointAndDirection(Vector2D.of(1e-3, 1e-3), Vector2D.Unit.PLUS_X, precision);
+        Line diffLine = Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_Y, precision);
+
+        LinecastPoint2D a = new LinecastPoint2D(Vector2D.ZERO, Vector2D.Unit.MINUS_Y, line);
+        LinecastPoint2D aDup1 = new LinecastPoint2D(Vector2D.of(1e-3, 0), Vector2D.Unit.MINUS_Y, line);
+        LinecastPoint2D aDup2 = new LinecastPoint2D(Vector2D.of(1e-3, 1e-3), Vector2D.of(1e-3, -1), eqLine);
+
+        LinecastPoint2D b = new LinecastPoint2D(Vector2D.ZERO, Vector2D.Unit.MINUS_X, diffLine);
+        LinecastPoint2D bDup = new LinecastPoint2D(Vector2D.of(-1e-3, 1e-4), Vector2D.Unit.MINUS_X, diffLine);
+
+        LinecastPoint2D c = new LinecastPoint2D(Vector2D.of(0.5, 0), Vector2D.Unit.MINUS_Y, line);
+
+        LinecastPoint2D d = new LinecastPoint2D(Vector2D.of(1, 0), Vector2D.Unit.MINUS_Y, line);
+
+        List<LinecastPoint2D> list = new ArrayList<>(Arrays.asList(d, aDup1, bDup, b, c, a, aDup2));
+
+        // act
+        LinecastPoint2D.sortAndFilter(list);
+
+        // assert
+        Assert.assertEquals(4, list.size());
+
+        Assert.assertSame(b, list.get(0));
+        Assert.assertSame(a, list.get(1));
+        Assert.assertSame(c, list.get(2));
+        Assert.assertSame(d, list.get(3));
+    }
 }
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java
index 5d781ae..6b3b109 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java
@@ -847,7 +847,7 @@ public class RegionBSPTree2DTest {
     @Test
     public void testFrom_boundarySource_noBoundaries() {
         // arrange
-        BoundarySource2D src = () -> new ArrayList<Segment>().stream();
+        BoundarySource2D src = Boundaries2D.from();
 
         // act
         RegionBSPTree2D tree = RegionBSPTree2D.from(src);
@@ -1084,6 +1084,23 @@ public class RegionBSPTree2DTest {
     }
 
     @Test
+    public void testLinecast_removesDuplicatePoints() {
+        // arrange
+        RegionBSPTree2D tree = RegionBSPTree2D.empty();
+        tree.insert(Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_Y, TEST_PRECISION).span());
+        tree.insert(Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION).span());
+
+        // act/assert
+        LinecastChecker2D.with(tree)
+            .returns(Vector2D.ZERO, Vector2D.Unit.MINUS_Y)
+            .whenGiven(Line.fromPoints(Vector2D.of(1, 1), Vector2D.of(-1, -1), TEST_PRECISION));
+
+        LinecastChecker2D.with(tree)
+            .returns(Vector2D.ZERO, Vector2D.Unit.MINUS_Y)
+            .whenGiven(Segment.fromPoints(Vector2D.of(1, 1), Vector2D.of(-1, -1), TEST_PRECISION));
+    }
+
+    @Test
     public void testTransform() {
         // arrange
         RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.of(1, 1), Vector2D.of(3, 2), TEST_PRECISION)


[commons-geometry] 05/08: GEOMETRY-68: removing Equivalency interface in favor of defining eq() methods that accept DoublePrecisionContext objects as arguments

Posted by er...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

erans pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git

commit 66740f454c4a4ff81afcf0d57b034c4b4d2b6e43
Author: Matt Juntunen <ma...@hotmail.com>
AuthorDate: Fri Jan 3 22:30:37 2020 -0500

    GEOMETRY-68: removing Equivalency interface in favor of defining eq() methods that accept DoublePrecisionContext objects as arguments
---
 .../apache/commons/geometry/core/Equivalency.java  | 33 --------------------
 .../geometry/euclidean/oned/OrientedPoint.java     | 32 ++++++++------------
 .../commons/geometry/euclidean/threed/Line3D.java  | 30 ++++++-------------
 .../geometry/euclidean/threed/LinecastPoint3D.java | 34 +++++++++------------
 .../commons/geometry/euclidean/threed/Plane.java   | 34 +++++++--------------
 .../geometry/euclidean/threed/SubPlane.java        |  2 +-
 .../commons/geometry/euclidean/twod/Line.java      | 34 +++++++--------------
 .../geometry/euclidean/twod/LinecastPoint2D.java   | 35 +++++++++-------------
 .../commons/geometry/euclidean/twod/SubLine.java   |  2 +-
 .../geometry/euclidean/oned/OrientedPointTest.java | 17 +++++------
 .../geometry/euclidean/threed/Line3DTest.java      | 35 ++++++++++------------
 .../euclidean/threed/LinecastPoint3DTest.java      | 10 +++----
 .../geometry/euclidean/threed/PlaneTest.java       | 14 ++++-----
 .../commons/geometry/euclidean/twod/LineTest.java  | 35 ++++++++++------------
 .../euclidean/twod/LinecastPoint2DTest.java        | 10 +++----
 .../euclidean/twod/RegionBSPTree2DTest.java        |  2 +-
 .../commons/geometry/spherical/oned/CutAngle.java  | 30 +++++++------------
 .../geometry/spherical/twod/GreatCircle.java       | 24 +++++++--------
 .../geometry/spherical/twod/SubGreatCircle.java    |  2 +-
 .../geometry/spherical/oned/CutAngleTest.java      | 16 +++++-----
 .../geometry/spherical/twod/GreatCircleTest.java   | 20 ++++++-------
 21 files changed, 172 insertions(+), 279 deletions(-)

diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Equivalency.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Equivalency.java
deleted file mode 100644
index 64b47de..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Equivalency.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.commons.geometry.core;
-
-/** Interface for determining equivalency, not exact equality, between
- * two objects. This is performs a function similar to {@link Object#equals(Object)}
- * but allows fuzzy comparisons to occur instead of strict equality. This is
- * especially useful when comparing objects with floating point values that
- * may not be exact but are operationally equivalent.
- * @param <T> The type being compared
- */
-public interface Equivalency<T> {
-
-    /** Determine if this object is equivalent (effectively equal) to the argument.
-     * @param other the object to compare for equivalency
-     * @return true if this object is equivalent to the argument; false otherwise
-     */
-    boolean eq(T other);
-}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java
index 78491a9..4973dca 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java
@@ -20,7 +20,6 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 
-import org.apache.commons.geometry.core.Equivalency;
 import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
@@ -39,7 +38,7 @@ import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
  * <p>Instances of this class are guaranteed to be immutable.</p>
  */
 public final class OrientedPoint extends AbstractHyperplane<Vector1D>
-    implements Hyperplane<Vector1D>, Equivalency<OrientedPoint> {
+    implements Hyperplane<Vector1D> {
     /** Hyperplane location as a point. */
     private final Vector1D point;
 
@@ -184,27 +183,20 @@ public final class OrientedPoint extends AbstractHyperplane<Vector1D>
         return new SubOrientedPoint(this);
     }
 
-    /** {@inheritDoc}
-     *
+    /** Return true if this instance should be considered equivalent to the argument, using the
+     * given precision context for comparison.
      * <p>Instances are considered equivalent if they
-     * <ul>
-     *  <li>contain equal {@link DoublePrecisionContext precision contexts},</li>
-     *  <li>have equivalent locations as evaluated by the precision context, and</li>
-     *  <li>point in the same direction</li>
-     * </ul>
+     * <ol>
+     *      <li>have equivalent locations and</li>
+     *      <li>point in the same direction.</li>
+     * </ol>
      * @param other the point to compare with
+     * @param precision precision context to use for the comparison
      * @return true if this instance should be considered equivalent to the argument
+     * @see Vector1D#eq(Vector1D, DoublePrecisionContext)
      */
-    @Override
-    public boolean eq(final OrientedPoint other) {
-        if (this == other) {
-            return true;
-        }
-
-        final DoublePrecisionContext precision = getPrecision();
-
-        return precision.equals(other.getPrecision()) &&
-                point.eq(other.point, precision) &&
+    public boolean eq(final OrientedPoint other, final DoublePrecisionContext precision) {
+        return point.eq(other.point, precision) &&
                 positiveFacing == other.positiveFacing;
     }
 
@@ -525,7 +517,7 @@ public final class OrientedPoint extends AbstractHyperplane<Vector1D>
             final OrientedPoint baseHyper = base.getHyperplane();
             final OrientedPoint inputHyper = (OrientedPoint) sub.getHyperplane();
 
-            if (!baseHyper.eq(inputHyper)) {
+            if (!baseHyper.eq(inputHyper, baseHyper.getPrecision())) {
                 throw new IllegalArgumentException("Argument is not on the same " +
                         "hyperplane. Expected " + baseHyper + " but was " +
                         inputHyper);
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line3D.java
index fe16fba..5eb1f0b 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line3D.java
@@ -19,7 +19,6 @@ package org.apache.commons.geometry.euclidean.threed;
 import java.util.Objects;
 
 import org.apache.commons.geometry.core.Embedding;
-import org.apache.commons.geometry.core.Equivalency;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.oned.AffineTransformMatrix1D;
@@ -30,7 +29,7 @@ import org.apache.commons.geometry.euclidean.oned.Vector1D;
  *
  * <p>Instances of this class are guaranteed to be immutable.</p>
  */
-public final class Line3D implements Embedding<Vector3D, Vector1D>, Equivalency<Line3D> {
+public final class Line3D implements Embedding<Vector3D, Vector1D> {
     /** Line point closest to the origin. */
     private final Vector3D origin;
 
@@ -320,28 +319,17 @@ public final class Line3D implements Embedding<Vector3D, Vector1D>, Equivalency<
         return new SubLine3D(this);
     }
 
-    /**{@inheritDoc}
-     *
-     * <p>Instances are considered equivalent if they</p>
-     * <ul>
-     *   <li>contain equal {@link DoublePrecisionContext precision contexts},</li>
-     *   <li>have equivalent origin locations (as evaluated by the precision context), and</li>
-     *   <li>point in the same direction (as evaluated by the precision context)</li>
-     * </ul>
+    /** Return true if this instance should be considered equivalent to the argument, using the
+     * given precision context for comparison. Instances are considered equivalent if they have
+     * equivalent {@code origin}s and {@code direction}s.
      * @param other the point to compare with
+     * @param ctx precision context to use for the comparison
      * @return true if this instance should be considered equivalent to the argument
+     * @see Vector3D#eq(Vector3D, DoublePrecisionContext)
      */
-    @Override
-    public boolean eq(final Line3D other) {
-        if (this == other) {
-            return true;
-        }
-
-        final DoublePrecisionContext testPrecision = getPrecision();
-
-        return testPrecision.equals(other.getPrecision()) &&
-                getOrigin().eq(other.getOrigin(), precision) &&
-                getDirection().eq(other.getDirection(), precision);
+    public boolean eq(final Line3D other, final DoublePrecisionContext ctx) {
+        return getOrigin().eq(other.getOrigin(), ctx) &&
+                getDirection().eq(other.getDirection(), ctx);
     }
 
     /** {@inheritDoc} */
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3D.java
index e1776e4..cc1e6e4 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3D.java
@@ -22,7 +22,6 @@ import java.util.Comparator;
 import java.util.List;
 import java.util.ListIterator;
 
-import org.apache.commons.geometry.core.Equivalency;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.AbstractLinecastPoint;
 
@@ -31,8 +30,7 @@ import org.apache.commons.geometry.euclidean.AbstractLinecastPoint;
  * of the target at the point of intersection.
  * @see Linecastable3D
  */
-public class LinecastPoint3D extends AbstractLinecastPoint<Vector3D, Vector3D.Unit, Line3D>
-    implements Equivalency<LinecastPoint3D> {
+public class LinecastPoint3D extends AbstractLinecastPoint<Vector3D, Vector3D.Unit, Line3D>  {
 
     /** Comparator that sorts intersection instances by increasing abscissa order. If two abscissa
      * values are equal, the comparison uses {@link Vector3D#COORDINATE_ASCENDING_ORDER} with the
@@ -55,28 +53,22 @@ public class LinecastPoint3D extends AbstractLinecastPoint<Vector3D, Vector3D.Un
         super(point, normal.normalize(), line);
     }
 
-    /** {@inheritDoc}
-     *
-     * <p>
-     * Instances are considered equivalent if they have equivalent points, normals, and lines.
-     * </p>
+    /** Return true if this instance should be considered equivalent to the argument, using the
+     * given precision context for comparison. Instances are considered equivalent if they have
+     * equivalent points, normals, and lines.
+     * @param other other point to compare with
+     * @param precision context to use for the comparison
+     * @return true if this instance should be considered equivalent to the argument
      */
-    @Override
-    public boolean eq(final LinecastPoint3D other) {
-        if (this == other) {
-            return true;
-        }
-
-        final DoublePrecisionContext precision = getLine().getPrecision();
-
-        return getLine().eq(other.getLine()) &&
+    public boolean eq(final LinecastPoint3D other, final DoublePrecisionContext precision) {
+        return getLine().eq(other.getLine(), precision) &&
                 getPoint().eq(other.getPoint(), precision) &&
                 getNormal().eq(other.getNormal(), precision);
     }
 
     /** Sort the given list of linecast points by increasing abscissa value and filter to remove
-     * duplicate entries (as determined by the {@link #eq(LinecastPoint3D)} method). The argument
-     * is modified.
+     * duplicate entries (as determined by the {@link #eq(LinecastPoint3D, DoublePrecisionContext)} method).
+     * The argument is modified.
      * @param pts list of points to sort and filter
      */
     public static void sortAndFilter(final List<LinecastPoint3D> pts) {
@@ -111,8 +103,10 @@ public class LinecastPoint3D extends AbstractLinecastPoint<Vector3D, Vector3D.Un
      * @return true if the given linecast point is equivalent to any of those in the given list
      */
     private static boolean containsEq(final LinecastPoint3D pt, final List<LinecastPoint3D> list) {
+        final DoublePrecisionContext precision = pt.getLine().getPrecision();
+
         for (LinecastPoint3D listPt : list) {
-            if (listPt.eq(pt)) {
+            if (listPt.eq(pt, precision)) {
                 return true;
             }
         }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java
index 1ab2ba4..7ffe3ee 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java
@@ -21,7 +21,6 @@ import java.util.Collection;
 import java.util.Iterator;
 import java.util.Objects;
 
-import org.apache.commons.geometry.core.Equivalency;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
 import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
@@ -36,7 +35,7 @@ import org.apache.commons.geometry.euclidean.twod.Vector2D;
 /** Class representing a plane in 3 dimensional Euclidean space.
  */
 public final class Plane extends AbstractHyperplane<Vector3D>
-    implements EmbeddingHyperplane<Vector3D, Vector2D>, Equivalency<Plane> {
+    implements EmbeddingHyperplane<Vector3D, Vector2D> {
     /** First normalized vector of the plane frame (in plane). */
     private final Vector3D u;
 
@@ -510,27 +509,16 @@ public final class Plane extends AbstractHyperplane<Vector3D>
     }
 
 
-    /** {@inheritDoc}
-    *
-    * <p>Instances are considered equivalent if they
-    * <ul>
-    *   <li>contain equal {@link DoublePrecisionContext precision contexts},</li>
-    *   <li>have equivalent origins (as evaluated by the precision context), and</li>
-    *   <li>have equivalent {@code u} and {@code v} vectors (as evaluated by the precision context)</li>
-    * </ul>
-    * @param other the point to compare with
-    * @return true if this instance should be considered equivalent to the argument
-    */
-    @Override
-    public boolean eq(Plane other) {
-        if (this == other) {
-            return true;
-        }
-
-        final DoublePrecisionContext precision = getPrecision();
-
-        return precision.equals(other.getPrecision()) &&
-                getOrigin().eq(other.getOrigin(), precision) &&
+    /** Return true if this instance should be considered equivalent to the argument, using the
+     * given precision context for comparison. Instances are considered equivalent if they
+     * have equivalent {@code origin} points and {@code u} and {@code v} vectors.
+     * @param other the point to compare with
+     * @param precision precision context to use for the comparison
+     * @return true if this instance should be considered equivalent to the argument
+     * @see Vector3D#eq(Vector3D, DoublePrecisionContext)
+     */
+    public boolean eq(final Plane other, final DoublePrecisionContext precision) {
+        return getOrigin().eq(other.getOrigin(), precision) &&
                 u.eq(other.u, precision) &&
                 v.eq(other.v, precision);
     }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubPlane.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubPlane.java
index e738536..2fb4e17 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubPlane.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubPlane.java
@@ -143,7 +143,7 @@ public final class SubPlane extends AbstractSubPlane<RegionBSPTree2D> {
     private void validatePlane(final Plane inputPlane) {
         final Plane plane = getPlane();
 
-        if (!plane.eq(inputPlane)) {
+        if (!plane.eq(inputPlane, plane.getPrecision())) {
             throw new IllegalArgumentException("Argument is not on the same " +
                     "plane. Expected " + plane + " but was " +
                     inputPlane);
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java
index adedde9..a893206 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java
@@ -18,7 +18,6 @@ package org.apache.commons.geometry.euclidean.twod;
 
 import java.util.Objects;
 
-import org.apache.commons.geometry.core.Equivalency;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
 import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
@@ -57,7 +56,7 @@ import org.apache.commons.numbers.arrays.LinearCombination;
  * points with positive offsets.</p>
  */
 public final class Line extends AbstractHyperplane<Vector2D>
-    implements EmbeddingHyperplane<Vector2D, Vector1D>, Equivalency<Line> {
+    implements EmbeddingHyperplane<Vector2D, Vector1D> {
     /** The direction of the line as a normalized vector. */
     private final Vector2D direction;
 
@@ -434,27 +433,16 @@ public final class Line extends AbstractHyperplane<Vector2D>
         return getPrecision().eqZero(area);
     }
 
-    /** {@inheritDoc}
-    *
-    * <p>Instances are considered equivalent if they
-    * <ul>
-    *   <li>contain equal {@link DoublePrecisionContext precision contexts},</li>
-    *   <li>have equivalent origin locations (as evaluated by the precision context), and</li>
-    *   <li>point in the same direction (as evaluated by the precision context)</li>
-    * </ul>
-    * @param other the point to compare with
-    * @return true if this instance should be considered equivalent to the argument
-    */
-    @Override
-    public boolean eq(final Line other) {
-        if (this == other) {
-            return true;
-        }
-
-        final DoublePrecisionContext precision = getPrecision();
-
-        return precision.equals(other.getPrecision()) &&
-                getOrigin().eq(other.getOrigin(), precision) &&
+    /** Return true if this instance should be considered equivalent to the argument, using the
+     * given precision context for comparison. Instances are considered equivalent if they have
+     * equivalent {@code origin} points and make similar angles with the x-axis.
+     * @param other the point to compare with
+     * @param precision precision context to use for the comparison
+     * @return true if this instance should be considered equivalent to the argument
+     * @see Vector2D#eq(Vector2D, DoublePrecisionContext)
+     */
+    public boolean eq(final Line other, final DoublePrecisionContext precision) {
+        return getOrigin().eq(other.getOrigin(), precision) &&
                 precision.eq(getAngle(), other.getAngle());
     }
 
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2D.java
index de6d6e6..d175330 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2D.java
@@ -22,7 +22,6 @@ import java.util.Comparator;
 import java.util.List;
 import java.util.ListIterator;
 
-import org.apache.commons.geometry.core.Equivalency;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.AbstractLinecastPoint;
 
@@ -31,8 +30,7 @@ import org.apache.commons.geometry.euclidean.AbstractLinecastPoint;
  * of the target at the point of intersection.
  * @see Linecastable2D
  */
-public class LinecastPoint2D extends AbstractLinecastPoint<Vector2D, Vector2D.Unit, Line>
-    implements Equivalency<LinecastPoint2D> {
+public class LinecastPoint2D extends AbstractLinecastPoint<Vector2D, Vector2D.Unit, Line> {
 
     /** Comparator that sorts intersection instances by increasing abscissa order. If two abscissa
      * values are equal, the comparison uses {@link Vector2D#COORDINATE_ASCENDING_ORDER} with the
@@ -55,27 +53,20 @@ public class LinecastPoint2D extends AbstractLinecastPoint<Vector2D, Vector2D.Un
         super(point, normal.normalize(), line);
     }
 
-    /** {@inheritDoc}
-     *
-     * <p>
-     * Instances are considered equivalent if they have equivalent points, normals, and lines.
-     * </p>
+    /** Return true if this instance should be considered equivalent to the argument, using the
+     * given precision context for comparison. Instances are considered equivalent if they have equivalent
+     * points, normals, and lines.
+     * @param other point to compare with
+     * @param precision precision context to use for the comparison
+     * @return true if this instance should be considered equivalent to the argument
      */
-    @Override
-    public boolean eq(final LinecastPoint2D other) {
-        if (this == other) {
-            return true;
-        }
-
-        final DoublePrecisionContext precision = getLine().getPrecision();
-
-        return getLine().eq(other.getLine()) &&
-                getPoint().eq(other.getPoint(), precision) &&
+    public boolean eq(final LinecastPoint2D other, final DoublePrecisionContext precision) {
+        return getPoint().eq(other.getPoint(), precision) &&
                 getNormal().eq(other.getNormal(), precision);
     }
 
-    /** Sort the given list of linecast points by increasing abscissa value and filter
-     * to remove duplicate entries (as determined by the {@link #eq(LinecastPoint2D)} method).
+    /** Sort the given list of linecast points by increasing abscissa value and filter to remove
+     * duplicate entries (as determined by the {@link #eq(LinecastPoint2D, DoublePrecisionContext)} method).
      * The argument is modified.
      * @param pts list of points to sort and filter
      */
@@ -111,8 +102,10 @@ public class LinecastPoint2D extends AbstractLinecastPoint<Vector2D, Vector2D.Un
      * @return true if the given linecast point is equivalent to any of those in the given list
      */
     private static boolean containsEq(final LinecastPoint2D pt, final List<LinecastPoint2D> list) {
+        final DoublePrecisionContext precision = pt.getLine().getPrecision();
+
         for (LinecastPoint2D listPt : list) {
-            if (listPt.eq(pt)) {
+            if (listPt.eq(pt, precision)) {
                 return true;
             }
         }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/SubLine.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/SubLine.java
index dba65a4..9462880 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/SubLine.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/SubLine.java
@@ -164,7 +164,7 @@ public final class SubLine extends AbstractSubLine {
     private void validateLine(final Line inputLine) {
         final Line line = getLine();
 
-        if (!line.eq(inputLine)) {
+        if (!line.eq(inputLine, line.getPrecision())) {
             throw new IllegalArgumentException("Argument is not on the same " +
                     "line. Expected " + line + " but was " +
                     inputLine);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/OrientedPointTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/OrientedPointTest.java
index 87038e9..a090e88 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/OrientedPointTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/OrientedPointTest.java
@@ -251,22 +251,21 @@ public class OrientedPointTest {
         DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
 
         OrientedPoint a = OrientedPoint.createPositiveFacing(0, precision);
-
         OrientedPoint b = OrientedPoint.createPositiveFacing(0, TEST_PRECISION);
-        OrientedPoint c = OrientedPoint.createNegativeFacing(0, precision);
-        OrientedPoint d = OrientedPoint.createPositiveFacing(2e-3, precision);
 
+        OrientedPoint c = OrientedPoint.createPositiveFacing(2e-3, precision);
+        OrientedPoint d = OrientedPoint.createNegativeFacing(0, precision);
         OrientedPoint e = OrientedPoint.createPositiveFacing(1e-4, precision);
 
         // act/assert
-        Assert.assertTrue(a.eq(a));
+        Assert.assertTrue(a.eq(a, precision));
+        Assert.assertTrue(a.eq(b, precision));
 
-        Assert.assertFalse(a.eq(b));
-        Assert.assertFalse(a.eq(c));
-        Assert.assertFalse(a.eq(d));
+        Assert.assertFalse(a.eq(c, precision));
+        Assert.assertFalse(a.eq(d, precision));
 
-        Assert.assertTrue(a.eq(e));
-        Assert.assertTrue(e.eq(a));
+        Assert.assertTrue(a.eq(e, precision));
+        Assert.assertTrue(e.eq(a, precision));
     }
 
     @Test
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Line3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Line3DTest.java
index 88c2d48..22df394 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Line3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Line3DTest.java
@@ -399,36 +399,33 @@ public class Line3DTest {
     @Test
     public void testEq() {
         // arrange
-        DoublePrecisionContext precision1 = new EpsilonDoublePrecisionContext(1e-3);
-        DoublePrecisionContext precision2 = new EpsilonDoublePrecisionContext(1e-2);
+        DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
 
         Vector3D p = Vector3D.of(1, 2, 3);
         Vector3D dir = Vector3D.of(1, 0, 0);
 
-        Line3D a = Line3D.fromPointAndDirection(p, dir, precision1);
-        Line3D b = Line3D.fromPointAndDirection(Vector3D.ZERO, dir, precision1);
-        Line3D c = Line3D.fromPointAndDirection(p, Vector3D.of(1, 1, 0), precision1);
-        Line3D d = Line3D.fromPointAndDirection(p, dir, precision2);
+        Line3D a = Line3D.fromPointAndDirection(p, dir, precision);
+        Line3D b = Line3D.fromPointAndDirection(Vector3D.ZERO, dir, precision);
+        Line3D c = Line3D.fromPointAndDirection(p, Vector3D.of(1, 1, 0), precision);
 
-        Line3D e = Line3D.fromPointAndDirection(p, dir, precision1);
-        Line3D f = Line3D.fromPointAndDirection(p.add(Vector3D.of(1e-4, 1e-4, 1e-4)), dir, precision1);
-        Line3D g = Line3D.fromPointAndDirection(p, Vector3D.of(1 + 1e-4, 1e-4, 1e-4), precision1);
+        Line3D d = Line3D.fromPointAndDirection(p, dir, precision);
+        Line3D e = Line3D.fromPointAndDirection(p.add(Vector3D.of(1e-4, 1e-4, 1e-4)), dir, precision);
+        Line3D f = Line3D.fromPointAndDirection(p, Vector3D.of(1 + 1e-4, 1e-4, 1e-4), precision);
 
         // act/assert
-        Assert.assertTrue(a.eq(a));
+        Assert.assertTrue(a.eq(a, precision));
 
-        Assert.assertTrue(a.eq(e));
-        Assert.assertTrue(e.eq(a));
+        Assert.assertTrue(a.eq(d, precision));
+        Assert.assertTrue(d.eq(a, precision));
 
-        Assert.assertTrue(a.eq(f));
-        Assert.assertTrue(f.eq(a));
+        Assert.assertTrue(a.eq(e, precision));
+        Assert.assertTrue(e.eq(a, precision));
 
-        Assert.assertTrue(a.eq(g));
-        Assert.assertTrue(g.eq(a));
+        Assert.assertTrue(a.eq(f, precision));
+        Assert.assertTrue(f.eq(a, precision));
 
-        Assert.assertFalse(a.eq(b));
-        Assert.assertFalse(a.eq(c));
-        Assert.assertFalse(a.eq(d));
+        Assert.assertFalse(a.eq(b, precision));
+        Assert.assertFalse(a.eq(c, precision));
     }
 
     @Test
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3DTest.java
index aa6784d..59c74e8 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3DTest.java
@@ -134,13 +134,13 @@ public class LinecastPoint3DTest {
                 Vector3D.of(1 + 1e-3, 1 + 1e-3, 1 + 1e-3), Vector3D.Unit.from(1 + 1e-3, 1e-3, 1e-3), otherLine);
 
         // act/assert
-        Assert.assertTrue(a.eq(a));
+        Assert.assertTrue(a.eq(a, precision));
 
-        Assert.assertFalse(a.eq(b));
-        Assert.assertFalse(a.eq(c));
+        Assert.assertFalse(a.eq(b, precision));
+        Assert.assertFalse(a.eq(c, precision));
 
-        Assert.assertTrue(a.eq(d));
-        Assert.assertTrue(a.eq(e));
+        Assert.assertTrue(a.eq(d, precision));
+        Assert.assertTrue(a.eq(e, precision));
     }
 
     @Test
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlaneTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlaneTest.java
index ed5f027..ba51155 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlaneTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlaneTest.java
@@ -1000,15 +1000,15 @@ public class PlaneTest {
         Plane f = Plane.fromPointAndPlaneVectors(ptPrime, uPrime, vPrime, new EpsilonDoublePrecisionContext(eps));
 
         // act/assert
-        Assert.assertTrue(a.eq(a));
+        Assert.assertTrue(a.eq(a, precision));
 
-        Assert.assertFalse(a.eq(b));
-        Assert.assertFalse(a.eq(c));
-        Assert.assertFalse(a.eq(d));
-        Assert.assertFalse(a.eq(e));
+        Assert.assertFalse(a.eq(b, precision));
+        Assert.assertFalse(a.eq(c, precision));
+        Assert.assertFalse(a.eq(d, precision));
 
-        Assert.assertTrue(a.eq(f));
-        Assert.assertTrue(f.eq(a));
+        Assert.assertTrue(a.eq(e, precision));
+        Assert.assertTrue(a.eq(f, precision));
+        Assert.assertTrue(f.eq(a, precision));
     }
 
     @Test
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineTest.java
index 66a232f..2b36179 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineTest.java
@@ -1120,36 +1120,33 @@ public class LineTest {
     @Test
     public void testEq() {
         // arrange
-        DoublePrecisionContext precision1 = new EpsilonDoublePrecisionContext(1e-3);
-        DoublePrecisionContext precision2 = new EpsilonDoublePrecisionContext(1e-2);
+        DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-3);
 
         Vector2D p = Vector2D.of(1, 2);
         double angle = 1.0;
 
-        Line a = Line.fromPointAndAngle(p, angle, precision1);
-        Line b = Line.fromPointAndAngle(Vector2D.ZERO, angle, precision1);
-        Line c = Line.fromPointAndAngle(p, angle + 1.0, precision1);
-        Line d = Line.fromPointAndAngle(p, angle, precision2);
+        Line a = Line.fromPointAndAngle(p, angle, precision);
+        Line b = Line.fromPointAndAngle(Vector2D.ZERO, angle, precision);
+        Line c = Line.fromPointAndAngle(p, angle + 1.0, precision);
 
-        Line e = Line.fromPointAndAngle(p, angle, precision1);
-        Line f = Line.fromPointAndAngle(p.add(Vector2D.of(1e-4, 1e-4)), angle, precision1);
-        Line g = Line.fromPointAndAngle(p, angle + 1e-4, precision1);
+        Line d = Line.fromPointAndAngle(p, angle, precision);
+        Line e = Line.fromPointAndAngle(p.add(Vector2D.of(1e-4, 1e-4)), angle, precision);
+        Line f = Line.fromPointAndAngle(p, angle + 1e-4, precision);
 
         // act/assert
-        Assert.assertTrue(a.eq(a));
+        Assert.assertTrue(a.eq(a, precision));
 
-        Assert.assertTrue(a.eq(e));
-        Assert.assertTrue(e.eq(a));
+        Assert.assertTrue(a.eq(d, precision));
+        Assert.assertTrue(d.eq(a, precision));
 
-        Assert.assertTrue(a.eq(f));
-        Assert.assertTrue(f.eq(a));
+        Assert.assertTrue(a.eq(e, precision));
+        Assert.assertTrue(e.eq(a, precision));
 
-        Assert.assertTrue(a.eq(g));
-        Assert.assertTrue(g.eq(a));
+        Assert.assertTrue(a.eq(f, precision));
+        Assert.assertTrue(f.eq(a, precision));
 
-        Assert.assertFalse(a.eq(b));
-        Assert.assertFalse(a.eq(c));
-        Assert.assertFalse(a.eq(d));
+        Assert.assertFalse(a.eq(b, precision));
+        Assert.assertFalse(a.eq(c, precision));
     }
 
     @Test
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2DTest.java
index e000b4f..08f5184 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2DTest.java
@@ -134,13 +134,13 @@ public class LinecastPoint2DTest {
                 Vector2D.of(1 + 1e-3, 1 + 1e-3), Vector2D.Unit.from(1 + 1e-3, 1e-3), otherLine);
 
         // act/assert
-        Assert.assertTrue(a.eq(a));
+        Assert.assertTrue(a.eq(a, precision));
 
-        Assert.assertFalse(a.eq(b));
-        Assert.assertFalse(a.eq(c));
+        Assert.assertFalse(a.eq(b, precision));
+        Assert.assertFalse(a.eq(c, precision));
 
-        Assert.assertTrue(a.eq(d));
-        Assert.assertTrue(a.eq(e));
+        Assert.assertTrue(a.eq(d, precision));
+        Assert.assertTrue(a.eq(e, precision));
     }
 
     @Test
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java
index 6b3b109..e63b15f 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java
@@ -1149,7 +1149,7 @@ public class RegionBSPTree2DTest {
         Assert.assertNull(segment.getEndPoint());
 
         Line expectedLine = Line.fromPointAndAngle(Vector2D.of(-1, 0), PlaneAngleRadians.PI_OVER_TWO, TEST_PRECISION);
-        Assert.assertTrue(expectedLine.eq(segment.getLine()));
+        Assert.assertTrue(expectedLine.eq(segment.getLine(), expectedLine.getPrecision()));
     }
 
     @Test
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/CutAngle.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/CutAngle.java
index 0547868..cf1735a 100644
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/CutAngle.java
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/CutAngle.java
@@ -20,7 +20,6 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 
-import org.apache.commons.geometry.core.Equivalency;
 import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
@@ -56,8 +55,7 @@ import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
  *
  * <p>Instances of this class are guaranteed to be immutable.</p>
  */
-public final class CutAngle extends AbstractHyperplane<Point1S>
-    implements Equivalency<CutAngle> {
+public final class CutAngle extends AbstractHyperplane<Point1S> {
     /** Hyperplane location as a point. */
     private final Point1S point;
 
@@ -117,27 +115,21 @@ public final class CutAngle extends AbstractHyperplane<Point1S>
         return positiveFacing;
     }
 
-    /** {@inheritDoc}
-     *
+    /** Return true if this instance should be considered equivalent to the argument, using the
+     * given precision context for comparison.
      * <p>The instances are considered equivalent if they
      * <ol>
-     *    <li>have equal precision contexts,</li>
-     *    <li>have equivalent point locations as evaluated by the precision
-     *          context (points separated by multiples of 2pi are considered equivalent), and
+     *    <li>have equivalent point locations (points separated by multiples of 2pi are
+     *      considered equivalent) and
      *    <li>point in the same direction.</li>
      * </ol>
+     * @param other point to compare with
+     * @param precision precision context to use for the comparison
+     * @return true if this instance should be considered equivalent to the argument
      * @see Point1S#eq(Point1S, DoublePrecisionContext)
      */
-    @Override
-    public boolean eq(final CutAngle other) {
-        if (this == other) {
-            return true;
-        }
-
-        final DoublePrecisionContext precision = getPrecision();
-
-        return precision.equals(other.getPrecision()) &&
-                point.eq(other.point, precision) &&
+    public boolean eq(final CutAngle other, final DoublePrecisionContext precision) {
+        return point.eq(other.point, precision) &&
                 positiveFacing == other.positiveFacing;
     }
 
@@ -496,7 +488,7 @@ public final class CutAngle extends AbstractHyperplane<Point1S>
             final CutAngle baseHyper = base.getHyperplane();
             final CutAngle inputHyper = (CutAngle) sub.getHyperplane();
 
-            if (!baseHyper.eq(inputHyper)) {
+            if (!baseHyper.eq(inputHyper, baseHyper.getPrecision())) {
                 throw new IllegalArgumentException("Argument is not on the same " +
                         "hyperplane. Expected " + baseHyper + " but was " +
                         inputHyper);
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircle.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircle.java
index 584cd2f..c1ffffb 100644
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircle.java
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircle.java
@@ -18,7 +18,6 @@ package org.apache.commons.geometry.spherical.twod;
 
 import java.util.Objects;
 
-import org.apache.commons.geometry.core.Equivalency;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
 import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
@@ -38,7 +37,7 @@ import org.apache.commons.numbers.angle.PlaneAngleRadians;
  * <p>Instances of this class are guaranteed to be immutable.</p>
  */
 public final class GreatCircle extends AbstractHyperplane<Point2S>
-    implements EmbeddingHyperplane<Point2S, Point1S>, Equivalency<GreatCircle> {
+    implements EmbeddingHyperplane<Point2S, Point1S> {
     /** Pole or circle center. */
     private final Vector3D.Unit pole;
 
@@ -318,17 +317,16 @@ public final class GreatCircle extends AbstractHyperplane<Point2S>
         return Point2S.from(vectorAt(point.getAzimuth()));
     }
 
-    /** {@inheritDoc} */
-    @Override
-    public boolean eq(final GreatCircle other) {
-        if (this == other) {
-            return true;
-        }
-
-        final DoublePrecisionContext precision = getPrecision();
-
-        return precision.equals(other.getPrecision()) &&
-                pole.eq(other.pole, precision) &&
+    /** Return true if this instance should be considered equivalent to the argument, using the
+     * given precision context for comparison. Instances are considered equivalent if have equivalent
+     * {@code pole}, {@code u}, and {@code v} vectors.
+     * @param other great circle to compare with
+     * @param precision precision context to use for the comparison
+     * @return true if this instance should be considered equivalent to the argument
+     * @see Vector3D#eq(Vector3D, DoublePrecisionContext)
+     */
+    public boolean eq(final GreatCircle other, final DoublePrecisionContext precision) {
+        return pole.eq(other.pole, precision) &&
                 u.eq(other.u, precision) &&
                 v.eq(other.v, precision);
     }
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/SubGreatCircle.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/SubGreatCircle.java
index d2f918c..49e5c20 100644
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/SubGreatCircle.java
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/SubGreatCircle.java
@@ -173,7 +173,7 @@ public final class SubGreatCircle extends AbstractSubGreatCircle {
     private void validateGreatCircle(final GreatCircle inputCircle) {
         final GreatCircle circle = getCircle();
 
-        if (!circle.eq(inputCircle)) {
+        if (!circle.eq(inputCircle, circle.getPrecision())) {
             throw new IllegalArgumentException("Argument is not on the same " +
                     "great circle. Expected " + circle + " but was " +
                     inputCircle);
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/CutAngleTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/CutAngleTest.java
index 1bf033c..134de24 100644
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/CutAngleTest.java
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/CutAngleTest.java
@@ -291,16 +291,16 @@ public class CutAngleTest {
         CutAngle h = CutAngle.fromPointAndDirection(Point1S.of(-1e-4), true, precision);
 
         // act/assert
-        Assert.assertTrue(a.eq(a));
+        Assert.assertTrue(a.eq(a, precision));
 
-        Assert.assertFalse(a.eq(b));
-        Assert.assertFalse(a.eq(c));
-        Assert.assertFalse(a.eq(d));
+        Assert.assertFalse(a.eq(b, precision));
+        Assert.assertFalse(a.eq(c, precision));
 
-        Assert.assertTrue(a.eq(e));
-        Assert.assertTrue(a.eq(f));
-        Assert.assertTrue(a.eq(g));
-        Assert.assertTrue(a.eq(h));
+        Assert.assertTrue(a.eq(d, precision));
+        Assert.assertTrue(a.eq(e, precision));
+        Assert.assertTrue(a.eq(f, precision));
+        Assert.assertTrue(a.eq(g, precision));
+        Assert.assertTrue(a.eq(h, precision));
     }
 
     @Test
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatCircleTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatCircleTest.java
index a554ee8..bf95bc6 100644
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatCircleTest.java
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatCircleTest.java
@@ -640,20 +640,20 @@ public class GreatCircleTest {
                 new EpsilonDoublePrecisionContext(eps));
 
         // act/assert
-        Assert.assertTrue(a.eq(a));
+        Assert.assertTrue(a.eq(a, precision));
 
-        Assert.assertFalse(a.eq(b));
-        Assert.assertFalse(a.eq(c));
-        Assert.assertFalse(a.eq(d));
+        Assert.assertFalse(a.eq(b, precision));
+        Assert.assertFalse(a.eq(c, precision));
 
-        Assert.assertTrue(a.eq(e));
-        Assert.assertTrue(e.eq(a));
+        Assert.assertTrue(a.eq(d, precision));
+        Assert.assertTrue(a.eq(e, precision));
+        Assert.assertTrue(e.eq(a, precision));
 
-        Assert.assertTrue(a.eq(f));
-        Assert.assertTrue(f.eq(a));
+        Assert.assertTrue(a.eq(f, precision));
+        Assert.assertTrue(f.eq(a, precision));
 
-        Assert.assertTrue(g.eq(e));
-        Assert.assertTrue(e.eq(g));
+        Assert.assertTrue(g.eq(e, precision));
+        Assert.assertTrue(e.eq(g, precision));
     }
 
     @Test


[commons-geometry] 06/08: GEOMETRY-83: adding shape-generation packages with Parallelogram and Parallelepiped classes; adding back RegionBSPTreeXX.from(Iterable) factory methods

Posted by er...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

erans pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git

commit 04545980fc69c696d85efaab186a9a4313d74687
Author: Matt Juntunen <ma...@hotmail.com>
AuthorDate: Sat Jan 4 00:12:19 2020 -0500

    GEOMETRY-83: adding shape-generation packages with Parallelogram and Parallelepiped classes; adding back RegionBSPTreeXX.from(Iterable) factory methods
---
 .../euclidean/threed/BoundarySource3D.java         |  20 +++
 .../geometry/euclidean/threed/RegionBSPTree3D.java |  12 ++
 .../Parallelepiped.java}                           |  48 +++---
 .../shapes/package-info.java}                      |  21 +--
 .../geometry/euclidean/twod/BoundarySource2D.java  |  20 +++
 .../geometry/euclidean/twod/RegionBSPTree2D.java   |  12 ++
 .../Parallelogram.java}                            |  49 +++---
 .../package-info.java}                             |  21 +--
 .../euclidean/DocumentationExamplesTest.java       |  16 +-
 .../euclidean/threed/BoundarySource3DTest.java     |  98 ++++++++++++
 .../BoundarySourceLinecastWrapper3DTest.java       |   7 +-
 .../euclidean/threed/RegionBSPTree3DTest.java      | 169 ++++++++++-----------
 .../geometry/euclidean/threed/SubPlaneTest.java    |  20 ++-
 .../ParallelepipedTest.java}                       | 101 +++---------
 .../geometry/euclidean/twod/Boundaries2DTest.java  | 161 --------------------
 .../euclidean/twod/BoundarySource2DTest.java       |  93 ++++++++++++
 .../twod/BoundarySourceLinecastWrapper2DTest.java  |   7 +-
 .../euclidean/twod/RegionBSPTree2DTest.java        |  77 +++++++---
 .../euclidean/twod/shapes/ParallelogramTest.java   | 103 +++++++++++++
 .../geometry/spherical/twod/RegionBSPTree2S.java   |  12 ++
 .../spherical/twod/RegionBSPTree2STest.java        |  31 +++-
 src/site/xdoc/index.xml                            |   8 +-
 src/site/xdoc/userguide/index.xml                  |   7 +-
 23 files changed, 635 insertions(+), 478 deletions(-)

diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3D.java
index 556a559..4f29f87 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3D.java
@@ -16,6 +16,9 @@
  */
 package org.apache.commons.geometry.euclidean.threed;
 
+import java.util.Arrays;
+import java.util.Collection;
+
 import org.apache.commons.geometry.core.partitioning.BoundarySource;
 
 /** Extension of the {@link BoundarySource} interface for Euclidean 3D
@@ -32,4 +35,21 @@ public interface BoundarySource3D extends BoundarySource<ConvexSubPlane> {
     default RegionBSPTree3D toTree() {
         return RegionBSPTree3D.from(this);
     }
+
+    /** Return a {@link BoundarySource3D} instance containing the given convex subplanes.
+     * @param boundaries convex subplanes to include in the boundary source
+     * @return a boundary source containing the given boundaries
+     */
+    static BoundarySource3D from(final ConvexSubPlane... boundaries) {
+        return from(Arrays.asList(boundaries));
+    }
+
+    /** Return a {@link BoundarySource3D} instance containing the given convex subplanes. The given
+     * collection is used directly as the source of the subplanes; no copy is made.
+     * @param boundaries convex subplanes to include in the boundary source
+     * @return a boundary source containing the given boundaries
+     */
+    static BoundarySource3D from(final Collection<ConvexSubPlane> boundaries) {
+        return () -> boundaries.stream();
+    }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java
index 867dd27..c96ed7e 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java
@@ -190,6 +190,18 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
         return new RegionBSPTree3D(false);
     }
 
+    /** Construct a new tree from the given boundaries. If no boundaries
+     * are present, the returned tree contains the full space.
+     * @param boundaries boundaries to construct the tree from
+     * @return a new tree instance constructed from the given boundaries
+     */
+    public static RegionBSPTree3D from(final Iterable<ConvexSubPlane> boundaries) {
+        RegionBSPTree3D tree = RegionBSPTree3D.full();
+        tree.insert(boundaries);
+
+        return tree;
+    }
+
     /** Construct a new tree from the boundaries in the given boundary source. If no boundaries
      * are present in the given source, their the returned tree contains the full space.
      * @param boundarySrc boundary source to construct a tree from
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Boundaries3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/shapes/Parallelepiped.java
similarity index 65%
rename from commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Boundaries3D.java
rename to commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/shapes/Parallelepiped.java
index 5312e03..31d468c 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Boundaries3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/shapes/Parallelepiped.java
@@ -14,49 +14,39 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.commons.geometry.euclidean.threed;
+package org.apache.commons.geometry.euclidean.threed.shapes;
 
 import java.util.Arrays;
-import java.util.Collection;
+import java.util.List;
 
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.ConvexSubPlane;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
 
-/** Utility class for constructing {@link BoundarySource3D} instances.
+/** Class containing utility methods for constructing parallelepipeds. Parallelepipeds
+ * are 3 dimensional figures formed by six parallelograms. For example, cubes and rectangular
+ * prisms are parallelepipeds.
+ * @see <a href="https://en.wikipedia.org/wiki/Parallelepiped">Parallelepiped</a>
  */
-public final class Boundaries3D {
+public final class Parallelepiped {
 
-    /** Private constructor. */
-    private Boundaries3D() {
-    }
-
-    /** Return a {@link BoundarySource3D} instance containing the given convex subplanes.
-     * @param boundaries convex subplanes to include in the boundary source
-     * @return a boundary source containing the given boundaries
-     */
-    public static BoundarySource3D from(final ConvexSubPlane... boundaries) {
-        return from(Arrays.asList(boundaries));
-    }
-
-    /** Return a {@link BoundarySource3D} instance containing the given convex subplanes. The given
-     * collection is used directly as the source of the subplanes; no copy is made.
-     * @param boundaries convex subplanes to include in the boundary source
-     * @return a boundary source containing the given boundaries
+    /** Utility class; no instantiation.
      */
-    public static BoundarySource3D from(final Collection<ConvexSubPlane> boundaries) {
-        return () -> boundaries.stream();
+    private Parallelepiped() {
     }
 
-    /** Return a {@link BoundarySource3D} instance defining an axis-aligned rectangular prism. The points {@code a}
-     * and {@code b} are taken to represent opposite corner points in the prism and may be specified in
-     * any order.
+    /** Return a list of {@link ConvexSubPlane}s defining an axis-aligned parallelepiped, ie, a rectangular prism.
+     * The points {@code a} and {@code b} are taken to represent opposite corner points in the prism and may be
+     * specified in any order.
      * @param a first corner point in the prism (opposite of {@code b})
      * @param b second corner point in the prism (opposite of {@code a})
-     * @param precision precision context used to construct boundary instances
-     * @return a boundary source defining the boundaries of the rectangular prism
+     * @param precision precision context used to construct convex subplane instances
+     * @return a list containing the boundaries of the rectangular prism
      * @throws IllegalArgumentException if the width, height, or depth of the defined prism is zero
      *      as evaluated by the precision context.
      */
-    public static BoundarySource3D rect(final Vector3D a, final Vector3D b, final DoublePrecisionContext precision) {
+    public static List<ConvexSubPlane> axisAligned(final Vector3D a, final Vector3D b,
+            final DoublePrecisionContext precision) {
 
         final double minX = Math.min(a.getX(), b.getX());
         final double maxX = Math.max(a.getX(), b.getX());
@@ -83,7 +73,7 @@ public final class Boundaries3D {
             Vector3D.of(minX, maxY, maxZ)
         };
 
-        return from(
+        return Arrays.asList(
             // -z and +z sides
             ConvexSubPlane.fromVertexLoop(Arrays.asList(vertices[0], vertices[3], vertices[2], vertices[1]), precision),
             ConvexSubPlane.fromVertexLoop(Arrays.asList(vertices[4], vertices[5], vertices[6], vertices[7]), precision),
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/shapes/package-info.java
similarity index 57%
copy from commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2D.java
copy to commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/shapes/package-info.java
index ade62b3..5009b79 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/shapes/package-info.java
@@ -14,22 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.commons.geometry.euclidean.twod;
-
-import org.apache.commons.geometry.core.partitioning.BoundarySource;
-
-/** Extension of the {@link BoundarySource} interface for Euclidean 2D
- * space.
+/**
+ * This package provides utilities for constructing basic 3D shapes.
  */
-public interface BoundarySource2D extends BoundarySource<Segment> {
-
-    /** Construct a new BSP tree from the boundaries contained in this
-     * instance.
-     * @return a new BSP tree constructed from the boundaries in this
-     *      instance
-     * @see RegionBSPTree2D#from(BoundarySource)
-     */
-    default RegionBSPTree2D toTree() {
-        return RegionBSPTree2D.from(this);
-    }
-}
+package org.apache.commons.geometry.euclidean.threed.shapes;
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2D.java
index ade62b3..a4e5ae5 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2D.java
@@ -16,6 +16,9 @@
  */
 package org.apache.commons.geometry.euclidean.twod;
 
+import java.util.Arrays;
+import java.util.Collection;
+
 import org.apache.commons.geometry.core.partitioning.BoundarySource;
 
 /** Extension of the {@link BoundarySource} interface for Euclidean 2D
@@ -32,4 +35,21 @@ public interface BoundarySource2D extends BoundarySource<Segment> {
     default RegionBSPTree2D toTree() {
         return RegionBSPTree2D.from(this);
     }
+
+    /** Return a {@link BoundarySource2D} instance containing the given segments.
+     * @param boundaries segments to include in the boundary source
+     * @return a boundary source containing the given boundaries
+     */
+    static BoundarySource2D from(final Segment... boundaries) {
+        return from(Arrays.asList(boundaries));
+    }
+
+    /** Return a {@link BoundarySource2D} instance containing the given segments. The given
+     * collection is used directly as the source of the segments; no copy is made.
+     * @param boundaries segments to include in the boundary source
+     * @return a boundary source containing the given boundaries
+     */
+    static BoundarySource2D from(final Collection<Segment> boundaries) {
+        return () -> boundaries.stream();
+    }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java
index c659164..68146ee 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java
@@ -274,6 +274,18 @@ public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, Regio
         return new RegionBSPTree2D(false);
     }
 
+    /** Construct a new tree from the given boundaries. If no boundaries
+     * are present, the returned tree contains the full space.
+     * @param boundaries boundaries to construct the tree from
+     * @return a new tree instance constructed from the given boundaries
+     */
+    public static RegionBSPTree2D from(final Iterable<Segment> boundaries) {
+        RegionBSPTree2D tree = RegionBSPTree2D.full();
+        tree.insert(boundaries);
+
+        return tree;
+    }
+
     /** Construct a new tree from the boundaries in the given boundary source. If no boundaries
      * are present in the given source, their the returned tree contains the full space.
      * @param boundarySrc boundary source to construct a tree from
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Boundaries2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/shapes/Parallelogram.java
similarity index 54%
rename from commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Boundaries2D.java
rename to commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/shapes/Parallelogram.java
index ca8d6fc..cd1b317 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Boundaries2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/shapes/Parallelogram.java
@@ -14,49 +14,36 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.commons.geometry.euclidean.twod;
+package org.apache.commons.geometry.euclidean.twod.shapes;
 
 import java.util.Arrays;
-import java.util.Collection;
 
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.twod.Polyline;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
 
-/** Utility class for constructing {@link BoundarySource2D} instances.
+/** Class containing utility methods for constructing parallelograms. Parallelograms
+ * are quadrilaterals with two pairs of parallel sides.
+ * @see <a href="https://en.wikipedia.org/wiki/Parallelogram">Parallelogram</a>
  */
-public final class Boundaries2D {
+public final class Parallelogram {
 
-    /** Private constructor. */
-    private Boundaries2D() {
-    }
-
-    /** Return a {@link BoundarySource2D} instance containing the given segments.
-     * @param boundaries segments to include in the boundary source
-     * @return a boundary source containing the given boundaries
-     */
-    public static BoundarySource2D from(final Segment... boundaries) {
-        return from(Arrays.asList(boundaries));
-    }
-
-    /** Return a {@link BoundarySource2D} instance containing the given segments. The given
-     * collection is used directly as the source of the segments; no copy is made.
-     * @param boundaries segments to include in the boundary source
-     * @return a boundary source containing the given boundaries
+    /** Utility class; no instantiation.
      */
-    public static BoundarySource2D from(final Collection<Segment> boundaries) {
-        return () -> boundaries.stream();
+    private Parallelogram() {
     }
 
-    /** Return a {@link BoundarySource2D} instance defining an axis-aligned rectangular region. The points {@code a}
+    /** Return a {@link Polyline} defining an axis-aligned rectangle. The points {@code a}
      * and {@code b} are taken to represent opposite corner points in the rectangle and may be specified in
      * any order.
      * @param a first corner point in the rectangle (opposite of {@code b})
      * @param b second corner point in the rectangle (opposite of {@code a})
-     * @param precision precision context used to construct prism instances
-     * @return a boundary source defining the boundaries of the rectangular region
+     * @param precision precision context used to construct segment instances
+     * @return a polyline defining the axis-aligned rectangle
      * @throws IllegalArgumentException if the width or height of the defined rectangle is zero
      *      as evaluated by the precision context.
      */
-    public static BoundarySource2D rect(final Vector2D a, final Vector2D b,
+    public static Polyline axisAligned(final Vector2D a, final Vector2D b,
             final DoublePrecisionContext precision) {
 
         final double minX = Math.min(a.getX(), b.getX());
@@ -75,11 +62,9 @@ public final class Boundaries2D {
         final Vector2D upperRight = Vector2D.of(maxX, maxY);
         final Vector2D lowerRight = Vector2D.of(maxX, minY);
 
-        return from(
-                Segment.fromPoints(lowerLeft, lowerRight, precision),
-                Segment.fromPoints(upperRight, upperLeft, precision),
-                Segment.fromPoints(lowerRight, upperRight, precision),
-                Segment.fromPoints(upperLeft, lowerLeft, precision)
-            );
+        return Polyline.fromVertexLoop(Arrays.asList(
+                    lowerLeft, lowerRight,
+                    upperRight, upperLeft
+                ), precision);
     }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/shapes/package-info.java
similarity index 57%
copy from commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2D.java
copy to commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/shapes/package-info.java
index ade62b3..ad19dbe 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/shapes/package-info.java
@@ -14,22 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.commons.geometry.euclidean.twod;
-
-import org.apache.commons.geometry.core.partitioning.BoundarySource;
-
-/** Extension of the {@link BoundarySource} interface for Euclidean 2D
- * space.
+/**
+ * This package provides utilities for constructing basic 2D shapes.
  */
-public interface BoundarySource2D extends BoundarySource<Segment> {
-
-    /** Construct a new BSP tree from the boundaries contained in this
-     * instance.
-     * @return a new BSP tree constructed from the boundaries in this
-     *      instance
-     * @see RegionBSPTree2D#from(BoundarySource)
-     */
-    default RegionBSPTree2D toTree() {
-        return RegionBSPTree2D.from(this);
-    }
-}
+package org.apache.commons.geometry.euclidean.twod.shapes;
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
index 221820e..29cdf78 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
@@ -28,7 +28,6 @@ import org.apache.commons.geometry.euclidean.oned.Interval;
 import org.apache.commons.geometry.euclidean.oned.RegionBSPTree1D;
 import org.apache.commons.geometry.euclidean.oned.Vector1D;
 import org.apache.commons.geometry.euclidean.threed.AffineTransformMatrix3D;
-import org.apache.commons.geometry.euclidean.threed.Boundaries3D;
 import org.apache.commons.geometry.euclidean.threed.ConvexSubPlane;
 import org.apache.commons.geometry.euclidean.threed.Line3D;
 import org.apache.commons.geometry.euclidean.threed.LinecastPoint3D;
@@ -37,7 +36,7 @@ import org.apache.commons.geometry.euclidean.threed.RegionBSPTree3D;
 import org.apache.commons.geometry.euclidean.threed.Transform3D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
-import org.apache.commons.geometry.euclidean.twod.Boundaries2D;
+import org.apache.commons.geometry.euclidean.threed.shapes.Parallelepiped;
 import org.apache.commons.geometry.euclidean.twod.Line;
 import org.apache.commons.geometry.euclidean.twod.LinecastPoint2D;
 import org.apache.commons.geometry.euclidean.twod.Polyline;
@@ -45,6 +44,7 @@ import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
 import org.apache.commons.geometry.euclidean.twod.Segment;
 import org.apache.commons.geometry.euclidean.twod.Transform2D;
 import org.apache.commons.geometry.euclidean.twod.Vector2D;
+import org.apache.commons.geometry.euclidean.twod.shapes.Parallelogram;
 import org.apache.commons.numbers.angle.PlaneAngleRadians;
 import org.junit.Assert;
 import org.junit.Test;
@@ -63,9 +63,8 @@ public class DocumentationExamplesTest {
 
         // create a binary space partitioning tree representing the unit cube
         // centered on the origin
-        RegionBSPTree3D region = Boundaries3D.rect(
-                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), precision)
-                .toTree();
+        RegionBSPTree3D region = RegionBSPTree3D.from(
+                Parallelepiped.axisAligned(Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), precision));
 
         // create a rotated copy of the region
         Transform3D rotation = QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Z, 0.25 * Math.PI);
@@ -294,9 +293,9 @@ public class DocumentationExamplesTest {
     public void testLinecast2DExample() {
         DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
 
-        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(2, 1), precision).toTree();
+        Polyline path = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(2, 1), precision);
 
-        LinecastPoint2D pt = tree.linecastFirst(
+        LinecastPoint2D pt = path.linecastFirst(
                 Segment.fromPoints(Vector2D.of(1, 0.5), Vector2D.of(4, 0.5), precision));
 
         Vector2D intersection = pt.getPoint(); // (2.0, 0.5)
@@ -407,7 +406,8 @@ public class DocumentationExamplesTest {
     public void testLinecast3DExample() {
         DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
 
-        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 2, 3), precision).toTree();
+        RegionBSPTree3D tree = RegionBSPTree3D.from(
+                Parallelepiped.axisAligned(Vector3D.ZERO, Vector3D.of(1, 2, 3), precision));
 
         List<LinecastPoint3D> pts = tree.linecast(
                 Line3D.fromPoints(Vector3D.of(0.5, 0.5, -10), Vector3D.of(0.5, 0.5, 10), precision));
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3DTest.java
new file mode 100644
index 0000000..5c03f66
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3DTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.threed;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class BoundarySource3DTest {
+
+    private static final double TEST_EPS = 1e-10;
+
+    private static final DoublePrecisionContext TEST_PRECISION =
+            new EpsilonDoublePrecisionContext(TEST_EPS);
+
+    @Test
+    public void testFrom_varargs_empty() {
+        // act
+        BoundarySource3D src = BoundarySource3D.from();
+
+        // assert
+        List<ConvexSubPlane> segments = src.boundaryStream().collect(Collectors.toList());
+        Assert.assertEquals(0, segments.size());
+    }
+
+    @Test
+    public void testFrom_varargs() {
+        // act
+        ConvexSubPlane a = ConvexSubPlane.fromVertexLoop(
+                Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION);
+        ConvexSubPlane b = ConvexSubPlane.fromVertexLoop(
+                Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_Z), TEST_PRECISION);
+
+        BoundarySource3D src = BoundarySource3D.from(a, b);
+
+        // assert
+        List<ConvexSubPlane> boundaries = src.boundaryStream().collect(Collectors.toList());
+        Assert.assertEquals(2, boundaries.size());
+
+        Assert.assertSame(a, boundaries.get(0));
+        Assert.assertSame(b, boundaries.get(1));
+    }
+
+    @Test
+    public void testFrom_list_empty() {
+        // arrange
+        List<ConvexSubPlane> input = new ArrayList<>();
+
+        // act
+        BoundarySource3D src = BoundarySource3D.from(input);
+
+        // assert
+        List<ConvexSubPlane> segments = src.boundaryStream().collect(Collectors.toList());
+        Assert.assertEquals(0, segments.size());
+    }
+
+    @Test
+    public void testFrom_list() {
+        // act
+        ConvexSubPlane a = ConvexSubPlane.fromVertexLoop(
+                Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION);
+        ConvexSubPlane b = ConvexSubPlane.fromVertexLoop(
+                Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_Z), TEST_PRECISION);
+
+        List<ConvexSubPlane> input = new ArrayList<>();
+        input.add(a);
+        input.add(b);
+
+        BoundarySource3D src = BoundarySource3D.from(input);
+
+        // assert
+        List<ConvexSubPlane> segments = src.boundaryStream().collect(Collectors.toList());
+        Assert.assertEquals(2, segments.size());
+
+        Assert.assertSame(a, segments.get(0));
+        Assert.assertSame(b, segments.get(1));
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3DTest.java
index 021fe46..702ea6b 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3DTest.java
@@ -20,6 +20,7 @@ import java.util.Arrays;
 
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.shapes.Parallelepiped;
 import org.junit.Test;
 
 public class BoundarySourceLinecastWrapper3DTest {
@@ -30,7 +31,7 @@ public class BoundarySourceLinecastWrapper3DTest {
             new EpsilonDoublePrecisionContext(TEST_EPS);
 
     private static final BoundarySource3D UNIT_CUBE =
-            Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION);
+            BoundarySource3D.from(Parallelepiped.axisAligned(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION));
 
     @Test
     public void testLinecast_line_simple() {
@@ -98,7 +99,7 @@ public class BoundarySourceLinecastWrapper3DTest {
     @Test
     public void testLinecast_line_removesDuplicatePoints() {
         // arrange
-        BoundarySource3D src = Boundaries3D.from(
+        BoundarySource3D src = BoundarySource3D.from(
                     ConvexSubPlane.fromVertexLoop(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION),
                     ConvexSubPlane.fromVertexLoop(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X), TEST_PRECISION)
                 );
@@ -231,7 +232,7 @@ public class BoundarySourceLinecastWrapper3DTest {
     @Test
     public void testLinecast_segment_removesDuplicatePoints() {
         // arrange
-        BoundarySource3D src = Boundaries3D.from(
+        BoundarySource3D src = BoundarySource3D.from(
                     ConvexSubPlane.fromVertexLoop(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION),
                     ConvexSubPlane.fromVertexLoop(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X), TEST_PRECISION)
                 );
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java
index 4b3e8ec..e3e8c52 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java
@@ -31,6 +31,7 @@ import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
 import org.apache.commons.geometry.euclidean.threed.RegionBSPTree3D.RegionNode3D;
+import org.apache.commons.geometry.euclidean.threed.shapes.Parallelepiped;
 import org.apache.commons.numbers.angle.PlaneAngleRadians;
 import org.junit.Assert;
 import org.junit.Test;
@@ -125,7 +126,7 @@ public class RegionBSPTree3DTest {
     @Test
     public void testBoundaries() {
         // arrange
-        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION).toTree();
+        RegionBSPTree3D tree = createRect(Vector3D.ZERO, Vector3D.of(1, 1, 1));
 
         // act
         List<ConvexSubPlane> subplanes = new ArrayList<>();
@@ -138,7 +139,7 @@ public class RegionBSPTree3DTest {
     @Test
     public void testGetBoundaries() {
         // arrange
-        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION).toTree();
+        RegionBSPTree3D tree = createRect(Vector3D.ZERO, Vector3D.of(1, 1, 1));
 
         // act
         List<ConvexSubPlane> subplanes = tree.getBoundaries();
@@ -150,8 +151,7 @@ public class RegionBSPTree3DTest {
     @Test
     public void testBoundaryStream() {
         // arrange
-        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION)
-                .toTree();
+        RegionBSPTree3D tree = createRect(Vector3D.ZERO, Vector3D.of(1, 1, 1));
 
         // act
         List<ConvexSubPlane> subplanes = tree.boundaryStream().collect(Collectors.toList());
@@ -175,8 +175,7 @@ public class RegionBSPTree3DTest {
     @Test
     public void testToTree_returnsNewTree() {
         // arrange
-        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 2, 1), TEST_PRECISION)
-                .toTree();
+        RegionBSPTree3D tree = createRect(Vector3D.ZERO, Vector3D.of(1, 2, 1));
 
         // act
         RegionBSPTree3D result = tree.toTree();
@@ -211,6 +210,38 @@ public class RegionBSPTree3DTest {
     }
 
     @Test
+    public void testFrom_boundaries_noBoundaries() {
+        // act
+        RegionBSPTree3D tree = RegionBSPTree3D.from(Arrays.asList());
+
+        // assert
+        Assert.assertNull(tree.getBarycenter());
+        Assert.assertTrue(tree.isFull());
+        Assert.assertFalse(tree.isEmpty());
+    }
+
+    @Test
+    public void testFrom_boundaries() {
+        // act
+        RegionBSPTree3D tree = RegionBSPTree3D.from(Arrays.asList(
+                    ConvexSubPlane.fromVertexLoop(Arrays.asList(
+                            Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION),
+                    ConvexSubPlane.fromVertexLoop(Arrays.asList(
+                            Vector3D.ZERO, Vector3D.Unit.MINUS_Z, Vector3D.Unit.PLUS_X), TEST_PRECISION)
+                ));
+
+        // assert
+        Assert.assertFalse(tree.isFull());
+        Assert.assertFalse(tree.isEmpty());
+
+        EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.INSIDE,
+                Vector3D.of(1, 1, -1), Vector3D.of(-1, 1, -1));
+        EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.OUTSIDE,
+                Vector3D.of(1, 1, 1), Vector3D.of(-1, 1, 1), Vector3D.of(1, -1, 1),
+                Vector3D.of(-1, -1, 1), Vector3D.of(1, -1, -1), Vector3D.of(-1, -1, -1));
+    }
+
+    @Test
     public void testFromConvexVolume_full() {
         // arrange
         ConvexVolume volume = ConvexVolume.full();
@@ -303,8 +334,7 @@ public class RegionBSPTree3DTest {
     @Test
     public void testLinecast() {
         // arrange
-        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION)
-                .toTree();
+        RegionBSPTree3D tree = createRect(Vector3D.ZERO, Vector3D.of(1, 1, 1));
 
         // act/assert
         LinecastChecker3D.with(tree)
@@ -332,8 +362,7 @@ public class RegionBSPTree3DTest {
     @Test
     public void testLinecast_complementedTree() {
         // arrange
-        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION)
-                .toTree();
+        RegionBSPTree3D tree = createRect(Vector3D.ZERO, Vector3D.of(1, 1, 1));
 
         tree.complement();
 
@@ -364,19 +393,18 @@ public class RegionBSPTree3DTest {
     public void testLinecast_complexRegion() {
         // arrange
         RegionBSPTree3D a = RegionBSPTree3D.empty();
-        Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(0.5, 1, 1), TEST_PRECISION).boundaryStream()
+        Parallelepiped.axisAligned(Vector3D.ZERO, Vector3D.of(0.5, 1, 1), TEST_PRECISION).stream()
             .map(ConvexSubPlane::reverse)
             .forEach(a::insert);
         a.complement();
 
         RegionBSPTree3D b = RegionBSPTree3D.empty();
-        Boundaries3D.rect(Vector3D.of(0.5, 0, 0), Vector3D.of(1, 1, 1), TEST_PRECISION).boundaryStream()
+        Parallelepiped.axisAligned(Vector3D.of(0.5, 0, 0), Vector3D.of(1, 1, 1), TEST_PRECISION).stream()
             .map(ConvexSubPlane::reverse)
             .forEach(b::insert);
         b.complement();
 
-        RegionBSPTree3D c = Boundaries3D.rect(Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(1.5, 1.5, 1.5), TEST_PRECISION)
-                .toTree();
+        RegionBSPTree3D c = createRect(Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(1.5, 1.5, 1.5));
 
         RegionBSPTree3D tree = RegionBSPTree3D.empty();
         tree.union(a, b);
@@ -412,8 +440,7 @@ public class RegionBSPTree3DTest {
     @Test
     public void testLinecastFirst_multipleDirections() {
         // arrange
-        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.of(-1, -1, -1), Vector3D.of(1, 1, 1), TEST_PRECISION)
-                .toTree();
+        RegionBSPTree3D tree = createRect(Vector3D.of(-1, -1, -1), Vector3D.of(1, 1, 1));
 
         Line3D xPlus = Line3D.fromPoints(Vector3D.ZERO, Vector3D.of(1, 0, 0), TEST_PRECISION);
         Line3D xMinus = Line3D.fromPoints(Vector3D.ZERO, Vector3D.of(-1, 0, 0), TEST_PRECISION);
@@ -482,7 +509,7 @@ public class RegionBSPTree3DTest {
         Vector3D upperCorner = Vector3D.of(1, 1, 1);
         Vector3D center = lowerCorner.lerp(upperCorner, 0.5);
 
-        RegionBSPTree3D tree = Boundaries3D.rect(lowerCorner, upperCorner, TEST_PRECISION).toTree();
+        RegionBSPTree3D tree = createRect(lowerCorner, upperCorner);
 
         Line3D upDiagonal = Line3D.fromPoints(lowerCorner, upperCorner, TEST_PRECISION);
         Line3D downDiagonal = upDiagonal.reverse();
@@ -512,7 +539,7 @@ public class RegionBSPTree3DTest {
         Vector3D lowerCorner = Vector3D.ZERO;
         Vector3D upperCorner = Vector3D.of(1, 1, 1);
 
-        RegionBSPTree3D tree = Boundaries3D.rect(lowerCorner, upperCorner, TEST_PRECISION).toTree();
+        RegionBSPTree3D tree = createRect(lowerCorner, upperCorner);
 
         Vector3D firstPointOnLine = Vector3D.of(0.5, -1.0, 0);
         Vector3D secondPointOnLine = Vector3D.of(0.5, 2.0, 0);
@@ -539,7 +566,7 @@ public class RegionBSPTree3DTest {
         Vector3D lowerCorner = Vector3D.ZERO;
         Vector3D upperCorner = Vector3D.of(1, 1, 1);
 
-        RegionBSPTree3D tree = Boundaries3D.rect(lowerCorner, upperCorner, TEST_PRECISION).toTree();
+        RegionBSPTree3D tree = createRect(lowerCorner, upperCorner);
 
         Vector3D pt = Vector3D.of(0.5, 0.5, 0);
         Line3D intoBoxLine = Line3D.fromPoints(pt, pt.add(Vector3D.Unit.PLUS_Z), TEST_PRECISION);
@@ -559,7 +586,7 @@ public class RegionBSPTree3DTest {
         Vector3D lowerCorner = Vector3D.ZERO;
         Vector3D upperCorner = Vector3D.of(1, 1, 1);
 
-        RegionBSPTree3D tree = Boundaries3D.rect(lowerCorner, upperCorner, TEST_PRECISION).toTree();
+        RegionBSPTree3D tree = createRect(lowerCorner, upperCorner);
 
         Line3D intoBoxLine = Line3D.fromPoints(lowerCorner, upperCorner, TEST_PRECISION);
         Line3D outOfBoxLine = intoBoxLine.reverse();
@@ -578,7 +605,7 @@ public class RegionBSPTree3DTest {
         Vector3D lowerCorner = Vector3D.ZERO;
         Vector3D upperCorner = Vector3D.of(1, 1, 1);
 
-        RegionBSPTree3D tree = Boundaries3D.rect(lowerCorner, upperCorner, TEST_PRECISION).toTree();
+        RegionBSPTree3D tree = createRect(lowerCorner, upperCorner);
 
         Line3D line = Line3D.fromPointAndDirection(Vector3D.of(0.5, 0.5, 0.5), Vector3D.Unit.PLUS_X, TEST_PRECISION);
 
@@ -606,8 +633,7 @@ public class RegionBSPTree3DTest {
     @Test
     public void testInvertedRegion() {
         // arrange
-        RegionBSPTree3D tree = Boundaries3D.rect(
-                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), TEST_PRECISION).toTree();
+        RegionBSPTree3D tree = createRect(Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5));
 
         // act
         tree.complement();
@@ -632,8 +658,7 @@ public class RegionBSPTree3DTest {
     @Test
     public void testUnitBox() {
         // act
-        RegionBSPTree3D tree = Boundaries3D.rect(
-                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), TEST_PRECISION).toTree();
+        RegionBSPTree3D tree = createRect(Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5));
 
         // assert
         Assert.assertFalse(tree.isEmpty());
@@ -694,12 +719,8 @@ public class RegionBSPTree3DTest {
     public void testTwoBoxes_disjoint() {
         // act
         RegionBSPTree3D tree = RegionBSPTree3D.empty();
-        tree.union(Boundaries3D.rect(
-                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), TEST_PRECISION)
-                .toTree());
-        tree.union(Boundaries3D.rect(
-                Vector3D.of(1.5, -0.5, -0.5), Vector3D.of(2.5, 0.5, 0.5), TEST_PRECISION)
-                .toTree());
+        tree.union(createRect(Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5)));
+        tree.union(createRect(Vector3D.of(1.5, -0.5, -0.5), Vector3D.of(2.5, 0.5, 0.5)));
 
         // assert
         Assert.assertFalse(tree.isEmpty());
@@ -723,12 +744,8 @@ public class RegionBSPTree3DTest {
     public void testTwoBoxes_sharedSide() {
         // act
         RegionBSPTree3D tree = RegionBSPTree3D.empty();
-        tree.union(Boundaries3D.rect(
-                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), TEST_PRECISION)
-                .toTree());
-        tree.union(Boundaries3D.rect(
-                Vector3D.of(0.5, -0.5, -0.5), Vector3D.of(1.5, 0.5, 0.5), TEST_PRECISION)
-                .toTree());
+        tree.union(createRect(Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5)));
+        tree.union(createRect(Vector3D.of(0.5, -0.5, -0.5), Vector3D.of(1.5, 0.5, 0.5)));
 
         // assert
         Assert.assertFalse(tree.isEmpty());
@@ -755,12 +772,8 @@ public class RegionBSPTree3DTest {
 
         // act
         RegionBSPTree3D tree = RegionBSPTree3D.empty();
-        tree.union(Boundaries3D.rect(
-                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), precision)
-                .toTree());
-        tree.union(Boundaries3D.rect(
-                Vector3D.of(0.5 + 1e-7, -0.5, -0.5), Vector3D.of(1.5 + 1e-7, 0.5, 0.5), precision)
-                .toTree());
+        tree.union(createRect(Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), precision));
+        tree.union(createRect(Vector3D.of(0.5 + 1e-7, -0.5, -0.5), Vector3D.of(1.5 + 1e-7, 0.5, 0.5), precision));
 
         // assert
         Assert.assertFalse(tree.isEmpty());
@@ -783,12 +796,8 @@ public class RegionBSPTree3DTest {
     public void testTwoBoxes_sharedEdge() {
         // act
         RegionBSPTree3D tree = RegionBSPTree3D.empty();
-        tree.union(Boundaries3D.rect(
-                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), TEST_PRECISION)
-                .toTree());
-        tree.union(Boundaries3D.rect(
-                Vector3D.of(0.5, 0.5, -0.5), Vector3D.of(1.5, 1.5, 0.5), TEST_PRECISION)
-                .toTree());
+        tree.union(createRect(Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5)));
+        tree.union(createRect(Vector3D.of(0.5, 0.5, -0.5), Vector3D.of(1.5, 1.5, 0.5)));
 
         // assert
         Assert.assertFalse(tree.isEmpty());
@@ -814,12 +823,8 @@ public class RegionBSPTree3DTest {
     public void testTwoBoxes_sharedPoint() {
         // act
         RegionBSPTree3D tree = RegionBSPTree3D.empty();
-        tree.union(Boundaries3D.rect(
-                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), TEST_PRECISION)
-                .toTree());
-        tree.union(Boundaries3D.rect(
-                Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(1.5, 1.5, 1.5), TEST_PRECISION)
-                .toTree());
+        tree.union(createRect(Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5)));
+        tree.union(createRect(Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(1.5, 1.5, 1.5)));
 
         // assert
         Assert.assertFalse(tree.isEmpty());
@@ -921,8 +926,7 @@ public class RegionBSPTree3DTest {
     @Test
     public void testProjectToBoundary() {
         // arrange
-        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION)
-                .toTree();
+        RegionBSPTree3D tree = createRect(Vector3D.ZERO, Vector3D.of(1, 1, 1));
 
         // act/assert
         checkProject(tree, Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(0, 0.5, 0.5));
@@ -934,8 +938,7 @@ public class RegionBSPTree3DTest {
     @Test
     public void testProjectToBoundary_invertedRegion() {
         // arrange
-        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION)
-                .toTree();
+        RegionBSPTree3D tree = createRect(Vector3D.ZERO, Vector3D.of(1, 1, 1));
 
         tree.complement();
 
@@ -957,8 +960,7 @@ public class RegionBSPTree3DTest {
         double tolerance = 0.05;
         double size = 1.0;
         double radius = size * 0.5;
-        RegionBSPTree3D box = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(size, size, size), TEST_PRECISION)
-                .toTree();
+        RegionBSPTree3D box = createRect(Vector3D.ZERO, Vector3D.of(size, size, size));
         RegionBSPTree3D sphere = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
 
         // act
@@ -1038,8 +1040,7 @@ public class RegionBSPTree3DTest {
         double tolerance = 0.05;
         double size = 1.0;
         double radius = size * 0.5;
-        RegionBSPTree3D box = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(size, size, size), TEST_PRECISION)
-                .toTree();
+        RegionBSPTree3D box = createRect(Vector3D.ZERO, Vector3D.of(size, size, size));
         RegionBSPTree3D sphere = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
 
         // act
@@ -1115,11 +1116,8 @@ public class RegionBSPTree3DTest {
     public void testBoolean_xor_twoCubes() throws IOException {
         // arrange
         double size = 1.0;
-        RegionBSPTree3D box1 = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(size, size, size), TEST_PRECISION)
-                .toTree();
-        RegionBSPTree3D box2 = Boundaries3D.rect(
-                Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(0.5 + size, 0.5 + size, 0.5 + size), TEST_PRECISION)
-                .toTree();
+        RegionBSPTree3D box1 = createRect(Vector3D.ZERO, Vector3D.of(size, size, size));
+        RegionBSPTree3D box2 = createRect(Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(0.5 + size, 0.5 + size, 0.5 + size));
 
         // act
         RegionBSPTree3D result = RegionBSPTree3D.empty();
@@ -1156,8 +1154,7 @@ public class RegionBSPTree3DTest {
         double tolerance = 0.05;
         double size = 1.0;
         double radius = size * 0.5;
-        RegionBSPTree3D box = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(size, size, size), TEST_PRECISION)
-                .toTree();
+        RegionBSPTree3D box = createRect(Vector3D.ZERO, Vector3D.of(size, size, size));
         RegionBSPTree3D sphere = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
 
         // act
@@ -1233,8 +1230,7 @@ public class RegionBSPTree3DTest {
         double tolerance = 0.05;
         double size = 1.0;
         double radius = size * 0.5;
-        RegionBSPTree3D box = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(size, size, size), TEST_PRECISION)
-                .toTree();
+        RegionBSPTree3D box = createRect(Vector3D.ZERO, Vector3D.of(size, size, size));
         RegionBSPTree3D sphere = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
 
         // act
@@ -1308,8 +1304,7 @@ public class RegionBSPTree3DTest {
         double tolerance = 0.05;
         double size = 1.0;
         double radius = size * 0.5;
-        RegionBSPTree3D box = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(size, size, size), TEST_PRECISION)
-                .toTree();
+        RegionBSPTree3D box = createRect(Vector3D.ZERO, Vector3D.of(size, size, size));
         RegionBSPTree3D sphereToAdd = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
         RegionBSPTree3D sphereToRemove1 = createSphere(Vector3D.of(size * 0.5, 0, size * 0.5), radius, 8, 16);
         RegionBSPTree3D sphereToRemove2 = createSphere(Vector3D.of(size * 0.5, 1, size * 0.5), radius, 8, 16);
@@ -1358,9 +1353,7 @@ public class RegionBSPTree3DTest {
     @Test
     public void testToConvex_singleBox() {
         // arrange
-        RegionBSPTree3D tree = Boundaries3D.rect(
-                Vector3D.of(1, 2, 3), Vector3D.of(2, 3, 4), TEST_PRECISION)
-                .toTree();
+        RegionBSPTree3D tree = createRect(Vector3D.of(1, 2, 3), Vector3D.of(2, 3, 4));
 
         // act
         List<ConvexVolume> result = tree.toConvex();
@@ -1376,10 +1369,8 @@ public class RegionBSPTree3DTest {
     @Test
     public void testToConvex_multipleBoxes() {
         // arrange
-        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.of(4, 5, 6), Vector3D.of(5, 6, 7), TEST_PRECISION)
-                .toTree();
-        tree.union(Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(2, 1, 1), TEST_PRECISION)
-                .toTree());
+        RegionBSPTree3D tree = createRect(Vector3D.of(4, 5, 6), Vector3D.of(5, 6, 7));
+        tree.union(createRect(Vector3D.ZERO, Vector3D.of(2, 1, 1)));
 
         // act
         List<ConvexVolume> result = tree.toConvex();
@@ -1402,9 +1393,7 @@ public class RegionBSPTree3DTest {
     @Test
     public void testSplit() {
         // arrange
-        RegionBSPTree3D tree = Boundaries3D.rect(
-                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), TEST_PRECISION)
-                .toTree();
+        RegionBSPTree3D tree = createRect(Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5));
 
         Plane splitter = Plane.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION);
 
@@ -1426,8 +1415,7 @@ public class RegionBSPTree3DTest {
     @Test
     public void testGetNodeRegion() {
         // arrange
-        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION)
-                .toTree();
+        RegionBSPTree3D tree = createRect(Vector3D.ZERO, Vector3D.of(1, 1, 1));
 
         // act/assert
         ConvexVolume rootVol = tree.getRoot().getNodeRegion();
@@ -1504,6 +1492,15 @@ public class RegionBSPTree3DTest {
         return boundaries;
     }
 
+    private static RegionBSPTree3D createRect(final Vector3D a, final Vector3D b) {
+        return createRect(a, b, TEST_PRECISION);
+    }
+
+    private static RegionBSPTree3D createRect(final Vector3D a, final Vector3D b, final DoublePrecisionContext precision) {
+        return RegionBSPTree3D.from(
+                Parallelepiped.axisAligned(a, b, precision));
+    }
+
     private static RegionBSPTree3D createSphere(final Vector3D center, final double radius, final int stacks, final int slices) {
 
         final List<Plane> planes = new ArrayList<>();
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SubPlaneTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SubPlaneTest.java
index 99945f9..20914a3 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SubPlaneTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SubPlaneTest.java
@@ -32,11 +32,11 @@ import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
 import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
-import org.apache.commons.geometry.euclidean.twod.Boundaries2D;
 import org.apache.commons.geometry.euclidean.twod.ConvexArea;
 import org.apache.commons.geometry.euclidean.twod.Line;
 import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
 import org.apache.commons.geometry.euclidean.twod.Vector2D;
+import org.apache.commons.geometry.euclidean.twod.shapes.Parallelogram;
 import org.apache.commons.numbers.angle.PlaneAngleRadians;
 import org.junit.Assert;
 import org.junit.Test;
@@ -182,7 +182,8 @@ public class SubPlaneTest {
     public void testSplit_both() {
         // arrange
         SubPlane sp = new SubPlane(XY_PLANE, false);
-        sp.getSubspaceRegion().union(Boundaries2D.rect(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
+        sp.getSubspaceRegion().union(
+                Parallelogram.axisAligned(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
 
         Plane splitter = Plane.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION);
 
@@ -209,7 +210,8 @@ public class SubPlaneTest {
     public void testSplit_intersects_plusOnly() {
         // arrange
         SubPlane sp = new SubPlane(XY_PLANE, false);
-        sp.getSubspaceRegion().union(Boundaries2D.rect(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
+        sp.getSubspaceRegion().union(
+                Parallelogram.axisAligned(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
 
         Plane splitter = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0.1, 0, 1), TEST_PRECISION);
 
@@ -227,7 +229,8 @@ public class SubPlaneTest {
     public void testSplit_intersects_minusOnly() {
         // arrange
         SubPlane sp = new SubPlane(XY_PLANE, false);
-        sp.getSubspaceRegion().union(Boundaries2D.rect(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
+        sp.getSubspaceRegion().union(
+                Parallelogram.axisAligned(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
 
         Plane splitter = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0.1, 0, -1), TEST_PRECISION);
 
@@ -245,7 +248,8 @@ public class SubPlaneTest {
     public void testSplit_parallel_plusOnly() {
         // arrange
         SubPlane sp = new SubPlane(XY_PLANE, false);
-        sp.getSubspaceRegion().union(Boundaries2D.rect(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
+        sp.getSubspaceRegion().union(
+                Parallelogram.axisAligned(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
 
         Plane splitter = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_Z, TEST_PRECISION);
 
@@ -263,7 +267,8 @@ public class SubPlaneTest {
     public void testSplit_parallel_minusOnly() {
         // arrange
         SubPlane sp = new SubPlane(XY_PLANE, false);
-        sp.getSubspaceRegion().union(Boundaries2D.rect(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
+        sp.getSubspaceRegion().union(
+                Parallelogram.axisAligned(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
 
         Plane splitter = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.Unit.MINUS_Z, TEST_PRECISION);
 
@@ -281,7 +286,8 @@ public class SubPlaneTest {
     public void testSplit_coincident() {
         // arrange
         SubPlane sp = new SubPlane(XY_PLANE, false);
-        sp.getSubspaceRegion().union(Boundaries2D.rect(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
+        sp.getSubspaceRegion().union(
+                Parallelogram.axisAligned(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
 
         // act
         Split<SubPlane> split = sp.split(sp.getPlane());
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Boundaries3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/shapes/ParallelepipedTest.java
similarity index 54%
rename from commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Boundaries3DTest.java
rename to commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/shapes/ParallelepipedTest.java
index d44c728..49769bf 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Boundaries3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/shapes/ParallelepipedTest.java
@@ -14,21 +14,22 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.commons.geometry.euclidean.threed;
+package org.apache.commons.geometry.euclidean.threed.shapes;
 
-import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
-import java.util.stream.Collectors;
 
 import org.apache.commons.geometry.core.GeometryTestUtils;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.threed.BoundarySource3D;
+import org.apache.commons.geometry.euclidean.threed.ConvexSubPlane;
+import org.apache.commons.geometry.euclidean.threed.RegionBSPTree3D;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
 import org.junit.Assert;
 import org.junit.Test;
 
-public class Boundaries3DTest {
+public class ParallelepipedTest {
 
     private static final double TEST_EPS = 1e-10;
 
@@ -36,74 +37,10 @@ public class Boundaries3DTest {
             new EpsilonDoublePrecisionContext(TEST_EPS);
 
     @Test
-    public void testFrom_varargs_empty() {
+    public void testAxisAligned_minFirst() {
         // act
-        BoundarySource3D src = Boundaries3D.from();
-
-        // assert
-        List<ConvexSubPlane> segments = src.boundaryStream().collect(Collectors.toList());
-        Assert.assertEquals(0, segments.size());
-    }
-
-    @Test
-    public void testFrom_varargs() {
-        // act
-        ConvexSubPlane a = ConvexSubPlane.fromVertexLoop(
-                Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION);
-        ConvexSubPlane b = ConvexSubPlane.fromVertexLoop(
-                Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_Z), TEST_PRECISION);
-
-        BoundarySource3D src = Boundaries3D.from(a, b);
-
-        // assert
-        List<ConvexSubPlane> boundaries = src.boundaryStream().collect(Collectors.toList());
-        Assert.assertEquals(2, boundaries.size());
-
-        Assert.assertSame(a, boundaries.get(0));
-        Assert.assertSame(b, boundaries.get(1));
-    }
-
-    @Test
-    public void testFrom_list_empty() {
-        // arrange
-        List<ConvexSubPlane> input = new ArrayList<>();
-
-        // act
-        BoundarySource3D src = Boundaries3D.from(input);
-
-        // assert
-        List<ConvexSubPlane> segments = src.boundaryStream().collect(Collectors.toList());
-        Assert.assertEquals(0, segments.size());
-    }
-
-    @Test
-    public void testFrom_list() {
-        // act
-        ConvexSubPlane a = ConvexSubPlane.fromVertexLoop(
-                Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION);
-        ConvexSubPlane b = ConvexSubPlane.fromVertexLoop(
-                Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_Z), TEST_PRECISION);
-
-        List<ConvexSubPlane> input = new ArrayList<>();
-        input.add(a);
-        input.add(b);
-
-        BoundarySource3D src = Boundaries3D.from(input);
-
-        // assert
-        List<ConvexSubPlane> segments = src.boundaryStream().collect(Collectors.toList());
-        Assert.assertEquals(2, segments.size());
-
-        Assert.assertSame(a, segments.get(0));
-        Assert.assertSame(b, segments.get(1));
-    }
-
-    @Test
-    public void testRect_minFirst() {
-        // act
-        List<ConvexSubPlane> boundaries = Boundaries3D.rect(Vector3D.of(1, 2, 3), Vector3D.of(4, 5, 6), TEST_PRECISION)
-                .boundaryStream()
-                .collect(Collectors.toList());
+        List<ConvexSubPlane> boundaries =
+                Parallelepiped.axisAligned(Vector3D.of(1, 2, 3), Vector3D.of(4, 5, 6), TEST_PRECISION);
 
         // assert
         Assert.assertEquals(6, boundaries.size());
@@ -129,11 +66,10 @@ public class Boundaries3DTest {
     }
 
     @Test
-    public void testRect_maxFirst() {
+    public void testAxisAligned_maxFirst() {
         // act
-        List<ConvexSubPlane> boundaries = Boundaries3D.rect(Vector3D.of(4, 5, 6), Vector3D.of(1, 2, 3), TEST_PRECISION)
-                .boundaryStream()
-                .collect(Collectors.toList());
+        List<ConvexSubPlane> boundaries =
+                Parallelepiped.axisAligned(Vector3D.of(4, 5, 6), Vector3D.of(1, 2, 3), TEST_PRECISION);
 
         // assert
         Assert.assertEquals(6, boundaries.size());
@@ -159,9 +95,10 @@ public class Boundaries3DTest {
     }
 
     @Test
-    public void testRect_toTree() {
+    public void testAxisAligned_toTree() {
         // arrange
-        BoundarySource3D src = Boundaries3D.rect(Vector3D.of(1, 2, 3), Vector3D.of(4, 5, 6), TEST_PRECISION);
+        BoundarySource3D src = BoundarySource3D.from(
+                Parallelepiped.axisAligned(Vector3D.of(1, 2, 3), Vector3D.of(4, 5, 6), TEST_PRECISION));
 
         // act
         RegionBSPTree3D tree = src.toTree();
@@ -172,18 +109,18 @@ public class Boundaries3DTest {
     }
 
     @Test
-    public void testRect_illegalArgs() {
+    public void testAxisAligned_illegalArgs() {
         // act/assert
         GeometryTestUtils.assertThrows(() -> {
-            Boundaries3D.rect(Vector3D.of(1, 2, 3), Vector3D.of(1, 5, 6), TEST_PRECISION);
+            Parallelepiped.axisAligned(Vector3D.of(1, 2, 3), Vector3D.of(1, 5, 6), TEST_PRECISION);
         }, IllegalArgumentException.class);
 
         GeometryTestUtils.assertThrows(() -> {
-            Boundaries3D.rect(Vector3D.of(1, 2, 3), Vector3D.of(4, 2, 6), TEST_PRECISION);
+            Parallelepiped.axisAligned(Vector3D.of(1, 2, 3), Vector3D.of(4, 2, 6), TEST_PRECISION);
         }, IllegalArgumentException.class);
 
         GeometryTestUtils.assertThrows(() -> {
-            Boundaries3D.rect(Vector3D.of(1, 2, 3), Vector3D.of(1, 5, 3), TEST_PRECISION);
+            Parallelepiped.axisAligned(Vector3D.of(1, 2, 3), Vector3D.of(1, 5, 3), TEST_PRECISION);
         }, IllegalArgumentException.class);
     }
 
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Boundaries2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Boundaries2DTest.java
deleted file mode 100644
index 7c4bdff..0000000
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Boundaries2DTest.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.commons.geometry.euclidean.twod;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import org.apache.commons.geometry.core.GeometryTestUtils;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class Boundaries2DTest {
-
-    private static final double TEST_EPS = 1e-10;
-
-    private static final DoublePrecisionContext TEST_PRECISION =
-            new EpsilonDoublePrecisionContext(TEST_EPS);
-
-    @Test
-    public void testFrom_varargs_empty() {
-        // act
-        BoundarySource2D src = Boundaries2D.from();
-
-        // assert
-        List<Segment> segments = src.boundaryStream().collect(Collectors.toList());
-        Assert.assertEquals(0, segments.size());
-    }
-
-    @Test
-    public void testFrom_varargs() {
-        // act
-        Segment a = Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
-        Segment b = Segment.fromPoints(Vector2D.Unit.PLUS_X, Vector2D.of(1, 1), TEST_PRECISION);
-
-        BoundarySource2D src = Boundaries2D.from(a, b);
-
-        // assert
-        List<Segment> segments = src.boundaryStream().collect(Collectors.toList());
-        Assert.assertEquals(2, segments.size());
-
-        Assert.assertSame(a, segments.get(0));
-        Assert.assertSame(b, segments.get(1));
-    }
-
-    @Test
-    public void testFrom_list_empty() {
-        // arrange
-        List<Segment> input = new ArrayList<>();
-
-        // act
-        BoundarySource2D src = Boundaries2D.from(input);
-
-        // assert
-        List<Segment> segments = src.boundaryStream().collect(Collectors.toList());
-        Assert.assertEquals(0, segments.size());
-    }
-
-    @Test
-    public void testFrom_list() {
-        // act
-        Segment a = Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
-        Segment b = Segment.fromPoints(Vector2D.Unit.PLUS_X, Vector2D.of(1, 1), TEST_PRECISION);
-
-        List<Segment> input = new ArrayList<>();
-        input.add(a);
-        input.add(b);
-
-        BoundarySource2D src = Boundaries2D.from(input);
-
-        // assert
-        List<Segment> segments = src.boundaryStream().collect(Collectors.toList());
-        Assert.assertEquals(2, segments.size());
-
-        Assert.assertSame(a, segments.get(0));
-        Assert.assertSame(b, segments.get(1));
-    }
-
-    @Test
-    public void testRect_minFirst() {
-        // act
-        List<Segment> segments = Boundaries2D.rect(Vector2D.of(1, 2), Vector2D.of(3, 4), TEST_PRECISION)
-                .boundaryStream()
-                .collect(Collectors.toList());
-
-        // assert
-        Assert.assertEquals(4, segments.size());
-
-        assertSegment(segments.get(0), Vector2D.of(1, 2), Vector2D.of(3, 2));
-        assertSegment(segments.get(1), Vector2D.of(3, 4), Vector2D.of(1, 4));
-        assertSegment(segments.get(2), Vector2D.of(3, 2), Vector2D.of(3, 4));
-        assertSegment(segments.get(3), Vector2D.of(1, 4), Vector2D.of(1, 2));
-    }
-
-    @Test
-    public void testRect_maxFirst() {
-        // act
-        List<Segment> segments = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(-1, -2), TEST_PRECISION)
-                .boundaryStream()
-                .collect(Collectors.toList());
-
-        // assert
-        Assert.assertEquals(4, segments.size());
-
-        assertSegment(segments.get(0), Vector2D.of(-1, -2), Vector2D.of(0, -2));
-        assertSegment(segments.get(1), Vector2D.ZERO, Vector2D.of(-1, 0));
-        assertSegment(segments.get(2), Vector2D.of(0, -2), Vector2D.ZERO);
-        assertSegment(segments.get(3), Vector2D.of(-1, 0), Vector2D.of(-1, -2));
-    }
-
-    @Test
-    public void testRect_toTree() {
-        // act
-        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 4), TEST_PRECISION).toTree();
-
-        // assert
-        Assert.assertFalse(tree.isFull());
-        Assert.assertFalse(tree.isEmpty());
-
-        Assert.assertEquals(4, tree.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 2), tree.getBarycenter(), TEST_EPS);
-    }
-
-    @Test
-    public void testRect_illegalArgs() {
-        // act/assert
-        GeometryTestUtils.assertThrows(() -> {
-            Boundaries2D.rect(Vector2D.of(1, 1), Vector2D.of(1, 3), TEST_PRECISION);
-        }, IllegalArgumentException.class);
-
-        GeometryTestUtils.assertThrows(() -> {
-            Boundaries2D.rect(Vector2D.of(1, 1), Vector2D.of(3, 1), TEST_PRECISION);
-        }, IllegalArgumentException.class);
-
-        GeometryTestUtils.assertThrows(() -> {
-            Boundaries2D.rect(Vector2D.of(2, 3), Vector2D.of(2, 3), TEST_PRECISION);
-        }, IllegalArgumentException.class);
-    }
-
-    private static void assertSegment(Segment segment, Vector2D start, Vector2D end) {
-        EuclideanTestUtils.assertCoordinatesEqual(start, segment.getStartPoint(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(end, segment.getEndPoint(), TEST_EPS);
-    }
-}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2DTest.java
new file mode 100644
index 0000000..476caf9
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2DTest.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.twod;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class BoundarySource2DTest {
+
+    private static final double TEST_EPS = 1e-10;
+
+    private static final DoublePrecisionContext TEST_PRECISION =
+            new EpsilonDoublePrecisionContext(TEST_EPS);
+
+    @Test
+    public void testFrom_varargs_empty() {
+        // act
+        BoundarySource2D src = BoundarySource2D.from();
+
+        // assert
+        List<Segment> segments = src.boundaryStream().collect(Collectors.toList());
+        Assert.assertEquals(0, segments.size());
+    }
+
+    @Test
+    public void testFrom_varargs() {
+        // act
+        Segment a = Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+        Segment b = Segment.fromPoints(Vector2D.Unit.PLUS_X, Vector2D.of(1, 1), TEST_PRECISION);
+
+        BoundarySource2D src = BoundarySource2D.from(a, b);
+
+        // assert
+        List<Segment> segments = src.boundaryStream().collect(Collectors.toList());
+        Assert.assertEquals(2, segments.size());
+
+        Assert.assertSame(a, segments.get(0));
+        Assert.assertSame(b, segments.get(1));
+    }
+
+    @Test
+    public void testFrom_list_empty() {
+        // arrange
+        List<Segment> input = new ArrayList<>();
+
+        // act
+        BoundarySource2D src = BoundarySource2D.from(input);
+
+        // assert
+        List<Segment> segments = src.boundaryStream().collect(Collectors.toList());
+        Assert.assertEquals(0, segments.size());
+    }
+
+    @Test
+    public void testFrom_list() {
+        // act
+        Segment a = Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+        Segment b = Segment.fromPoints(Vector2D.Unit.PLUS_X, Vector2D.of(1, 1), TEST_PRECISION);
+
+        List<Segment> input = new ArrayList<>();
+        input.add(a);
+        input.add(b);
+
+        BoundarySource2D src = BoundarySource2D.from(input);
+
+        // assert
+        List<Segment> segments = src.boundaryStream().collect(Collectors.toList());
+        Assert.assertEquals(2, segments.size());
+
+        Assert.assertSame(a, segments.get(0));
+        Assert.assertSame(b, segments.get(1));
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2DTest.java
index b729dea..1a61782 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2DTest.java
@@ -18,6 +18,7 @@ package org.apache.commons.geometry.euclidean.twod;
 
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.twod.shapes.Parallelogram;
 import org.junit.Test;
 
 public class BoundarySourceLinecastWrapper2DTest {
@@ -28,7 +29,7 @@ public class BoundarySourceLinecastWrapper2DTest {
             new EpsilonDoublePrecisionContext(TEST_EPS);
 
     private static final BoundarySource2D UNIT_SQUARE =
-            Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION);
+            Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION);
 
     @Test
     public void testLinecast_line_simple() {
@@ -91,7 +92,7 @@ public class BoundarySourceLinecastWrapper2DTest {
     @Test
     public void testLinecast_line_removesDuplicatePoints() {
         // arrange
-        BoundarySource2D src = Boundaries2D.from(
+        BoundarySource2D src = BoundarySource2D.from(
                     Segment.fromPoints(Vector2D.of(-1, -1), Vector2D.ZERO, TEST_PRECISION),
                     Segment.fromPoints(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
                 );
@@ -219,7 +220,7 @@ public class BoundarySourceLinecastWrapper2DTest {
     @Test
     public void testLinecast_segment_removesDuplicatePoints() {
         // arrange
-        BoundarySource2D src = Boundaries2D.from(
+        BoundarySource2D src = BoundarySource2D.from(
                     Segment.fromPoints(Vector2D.of(-1, -1), Vector2D.ZERO, TEST_PRECISION),
                     Segment.fromPoints(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
                 );
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java
index e63b15f..a1f84d6 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java
@@ -32,6 +32,7 @@ import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
 import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D.RegionNode2D;
+import org.apache.commons.geometry.euclidean.twod.shapes.Parallelogram;
 import org.apache.commons.numbers.angle.PlaneAngleRadians;
 import org.junit.Assert;
 import org.junit.Test;
@@ -122,7 +123,7 @@ public class RegionBSPTree2DTest {
     @Test
     public void testBoundaries() {
         // arrange
-        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
+        RegionBSPTree2D tree = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
                 .toTree();
 
         // act
@@ -136,7 +137,7 @@ public class RegionBSPTree2DTest {
     @Test
     public void testGetBoundaries() {
         // arrange
-        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
+        RegionBSPTree2D tree = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
                 .toTree();
 
         // act
@@ -149,7 +150,7 @@ public class RegionBSPTree2DTest {
     @Test
     public void testBoundaryStream() {
         // arrange
-        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
+        RegionBSPTree2D tree = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
                 .toTree();
 
         // act
@@ -313,7 +314,7 @@ public class RegionBSPTree2DTest {
     public void testToConvex_square() {
         // arrange
         RegionBSPTree2D tree = RegionBSPTree2D.from(
-                Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION));
+                Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION));
 
         // act
         List<ConvexArea> result = tree.toConvex();
@@ -473,7 +474,7 @@ public class RegionBSPTree2DTest {
     @Test
     public void testSplit_bothSides() {
         // arrange
-        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(2, 1), TEST_PRECISION)
+        RegionBSPTree2D tree = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(2, 1), TEST_PRECISION)
                 .toTree();
 
         Line splitter = Line.fromPointAndAngle(Vector2D.ZERO, 0.25 * PlaneAngleRadians.PI, TEST_PRECISION);
@@ -498,7 +499,7 @@ public class RegionBSPTree2DTest {
     @Test
     public void testSplit_plusSideOnly() {
         // arrange
-        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(2, 1), TEST_PRECISION)
+        RegionBSPTree2D tree = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(2, 1), TEST_PRECISION)
                 .toTree();
 
         Line splitter = Line.fromPointAndAngle(Vector2D.of(0, 1), 0.25 * PlaneAngleRadians.PI, TEST_PRECISION);
@@ -520,7 +521,7 @@ public class RegionBSPTree2DTest {
     @Test
     public void testSplit_minusSideOnly() {
         // arrange
-        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(2, 1), TEST_PRECISION)
+        RegionBSPTree2D tree = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(2, 1), TEST_PRECISION)
                 .toTree();
 
         Line splitter = Line.fromPointAndAngle(Vector2D.of(0, 1), 0.25 * PlaneAngleRadians.PI, TEST_PRECISION)
@@ -767,9 +768,9 @@ public class RegionBSPTree2DTest {
     @Test
     public void testGeometricProperties_regionWithHole() {
         // arrange
-        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(3, 3), TEST_PRECISION)
+        RegionBSPTree2D tree = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(3, 3), TEST_PRECISION)
                 .toTree();
-        RegionBSPTree2D inner = Boundaries2D.rect(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION)
+        RegionBSPTree2D inner = Parallelogram.axisAligned(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION)
                 .toTree();
 
         tree.difference(inner);
@@ -806,9 +807,9 @@ public class RegionBSPTree2DTest {
     @Test
     public void testGeometricProperties_complementedRegionWithHole() {
         // arrange
-        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(3, 3), TEST_PRECISION)
+        RegionBSPTree2D tree = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(3, 3), TEST_PRECISION)
                 .toTree();
-        RegionBSPTree2D inner = Boundaries2D.rect(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION)
+        RegionBSPTree2D inner = Parallelogram.axisAligned(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION)
                 .toTree();
 
         tree.difference(inner);
@@ -845,9 +846,38 @@ public class RegionBSPTree2DTest {
     }
 
     @Test
+    public void testFrom_boundaries_noBoundaries() {
+        // act
+        RegionBSPTree2D tree = RegionBSPTree2D.from(Arrays.asList());
+
+        // assert
+        Assert.assertNull(tree.getBarycenter());
+        Assert.assertTrue(tree.isFull());
+        Assert.assertFalse(tree.isEmpty());
+    }
+
+    @Test
+    public void testFrom_boundaries() {
+        // act
+        RegionBSPTree2D tree = RegionBSPTree2D.from(Arrays.asList(
+                    Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION).span(),
+                    Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_Y, TEST_PRECISION)
+                        .segmentFrom(Vector2D.ZERO)
+                ));
+
+        // assert
+        Assert.assertFalse(tree.isFull());
+        Assert.assertFalse(tree.isEmpty());
+
+        checkClassify(tree, RegionLocation.INSIDE, Vector2D.of(-1, 1));
+        checkClassify(tree, RegionLocation.OUTSIDE,
+                Vector2D.of(1, 1), Vector2D.of(1, -1), Vector2D.of(-1, -1));
+    }
+
+    @Test
     public void testFrom_boundarySource_noBoundaries() {
         // arrange
-        BoundarySource2D src = Boundaries2D.from();
+        BoundarySource2D src = BoundarySource2D.from();
 
         // act
         RegionBSPTree2D tree = RegionBSPTree2D.from(src);
@@ -855,6 +885,7 @@ public class RegionBSPTree2DTest {
         // assert
         Assert.assertNull(tree.getBarycenter());
         Assert.assertTrue(tree.isFull());
+        Assert.assertFalse(tree.isEmpty());
     }
 
     @Test
@@ -915,7 +946,7 @@ public class RegionBSPTree2DTest {
     @Test
     public void testToTree_returnsNewTree() {
         // arrange
-        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 2), TEST_PRECISION).toTree();
+        RegionBSPTree2D tree = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(1, 2), TEST_PRECISION).toTree();
 
         // act
         RegionBSPTree2D result = tree.toTree();
@@ -951,7 +982,7 @@ public class RegionBSPTree2DTest {
     @Test
     public void testProject_rect() {
         // arrange
-        RegionBSPTree2D tree = Boundaries2D.rect(
+        RegionBSPTree2D tree = Parallelogram.axisAligned(
                     Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION).toTree();
 
         // act/assert
@@ -1005,7 +1036,7 @@ public class RegionBSPTree2DTest {
     @Test
     public void testLinecast() {
         // arrange
-        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
+        RegionBSPTree2D tree = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
                 .toTree();
 
         // act/assert
@@ -1029,7 +1060,7 @@ public class RegionBSPTree2DTest {
     @Test
     public void testLinecast_complementedTree() {
         // arrange
-        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
+        RegionBSPTree2D tree = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
                 .toTree();
 
         tree.complement();
@@ -1103,7 +1134,7 @@ public class RegionBSPTree2DTest {
     @Test
     public void testTransform() {
         // arrange
-        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.of(1, 1), Vector2D.of(3, 2), TEST_PRECISION)
+        RegionBSPTree2D tree = Parallelogram.axisAligned(Vector2D.of(1, 1), Vector2D.of(3, 2), TEST_PRECISION)
                 .toTree();
 
         AffineTransformMatrix2D transform = AffineTransformMatrix2D.createScale(0.5, 2)
@@ -1172,7 +1203,7 @@ public class RegionBSPTree2DTest {
     @Test
     public void testTransform_reflection() {
         // arrange
-        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION).toTree();
+        RegionBSPTree2D tree = Parallelogram.axisAligned(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION).toTree();
 
         Transform2D transform = FunctionTransform2D.from(v -> Vector2D.of(-v.getX(), v.getY()));
 
@@ -1194,7 +1225,7 @@ public class RegionBSPTree2DTest {
     @Test
     public void testTransform_doubleReflection() {
         // arrange
-        RegionBSPTree2D tree = Boundaries2D.rect(
+        RegionBSPTree2D tree = Parallelogram.axisAligned(
                     Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION).toTree();
 
         Transform2D transform = FunctionTransform2D.from(Vector2D::negate);
@@ -1217,18 +1248,18 @@ public class RegionBSPTree2DTest {
     @Test
     public void testBooleanOperations() {
         // arrange
-        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(3, 3), TEST_PRECISION).toTree();
+        RegionBSPTree2D tree = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(3, 3), TEST_PRECISION).toTree();
         RegionBSPTree2D temp;
 
         // act
-        temp = Boundaries2D.rect(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION).toTree();
+        temp = Parallelogram.axisAligned(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION).toTree();
         temp.complement();
         tree.intersection(temp);
 
-        temp = Boundaries2D.rect(Vector2D.of(3, 0), Vector2D.of(6, 3), TEST_PRECISION).toTree();
+        temp = Parallelogram.axisAligned(Vector2D.of(3, 0), Vector2D.of(6, 3), TEST_PRECISION).toTree();
         tree.union(temp);
 
-        temp = Boundaries2D.rect(Vector2D.of(2, 1), Vector2D.of(5, 2), TEST_PRECISION).toTree();
+        temp = Parallelogram.axisAligned(Vector2D.of(2, 1), Vector2D.of(5, 2), TEST_PRECISION).toTree();
         tree.difference(temp);
 
         temp.setFull();
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/shapes/ParallelogramTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/shapes/ParallelogramTest.java
new file mode 100644
index 0000000..748e409
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/shapes/ParallelogramTest.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.twod.shapes;
+
+import java.util.List;
+
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.twod.Polyline;
+import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
+import org.apache.commons.geometry.euclidean.twod.Segment;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ParallelogramTest {
+
+    private static final double TEST_EPS = 1e-10;
+
+    private static final DoublePrecisionContext TEST_PRECISION =
+            new EpsilonDoublePrecisionContext(TEST_EPS);
+
+    @Test
+    public void testAxisAligned_minFirst() {
+        // act
+        Polyline path = Parallelogram.axisAligned(Vector2D.of(1, 2), Vector2D.of(3, 4), TEST_PRECISION);
+
+        // assert
+        List<Segment> segments = path.getSegments();
+        Assert.assertEquals(4, segments.size());
+
+        assertSegment(segments.get(0), Vector2D.of(1, 2), Vector2D.of(3, 2));
+        assertSegment(segments.get(1), Vector2D.of(3, 2), Vector2D.of(3, 4));
+        assertSegment(segments.get(2), Vector2D.of(3, 4), Vector2D.of(1, 4));
+        assertSegment(segments.get(3), Vector2D.of(1, 4), Vector2D.of(1, 2));
+    }
+
+    @Test
+    public void testAxisAligned_maxFirst() {
+        // act
+        Polyline path = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(-1, -2), TEST_PRECISION);
+
+        // assert
+        List<Segment> segments = path.getSegments();
+        Assert.assertEquals(4, segments.size());
+
+        assertSegment(segments.get(0), Vector2D.of(-1, -2), Vector2D.of(0, -2));
+        assertSegment(segments.get(1), Vector2D.of(0, -2), Vector2D.ZERO);
+        assertSegment(segments.get(2), Vector2D.ZERO, Vector2D.of(-1, 0));
+        assertSegment(segments.get(3), Vector2D.of(-1, 0), Vector2D.of(-1, -2));
+    }
+
+    @Test
+    public void testAxisAligned_toTree() {
+        // act
+        RegionBSPTree2D tree = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(1, 4), TEST_PRECISION)
+                .toTree();
+
+        // assert
+        Assert.assertFalse(tree.isFull());
+        Assert.assertFalse(tree.isEmpty());
+
+        Assert.assertEquals(4, tree.getSize(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 2), tree.getBarycenter(), TEST_EPS);
+    }
+
+    @Test
+    public void testAxisAligned_illegalArgs() {
+        // act/assert
+        GeometryTestUtils.assertThrows(() -> {
+            Parallelogram.axisAligned(Vector2D.of(1, 1), Vector2D.of(1, 3), TEST_PRECISION);
+        }, IllegalArgumentException.class);
+
+        GeometryTestUtils.assertThrows(() -> {
+            Parallelogram.axisAligned(Vector2D.of(1, 1), Vector2D.of(3, 1), TEST_PRECISION);
+        }, IllegalArgumentException.class);
+
+        GeometryTestUtils.assertThrows(() -> {
+            Parallelogram.axisAligned(Vector2D.of(2, 3), Vector2D.of(2, 3), TEST_PRECISION);
+        }, IllegalArgumentException.class);
+    }
+
+    private static void assertSegment(Segment segment, Vector2D start, Vector2D end) {
+        EuclideanTestUtils.assertCoordinatesEqual(start, segment.getStartPoint(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(end, segment.getEndPoint(), TEST_EPS);
+    }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2S.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2S.java
index a443c75..8c721aa 100644
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2S.java
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2S.java
@@ -221,6 +221,18 @@ public class RegionBSPTree2S extends AbstractRegionBSPTree<Point2S, RegionBSPTre
         return new RegionBSPTree2S(true);
     }
 
+    /** Construct a new tree from the given boundaries. If no boundaries
+     * are present, the returned tree contains the full space.
+     * @param boundaries boundaries to construct the tree from
+     * @return a new tree instance constructed from the given boundaries
+     */
+    public static RegionBSPTree2S from(final Iterable<GreatArc> boundaries) {
+        RegionBSPTree2S tree = RegionBSPTree2S.full();
+        tree.insert(boundaries);
+
+        return tree;
+    }
+
     /** Construct a new tree from the boundaries in the given boundary source. If no boundaries
      * are present in the given source, their the returned tree contains the full space.
      * @param boundarySrc boundary source to construct a tree from
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2STest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2STest.java
index 0f0b22e..7a7943d 100644
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2STest.java
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2STest.java
@@ -21,7 +21,6 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
 
-import org.apache.commons.numbers.angle.PlaneAngleRadians;
 import org.apache.commons.geometry.core.GeometryTestUtils;
 import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.partitioning.Split;
@@ -32,6 +31,7 @@ import org.apache.commons.geometry.euclidean.threed.Vector3D;
 import org.apache.commons.geometry.spherical.SphericalTestUtils;
 import org.apache.commons.geometry.spherical.oned.Point1S;
 import org.apache.commons.geometry.spherical.twod.RegionBSPTree2S.RegionNode2S;
+import org.apache.commons.numbers.angle.PlaneAngleRadians;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -111,6 +111,35 @@ public class RegionBSPTree2STest {
     }
 
     @Test
+    public void testFrom_boundaries_noBoundaries() {
+        // act
+        RegionBSPTree2S tree = RegionBSPTree2S.from(Arrays.asList());
+
+        // assert
+        Assert.assertNull(tree.getBarycenter());
+        Assert.assertTrue(tree.isFull());
+        Assert.assertFalse(tree.isEmpty());
+    }
+
+    @Test
+    public void testFrom_boundaries() {
+        // act
+        RegionBSPTree2S tree = RegionBSPTree2S.from(Arrays.asList(
+                    EQUATOR.arc(Point2S.PLUS_I, Point2S.PLUS_J),
+                    X_MERIDIAN.arc(Point2S.PLUS_K, Point2S.PLUS_I),
+                    Y_MERIDIAN.arc(Point2S.PLUS_J, Point2S.PLUS_K)
+                ));
+
+        // assert
+        Assert.assertFalse(tree.isFull());
+        Assert.assertFalse(tree.isEmpty());
+
+        SphericalTestUtils.checkClassify(tree, RegionLocation.INSIDE, Point2S.of(1, 0.5));
+        SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE,
+                Point2S.of(-1, 0.5), Point2S.of(Math.PI, 0.5 * Math.PI));
+    }
+
+    @Test
     public void testFromBoundarySource() {
         // arrange
         ConvexArea2S area = ConvexArea2S.fromVertexLoop(Arrays.asList(
diff --git a/src/site/xdoc/index.xml b/src/site/xdoc/index.xml
index 3edcb1d..52d2095 100644
--- a/src/site/xdoc/index.xml
+++ b/src/site/xdoc/index.xml
@@ -41,14 +41,14 @@
       <p>The code below gives a small sample of the API by computing the intersection of cube with a rotated
         version of itself. See the <a href="userguide/index.html">user guide</a> for more details.
       </p>
-<source class="prettyprint">// construct a precision context to handle floating-point comparisons
+<source class="prettyprint">
+// construct a precision context to handle floating-point comparisons
 DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
 
 // create a binary space partitioning tree representing the unit cube
 // centered on the origin
-RegionBSPTree3D region = Boundaries3D.rect(
-        Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), precision)
-        .toTree();
+RegionBSPTree3D region = RegionBSPTree3D.from(
+        Parallelepiped.axisAligned(Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), precision));
 
 // create a rotated copy of the region
 Transform3D rotation = QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Z, 0.25 * Math.PI);
diff --git a/src/site/xdoc/userguide/index.xml b/src/site/xdoc/userguide/index.xml
index 4868ecc..4c54a05 100644
--- a/src/site/xdoc/userguide/index.xml
+++ b/src/site/xdoc/userguide/index.xml
@@ -611,9 +611,9 @@ List&lt;Polyline&gt; boundaries = tree.getBoundaryPaths(); // size = 1
         <source>
 DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
 
-RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(2, 1), precision).toTree();
+Polyline path = Parallelogram.axisAligned(Vector2D.ZERO, Vector2D.of(2, 1), precision);
 
-LinecastPoint2D pt = tree.linecastFirst(
+LinecastPoint2D pt = path.linecastFirst(
         Segment.fromPoints(Vector2D.of(1, 0.5), Vector2D.of(4, 0.5), precision));
 
 Vector2D intersection = pt.getPoint(); // (2.0, 0.5)
@@ -775,7 +775,8 @@ List&lt;ConvexSubPlane&gt; minusFacets = minus.getBoundaries(); // size = 4
         <source>
 DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
 
-RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 2, 3), precision).toTree();
+RegionBSPTree3D tree = RegionBSPTree3D.from(
+        Parallelepiped.axisAligned(Vector3D.ZERO, Vector3D.of(1, 2, 3), precision));
 
 List&lt;LinecastPoint3D&gt; pts = tree.linecast(
         Line3D.fromPoints(Vector3D.of(0.5, 0.5, -10), Vector3D.of(0.5, 0.5, 10), precision));


[commons-geometry] 01/08: GEOMETRY-68: adding Linecast2D/3D API

Posted by er...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

erans pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git

commit 6c10297aa665ab0342ec4ffaa6f013d394c69f05
Author: Matt Juntunen <ma...@hotmail.com>
AuthorDate: Sun Dec 29 00:55:06 2019 -0500

    GEOMETRY-68: adding Linecast2D/3D API
---
 .../geometry/euclidean/AbstractLinecastPoint.java  | 130 ++++++++++
 .../threed/BoundarySourceLinecastWrapper3D.java    |  85 ++++++
 .../geometry/euclidean/threed/ConvexSubPlane.java  |  27 ++
 .../geometry/euclidean/threed/ConvexVolume.java    |  14 +-
 .../geometry/euclidean/threed/LinecastPoint3D.java |  50 ++++
 .../geometry/euclidean/threed/Linecastable3D.java  |  71 +++++
 .../geometry/euclidean/threed/RegionBSPTree3D.java | 191 +++++++++++---
 .../twod/BoundarySourceLinecastWrapper2D.java      |  85 ++++++
 .../geometry/euclidean/twod/ConvexArea.java        |  14 +-
 .../geometry/euclidean/twod/LinecastPoint2D.java   |  50 ++++
 .../geometry/euclidean/twod/Linecastable2D.java    |  72 ++++++
 .../commons/geometry/euclidean/twod/Polyline.java  |  14 +-
 .../geometry/euclidean/twod/RegionBSPTree2D.java   | 192 +++++++++++++-
 .../BoundarySourceLinecastWrapper3DTest.java       | 213 +++++++++++++++
 .../euclidean/threed/ConvexSubPlaneTest.java       |  42 +++
 .../euclidean/threed/ConvexVolumeTest.java         |  41 +++
 .../euclidean/threed/LinecastChecker3D.java        | 191 ++++++++++++++
 .../euclidean/threed/LinecastPoint3DTest.java      | 126 +++++++++
 .../euclidean/threed/RegionBSPTree3DTest.java      | 286 +++++++++++++++------
 .../twod/BoundarySourceLinecastWrapper2DTest.java  | 204 +++++++++++++++
 .../geometry/euclidean/twod/ConvexAreaTest.java    |  41 +++
 .../geometry/euclidean/twod/LinecastChecker2D.java | 191 ++++++++++++++
 .../euclidean/twod/LinecastPoint2DTest.java        | 126 +++++++++
 .../geometry/euclidean/twod/PolylineTest.java      |  41 +++
 .../euclidean/twod/RegionBSPTree2DTest.java        | 111 ++++++++
 25 files changed, 2488 insertions(+), 120 deletions(-)

diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractLinecastPoint.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractLinecastPoint.java
new file mode 100644
index 0000000..756df65
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractLinecastPoint.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean;
+
+import java.util.Objects;
+
+import org.apache.commons.geometry.core.Embedding;
+import org.apache.commons.geometry.euclidean.oned.Vector1D;
+
+/** Base class for intersections discovered during linecast operations. This class contains
+ * the intersection point and the normal of the target boundary at the point of intersection
+ * along with the intersecting line and abscissa.
+ * @param <P> Euclidean point/vector implementation type
+ * @param <U> Unit-length Euclidean vector implementation type
+ * @param <L> Line implementation type
+ */
+public abstract class AbstractLinecastPoint<
+    P extends EuclideanVector<P>,
+    U extends P,
+    L extends Embedding<P, Vector1D>> {
+
+    /** Line intersection point. */
+    private final P point;
+
+    /** Normal of the target boundary at the intersection point. */
+    private final U normal;
+
+    /** The intersecting line. */
+    private final L line;
+
+    /** Abscissa of the intersection point along the intersecting line. */
+    private final double abscissa;
+
+    /** Construct a new instance from its components.
+     * @param point intersection point
+     * @param normal surface normal
+     * @param line line that the intersection point belongs to
+     */
+    protected AbstractLinecastPoint(final P point, final U normal, final L line) {
+        this.point = point;
+        this.normal = normal;
+        this.line = line;
+
+        this.abscissa = line.toSubspace(point).getX();
+    }
+
+    /** Get the line intersection point.
+     * @return the line intersection point
+     */
+    public P getPoint() {
+        return point;
+    }
+
+    /** Get the normal of the target boundary at the intersection point.
+     * @return the normal of the target boundary at the intersection point
+     */
+    public U getNormal() {
+        return normal;
+    }
+
+    /** Get the intersecting line.
+     * @return the intersecting line
+     */
+    public L getLine() {
+        return line;
+    }
+
+    /** Get the abscissa (1D position) of the intersection point
+     * along the linecast line.
+     * @return the abscissa of the intersection point.
+     */
+    public double getAbscissa() {
+        return abscissa;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(point, normal, line);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || !getClass().equals(obj.getClass())) {
+            return false;
+        }
+
+        final AbstractLinecastPoint<?, ?, ?> other = (AbstractLinecastPoint<?, ?, ?>) obj;
+
+        return Objects.equals(point, other.point) &&
+                Objects.equals(normal, other.normal) &&
+                Objects.equals(line, other.line);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName())
+            .append("[point= ")
+            .append(getPoint())
+            .append(", normal= ")
+            .append(getNormal())
+            .append(", abscissa= ")
+            .append(getAbscissa())
+            .append(", line= ")
+            .append(getLine())
+            .append(']');
+
+        return sb.toString();
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3D.java
new file mode 100644
index 0000000..30c33bb
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3D.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.threed;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/** Class that wraps a {@link BoundarySource3D} instance with the {@link Linecastable3D}
+ * interface. This class performs a brute-force computation of the intersections of the
+ * line or line segment against all boundaries. Some data structures may support more
+ * efficient algorithms and should therefore prefer those instead.
+ */
+final class BoundarySourceLinecastWrapper3D implements Linecastable3D {
+
+    /** The boundary source instance providing boundaries for the linecast operation. */
+    private final BoundarySource3D boundarySrc;
+
+    /** Construct a new instance for linecasting against the given boundary source.
+     * @param boundarySrc boundary source to linecast against.
+     */
+    BoundarySourceLinecastWrapper3D(final BoundarySource3D boundarySrc) {
+        this.boundarySrc = boundarySrc;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public List<LinecastPoint3D> linecast(final Segment3D segment) {
+        return getIntersectionStream(segment)
+                .sorted(LinecastPoint3D.ABSCISSA_ORDER)
+                .collect(Collectors.toList());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public LinecastPoint3D linecastFirst(final Segment3D segment) {
+        return getIntersectionStream(segment)
+                .min(LinecastPoint3D.ABSCISSA_ORDER)
+                .orElse(null);
+    }
+
+    /** Return a stream containing intersections between the boundary source and the
+     * given line segment.
+     * @param segment segment to intersect
+     * @return a stream containing linecast intersections
+     */
+    private Stream<LinecastPoint3D> getIntersectionStream(final Segment3D segment) {
+        return boundarySrc.boundaryStream()
+                .map(boundary -> computeIntersection(boundary, segment))
+                .filter(intersection -> intersection != null);
+    }
+
+    /** Compute the intersection between a boundary subplane and linecast intersecting segment. Null is
+     * returned if no intersection is discovered.
+     * @param boundary boundary from the boundary source
+     * @param segment linecast segment to intersect with
+     * @return the linecast intersection between the two arguments or null if there is no such
+     *      intersection
+     */
+    private LinecastPoint3D computeIntersection(final ConvexSubPlane boundary, final Segment3D segment) {
+        final Vector3D intersectionPt = boundary.intersection(segment);
+
+        if (intersectionPt != null) {
+            final Vector3D normal = boundary.getPlane().getNormal();
+
+            return new LinecastPoint3D(intersectionPt, normal, segment.getLine());
+        }
+
+        return null; // no intersection
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlane.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlane.java
index 3d42a6d..cd0d5b4 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlane.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlane.java
@@ -90,6 +90,33 @@ public final class ConvexSubPlane extends AbstractSubPlane<ConvexArea>
         return splitInternal(splitter, this, (p, r) -> new ConvexSubPlane(p, (ConvexArea) r));
     }
 
+    /** Get the unique intersection of this subplane with the given line. Null is
+     * returned if no unique intersection point exists (ie, the line and plane are
+     * parallel or coincident) or the line does not intersect the subplane.
+     * @param line line to intersect with this subplane
+     * @return the unique intersection point between the line and this subplane
+     *      or null if no such point exists.
+     * @see Plane#intersection(Line3D)
+     */
+    public Vector3D intersection(final Line3D line) {
+        final Vector3D pt = getPlane().intersection(line);
+        return (pt != null && contains(pt)) ? pt : null;
+    }
+
+    /** Get the unique intersection of this subplane with the given segment. Null
+     * is returned if the underlying line and plane do not have a unique intersection
+     * point (ie, they are parallel or coincident) or the intersection point is unique
+     * but in not contained in both the segment and subplane.
+     * @param segment segment to intersect with
+     * @return the unique intersection point between this subplane and the argument or
+     *      null if no such point exists.
+     * @see Plane#intersection(Line3D)
+     */
+    public Vector3D intersection(final Segment3D segment) {
+        final Vector3D pt = intersection(segment.getLine());
+        return (pt != null && segment.contains(pt)) ? pt : null;
+    }
+
     /** Get the vertices for the subplane. The vertices lie at the intersections of the
      * 2D area bounding lines.
      * @return the vertices for the subplane
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexVolume.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexVolume.java
index 1089027..03ba3db 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexVolume.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexVolume.java
@@ -32,7 +32,7 @@ import org.apache.commons.geometry.euclidean.twod.ConvexArea;
  * The boundaries of this area, if any, are composed of convex subplanes.
  */
 public final class ConvexVolume extends AbstractConvexHyperplaneBoundedRegion<Vector3D, ConvexSubPlane>
-    implements BoundarySource3D {
+    implements BoundarySource3D, Linecastable3D {
 
     /** Instance representing the full 3D volume. */
     private static final ConvexVolume FULL = new ConvexVolume(Collections.emptyList());
@@ -131,6 +131,18 @@ public final class ConvexVolume extends AbstractConvexHyperplaneBoundedRegion<Ve
 
     /** {@inheritDoc} */
     @Override
+    public List<LinecastPoint3D> linecast(final Segment3D segment) {
+        return new BoundarySourceLinecastWrapper3D(this).linecast(segment);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public LinecastPoint3D linecastFirst(final Segment3D segment) {
+        return new BoundarySourceLinecastWrapper3D(this).linecastFirst(segment);
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public ConvexSubPlane trim(final ConvexSubHyperplane<Vector3D> convexSubHyperplane) {
         return (ConvexSubPlane) super.trim(convexSubHyperplane);
     }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3D.java
new file mode 100644
index 0000000..0a2431d
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3D.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.threed;
+
+import java.util.Comparator;
+
+import org.apache.commons.geometry.euclidean.AbstractLinecastPoint;
+
+/** Class representing intersections resulting from linecast operations in Euclidean
+ * 3D space. This class contains the intersection point along with the boundary normal
+ * of the target at the point of intersection.
+ * @see Linecastable3D
+ */
+public class LinecastPoint3D extends AbstractLinecastPoint<Vector3D, Vector3D.Unit, Line3D> {
+
+    /** Comparator that sorts intersection instances by increasing abscissa order. If two abscissa
+     * values are equal, the comparison uses {@link Vector3D#COORDINATE_ASCENDING_ORDER} with the
+     * intersection normals.
+     */
+    public static final Comparator<LinecastPoint3D> ABSCISSA_ORDER = (a, b) -> {
+        int cmp = Double.compare(a.getAbscissa(), b.getAbscissa());
+        if (cmp == 0) {
+            cmp = Vector3D.COORDINATE_ASCENDING_ORDER.compare(a.getNormal(), b.getNormal());
+        }
+        return cmp;
+    };
+
+    /** Construct a new instance from its components.
+     * @param point intersection point
+     * @param normal normal of the target boundary at the intersection point
+     * @param line intersecting line
+     */
+    public LinecastPoint3D(final Vector3D point, final Vector3D normal, final Line3D line) {
+        super(point, normal.normalize(), line);
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Linecastable3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Linecastable3D.java
new file mode 100644
index 0000000..43f2cbb
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Linecastable3D.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.threed;
+
+import java.util.List;
+
+/** Interface for objects that support linecast operations in Euclidean 3D space.
+ *
+ * <p>
+ * Linecasting is a process that takes a line or line segment and intersects it with
+ * the boundaries of a region. This is similar to
+ * <a href="https://en.wikipedia.org/wiki/Ray_casting">raycasting</a> used
+ * for collision detection with the exception that the intersecting element can be a
+ * line or line segment and not just a ray.
+ * </p>
+ */
+public interface Linecastable3D {
+
+    /** Intersect the given line against the boundaries in this instance, returning a
+     * list of all intersections in order of increasing distance along the line. An empty
+     * list is returned if no intersections are discovered.
+     * @param line line the line to intersect
+     * @return a list of computed intersections in order of increasing distance
+     *      along the line
+     */
+    default List<LinecastPoint3D> linecast(final Line3D line) {
+        return linecast(line.span());
+    }
+
+    /** Intersect the given line segment against the boundaries in this instance, returning
+     * a list of all intersections in order of increasing distance along the line. An empty
+     * list is returned if no intersections are discovered.
+     * @param segment segment to intersect
+     * @return a list of computed intersections in order of increasing distance
+     *      along the line
+     */
+    List<LinecastPoint3D> linecast(Segment3D segment);
+
+    /** Intersect the given line against the boundaries in this instance, returning the first
+     * intersection found when traveling in the direction of the line from infinity.
+     * @param line the line to intersect
+     * @return the first intersection found or null if no intersection
+     *      is found
+     */
+    default LinecastPoint3D linecastFirst(final Line3D line) {
+        return linecastFirst(line.span());
+    }
+
+    /** Intersect the given line segment against the boundaries in this instance, returning
+     * the first intersection found when traveling in the direction of the line segment
+     * from its start point.
+     * @param segment segment to intersect
+     * @return the first intersection found or null if no intersection
+     *      is found
+     */
+    LinecastPoint3D linecastFirst(Segment3D segment);
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java
index db238bc..54a5b30 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java
@@ -17,6 +17,7 @@
 package org.apache.commons.geometry.euclidean.threed;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
@@ -36,7 +37,7 @@ import org.apache.commons.geometry.euclidean.twod.Vector2D;
  * Euclidean space.
  */
 public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, RegionBSPTree3D.RegionNode3D>
-    implements BoundarySource3D {
+    implements BoundarySource3D, Linecastable3D {
 
     /** Create a new, empty region. */
     public RegionBSPTree3D() {
@@ -136,18 +137,22 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
         return projector.getProjected();
     }
 
-    /** Find the first intersection of the given ray/line segment with the boundary of
-     * the region. The return value is the cut subhyperplane of the node containing the
-     * intersection. Null is returned if no intersection exists.
-     * @param ray ray to intersect with the region
-     * @return the node cut subhyperplane containing the intersection or null if no
-     *      intersection exists
-     */
-    public ConvexSubPlane raycastFirst(final Segment3D ray) {
-        final RaycastIntersectionVisitor visitor = new RaycastIntersectionVisitor(ray);
-        getRoot().accept(visitor);
+    /** {@inheritDoc} */
+    @Override
+    public List<LinecastPoint3D> linecast(final Segment3D segment) {
+        final LinecastVisitor visitor = new LinecastVisitor(segment);
+        accept(visitor);
 
-        return visitor.getIntersectionCut();
+        return visitor.getResults();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public LinecastPoint3D linecastFirst(final Segment3D segment) {
+        final LinecastFirstVisitor visitor = new LinecastFirstVisitor(segment);
+        accept(visitor);
+
+        return visitor.getResult();
     }
 
     /** {@inheritDoc} */
@@ -357,39 +362,137 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
         }
     }
 
-    /** BSP tree visitor that locates the node cut subhyperplane for the first intersection between a
-     * given line segment and BSP tree region boundary.
+    /** Base class for BSP tree visitors that perform linecast operations.
      */
-    private static final class RaycastIntersectionVisitor implements BSPTreeVisitor<Vector3D, RegionNode3D> {
+    private abstract static class AbstractLinecastVisitor implements BSPTreeVisitor<Vector3D, RegionNode3D> {
 
-        /** The line segment to intersect with the BSP tree. */
-        private final Segment3D segment;
+        /** The line segment to intersect with the boundaries of the BSP tree. */
+        private final Segment3D linecastSegment;
 
-        /** The node cut subhyperplane containing the first boundary intersection. */
-        private ConvexSubPlane intersectionCut;
+        /** Create a new instance with the given intersecting line segment.
+         * @param linecastSegment segment to intersect with the BSP tree region boundary
+         */
+        AbstractLinecastVisitor(final Segment3D linecastSegment) {
+            this.linecastSegment = linecastSegment;
+        }
 
-        /** Create a new instance that locates the first boundary intersection between the given line segment and
-         * the visited BSP tree.
-         * @param segment segment to intersect with the BSP tree region boundary
+        /** Get the intersecting segment for the linecast operation.
+         * @return the intersecting segment for the linecast operation
          */
-        RaycastIntersectionVisitor(final Segment3D segment) {
-            this.segment = segment;
+        protected Segment3D getLinecastSegment() {
+            return linecastSegment;
         }
 
-        /** Get the node cut subhyperplane containing the first intersection between the configured line segment
-         * and the BSP tree region boundary. This must be called after the tree nodes have been visited.
-         * @return the node cut subhyperplane containing the first intersection between the configured line segment
-         *      and the BSP tree region boundary or null if no such intersection was found
+        /** Compute the linecast point for the given intersection point and tree node, returning null
+         * if the point does not actually lie on the region boundary.
+         * @param pt intersection point
+         * @param node node containing the cut subhyperplane that the linecast line
+         *      intersected with
+         * @return a new linecast point instance or null if the intersection point does not lie
+         *      on the region boundary
          */
-        public ConvexSubPlane getIntersectionCut() {
-            return intersectionCut;
+        protected LinecastPoint3D computeLinecastPoint(final Vector3D pt, final RegionNode3D node) {
+            final Plane cut = (Plane) node.getCutHyperplane();
+            final RegionCutBoundary<Vector3D> boundary = node.getCutBoundary();
+
+            boolean onBoundary = false;
+            boolean negateNormal = false;
+
+            if (boundary.getInsideFacing() != null && boundary.getInsideFacing().contains(pt)) {
+                // on inside-facing boundary
+                onBoundary = true;
+                negateNormal = true;
+            } else  if (boundary.getOutsideFacing() != null && boundary.getOutsideFacing().contains(pt)) {
+                // on outside-facing boundary
+                onBoundary = true;
+            }
+
+            if (onBoundary) {
+                Vector3D normal = cut.getNormal();
+                if (negateNormal) {
+                    normal = normal.negate();
+                }
+
+                return new LinecastPoint3D(pt, normal, getLinecastSegment().getLine());
+            }
+
+            return null;
+        }
+    }
+
+    /** BSP tree visitor that performs a linecast operation against the boundaries of the visited tree, returning
+     * all computed boundary intersections, in order of their abscissa position along the intersecting line.
+     */
+    private static final class LinecastVisitor extends AbstractLinecastVisitor {
+
+        /** Results of the linecast operation. */
+        private final List<LinecastPoint3D> results = new ArrayList<>();
+
+        /** Create a new instance with the given intersecting line segment.
+         * @param linecastSegment segment to intersect with the BSP tree region boundary
+         */
+        LinecastVisitor(final Segment3D linecastSegment) {
+            super(linecastSegment);
+        }
+
+        /** Get the ordered results of the linecast operation.
+         * @return the ordered results of the linecast operation
+         */
+        public List<LinecastPoint3D> getResults() {
+            // sort the results before returning
+            Collections.sort(results, LinecastPoint3D.ABSCISSA_ORDER);
+
+            return results;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Result visit(final RegionNode3D node) {
+            if (node.isInternal()) {
+                // check if the line segment intersects the cut subhyperplane
+                final Segment3D segment = getLinecastSegment();
+                final Line3D line = segment.getLine();
+                final Vector3D pt = ((Plane) node.getCutHyperplane()).intersection(line);
+
+                if (pt != null && segment.contains(pt)) {
+                    final LinecastPoint3D resultPoint = computeLinecastPoint(pt, node);
+                    if (resultPoint != null) {
+                        results.add(resultPoint);
+                    }
+                }
+            }
+
+            return Result.CONTINUE;
+        }
+    }
+
+    /** BSP tree visitor that performs a linecast operation against the boundaries of the visited tree, returning
+     * only the intersection closest to the start of the line segment.
+     */
+    private static final class LinecastFirstVisitor extends AbstractLinecastVisitor {
+
+        /** The result of the linecast operation. */
+        private LinecastPoint3D result;
+
+        /** Create a new instance with the given intersecting line segment.
+         * @param linecastSegment segment to intersect with the BSP tree region boundary
+         */
+        LinecastFirstVisitor(final Segment3D linecastSegment) {
+            super(linecastSegment);
+        }
+
+        /** Get the {@link LinecastPoint2D} resulting from the linecast operation.
+         * @return the linecast result point
+         */
+        public LinecastPoint3D getResult() {
+            return result;
         }
 
         /** {@inheritDoc} */
         @Override
         public Order visitOrder(final RegionNode3D internalNode) {
             final Plane cut = (Plane) internalNode.getCutHyperplane();
-            final Line3D line = segment.getLine();
+            final Line3D line = getLinecastSegment().getLine();
 
             final boolean plusIsNear = line.getDirection().dot(cut.getNormal()) < 0;
 
@@ -403,20 +506,24 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
         public Result visit(final RegionNode3D node) {
             if (node.isInternal()) {
                 // check if the line segment intersects the cut subhyperplane
+                final Segment3D segment = getLinecastSegment();
                 final Line3D line = segment.getLine();
-                final Vector3D intersection = ((Plane) node.getCutHyperplane()).intersection(line);
-
-                if (intersection != null && segment.contains(intersection)) {
-
-                    final RegionCutBoundary<Vector3D> boundary = node.getCutBoundary();
-
-                    // check if the intersection point lies on the region boundary
-                    if ((boundary.getInsideFacing() != null && boundary.getInsideFacing().contains(intersection)) ||
-                            boundary.getOutsideFacing() != null && boundary.getOutsideFacing().contains(intersection)) {
-
-                        intersectionCut = (ConvexSubPlane) node.getCut();
+                final Vector3D pt = ((Plane) node.getCutHyperplane()).intersection(line);
 
+                if (pt != null) {
+                    if (result != null && line.getPrecision()
+                        .compare(result.getAbscissa(), line.abscissa(pt)) < 0) {
+                        // we have a result and we are now sure that no other intersection points will be
+                        // found that are closer or at the same position on the intersecting line.
                         return Result.TERMINATE;
+                    } else if (segment.contains(pt)) {
+                        // we've potentially found a new linecast point; it just needs to lie on
+                        // the boundary and be closer than any current result
+                        LinecastPoint3D potentialResult = computeLinecastPoint(pt, node);
+                        if (potentialResult != null && (result == null ||
+                                LinecastPoint3D.ABSCISSA_ORDER.compare(potentialResult, result) < 0)) {
+                            result = potentialResult;
+                        }
                     }
                 }
             }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2D.java
new file mode 100644
index 0000000..836aee5
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2D.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.twod;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/** Class that wraps a {@link BoundarySource2D} instance with the {@link Linecastable2D}
+ * interface. This class performs a brute-force computation of the intersections of the
+ * line or line segment against all boundaries. Some data structures may support more
+ * efficient algorithms and should therefore prefer those instead.
+ */
+final class BoundarySourceLinecastWrapper2D implements Linecastable2D {
+
+    /** The boundary source instance providing boundaries for the linecast operation. */
+    private final BoundarySource2D boundarySrc;
+
+    /** Construct a new instance for linecasting against the given boundary source.
+     * @param boundarySrc boundary source to linecast against.
+     */
+    BoundarySourceLinecastWrapper2D(final BoundarySource2D boundarySrc) {
+        this.boundarySrc = boundarySrc;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public List<LinecastPoint2D> linecast(final Segment segment) {
+        return getIntersectionStream(segment)
+                .sorted(LinecastPoint2D.ABSCISSA_ORDER)
+                .collect(Collectors.toList());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public LinecastPoint2D linecastFirst(final Segment segment) {
+        return getIntersectionStream(segment)
+                .min(LinecastPoint2D.ABSCISSA_ORDER)
+                .orElse(null);
+    }
+
+    /** Return a stream containing intersections between the boundary source and the
+     * given line segment.
+     * @param segment segment to intersect
+     * @return a stream containing linecast intersections
+     */
+    private Stream<LinecastPoint2D> getIntersectionStream(final Segment segment) {
+        return boundarySrc.boundaryStream()
+                .map(boundary -> computeIntersection(boundary, segment))
+                .filter(intersection -> intersection != null);
+    }
+
+    /** Compute the intersection between a boundary segment and linecast intersecting segment. Null is
+     * returned if no intersection is discovered.
+     * @param boundary boundary from the boundary source
+     * @param segment linecast segment to intersect with
+     * @return the linecast intersection between the two arguments or null if there is no such
+     *      intersection
+     */
+    private LinecastPoint2D computeIntersection(final Segment boundary, final Segment segment) {
+        final Vector2D intersectionPt = boundary.intersection(segment);
+
+        if (intersectionPt != null) {
+            final Vector2D normal = boundary.getLine().getOffsetDirection();
+
+            return new LinecastPoint2D(intersectionPt, normal, segment.getLine());
+        }
+
+        return null; // no intersection
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ConvexArea.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ConvexArea.java
index 87594f9..2061053 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ConvexArea.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ConvexArea.java
@@ -35,7 +35,7 @@ import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
  * The boundaries of this area, if any, are composed of line segments.
  */
 public final class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vector2D, Segment>
-    implements BoundarySource2D {
+    implements BoundarySource2D, Linecastable2D {
 
     /** Instance representing the full 2D plane. */
     private static final ConvexArea FULL = new ConvexArea(Collections.emptyList());
@@ -165,6 +165,18 @@ public final class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vect
         return splitInternal(splitter, this, Segment.class, ConvexArea::new);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public List<LinecastPoint2D> linecast(final Segment segment) {
+        return new BoundarySourceLinecastWrapper2D(this).linecast(segment);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public LinecastPoint2D linecastFirst(final Segment segment) {
+        return new BoundarySourceLinecastWrapper2D(this).linecastFirst(segment);
+    }
+
     /** Return an instance representing the full 2D area.
      * @return an instance representing the full 2D area.
      */
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2D.java
new file mode 100644
index 0000000..8bfc55a
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2D.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.twod;
+
+import java.util.Comparator;
+
+import org.apache.commons.geometry.euclidean.AbstractLinecastPoint;
+
+/** Class representing intersections resulting from linecast operations in Euclidean
+ * 2D space. This class contains the intersection point along with the boundary normal
+ * of the target at the point of intersection.
+ * @see Linecastable2D
+ */
+public class LinecastPoint2D extends AbstractLinecastPoint<Vector2D, Vector2D.Unit, Line> {
+
+    /** Comparator that sorts intersection instances by increasing abscissa order. If two abscissa
+     * values are equal, the comparison uses {@link Vector2D#COORDINATE_ASCENDING_ORDER} with the
+     * intersection normals.
+     */
+    public static final Comparator<LinecastPoint2D> ABSCISSA_ORDER = (a, b) -> {
+        int cmp = Double.compare(a.getAbscissa(), b.getAbscissa());
+        if (cmp == 0) {
+            cmp = Vector2D.COORDINATE_ASCENDING_ORDER.compare(a.getNormal(), b.getNormal());
+        }
+        return cmp;
+    };
+
+    /** Construct a new instance from its components.
+     * @param point the linecast intersection point
+     * @param normal the surface of the linecast target at the intersection point
+     * @param line intersecting line
+     */
+    public LinecastPoint2D(final Vector2D point, final Vector2D normal, final Line line) {
+        super(point, normal.normalize(), line);
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Linecastable2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Linecastable2D.java
new file mode 100644
index 0000000..a9ee49e
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Linecastable2D.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.twod;
+
+import java.util.List;
+
+/** Interface for objects that support linecast operations in Euclidean 2D space.
+ *
+ * <p>
+ * Linecasting is a process that takes a line or line segment and intersects it with
+ * the boundaries of a region. This is similar to
+ * <a href="https://en.wikipedia.org/wiki/Ray_casting">raycasting</a> used
+ * for collision detection with the exception that the intersecting element can be a
+ * line or line segment and not just a ray.
+ * </p>
+ */
+public interface Linecastable2D {
+
+    /** Intersect the given line against the boundaries in this instance, returning a
+     * list of all intersections in order of increasing position along the line. An empty
+     * list is returned if no intersections are discovered.
+     * @param line line the line to intersect
+     * @return a list of computed intersections in order of increasing position
+     *      along the line
+     */
+    default List<LinecastPoint2D> linecast(final Line line) {
+        return linecast(line.span());
+    }
+
+    /** Intersect the given line segment against the boundaries in this instance, returning
+     * a list of all intersections in order of increasing position along the line. An empty
+     * list is returned if no intersections are discovered.
+     * @param segment segment to intersect
+     * @return a list of computed intersections in order of increasing position
+     *      along the line
+     */
+    List<LinecastPoint2D> linecast(Segment segment);
+
+    /** Intersect the given line against the boundaries in this instance, returning
+     * the first intersection found when traveling in the direction of the line from
+     * infinity.
+     * @param line the line to intersect
+     * @return the first intersection found or null if no intersection
+     *      is found
+     */
+    default LinecastPoint2D linecastFirst(final Line line) {
+        return linecastFirst(line.span());
+    }
+
+    /** Intersect the given line segment against the boundaries in this instance, returning
+     * the first intersection found when traveling in the direction of the line segment
+     * from its start point.
+     * @param segment segment to intersect
+     * @return the first intersection found or null if no intersection
+     *      is found
+     */
+    LinecastPoint2D linecastFirst(Segment segment);
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Polyline.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Polyline.java
index 9ceb1b6..65948f0 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Polyline.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Polyline.java
@@ -36,7 +36,7 @@ import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
  * <p>Instances of this class are guaranteed to be immutable.</p>
  * @see <a href="https://en.wikipedia.org/wiki/Polygonal_chain">Polygonal chain</a>
  */
-public class Polyline implements BoundarySource2D {
+public class Polyline implements BoundarySource2D, Linecastable2D {
     /** Polyline instance containing no segments. */
     private static final Polyline EMPTY = new Polyline(Collections.emptyList());
 
@@ -257,6 +257,18 @@ public class Polyline implements BoundarySource2D {
         return new SimplifiedPolyline(simplified);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public List<LinecastPoint2D> linecast(final Segment segment) {
+        return new BoundarySourceLinecastWrapper2D(this).linecast(segment);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public LinecastPoint2D linecastFirst(final Segment segment) {
+        return new BoundarySourceLinecastWrapper2D(this).linecastFirst(segment);
+    }
+
     /** Return a string representation of the segment polyline.
      *
      * <p>In order to keep the string representation short but useful, the exact format of the return
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java
index 8aa61b7..cf171a0 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java
@@ -28,12 +28,14 @@ import org.apache.commons.geometry.core.partitioning.Hyperplane;
 import org.apache.commons.geometry.core.partitioning.Split;
 import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
 import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor;
+import org.apache.commons.geometry.core.partitioning.bsp.RegionCutBoundary;
 
 /** Binary space partitioning (BSP) tree representing a region in two dimensional
  * Euclidean space.
  */
 public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, RegionBSPTree2D.RegionNode2D>
-    implements BoundarySource2D {
+    implements BoundarySource2D, Linecastable2D {
 
     /** List of line segment paths comprising the region boundary. */
     private List<Polyline> boundaryPaths;
@@ -155,6 +157,24 @@ public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, Regio
         return projector.getProjected();
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public List<LinecastPoint2D> linecast(final Segment segment) {
+        final LinecastVisitor visitor = new LinecastVisitor(segment);
+        accept(visitor);
+
+        return visitor.getResults();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public LinecastPoint2D linecastFirst(final Segment segment) {
+        final LinecastFirstVisitor visitor = new LinecastFirstVisitor(segment);
+        accept(visitor);
+
+        return visitor.getResult();
+    }
+
     /** Compute the line segment paths comprising the region boundary.
      * @return the line segment paths comprising the region boundary
      */
@@ -324,4 +344,174 @@ public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, Regio
             return cmp < 0 ? a : b;
         }
     }
+
+    /** Base class for BSP tree visitors that perform linecast operations.
+     */
+    private abstract static class AbstractLinecastVisitor implements BSPTreeVisitor<Vector2D, RegionNode2D> {
+
+        /** The line segment to intersect with the boundaries of the BSP tree. */
+        private final Segment linecastSegment;
+
+        /** Create a new instance with the given intersecting line segment.
+         * @param linecastSegment segment to intersect with the BSP tree region boundary
+         */
+        AbstractLinecastVisitor(final Segment linecastSegment) {
+            this.linecastSegment = linecastSegment;
+        }
+
+        /** Get the intersecting segment for the linecast operation.
+         * @return the intersecting segment for the linecast operation
+         */
+        protected Segment getLinecastSegment() {
+            return linecastSegment;
+        }
+
+        /** Compute the linecast point for the given intersection point and tree node, returning null
+         * if the point does not actually lie on the region boundary.
+         * @param pt intersection point
+         * @param node node containing the cut subhyperplane that the linecast line
+         *      intersected with
+         * @return a new linecast point instance or null if the intersection point does not lie
+         *      on the region boundary
+         */
+        protected LinecastPoint2D computeLinecastPoint(final Vector2D pt, final RegionNode2D node) {
+            final Line cut = (Line) node.getCutHyperplane();
+            final RegionCutBoundary<Vector2D> boundary = node.getCutBoundary();
+
+            boolean onBoundary = false;
+            boolean negateNormal = false;
+
+            if (boundary.getInsideFacing() != null && boundary.getInsideFacing().contains(pt)) {
+                // on inside-facing boundary
+                onBoundary = true;
+                negateNormal = true;
+            } else  if (boundary.getOutsideFacing() != null && boundary.getOutsideFacing().contains(pt)) {
+                // on outside-facing boundary
+                onBoundary = true;
+            }
+
+            if (onBoundary) {
+                Vector2D normal = cut.getOffsetDirection();
+                if (negateNormal) {
+                    normal = normal.negate();
+                }
+
+                return new LinecastPoint2D(pt, normal, getLinecastSegment().getLine());
+            }
+
+            return null;
+        }
+    }
+
+    /** BSP tree visitor that performs a linecast operation against the boundaries of the visited tree, returning
+     * all computed boundary intersections, in order of their abscissa position along the intersecting line.
+     */
+    private static final class LinecastVisitor extends AbstractLinecastVisitor {
+
+        /** Results of the linecast operation. */
+        private final List<LinecastPoint2D> results = new ArrayList<>();
+
+        /** Create a new instance with the given intersecting line segment.
+         * @param linecastSegment segment to intersect with the BSP tree region boundary
+         */
+        LinecastVisitor(final Segment linecastSegment) {
+            super(linecastSegment);
+        }
+
+        /** Get the ordered results of the linecast operation.
+         * @return the ordered results of the linecast operation
+         */
+        public List<LinecastPoint2D> getResults() {
+            // sort the results before returning
+            Collections.sort(results, LinecastPoint2D.ABSCISSA_ORDER);
+
+            return results;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Result visit(final RegionNode2D node) {
+            if (node.isInternal()) {
+                // check if the line segment intersects the cut subhyperplane
+                final Segment segment = getLinecastSegment();
+                final Line line = segment.getLine();
+                final Vector2D pt = ((Line) node.getCutHyperplane()).intersection(line);
+
+                if (pt != null && segment.contains(pt)) {
+                    final LinecastPoint2D resultPoint = computeLinecastPoint(pt, node);
+                    if (resultPoint != null) {
+                        results.add(resultPoint);
+                    }
+                }
+            }
+
+            return Result.CONTINUE;
+        }
+    }
+
+    /** BSP tree visitor that performs a linecast operation against the boundaries of the visited tree, returning
+     * only the intersection closest to the start of the line segment.
+     */
+    private static final class LinecastFirstVisitor extends AbstractLinecastVisitor {
+
+        /** The result of the linecast operation. */
+        private LinecastPoint2D result;
+
+        /** Create a new instance with the given intersecting line segment.
+         * @param linecastSegment segment to intersect with the BSP tree region boundary
+         */
+        LinecastFirstVisitor(final Segment linecastSegment) {
+            super(linecastSegment);
+        }
+
+        /** Get the {@link LinecastPoint2D} resulting from the linecast operation.
+         * @return the linecast result point
+         */
+        public LinecastPoint2D getResult() {
+            return result;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Order visitOrder(final RegionNode2D internalNode) {
+            final Line cut = (Line) internalNode.getCutHyperplane();
+            final Line line = getLinecastSegment().getLine();
+
+            final boolean plusIsNear = line.getDirection().dot(cut.getOffsetDirection()) < 0;
+
+            return plusIsNear ?
+                    Order.PLUS_NODE_MINUS :
+                    Order.MINUS_NODE_PLUS;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Result visit(final RegionNode2D node) {
+            if (node.isInternal()) {
+                // check if the line segment intersects the cut subhyperplane
+                final Segment segment = getLinecastSegment();
+                final Line line = segment.getLine();
+                final Vector2D pt = ((Line) node.getCutHyperplane()).intersection(line);
+
+                if (pt != null) {
+                    if (result != null && line.getPrecision()
+                        .compare(result.getAbscissa(), line.abscissa(pt)) < 0) {
+                        // we have a result and we are now sure that no other intersection points will be
+                        // found that are closer or at the same position on the intersecting line.
+                        return Result.TERMINATE;
+                    } else if (segment.contains(pt)) {
+                        // we've potentially found a new linecast point; it just needs to lie on
+                        // the boundary and be closer than any current result
+                        LinecastPoint2D potentialResult = computeLinecastPoint(pt, node);
+                        if (potentialResult != null && (result == null ||
+                                LinecastPoint2D.ABSCISSA_ORDER.compare(potentialResult, result) < 0)) {
+                            result = potentialResult;
+                        }
+                    }
+                }
+            }
+
+            return Result.CONTINUE;
+        }
+    }
 }
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3DTest.java
new file mode 100644
index 0000000..2301645
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecastWrapper3DTest.java
@@ -0,0 +1,213 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.threed;
+
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.junit.Test;
+
+public class BoundarySourceLinecastWrapper3DTest {
+
+    private static final double TEST_EPS = 1e-10;
+
+    private static final DoublePrecisionContext TEST_PRECISION =
+            new EpsilonDoublePrecisionContext(TEST_EPS);
+
+    private static final BoundarySource3D UNIT_CUBE =
+            Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION);
+
+    @Test
+    public void testLinecast_line_simple() {
+        // arrange
+        BoundarySourceLinecastWrapper3D wrapper = new BoundarySourceLinecastWrapper3D(UNIT_CUBE);
+
+        // act/assert
+
+        // no intersections
+        LinecastChecker3D.with(wrapper)
+            .returnsNothing()
+            .whenGiven(Line3D.fromPointAndDirection(Vector3D.of(0, 4, 4), Vector3D.Unit.MINUS_X, TEST_PRECISION));
+
+        // through center; two directions
+        LinecastChecker3D.with(wrapper)
+            .returns(Vector3D.of(0, 0.5, 0.5), Vector3D.Unit.MINUS_X)
+            .and(Vector3D.of(1, 0.5, 0.5), Vector3D.Unit.PLUS_X)
+            .whenGiven(Line3D.fromPointAndDirection(Vector3D.of(0.5, 0.5, 0.5), Vector3D.Unit.PLUS_X, TEST_PRECISION));
+
+        LinecastChecker3D.with(wrapper)
+            .returns(Vector3D.of(1, 0.5, 0.5), Vector3D.Unit.PLUS_X)
+            .and(Vector3D.of(0, 0.5, 0.5), Vector3D.Unit.MINUS_X)
+            .whenGiven(Line3D.fromPointAndDirection(Vector3D.of(0.5, 0.5, 0.5), Vector3D.Unit.MINUS_X, TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast_line_alongFace() {
+        // arrange
+        BoundarySourceLinecastWrapper3D wrapper = new BoundarySourceLinecastWrapper3D(UNIT_CUBE);
+
+        // act/assert
+        LinecastChecker3D.with(wrapper)
+            .returns(Vector3D.ZERO, Vector3D.Unit.MINUS_Y)
+            .and(Vector3D.ZERO, Vector3D.Unit.MINUS_Z)
+            .and(Vector3D.of(0, 1, 1), Vector3D.Unit.PLUS_Z)
+            .and(Vector3D.of(0, 1, 1), Vector3D.Unit.PLUS_Y)
+            .whenGiven(Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.of(0, 1, 1), TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast_line_corners() {
+        // arrange
+        BoundarySourceLinecastWrapper3D wrapper = new BoundarySourceLinecastWrapper3D(UNIT_CUBE);
+
+        // act/assert
+
+        // through single corner vertex
+        LinecastChecker3D.with(wrapper)
+            .returns(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Z)
+            .and(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Y)
+            .and(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X)
+            .whenGiven(Line3D.fromPointAndDirection(Vector3D.of(1, 1, 1), Vector3D.of(1, -1, -1), TEST_PRECISION));
+
+        // through two corner vertices
+        LinecastChecker3D.with(wrapper)
+            .returns(Vector3D.ZERO, Vector3D.Unit.MINUS_X)
+            .and(Vector3D.ZERO, Vector3D.Unit.MINUS_Y)
+            .and(Vector3D.ZERO, Vector3D.Unit.MINUS_Z)
+            .and(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Z)
+            .and(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Y)
+            .and(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X)
+            .whenGiven(Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast_segment_simple() {
+        // arrange
+        BoundarySourceLinecastWrapper3D wrapper = new BoundarySourceLinecastWrapper3D(UNIT_CUBE);
+
+        // act/assert
+
+        // no intersections; underlying line does not intersect
+        LinecastChecker3D.with(wrapper)
+            .returnsNothing()
+            .whenGiven(Line3D.fromPointAndDirection(Vector3D.of(0, 4, 4), Vector3D.Unit.MINUS_X, TEST_PRECISION)
+                    .segment(-10, 10));
+
+        // no intersections; underlying line does intersect
+        LinecastChecker3D.with(wrapper)
+            .returnsNothing()
+            .whenGiven(Line3D.fromPointAndDirection(Vector3D.of(0.5, 0.5, 0.5), Vector3D.Unit.PLUS_X, TEST_PRECISION)
+                    .segment(2, 10));
+
+        // no boundaries excluded; two directions
+        LinecastChecker3D.with(wrapper)
+            .returns(Vector3D.of(0, 0.5, 0.5), Vector3D.Unit.MINUS_X)
+            .and(Vector3D.of(1, 0.5, 0.5), Vector3D.Unit.PLUS_X)
+            .whenGiven(Line3D.fromPointAndDirection(Vector3D.of(0.5, 0.5, 0.5), Vector3D.Unit.PLUS_X, TEST_PRECISION)
+                    .segment(-10, 10));
+
+        LinecastChecker3D.with(wrapper)
+            .returns(Vector3D.of(1, 0.5, 0.5), Vector3D.Unit.PLUS_X)
+            .and(Vector3D.of(0, 0.5, 0.5), Vector3D.Unit.MINUS_X)
+            .whenGiven(Line3D.fromPointAndDirection(Vector3D.of(0.5, 0.5, 0.5), Vector3D.Unit.MINUS_X, TEST_PRECISION)
+                    .segment(-10, 10));
+    }
+
+    @Test
+    public void testLinecast_segment_boundaryExcluded() {
+        // arrange
+        BoundarySourceLinecastWrapper3D wrapper = new BoundarySourceLinecastWrapper3D(UNIT_CUBE);
+
+        // act/assert
+        Vector3D center = Vector3D.of(0.5, 0.5, 0.5);
+        LinecastChecker3D.with(wrapper)
+            .returns(Vector3D.of(1, 0.5, 0.5), Vector3D.Unit.PLUS_X)
+            .whenGiven(Line3D.fromPointAndDirection(center, Vector3D.Unit.PLUS_X, TEST_PRECISION)
+                    .segmentFrom(center));
+
+        LinecastChecker3D.with(wrapper)
+            .returns(Vector3D.of(1, 0.5, 0.5), Vector3D.Unit.PLUS_X)
+            .whenGiven(Line3D.fromPointAndDirection(center, Vector3D.Unit.MINUS_X, TEST_PRECISION)
+                    .segmentTo(center));
+    }
+
+    @Test
+    public void testLinecast_segment_startEndPointsOnBoundaries() {
+        // arrange
+        BoundarySourceLinecastWrapper3D wrapper = new BoundarySourceLinecastWrapper3D(UNIT_CUBE);
+
+        // act/assert
+        LinecastChecker3D.with(wrapper)
+            .returns(Vector3D.of(1, 0.5, 0.5), Vector3D.Unit.PLUS_X)
+            .and(Vector3D.of(0, 0.5, 0.5), Vector3D.Unit.MINUS_X)
+            .whenGiven(Segment3D.fromPoints(Vector3D.of(1, 0.5, 0.5), Vector3D.of(0, 0.5, 0.5), TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast_segment_alongFace() {
+        // arrange
+        BoundarySourceLinecastWrapper3D wrapper = new BoundarySourceLinecastWrapper3D(UNIT_CUBE);
+
+        // act/assert
+
+        // includes two intersecting boundaries
+        LinecastChecker3D.with(wrapper)
+            .returns(Vector3D.of(0, 1, 0), Vector3D.Unit.MINUS_X)
+            .and(Vector3D.of(1, 1, 0), Vector3D.Unit.PLUS_X)
+            .whenGiven(Segment3D.fromPoints(Vector3D.of(-1, 1, 0), Vector3D.of(2, 1, 0), TEST_PRECISION));
+
+        // one intersecting boundary
+        LinecastChecker3D.with(wrapper)
+            .returns(Vector3D.of(1, 1, 0), Vector3D.Unit.PLUS_X)
+            .whenGiven(Segment3D.fromPoints(Vector3D.of(0.25, 1, 0), Vector3D.of(2, 1, 0), TEST_PRECISION));
+
+        // no intersecting boundary
+        LinecastChecker3D.with(wrapper)
+            .returnsNothing()
+            .whenGiven(Segment3D.fromPoints(Vector3D.of(0.25, 1, 0), Vector3D.of(0.75, 1, 0), TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast_segment_corners() {
+        // arrange
+        BoundarySourceLinecastWrapper3D wrapper = new BoundarySourceLinecastWrapper3D(UNIT_CUBE);
+
+        Vector3D corner = Vector3D.of(1, 1, 1);
+
+        // act/assert
+
+        // through corner
+        LinecastChecker3D.with(wrapper)
+            .returns(corner, Vector3D.Unit.PLUS_Z)
+            .and(corner, Vector3D.Unit.PLUS_Y)
+            .and(corner, Vector3D.Unit.PLUS_X)
+            .whenGiven(Segment3D.fromPoints(Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(2, 2, 2), TEST_PRECISION));
+
+        // starts on corner
+        LinecastChecker3D.with(wrapper)
+            .returns(corner, Vector3D.Unit.PLUS_Z)
+            .and(corner, Vector3D.Unit.PLUS_Y)
+            .and(corner, Vector3D.Unit.PLUS_X)
+            .whenGiven(Segment3D.fromPoints(corner, Vector3D.of(2, 0, 2), TEST_PRECISION));
+
+        // ends on corner
+        LinecastChecker3D.with(wrapper)
+            .returns(corner, Vector3D.Unit.PLUS_Z)
+            .and(corner, Vector3D.Unit.PLUS_Y)
+            .and(corner, Vector3D.Unit.PLUS_X)
+            .whenGiven(Segment3D.fromPoints(Vector3D.of(0, 2, 2), corner, TEST_PRECISION));
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlaneTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlaneTest.java
index 1c90c3b..0f3ba79 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlaneTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlaneTest.java
@@ -599,6 +599,48 @@ public class ConvexSubPlaneTest {
     }
 
     @Test
+    public void testIntersection_line() {
+        // arrange
+        ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(Arrays.asList(
+                Vector3D.of(0, 0, 2), Vector3D.of(1, 0, 2), Vector3D.of(1, 1, 2), Vector3D.of(0, 1, 2)),
+                TEST_PRECISION);
+
+        // act/assert
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 2),
+                sp.intersection(Line3D.fromPoints(Vector3D.of(0.5, 0.5, 2), Vector3D.ZERO, TEST_PRECISION)), TEST_EPS);
+
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, 2),
+                sp.intersection(Line3D.fromPoints(Vector3D.of(1, 1, 2), Vector3D.of(1, 1, 0), TEST_PRECISION)), TEST_EPS);
+
+        Assert.assertNull(sp.intersection(Line3D.fromPoints(Vector3D.ZERO, Vector3D.Unit.PLUS_X, TEST_PRECISION)));
+        Assert.assertNull(sp.intersection(Line3D.fromPoints(Vector3D.of(0, 0, 2), Vector3D.of(1, 1, 2), TEST_PRECISION)));
+
+        Assert.assertNull(sp.intersection(Line3D.fromPoints(Vector3D.of(4, 4, 2), Vector3D.of(4, 4, 0), TEST_PRECISION)));
+    }
+
+    @Test
+    public void testIntersection_segment() {
+        // arrange
+        ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(Arrays.asList(
+                Vector3D.of(0, 0, 2), Vector3D.of(1, 0, 2), Vector3D.of(1, 1, 2), Vector3D.of(0, 1, 2)),
+                TEST_PRECISION);
+
+        // act/assert
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 2),
+                sp.intersection(Segment3D.fromPoints(Vector3D.of(0.5, 0.5, 2), Vector3D.ZERO, TEST_PRECISION)), TEST_EPS);
+
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, 2),
+                sp.intersection(Segment3D.fromPoints(Vector3D.of(1, 1, 2), Vector3D.of(1, 1, 0), TEST_PRECISION)), TEST_EPS);
+
+        Assert.assertNull(sp.intersection(Segment3D.fromPoints(Vector3D.of(0.5, 0.5, 4), Vector3D.of(0.5, 0.5, 3), TEST_PRECISION)));
+
+        Assert.assertNull(sp.intersection(Segment3D.fromPoints(Vector3D.ZERO, Vector3D.Unit.PLUS_X, TEST_PRECISION)));
+        Assert.assertNull(sp.intersection(Segment3D.fromPoints(Vector3D.of(0, 0, 2), Vector3D.of(1, 1, 2), TEST_PRECISION)));
+
+        Assert.assertNull(sp.intersection(Segment3D.fromPoints(Vector3D.of(4, 4, 2), Vector3D.of(4, 4, 0), TEST_PRECISION)));
+    }
+
+    @Test
     public void testToString() {
         // arrange
         ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(Arrays.asList(
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexVolumeTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexVolumeTest.java
index cd748e6..eff7ca0 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexVolumeTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexVolumeTest.java
@@ -216,6 +216,47 @@ public class ConvexVolumeTest {
     }
 
     @Test
+    public void testLinecast_full() {
+        // arrange
+        ConvexVolume volume = ConvexVolume.full();
+
+        // act/assert
+        LinecastChecker3D.with(volume)
+            .returnsNothing()
+            .whenGiven(Line3D.fromPoints(Vector3D.ZERO, Vector3D.Unit.PLUS_X, TEST_PRECISION));
+
+        LinecastChecker3D.with(volume)
+            .returnsNothing()
+            .whenGiven(Segment3D.fromPoints(Vector3D.Unit.MINUS_X, Vector3D.Unit.PLUS_X, TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast() {
+        // arrange
+        ConvexVolume volume = rect(Vector3D.of(0.5, 0.5, 0.5), 0.5, 0.5, 0.5);
+
+        // act/assert
+        LinecastChecker3D.with(volume)
+            .returnsNothing()
+            .whenGiven(Line3D.fromPoints(Vector3D.of(0, 5, 5), Vector3D.of(1, 5, 5), TEST_PRECISION));
+
+        LinecastChecker3D.with(volume)
+            .returns(Vector3D.ZERO, Vector3D.Unit.MINUS_X)
+            .and(Vector3D.ZERO, Vector3D.Unit.MINUS_Y)
+            .and(Vector3D.ZERO, Vector3D.Unit.MINUS_Z)
+            .and(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Z)
+            .and(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Y)
+            .and(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X)
+            .whenGiven(Line3D.fromPoints(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION));
+
+        LinecastChecker3D.with(volume)
+            .returns(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Z)
+            .and(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Y)
+            .and(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X)
+            .whenGiven(Segment3D.fromPoints(Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(1, 1, 1), TEST_PRECISION));
+    }
+
+    @Test
     public void testTransform() {
         // arrange
         ConvexVolume vol = rect(Vector3D.ZERO, 0.5, 0.5, 0.5);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LinecastChecker3D.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LinecastChecker3D.java
new file mode 100644
index 0000000..a66ca05
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LinecastChecker3D.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.threed;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.junit.Assert;
+
+/** Helper class designed to assist with linecast test assertions in 3D.
+ */
+class LinecastChecker3D {
+
+    private static final double TEST_EPS = 1e-10;
+
+    private static final DoublePrecisionContext TEST_PRECISION =
+            new EpsilonDoublePrecisionContext(TEST_EPS);
+
+    /** The linecastable target. */
+    private final Linecastable3D target;
+
+    /** List of expected results from the line cast operation. */
+    private final List<ExpectedResult> expectedResults = new ArrayList<>();
+
+    /** Construct a new instance that performs linecast assertions against the
+     * given target.
+     * @param target
+     */
+    LinecastChecker3D(final Linecastable3D target) {
+        this.target = target;
+    }
+
+    /** Configure the instance to expect no results (an empty list from linecast() and null from
+     * linecastFirst()) from the next linecast operation performed by {@link #whenGiven(Line)}
+     * or {@link #whenGiven(Segment)}.
+     * @return
+     */
+    public LinecastChecker3D returnsNothing() {
+        expectedResults.clear();
+
+        return this;
+    }
+
+    /** Configure the instance to expect a linecast point with the given parameters on the next
+     * linecast operation. Multiple calls to this method and/or {@link #and(Vector3D, Vector3D)}
+     * create an internal ordered list of results.
+     * @param point
+     * @param normal
+     * @return
+     */
+    public LinecastChecker3D returns(final Vector3D point, final Vector3D normal) {
+        expectedResults.add(new ExpectedResult(point, normal));
+
+        return this;
+    }
+
+    /** Fluent API alias for {@link #returns(Vector3D, Vector3D)}.
+     * @param point
+     * @param normal
+     * @return
+     */
+    public LinecastChecker3D and(final Vector3D point, final Vector3D normal) {
+        return returns(point, normal);
+    }
+
+    /** Perform {@link Linecastable2D#linecast(Line)} and {@link Linecastable2D#linecastFirst(Line)}
+     * operations using the given line and assert that the results match the configured expected
+     * values.
+     * @param line
+     */
+    public void whenGiven(final Line3D line) {
+        checkLinecastResults(target.linecast(line), line);
+        checkLinecastFirstResult(target.linecastFirst(line), line);
+    }
+
+    /** Perform {@link Linecastable2D#linecast(Segment)} and {@link Linecastable2D#linecastFirst(Segment)}
+     * operations using the given line segment and assert that the results match the configured
+     * expected results.
+     * @param segment
+     */
+    public void whenGiven(final Segment3D segment) {
+        Line3D line = segment.getLine();
+
+        checkLinecastResults(target.linecast(segment), line);
+        checkLinecastFirstResult(target.linecastFirst(segment), line);
+    }
+
+    /** Check that the given set of linecast result points matches those expected.
+     * @param results
+     * @param line
+     */
+    private void checkLinecastResults(List<LinecastPoint3D> results, Line3D line) {
+        Assert.assertNotNull("Linecast result list cannot be null", results);
+        Assert.assertEquals("Unexpected result size for linecast", expectedResults.size(), results.size());
+
+        for (int i = 0; i < expectedResults.size(); ++i) {
+            LinecastPoint3D expected = toLinecastPoint(expectedResults.get(i), line);
+            LinecastPoint3D actual = results.get(i);
+
+            if (!eq(expected, actual)) {
+                Assert.fail("Unexpected linecast point at index " + i + " expected " + expected +
+                        " but was " + actual);
+            }
+        }
+    }
+
+    /** Check that the given linecastFirst result matches that expected.
+     * @param result
+     * @param line
+     */
+    private void checkLinecastFirstResult(LinecastPoint3D result, Line3D line) {
+        if (expectedResults.isEmpty()) {
+            Assert.assertNull("Expected linecastFirst result to be null", result);
+        } else {
+            LinecastPoint3D expected = toLinecastPoint(expectedResults.get(0), line);
+
+            Assert.assertNotNull("Expected linecastFirst result to not be null", result);
+
+            if (!eq(expected, result)) {
+                Assert.fail("Unexpected result from linecastFirst: expected " + expected +
+                        " but was " + result);
+            }
+        }
+    }
+
+    /** Fluent API method for creating new instances.
+     * @param src
+     * @return
+     */
+    public static LinecastChecker3D with(final Linecastable3D src) {
+        return new LinecastChecker3D(src);
+    }
+
+    /** Return true if the given linecast points are equivalent according to the test precision.
+     * @param expected
+     * @param actual
+     * @return
+     */
+    private static boolean eq(LinecastPoint3D a, LinecastPoint3D b) {
+        return a.getPoint().eq(b.getPoint(), TEST_PRECISION) &&
+                a.getNormal().eq(b.getNormal(), TEST_PRECISION) &&
+                a.getLine().equals(b.getLine()) &&
+                TEST_PRECISION.eq(a.getAbscissa(), b.getAbscissa());
+    }
+
+    /** Convert an {@link ExpectedResult} struct to a {@link LinecastPoint2D} instance
+     * using the given line.
+     * @param expected
+     * @param line
+     * @return
+     */
+    private static LinecastPoint3D toLinecastPoint(ExpectedResult expected, Line3D line) {
+        return new LinecastPoint3D(expected.getPoint(), expected.getNormal(), line);
+    }
+
+    /** Class containing intermediate expected results for a linecast operation.
+     */
+    private static final class ExpectedResult {
+        private final Vector3D point;
+        private final Vector3D normal;
+
+        ExpectedResult(final Vector3D point, final Vector3D normal) {
+            this.point = point;
+            this.normal = normal;
+        }
+
+        public Vector3D getPoint() {
+            return point;
+        }
+
+        public Vector3D getNormal() {
+            return normal;
+        }
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3DTest.java
new file mode 100644
index 0000000..fa4aea6
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3DTest.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.threed;
+
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class LinecastPoint3DTest {
+
+    private static final double TEST_EPS = 1e-10;
+
+    private static final DoublePrecisionContext TEST_PRECISION =
+            new EpsilonDoublePrecisionContext(TEST_EPS);
+
+    private static final Line3D X_AXIS =
+            Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+    private static final Line3D Y_AXIS =
+            Line3D.fromPointAndDirection(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+
+    @Test
+    public void testProperties() {
+        // arrange
+        Vector3D pt = Vector3D.of(1, 1, 1);
+        Vector3D normal = Vector3D.Unit.MINUS_X;
+
+        LinecastPoint3D it = new LinecastPoint3D(pt, normal, X_AXIS);
+
+        // act
+        Assert.assertSame(pt, it.getPoint());
+        Assert.assertSame(normal, it.getNormal());
+        Assert.assertSame(X_AXIS, it.getLine());
+        Assert.assertEquals(1.0, it.getAbscissa(), TEST_EPS);
+    }
+
+    @Test
+    public void testCompareTo() {
+        // arrange
+        LinecastPoint3D a = new LinecastPoint3D(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X, X_AXIS);
+
+        LinecastPoint3D b = new LinecastPoint3D(Vector3D.of(2, 2, 2), Vector3D.Unit.PLUS_X, X_AXIS);
+        LinecastPoint3D c = new LinecastPoint3D(Vector3D.of(-3, 3, 3), Vector3D.Unit.PLUS_X, X_AXIS);
+        LinecastPoint3D d = new LinecastPoint3D(Vector3D.of(1, 4, 4), Vector3D.Unit.PLUS_Y, X_AXIS);
+        LinecastPoint3D e = new LinecastPoint3D(Vector3D.of(1, 4, 4), Vector3D.Unit.PLUS_X, X_AXIS);
+
+        // act/assert
+        Assert.assertEquals(-1, LinecastPoint3D.ABSCISSA_ORDER.compare(a, b));
+        Assert.assertEquals(1, LinecastPoint3D.ABSCISSA_ORDER.compare(a, c));
+        Assert.assertEquals(1, LinecastPoint3D.ABSCISSA_ORDER.compare(a, d));
+        Assert.assertEquals(0, LinecastPoint3D.ABSCISSA_ORDER.compare(a, e));
+    }
+
+    @Test
+    public void testHashCode() {
+        // arrange
+        LinecastPoint3D a = new LinecastPoint3D(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X, X_AXIS);
+        LinecastPoint3D b = new LinecastPoint3D(Vector3D.of(2, 2, 2), Vector3D.Unit.PLUS_X, X_AXIS);
+        LinecastPoint3D c = new LinecastPoint3D(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Y, X_AXIS);
+        LinecastPoint3D d = new LinecastPoint3D(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X, Y_AXIS);
+        LinecastPoint3D e = new LinecastPoint3D(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X, X_AXIS);
+
+        // act
+        int hash = a.hashCode();
+
+        // assert
+        Assert.assertEquals(hash, a.hashCode());
+
+        Assert.assertNotEquals(hash, b.hashCode());
+        Assert.assertNotEquals(hash, c.hashCode());
+        Assert.assertNotEquals(hash, d.hashCode());
+
+        Assert.assertEquals(hash, e.hashCode());
+    }
+
+    @Test
+    public void testEquals() {
+        // arrange
+        LinecastPoint3D a = new LinecastPoint3D(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X, X_AXIS);
+        LinecastPoint3D b = new LinecastPoint3D(Vector3D.of(2, 2, 2), Vector3D.Unit.PLUS_X, X_AXIS);
+        LinecastPoint3D c = new LinecastPoint3D(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_Y, X_AXIS);
+        LinecastPoint3D d = new LinecastPoint3D(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X, Y_AXIS);
+        LinecastPoint3D e = new LinecastPoint3D(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X, X_AXIS);
+
+        // act/assert
+        Assert.assertTrue(a.equals(a));
+
+        Assert.assertFalse(a.equals(null));
+        Assert.assertFalse(a.equals(new Object()));
+
+        Assert.assertFalse(a.equals(b));
+        Assert.assertFalse(a.equals(c));
+        Assert.assertFalse(a.equals(d));
+
+        Assert.assertTrue(a.equals(e));
+        Assert.assertTrue(e.equals(a));
+    }
+
+    @Test
+    public void testToString() {
+        // arrange
+        LinecastPoint3D it = new LinecastPoint3D(Vector3D.of(1, 1, 1), Vector3D.Unit.PLUS_X, X_AXIS);
+
+        // act
+        String str = it.toString();
+
+        // assert
+        GeometryTestUtils.assertContains("LinecastPoint3D[point= (1.0, 1.0, 1.0), normal= (1.0, 0.0, 0.0)", str);
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java
index 8dbcc6b..4baeed8 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java
@@ -271,7 +271,129 @@ public class RegionBSPTree3DTest {
     }
 
     @Test
-    public void testRaycastFirstFace() {
+    public void testLinecast_empty() {
+        // arrange
+        RegionBSPTree3D tree = RegionBSPTree3D.empty();
+
+        // act/assert
+        LinecastChecker3D.with(tree)
+            .returnsNothing()
+            .whenGiven(Line3D.fromPoints(Vector3D.ZERO, Vector3D.Unit.PLUS_X, TEST_PRECISION));
+
+        LinecastChecker3D.with(tree)
+            .returnsNothing()
+            .whenGiven(Segment3D.fromPoints(Vector3D.Unit.MINUS_X, Vector3D.Unit.PLUS_X, TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast_full() {
+        // arrange
+        RegionBSPTree3D tree = RegionBSPTree3D.full();
+
+        // act/assert
+        LinecastChecker3D.with(tree)
+            .returnsNothing()
+            .whenGiven(Line3D.fromPoints(Vector3D.ZERO, Vector3D.Unit.PLUS_X, TEST_PRECISION));
+
+        LinecastChecker3D.with(tree)
+            .returnsNothing()
+            .whenGiven(Segment3D.fromPoints(Vector3D.Unit.MINUS_X, Vector3D.Unit.PLUS_X, TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast() {
+        // arrange
+        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION)
+                .toTree();
+
+        // act/assert
+        LinecastChecker3D.with(tree)
+            .returnsNothing()
+            .whenGiven(Line3D.fromPoints(Vector3D.of(0, 5, 5), Vector3D.of(1, 6, 6), TEST_PRECISION));
+
+        Vector3D corner = Vector3D.of(1, 1, 1);
+
+        LinecastChecker3D.with(tree)
+            .returns(Vector3D.ZERO, Vector3D.Unit.MINUS_X)
+            .and(Vector3D.ZERO, Vector3D.Unit.MINUS_Y)
+            .and(Vector3D.ZERO, Vector3D.Unit.MINUS_Z)
+            .and(corner, Vector3D.Unit.PLUS_Z)
+            .and(corner, Vector3D.Unit.PLUS_Y)
+            .and(corner, Vector3D.Unit.PLUS_X)
+            .whenGiven(Line3D.fromPoints(Vector3D.ZERO, corner, TEST_PRECISION));
+
+        LinecastChecker3D.with(tree)
+            .returns(corner, Vector3D.Unit.PLUS_Z)
+            .and(corner, Vector3D.Unit.PLUS_Y)
+            .and(corner, Vector3D.Unit.PLUS_X)
+            .whenGiven(Segment3D.fromPoints(Vector3D.of(0.5, 0.5, 0.5), corner, TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast_complementedTree() {
+        // arrange
+        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION)
+                .toTree();
+
+        tree.complement();
+
+        // act/assert
+        LinecastChecker3D.with(tree)
+            .returnsNothing()
+            .whenGiven(Line3D.fromPoints(Vector3D.of(0, 5, 5), Vector3D.of(1, 6, 6), TEST_PRECISION));
+
+        Vector3D corner = Vector3D.of(1, 1, 1);
+
+        LinecastChecker3D.with(tree)
+            .returns(Vector3D.ZERO, Vector3D.Unit.PLUS_Z)
+            .and(Vector3D.ZERO, Vector3D.Unit.PLUS_Y)
+            .and(Vector3D.ZERO, Vector3D.Unit.PLUS_X)
+            .and(corner, Vector3D.Unit.MINUS_X)
+            .and(corner, Vector3D.Unit.MINUS_Y)
+            .and(corner, Vector3D.Unit.MINUS_Z)
+            .whenGiven(Line3D.fromPoints(Vector3D.ZERO, corner, TEST_PRECISION));
+
+        LinecastChecker3D.with(tree)
+            .returns(corner, Vector3D.Unit.MINUS_X)
+            .and(corner, Vector3D.Unit.MINUS_Y)
+            .and(corner, Vector3D.Unit.MINUS_Z)
+            .whenGiven(Segment3D.fromPoints(Vector3D.of(0.5, 0.5, 0.5), corner, TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast_complexRegion() {
+        // arrange
+        RegionBSPTree3D a = RegionBSPTree3D.empty();
+        Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(0.5, 1, 1), TEST_PRECISION).boundaryStream()
+            .map(ConvexSubPlane::reverse)
+            .forEach(a::insert);
+        a.complement();
+
+        RegionBSPTree3D b = RegionBSPTree3D.empty();
+        Boundaries3D.rect(Vector3D.of(0.5, 0, 0), Vector3D.of(1, 1, 1), TEST_PRECISION).boundaryStream()
+            .map(ConvexSubPlane::reverse)
+            .forEach(b::insert);
+        b.complement();
+
+        RegionBSPTree3D c = Boundaries3D.rect(Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(1.5, 1.5, 1.5), TEST_PRECISION)
+                .toTree();
+
+        RegionBSPTree3D tree = RegionBSPTree3D.empty();
+        tree.union(a, b);
+        tree.union(c);
+
+        // act/assert
+        Vector3D corner = Vector3D.of(1.5, 1.5, 1.5);
+
+        LinecastChecker3D.with(tree)
+            .returns(corner, Vector3D.Unit.PLUS_Z)
+            .and(corner, Vector3D.Unit.PLUS_Y)
+            .and(corner, Vector3D.Unit.PLUS_X)
+            .whenGiven(Segment3D.fromPoints(Vector3D.of(0.25, 0.25, 0.25), Vector3D.of(2, 2, 2), TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecastFirst_multipleDirections() {
         // arrange
         RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.of(-1, -1, -1), Vector3D.of(1, 1, 1), TEST_PRECISION)
                 .toTree();
@@ -286,40 +408,58 @@ public class RegionBSPTree3DTest {
         Line3D zMinus = Line3D.fromPoints(Vector3D.ZERO, Vector3D.of(0, 0, -1), TEST_PRECISION);
 
         // act/assert
-        assertSubPlaneNormal(Vector3D.of(-1, 0, 0), tree.raycastFirst(xPlus.segmentFrom(Vector3D.of(-1.1, 0, 0))));
-        assertSubPlaneNormal(Vector3D.of(-1, 0, 0), tree.raycastFirst(xPlus.segmentFrom(Vector3D.of(-1, 0, 0))));
-        assertSubPlaneNormal(Vector3D.of(1, 0, 0), tree.raycastFirst(xPlus.segmentFrom(Vector3D.of(-0.9, 0, 0))));
-        Assert.assertEquals(null, tree.raycastFirst(xPlus.segmentFrom(Vector3D.of(1.1, 0, 0))));
-
-        assertSubPlaneNormal(Vector3D.of(1, 0, 0), tree.raycastFirst(xMinus.segmentFrom(Vector3D.of(1.1, 0, 0))));
-        assertSubPlaneNormal(Vector3D.of(1, 0, 0), tree.raycastFirst(xMinus.segmentFrom(Vector3D.of(1, 0, 0))));
-        assertSubPlaneNormal(Vector3D.of(-1, 0, 0), tree.raycastFirst(xMinus.segmentFrom(Vector3D.of(0.9, 0, 0))));
-        Assert.assertEquals(null, tree.raycastFirst(xMinus.segmentFrom(Vector3D.of(-1.1, 0, 0))));
-
-        assertSubPlaneNormal(Vector3D.of(0, -1, 0), tree.raycastFirst(yPlus.segmentFrom(Vector3D.of(0, -1.1, 0))));
-        assertSubPlaneNormal(Vector3D.of(0, -1, 0), tree.raycastFirst(yPlus.segmentFrom(Vector3D.of(0, -1, 0))));
-        assertSubPlaneNormal(Vector3D.of(0, 1, 0), tree.raycastFirst(yPlus.segmentFrom(Vector3D.of(0, -0.9, 0))));
-        Assert.assertEquals(null, tree.raycastFirst(yPlus.segmentFrom(Vector3D.of(0, 1.1, 0))));
-
-        assertSubPlaneNormal(Vector3D.of(0, 1, 0), tree.raycastFirst(yMinus.segmentFrom(Vector3D.of(0, 1.1, 0))));
-        assertSubPlaneNormal(Vector3D.of(0, 1, 0), tree.raycastFirst(yMinus.segmentFrom(Vector3D.of(0, 1, 0))));
-        assertSubPlaneNormal(Vector3D.of(0, -1, 0), tree.raycastFirst(yMinus.segmentFrom(Vector3D.of(0, 0.9, 0))));
-        Assert.assertEquals(null, tree.raycastFirst(yMinus.segmentFrom(Vector3D.of(0, -1.1, 0))));
-
-        assertSubPlaneNormal(Vector3D.of(0, 0, -1), tree.raycastFirst(zPlus.segmentFrom(Vector3D.of(0, 0, -1.1))));
-        assertSubPlaneNormal(Vector3D.of(0, 0, -1), tree.raycastFirst(zPlus.segmentFrom(Vector3D.of(0, 0, -1))));
-        assertSubPlaneNormal(Vector3D.of(0, 0, 1), tree.raycastFirst(zPlus.segmentFrom(Vector3D.of(0, 0, -0.9))));
-        Assert.assertEquals(null, tree.raycastFirst(zPlus.segmentFrom(Vector3D.of(0, 0, 1.1))));
-
-        assertSubPlaneNormal(Vector3D.of(0, 0, 1), tree.raycastFirst(zMinus.segmentFrom(Vector3D.of(0, 0, 1.1))));
-        assertSubPlaneNormal(Vector3D.of(0, 0, 1), tree.raycastFirst(zMinus.segmentFrom(Vector3D.of(0, 0, 1))));
-        assertSubPlaneNormal(Vector3D.of(0, 0, -1), tree.raycastFirst(zMinus.segmentFrom(Vector3D.of(0, 0, 0.9))));
-        Assert.assertEquals(null, tree.raycastFirst(zMinus.segmentFrom(Vector3D.of(0, 0, -1.1))));
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 0),
+                tree.linecastFirst(xPlus.segmentFrom(Vector3D.of(-1.1, 0, 0))).getNormal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 0),
+                tree.linecastFirst(xPlus.segmentFrom(Vector3D.of(-1, 0, 0))).getNormal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 0, 0),
+                tree.linecastFirst(xPlus.segmentFrom(Vector3D.of(-0.9, 0, 0))).getNormal(), TEST_EPS);
+        Assert.assertNull(tree.linecastFirst(xPlus.segmentFrom(Vector3D.of(1.1, 0, 0))));
+
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 0, 0),
+                tree.linecastFirst(xMinus.segmentFrom(Vector3D.of(1.1, 0, 0))).getNormal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 0, 0),
+                tree.linecastFirst(xMinus.segmentFrom(Vector3D.of(1, 0, 0))).getNormal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 0),
+                tree.linecastFirst(xMinus.segmentFrom(Vector3D.of(0.9, 0, 0))).getNormal(), TEST_EPS);
+        Assert.assertNull(tree.linecastFirst(xMinus.segmentFrom(Vector3D.of(-1.1, 0, 0))));
+
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, -1, 0),
+                tree.linecastFirst(yPlus.segmentFrom(Vector3D.of(0, -1.1, 0))).getNormal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, -1, 0),
+                tree.linecastFirst(yPlus.segmentFrom(Vector3D.of(0, -1, 0))).getNormal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 1, 0),
+                tree.linecastFirst(yPlus.segmentFrom(Vector3D.of(0, -0.9, 0))).getNormal(), TEST_EPS);
+        Assert.assertNull(tree.linecastFirst(yPlus.segmentFrom(Vector3D.of(0, 1.1, 0))));
+
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 1, 0),
+                tree.linecastFirst(yMinus.segmentFrom(Vector3D.of(0, 1.1, 0))).getNormal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 1, 0),
+                tree.linecastFirst(yMinus.segmentFrom(Vector3D.of(0, 1, 0))).getNormal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, -1, 0),
+                tree.linecastFirst(yMinus.segmentFrom(Vector3D.of(0, 0.9, 0))).getNormal(), TEST_EPS);
+        Assert.assertNull(tree.linecastFirst(yMinus.segmentFrom(Vector3D.of(0, -1.1, 0))));
+
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, -1),
+                tree.linecastFirst(zPlus.segmentFrom(Vector3D.of(0, 0, -1.1))).getNormal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, -1),
+                tree.linecastFirst(zPlus.segmentFrom(Vector3D.of(0, 0, -1))).getNormal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, 1),
+                tree.linecastFirst(zPlus.segmentFrom(Vector3D.of(0, 0, -0.9))).getNormal(), TEST_EPS);
+        Assert.assertNull(tree.linecastFirst(zPlus.segmentFrom(Vector3D.of(0, 0, 1.1))));
+
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, 1),
+                tree.linecastFirst(zMinus.segmentFrom(Vector3D.of(0, 0, 1.1))).getNormal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, 1),
+                tree.linecastFirst(zMinus.segmentFrom(Vector3D.of(0, 0, 1))).getNormal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, -1),
+                tree.linecastFirst(zMinus.segmentFrom(Vector3D.of(0, 0, 0.9))).getNormal(), TEST_EPS);
+        Assert.assertNull(tree.linecastFirst(zMinus.segmentFrom(Vector3D.of(0, 0, -1.1))));
     }
 
     // issue GEOMETRY-38
     @Test
-    public void testRaycastFirstFace_linePassesThroughVertex() {
+    public void testLinecastFirst_linePassesThroughVertex() {
         // arrange
         Vector3D lowerCorner = Vector3D.ZERO;
         Vector3D upperCorner = Vector3D.of(1, 1, 1);
@@ -331,21 +471,21 @@ public class RegionBSPTree3DTest {
         Line3D downDiagonal = upDiagonal.reverse();
 
         // act/assert
-        ConvexSubPlane upFromOutsideResult = tree.raycastFirst(upDiagonal.segmentFrom(Vector3D.of(-1, -1, -1)));
+        LinecastPoint3D upFromOutsideResult = tree.linecastFirst(upDiagonal.segmentFrom(Vector3D.of(-1, -1, -1)));
         Assert.assertNotNull(upFromOutsideResult);
-        EuclideanTestUtils.assertCoordinatesEqual(lowerCorner, upFromOutsideResult.getPlane().intersection(upDiagonal), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(lowerCorner, upFromOutsideResult.getPoint(), TEST_EPS);
 
-        ConvexSubPlane upFromCenterResult = tree.raycastFirst(upDiagonal.segmentFrom(center));
+        LinecastPoint3D upFromCenterResult = tree.linecastFirst(upDiagonal.segmentFrom(center));
         Assert.assertNotNull(upFromCenterResult);
-        EuclideanTestUtils.assertCoordinatesEqual(upperCorner, upFromCenterResult.getPlane().intersection(upDiagonal), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(upperCorner, upFromCenterResult.getPoint(), TEST_EPS);
 
-        ConvexSubPlane downFromOutsideResult = tree.raycastFirst(downDiagonal.segmentFrom(Vector3D.of(2, 2, 2)));
+        LinecastPoint3D downFromOutsideResult = tree.linecastFirst(downDiagonal.segmentFrom(Vector3D.of(2, 2, 2)));
         Assert.assertNotNull(downFromOutsideResult);
-        EuclideanTestUtils.assertCoordinatesEqual(upperCorner, downFromOutsideResult.getPlane().intersection(downDiagonal), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(upperCorner, downFromOutsideResult.getPoint(), TEST_EPS);
 
-        ConvexSubPlane downFromCenterResult = tree.raycastFirst(downDiagonal.segmentFrom(center));
+        LinecastPoint3D downFromCenterResult = tree.linecastFirst(downDiagonal.segmentFrom(center));
         Assert.assertNotNull(downFromCenterResult);
-        EuclideanTestUtils.assertCoordinatesEqual(lowerCorner, downFromCenterResult.getPlane().intersection(downDiagonal), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(lowerCorner, downFromCenterResult.getPoint(), TEST_EPS);
     }
 
     // Issue GEOMETRY-43
@@ -365,19 +505,19 @@ public class RegionBSPTree3DTest {
         Vector3D expectedIntersection2 = Vector3D.of(0.5, 1.0, 0.0);
 
         // act/assert
-        ConvexSubPlane bottom = tree.raycastFirst(bottomLine.segmentFrom(firstPointOnLine));
+        LinecastPoint3D bottom = tree.linecastFirst(bottomLine.segmentFrom(firstPointOnLine));
         Assert.assertNotNull(bottom);
-        EuclideanTestUtils.assertCoordinatesEqual(expectedIntersection1, bottom.getHyperplane().intersection(bottomLine), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(expectedIntersection1, bottom.getPoint(), TEST_EPS);
 
-        bottom = tree.raycastFirst(bottomLine.segmentFrom(Vector3D.of(0.5, 0.1, 0.0)));
+        bottom = tree.linecastFirst(bottomLine.segmentFrom(Vector3D.of(0.5, 0.1, 0.0)));
         Assert.assertNotNull(bottom);
-        Vector3D intersection = bottom.getPlane().intersection(bottomLine);
+        Vector3D intersection = bottom.getPoint();
         Assert.assertNotNull(intersection);
         EuclideanTestUtils.assertCoordinatesEqual(expectedIntersection2, intersection, TEST_EPS);
     }
 
     @Test
-    public void testRaycastFirstFace_rayPointOnFace() {
+    public void testLinecastFirst_rayPointOnFace() {
         // arrange
         Vector3D lowerCorner = Vector3D.ZERO;
         Vector3D upperCorner = Vector3D.of(1, 1, 1);
@@ -389,17 +529,15 @@ public class RegionBSPTree3DTest {
         Line3D outOfBoxLine = Line3D.fromPoints(pt, pt.add(Vector3D.Unit.MINUS_Z), TEST_PRECISION);
 
         // act/assert
-        ConvexSubPlane intoBoxResult = tree.raycastFirst(intoBoxLine.segmentFrom(pt));
-        Vector3D intoBoxPt = intoBoxResult.getPlane().intersection(intoBoxLine);
-        EuclideanTestUtils.assertCoordinatesEqual(pt, intoBoxPt, TEST_EPS);
+        LinecastPoint3D intoBoxResult = tree.linecastFirst(intoBoxLine.segmentFrom(pt));
+        EuclideanTestUtils.assertCoordinatesEqual(pt, intoBoxResult.getPoint(), TEST_EPS);
 
-        ConvexSubPlane outOfBoxResult = tree.raycastFirst(outOfBoxLine.segmentFrom(pt));
-        Vector3D outOfBoxPt = outOfBoxResult.getPlane().intersection(outOfBoxLine);
-        EuclideanTestUtils.assertCoordinatesEqual(pt, outOfBoxPt, TEST_EPS);
+        LinecastPoint3D outOfBoxResult = tree.linecastFirst(outOfBoxLine.segmentFrom(pt));
+        EuclideanTestUtils.assertCoordinatesEqual(pt, outOfBoxResult.getPoint(), TEST_EPS);
     }
 
     @Test
-    public void testRaycastFirstFace_rayPointOnVertex() {
+    public void testLinecastFirst_rayPointOnVertex() {
         // arrange
         Vector3D lowerCorner = Vector3D.ZERO;
         Vector3D upperCorner = Vector3D.of(1, 1, 1);
@@ -410,17 +548,15 @@ public class RegionBSPTree3DTest {
         Line3D outOfBoxLine = intoBoxLine.reverse();
 
         // act/assert
-        ConvexSubPlane intoBoxResult = tree.raycastFirst(intoBoxLine.segmentFrom(lowerCorner));
-        Vector3D intoBoxPt = intoBoxResult.getPlane().intersection(intoBoxLine);
-        EuclideanTestUtils.assertCoordinatesEqual(lowerCorner, intoBoxPt, TEST_EPS);
+        LinecastPoint3D intoBoxResult = tree.linecastFirst(intoBoxLine.segmentFrom(lowerCorner));
+        EuclideanTestUtils.assertCoordinatesEqual(lowerCorner, intoBoxResult.getPoint(), TEST_EPS);
 
-        ConvexSubPlane outOfBoxResult = tree.raycastFirst(outOfBoxLine.segmentFrom(lowerCorner));
-        Vector3D outOfBoxPt = outOfBoxResult.getPlane().intersection(outOfBoxLine);
-        EuclideanTestUtils.assertCoordinatesEqual(lowerCorner, outOfBoxPt, TEST_EPS);
+        LinecastPoint3D outOfBoxResult = tree.linecastFirst(outOfBoxLine.segmentFrom(lowerCorner));
+        EuclideanTestUtils.assertCoordinatesEqual(lowerCorner, outOfBoxResult.getPoint(), TEST_EPS);
     }
 
     @Test
-    public void testRaycastFirstFace_onlyReturnsPointsWithinSegment() throws IOException, ParseException {
+    public void testLinecastFirst_onlyReturnsPointsWithinSegment() throws IOException, ParseException {
         // arrange
         Vector3D lowerCorner = Vector3D.ZERO;
         Vector3D upperCorner = Vector3D.of(1, 1, 1);
@@ -430,18 +566,24 @@ public class RegionBSPTree3DTest {
         Line3D line = Line3D.fromPointAndDirection(Vector3D.of(0.5, 0.5, 0.5), Vector3D.Unit.PLUS_X, TEST_PRECISION);
 
         // act/assert
-        assertSubPlaneNormal(Vector3D.Unit.MINUS_X, tree.raycastFirst(line.span()));
-        assertSubPlaneNormal(Vector3D.Unit.PLUS_X, tree.raycastFirst(line.reverse().span()));
-
-        assertSubPlaneNormal(Vector3D.Unit.MINUS_X, tree.raycastFirst(line.segment(Vector3D.of(-2, 0.5, 0.5), Vector3D.of(0.5, 0.5, 0.5))));
-        assertSubPlaneNormal(Vector3D.Unit.MINUS_X, tree.raycastFirst(line.segment(Vector3D.of(-2, 0.5, 0.5), Vector3D.of(0, 0.5, 0.5))));
-
-        assertSubPlaneNormal(Vector3D.Unit.PLUS_X, tree.raycastFirst(line.segment(Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(2, 0.5, 0.5))));
-        assertSubPlaneNormal(Vector3D.Unit.PLUS_X, tree.raycastFirst(line.segment(Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(1, 0.5, 0.5))));
-
-        Assert.assertNull(tree.raycastFirst(line.segment(Vector3D.of(-2, 0.5, 0.5), Vector3D.of(-1, 0.5, 0.5))));
-        Assert.assertNull(tree.raycastFirst(line.segment(Vector3D.of(-2, 0.5, 0.5), Vector3D.of(-1, 0.5, 0.5))));
-        Assert.assertNull(tree.raycastFirst(line.segment(Vector3D.of(0.25, 0.5, 0.5), Vector3D.of(0.75, 0.5, 0.5))));
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.MINUS_X,
+                tree.linecastFirst(line.span()).getNormal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_X,
+                tree.linecastFirst(line.reverse().span()).getNormal(), TEST_EPS);
+
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.MINUS_X,
+                tree.linecastFirst(line.segment(Vector3D.of(-2, 0.5, 0.5), Vector3D.of(0.5, 0.5, 0.5))).getNormal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.MINUS_X,
+                tree.linecastFirst(line.segment(Vector3D.of(-2, 0.5, 0.5), Vector3D.of(0, 0.5, 0.5))).getNormal(), TEST_EPS);
+
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_X,
+                tree.linecastFirst(line.segment(Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(2, 0.5, 0.5))).getNormal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_X,
+                tree.linecastFirst(line.segment(Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(1, 0.5, 0.5))).getNormal(), TEST_EPS);
+
+        Assert.assertNull(tree.linecastFirst(line.segment(Vector3D.of(-2, 0.5, 0.5), Vector3D.of(-1, 0.5, 0.5))));
+        Assert.assertNull(tree.linecastFirst(line.segment(Vector3D.of(-2, 0.5, 0.5), Vector3D.of(-1, 0.5, 0.5))));
+        Assert.assertNull(tree.linecastFirst(line.segment(Vector3D.of(0.25, 0.5, 0.5), Vector3D.of(0.75, 0.5, 0.5))));
     }
 
     @Test
@@ -1402,10 +1544,6 @@ public class RegionBSPTree3DTest {
         return tree;
     }
 
-    private static void assertSubPlaneNormal(Vector3D expectedNormal, ConvexSubPlane sub) {
-        EuclideanTestUtils.assertCoordinatesEqual(expectedNormal, sub.getPlane().getNormal(), TEST_EPS);
-    }
-
     private static double cubeVolume(double size) {
         return size * size * size;
     }
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2DTest.java
new file mode 100644
index 0000000..b3d31d3
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecastWrapper2DTest.java
@@ -0,0 +1,204 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.twod;
+
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.junit.Test;
+
+public class BoundarySourceLinecastWrapper2DTest {
+
+    private static final double TEST_EPS = 1e-10;
+
+    private static final DoublePrecisionContext TEST_PRECISION =
+            new EpsilonDoublePrecisionContext(TEST_EPS);
+
+    private static final BoundarySource2D UNIT_SQUARE =
+            Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION);
+
+    @Test
+    public void testLinecast_line_simple() {
+        // arrange
+        BoundarySourceLinecastWrapper2D wrapper = new BoundarySourceLinecastWrapper2D(UNIT_SQUARE);
+
+        // act/assert
+
+        // no intersections
+        LinecastChecker2D.with(wrapper)
+            .returnsNothing()
+            .whenGiven(Line.fromPointAndDirection(Vector2D.of(0, 4), Vector2D.Unit.MINUS_X, TEST_PRECISION));
+
+        // through center; two directions
+        LinecastChecker2D.with(wrapper)
+            .returns(Vector2D.of(0, 0.5), Vector2D.Unit.MINUS_X)
+            .and(Vector2D.of(1, 0.5), Vector2D.Unit.PLUS_X)
+            .whenGiven(Line.fromPointAndDirection(Vector2D.of(0.5, 0.5), Vector2D.Unit.PLUS_X, TEST_PRECISION));
+
+        LinecastChecker2D.with(wrapper)
+            .returns(Vector2D.of(1, 0.5), Vector2D.Unit.PLUS_X)
+            .and(Vector2D.of(0, 0.5), Vector2D.Unit.MINUS_X)
+            .whenGiven(Line.fromPointAndDirection(Vector2D.of(0.5, 0.5), Vector2D.Unit.MINUS_X, TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast_line_alongFace() {
+        // arrange
+        BoundarySourceLinecastWrapper2D wrapper = new BoundarySourceLinecastWrapper2D(UNIT_SQUARE);
+
+        // act/assert
+        LinecastChecker2D.with(wrapper)
+            .returns(Vector2D.of(0, 1), Vector2D.Unit.MINUS_X)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X)
+            .whenGiven(Line.fromPointAndDirection(Vector2D.of(0, 1), Vector2D.Unit.PLUS_X, TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast_line_corners() {
+        // arrange
+        BoundarySourceLinecastWrapper2D wrapper = new BoundarySourceLinecastWrapper2D(UNIT_SQUARE);
+
+        // act/assert
+
+        // through single corner vertex
+        LinecastChecker2D.with(wrapper)
+            .returns(Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X)
+            .whenGiven(Line.fromPointAndDirection(Vector2D.of(0, 2), Vector2D.of(1, -1), TEST_PRECISION));
+
+        // through two corner vertices
+        LinecastChecker2D.with(wrapper)
+            .returns(Vector2D.ZERO, Vector2D.Unit.MINUS_X)
+            .and(Vector2D.ZERO, Vector2D.Unit.MINUS_Y)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X)
+            .whenGiven(Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast_segment_simple() {
+        // arrange
+        BoundarySourceLinecastWrapper2D wrapper = new BoundarySourceLinecastWrapper2D(UNIT_SQUARE);
+
+        // act/assert
+
+        // no intersections; underlying line does not intersect
+        LinecastChecker2D.with(wrapper)
+            .returnsNothing()
+            .whenGiven(Line.fromPointAndDirection(Vector2D.of(0, 4), Vector2D.Unit.MINUS_X, TEST_PRECISION)
+                    .segment(-10, 10));
+
+        // no intersections; underlying line does intersect
+        LinecastChecker2D.with(wrapper)
+            .returnsNothing()
+            .whenGiven(Line.fromPointAndDirection(Vector2D.of(0.5, 0.5), Vector2D.Unit.PLUS_X, TEST_PRECISION)
+                    .segment(2, 10));
+
+        // no boundaries excluded; two directions
+        LinecastChecker2D.with(wrapper)
+            .returns(Vector2D.of(0, 0.5), Vector2D.Unit.MINUS_X)
+            .and(Vector2D.of(1, 0.5), Vector2D.Unit.PLUS_X)
+            .whenGiven(Line.fromPointAndDirection(Vector2D.of(0.5, 0.5), Vector2D.Unit.PLUS_X, TEST_PRECISION)
+                    .segment(-10, 10));
+
+        LinecastChecker2D.with(wrapper)
+            .returns(Vector2D.of(1, 0.5), Vector2D.Unit.PLUS_X)
+            .and(Vector2D.of(0, 0.5), Vector2D.Unit.MINUS_X)
+            .whenGiven(Line.fromPointAndDirection(Vector2D.of(0.5, 0.5), Vector2D.Unit.MINUS_X, TEST_PRECISION)
+                    .segment(-10, 10));
+    }
+
+    @Test
+    public void testLinecast_segment_boundaryExcluded() {
+        // arrange
+        BoundarySourceLinecastWrapper2D wrapper = new BoundarySourceLinecastWrapper2D(UNIT_SQUARE);
+
+        // act/assert
+        Vector2D center = Vector2D.of(0.5, 0.5);
+        LinecastChecker2D.with(wrapper)
+            .returns(Vector2D.of(1, 0.5), Vector2D.Unit.PLUS_X)
+            .whenGiven(Line.fromPointAndDirection(center, Vector2D.Unit.PLUS_X, TEST_PRECISION)
+                    .segmentFrom(center));
+
+        LinecastChecker2D.with(wrapper)
+            .returns(Vector2D.of(1, 0.5), Vector2D.Unit.PLUS_X)
+            .whenGiven(Line.fromPointAndDirection(center, Vector2D.Unit.MINUS_X, TEST_PRECISION)
+                    .segmentTo(center));
+    }
+
+    @Test
+    public void testLinecast_segment_startEndPointsOnBoundaries() {
+        // arrange
+        BoundarySourceLinecastWrapper2D wrapper = new BoundarySourceLinecastWrapper2D(UNIT_SQUARE);
+
+        // act/assert
+        LinecastChecker2D.with(wrapper)
+            .returns(Vector2D.of(1, 0.5), Vector2D.Unit.PLUS_X)
+            .and(Vector2D.of(0, 0.5), Vector2D.Unit.MINUS_X)
+            .whenGiven(Segment.fromPoints(Vector2D.of(1, 0.5), Vector2D.of(0, 0.5), TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast_segment_alongFace() {
+        // arrange
+        BoundarySourceLinecastWrapper2D wrapper = new BoundarySourceLinecastWrapper2D(UNIT_SQUARE);
+
+        // act/assert
+
+        // includes two intersecting boundaries
+        LinecastChecker2D.with(wrapper)
+            .returns(Vector2D.of(0, 1), Vector2D.Unit.MINUS_X)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X)
+            .whenGiven(Segment.fromPoints(Vector2D.of(-1, 1), Vector2D.of(2, 1), TEST_PRECISION));
+
+        // one intersecting boundary
+        LinecastChecker2D.with(wrapper)
+            .returns(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X)
+            .whenGiven(Segment.fromPoints(Vector2D.of(0.25, 1), Vector2D.of(2, 1), TEST_PRECISION));
+
+        // no intersecting boundary
+        LinecastChecker2D.with(wrapper)
+            .returnsNothing()
+            .whenGiven(Segment.fromPoints(Vector2D.of(0.25, 1), Vector2D.of(0.75, 1), TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast_segment_corners() {
+        // arrange
+        BoundarySourceLinecastWrapper2D wrapper = new BoundarySourceLinecastWrapper2D(UNIT_SQUARE);
+
+        // act/assert
+
+        // through corner
+        LinecastChecker2D.with(wrapper)
+            .returns(Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X)
+            .whenGiven(Segment.fromPoints(Vector2D.of(0, 2), Vector2D.of(2, 0), TEST_PRECISION));
+
+        // starts on corner
+        LinecastChecker2D.with(wrapper)
+            .returns(Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X)
+            .whenGiven(Segment.fromPoints(Vector2D.of(1, 1), Vector2D.of(2, 0), TEST_PRECISION));
+
+        // ends on corner
+        LinecastChecker2D.with(wrapper)
+            .returns(Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X)
+            .whenGiven(Segment.fromPoints(Vector2D.of(0, 2), Vector2D.of(1, 1), TEST_PRECISION));
+    }
+}
+
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/ConvexAreaTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/ConvexAreaTest.java
index a054329..2ed0e37 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/ConvexAreaTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/ConvexAreaTest.java
@@ -664,6 +664,47 @@ public class ConvexAreaTest {
     }
 
     @Test
+    public void testLinecast_full() {
+        // arrange
+        ConvexArea area = ConvexArea.full();
+
+        // act/assert
+        LinecastChecker2D.with(area)
+            .returnsNothing()
+            .whenGiven(Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION));
+
+        LinecastChecker2D.with(area)
+            .returnsNothing()
+            .whenGiven(Segment.fromPoints(Vector2D.Unit.MINUS_X, Vector2D.Unit.PLUS_X, TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast() {
+        // arrange
+        ConvexArea area = ConvexArea.fromVertexLoop(Arrays.asList(
+                    Vector2D.ZERO, Vector2D.of(1, 0),
+                    Vector2D.of(1, 1), Vector2D.of(0, 1)
+                ), TEST_PRECISION);
+
+        // act/assert
+        LinecastChecker2D.with(area)
+            .returnsNothing()
+            .whenGiven(Line.fromPoints(Vector2D.of(0, 5), Vector2D.of(1, 6), TEST_PRECISION));
+
+        LinecastChecker2D.with(area)
+            .returns(Vector2D.ZERO, Vector2D.Unit.MINUS_X)
+            .and(Vector2D.ZERO, Vector2D.Unit.MINUS_Y)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X)
+            .whenGiven(Line.fromPoints(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION));
+
+        LinecastChecker2D.with(area)
+            .returns(Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X)
+            .whenGiven(Segment.fromPoints(Vector2D.of(0.5, 0.5), Vector2D.of(1, 1), TEST_PRECISION));
+    }
+
+    @Test
     public void testToString() {
         // arrange
         ConvexArea area = ConvexArea.full();
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LinecastChecker2D.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LinecastChecker2D.java
new file mode 100644
index 0000000..3cc22ad
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LinecastChecker2D.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.twod;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.junit.Assert;
+
+/** Helper class designed to assist with linecast test assertions in 2D.
+ */
+class LinecastChecker2D {
+
+    private static final double TEST_EPS = 1e-10;
+
+    private static final DoublePrecisionContext TEST_PRECISION =
+            new EpsilonDoublePrecisionContext(TEST_EPS);
+
+    /** The linecastable target. */
+    private final Linecastable2D target;
+
+    /** List of expected results from the line cast operation. */
+    private final List<ExpectedResult> expectedResults = new ArrayList<>();
+
+    /** Construct a new instance that performs linecast assertions against the
+     * given target.
+     * @param target
+     */
+    LinecastChecker2D(final Linecastable2D target) {
+        this.target = target;
+    }
+
+    /** Configure the instance to expect no results (an empty list from linecast() and null from
+     * linecastFirst()) from the next linecast operation performed by {@link #whenGiven(Line)}
+     * or {@link #whenGiven(Segment)}.
+     * @return
+     */
+    public LinecastChecker2D returnsNothing() {
+        expectedResults.clear();
+
+        return this;
+    }
+
+    /** Configure the instance to expect a linecast point with the given parameters on the next
+     * linecast operation. Multiple calls to this method and/or {@link #and(Vector2D, Vector2D)}
+     * create an internal ordered list of results.
+     * @param point
+     * @param normal
+     * @return
+     */
+    public LinecastChecker2D returns(final Vector2D point, final Vector2D normal) {
+        expectedResults.add(new ExpectedResult(point, normal));
+
+        return this;
+    }
+
+    /** Fluent API alias for {@link #returns(Vector2D, Vector2D)}.
+     * @param point
+     * @param normal
+     * @return
+     */
+    public LinecastChecker2D and(final Vector2D point, final Vector2D normal) {
+        return returns(point, normal);
+    }
+
+    /** Perform {@link Linecastable2D#linecast(Line)} and {@link Linecastable2D#linecastFirst(Line)}
+     * operations using the given line and assert that the results match the configured expected
+     * values.
+     * @param line
+     */
+    public void whenGiven(final Line line) {
+        checkLinecastResults(target.linecast(line), line);
+        checkLinecastFirstResult(target.linecastFirst(line), line);
+    }
+
+    /** Perform {@link Linecastable2D#linecast(Segment)} and {@link Linecastable2D#linecastFirst(Segment)}
+     * operations using the given line segment and assert that the results match the configured
+     * expected results.
+     * @param segment
+     */
+    public void whenGiven(final Segment segment) {
+        Line line = segment.getLine();
+
+        checkLinecastResults(target.linecast(segment), line);
+        checkLinecastFirstResult(target.linecastFirst(segment), line);
+    }
+
+    /** Check that the given set of linecast result points matches those expected.
+     * @param results
+     * @param line
+     */
+    private void checkLinecastResults(List<LinecastPoint2D> results, Line line) {
+        Assert.assertNotNull("Linecast result list cannot be null", results);
+        Assert.assertEquals("Unexpected result size for linecast", expectedResults.size(), results.size());
+
+        for (int i = 0; i < expectedResults.size(); ++i) {
+            LinecastPoint2D expected = toLinecastPoint(expectedResults.get(i), line);
+            LinecastPoint2D actual = results.get(i);
+
+            if (!eq(expected, actual)) {
+                Assert.fail("Unexpected linecast point at index " + i + " expected " + expected +
+                        " but was " + actual);
+            }
+        }
+    }
+
+    /** Check that the given linecastFirst result matches that expected.
+     * @param result
+     * @param line
+     */
+    private void checkLinecastFirstResult(LinecastPoint2D result, Line line) {
+        if (expectedResults.isEmpty()) {
+            Assert.assertNull("Expected linecastFirst result to be null", result);
+        } else {
+            LinecastPoint2D expected = toLinecastPoint(expectedResults.get(0), line);
+
+            Assert.assertNotNull("Expected linecastFirst result to not be null", result);
+
+            if (!eq(expected, result)) {
+                Assert.fail("Unexpected result from linecastFirst: expected " + expected +
+                        " but was " + result);
+            }
+        }
+    }
+
+    /** Fluent API method for creating new instances.
+     * @param src
+     * @return
+     */
+    public static LinecastChecker2D with(final Linecastable2D src) {
+        return new LinecastChecker2D(src);
+    }
+
+    /** Return true if the given linecast points are equivalent according to the test precision.
+     * @param expected
+     * @param actual
+     * @return
+     */
+    private static boolean eq(LinecastPoint2D a, LinecastPoint2D b) {
+        return a.getPoint().eq(b.getPoint(), TEST_PRECISION) &&
+                a.getNormal().eq(b.getNormal(), TEST_PRECISION) &&
+                a.getLine().equals(b.getLine()) &&
+                TEST_PRECISION.eq(a.getAbscissa(), b.getAbscissa());
+    }
+
+    /** Convert an {@link ExpectedResult} struct to a {@link LinecastPoint2D} instance
+     * using the given line.
+     * @param expected
+     * @param line
+     * @return
+     */
+    private static LinecastPoint2D toLinecastPoint(ExpectedResult expected, Line line) {
+        return new LinecastPoint2D(expected.getPoint(), expected.getNormal(), line);
+    }
+
+    /** Class containing intermediate expected results for a linecast operation.
+     */
+    private static final class ExpectedResult {
+        private final Vector2D point;
+        private final Vector2D normal;
+
+        ExpectedResult(final Vector2D point, final Vector2D normal) {
+            this.point = point;
+            this.normal = normal;
+        }
+
+        public Vector2D getPoint() {
+            return point;
+        }
+
+        public Vector2D getNormal() {
+            return normal;
+        }
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2DTest.java
new file mode 100644
index 0000000..29a0d6d
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2DTest.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.twod;
+
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class LinecastPoint2DTest {
+
+    private static final double TEST_EPS = 1e-10;
+
+    private static final DoublePrecisionContext TEST_PRECISION =
+            new EpsilonDoublePrecisionContext(TEST_EPS);
+
+    private static final Line X_AXIS =
+            Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION);
+
+    private static final Line Y_AXIS =
+            Line.fromPointAndDirection(Vector2D.ZERO, Vector2D.Unit.PLUS_Y, TEST_PRECISION);
+
+    @Test
+    public void testProperties() {
+        // arrange
+        Vector2D pt = Vector2D.of(1, 1);
+        Vector2D normal = Vector2D.Unit.PLUS_X;
+
+        LinecastPoint2D it = new LinecastPoint2D(pt, normal, X_AXIS);
+
+        // act
+        Assert.assertSame(pt, it.getPoint());
+        Assert.assertSame(normal, it.getNormal());
+        Assert.assertSame(X_AXIS, it.getLine());
+        Assert.assertEquals(1.0, it.getAbscissa(), TEST_EPS);
+    }
+
+    @Test
+    public void testAbscissaOrder() {
+        // arrange
+        LinecastPoint2D a = new LinecastPoint2D(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X, X_AXIS);
+
+        LinecastPoint2D b = new LinecastPoint2D(Vector2D.of(2, 2), Vector2D.Unit.PLUS_X, X_AXIS);
+        LinecastPoint2D c = new LinecastPoint2D(Vector2D.of(-3, 3), Vector2D.Unit.PLUS_Y, X_AXIS);
+        LinecastPoint2D d = new LinecastPoint2D(Vector2D.of(1, 4), Vector2D.Unit.PLUS_Y, X_AXIS);
+        LinecastPoint2D e = new LinecastPoint2D(Vector2D.of(1, 4), Vector2D.Unit.PLUS_X, X_AXIS);
+
+        // act/assert
+        Assert.assertEquals(-1, LinecastPoint2D.ABSCISSA_ORDER.compare(a, b));
+        Assert.assertEquals(1, LinecastPoint2D.ABSCISSA_ORDER.compare(a, c));
+        Assert.assertEquals(1, LinecastPoint2D.ABSCISSA_ORDER.compare(a, d));
+        Assert.assertEquals(0, LinecastPoint2D.ABSCISSA_ORDER.compare(a, e));
+    }
+
+    @Test
+    public void testHashCode() {
+        // arrange
+        LinecastPoint2D a = new LinecastPoint2D(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X, X_AXIS);
+        LinecastPoint2D b = new LinecastPoint2D(Vector2D.of(2, 2), Vector2D.Unit.PLUS_X, X_AXIS);
+        LinecastPoint2D c = new LinecastPoint2D(Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y, X_AXIS);
+        LinecastPoint2D d = new LinecastPoint2D(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X, Y_AXIS);
+        LinecastPoint2D e = new LinecastPoint2D(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X, X_AXIS);
+
+        // act
+        int hash = a.hashCode();
+
+        // assert
+        Assert.assertEquals(hash, a.hashCode());
+
+        Assert.assertNotEquals(hash, b.hashCode());
+        Assert.assertNotEquals(hash, c.hashCode());
+        Assert.assertNotEquals(hash, d.hashCode());
+
+        Assert.assertEquals(hash, e.hashCode());
+    }
+
+    @Test
+    public void testEquals() {
+        // arrange
+        LinecastPoint2D a = new LinecastPoint2D(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X, X_AXIS);
+        LinecastPoint2D b = new LinecastPoint2D(Vector2D.of(2, 2), Vector2D.Unit.PLUS_X, X_AXIS);
+        LinecastPoint2D c = new LinecastPoint2D(Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y, X_AXIS);
+        LinecastPoint2D d = new LinecastPoint2D(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X, Y_AXIS);
+        LinecastPoint2D e = new LinecastPoint2D(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X, X_AXIS);
+
+        // act/assert
+        Assert.assertTrue(a.equals(a));
+
+        Assert.assertFalse(a.equals(null));
+        Assert.assertFalse(a.equals(new Object()));
+
+        Assert.assertFalse(a.equals(b));
+        Assert.assertFalse(a.equals(c));
+        Assert.assertFalse(a.equals(d));
+
+        Assert.assertTrue(a.equals(e));
+        Assert.assertTrue(e.equals(a));
+    }
+
+    @Test
+    public void testToString() {
+        // arrange
+        LinecastPoint2D it = new LinecastPoint2D(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X, X_AXIS);
+
+        // act
+        String str = it.toString();
+
+        // assert
+        GeometryTestUtils.assertContains("LinecastPoint2D[point= (1.0, 1.0), normal= (1.0, 0.0)", str);
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolylineTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolylineTest.java
index e4318ec..f7af9c7 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolylineTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolylineTest.java
@@ -922,6 +922,47 @@ public class PolylineTest {
     }
 
     @Test
+    public void testLinecast_empty() {
+        // arrange
+        Polyline polyline = Polyline.empty();
+
+        // act/assert
+        LinecastChecker2D.with(polyline)
+            .returnsNothing()
+            .whenGiven(Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION));
+
+        LinecastChecker2D.with(polyline)
+            .returnsNothing()
+            .whenGiven(Segment.fromPoints(Vector2D.Unit.MINUS_X, Vector2D.Unit.PLUS_X, TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast() {
+        // arrange
+        Polyline polyline = Polyline.fromVertexLoop(Arrays.asList(
+                    Vector2D.ZERO, Vector2D.of(1, 0),
+                    Vector2D.of(1, 1), Vector2D.of(0, 1)
+                ), TEST_PRECISION);
+
+        // act/assert
+        LinecastChecker2D.with(polyline)
+            .returnsNothing()
+            .whenGiven(Line.fromPoints(Vector2D.of(0, 5), Vector2D.of(1, 6), TEST_PRECISION));
+
+        LinecastChecker2D.with(polyline)
+            .returns(Vector2D.ZERO, Vector2D.Unit.MINUS_X)
+            .and(Vector2D.ZERO, Vector2D.Unit.MINUS_Y)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X)
+            .whenGiven(Line.fromPoints(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION));
+
+        LinecastChecker2D.with(polyline)
+            .returns(Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X)
+            .whenGiven(Segment.fromPoints(Vector2D.of(0.5, 0.5), Vector2D.of(1, 1), TEST_PRECISION));
+    }
+
+    @Test
     public void testToString() {
         // arrange
         Line yAxis = Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_Y, TEST_PRECISION);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java
index 1fbb863..5d781ae 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java
@@ -973,6 +973,117 @@ public class RegionBSPTree2DTest {
     }
 
     @Test
+    public void testLinecast_empty() {
+        // arrange
+        RegionBSPTree2D tree = RegionBSPTree2D.empty();
+
+        // act/assert
+        LinecastChecker2D.with(tree)
+            .returnsNothing()
+            .whenGiven(Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION));
+
+        LinecastChecker2D.with(tree)
+            .returnsNothing()
+            .whenGiven(Segment.fromPoints(Vector2D.Unit.MINUS_X, Vector2D.Unit.PLUS_X, TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast_full() {
+        // arrange
+        RegionBSPTree2D tree = RegionBSPTree2D.full();
+
+        // act/assert
+        LinecastChecker2D.with(tree)
+            .returnsNothing()
+            .whenGiven(Line.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION));
+
+        LinecastChecker2D.with(tree)
+            .returnsNothing()
+            .whenGiven(Segment.fromPoints(Vector2D.Unit.MINUS_X, Vector2D.Unit.PLUS_X, TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast() {
+        // arrange
+        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
+                .toTree();
+
+        // act/assert
+        LinecastChecker2D.with(tree)
+            .returnsNothing()
+            .whenGiven(Line.fromPoints(Vector2D.of(0, 5), Vector2D.of(1, 6), TEST_PRECISION));
+
+        LinecastChecker2D.with(tree)
+            .returns(Vector2D.ZERO, Vector2D.Unit.MINUS_X)
+            .and(Vector2D.ZERO, Vector2D.Unit.MINUS_Y)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X)
+            .whenGiven(Line.fromPoints(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION));
+
+        LinecastChecker2D.with(tree)
+            .returns(Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.PLUS_X)
+            .whenGiven(Segment.fromPoints(Vector2D.of(0.5, 0.5), Vector2D.of(1, 1), TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast_complementedTree() {
+        // arrange
+        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
+                .toTree();
+
+        tree.complement();
+
+        // act/assert
+        LinecastChecker2D.with(tree)
+            .returnsNothing()
+            .whenGiven(Line.fromPoints(Vector2D.of(0, 5), Vector2D.of(1, 6), TEST_PRECISION));
+
+        LinecastChecker2D.with(tree)
+            .returns(Vector2D.ZERO, Vector2D.Unit.PLUS_Y)
+            .and(Vector2D.ZERO, Vector2D.Unit.PLUS_X)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.MINUS_X)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.MINUS_Y)
+            .whenGiven(Line.fromPoints(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION));
+
+        LinecastChecker2D.with(tree)
+            .returns(Vector2D.of(1, 1), Vector2D.Unit.MINUS_X)
+            .and(Vector2D.of(1, 1), Vector2D.Unit.MINUS_Y)
+            .whenGiven(Segment.fromPoints(Vector2D.of(0.5, 0.5), Vector2D.of(1, 1), TEST_PRECISION));
+    }
+
+    @Test
+    public void testLinecast_complexRegion() {
+        // arrange
+        RegionBSPTree2D a = Polyline.fromVertexLoop(Arrays.asList(
+                    Vector2D.ZERO, Vector2D.of(0, 1),
+                    Vector2D.of(0.5, 1), Vector2D.of(0.5, 0)
+                ), TEST_PRECISION).toTree();
+        a.complement();
+
+        RegionBSPTree2D b = Polyline.fromVertexLoop(Arrays.asList(
+                Vector2D.of(0.5, 0), Vector2D.of(0.5, 1),
+                Vector2D.of(1, 1), Vector2D.of(1, 0)
+            ), TEST_PRECISION).toTree();
+        b.complement();
+
+        RegionBSPTree2D c = Polyline.fromVertexLoop(Arrays.asList(
+                Vector2D.of(0.5, 0.5), Vector2D.of(1.5, 0.5),
+                Vector2D.of(1.5, 1.5), Vector2D.of(0.5, 1.5)
+            ), TEST_PRECISION).toTree();
+
+        RegionBSPTree2D tree = RegionBSPTree2D.empty();
+        tree.union(a, b);
+        tree.union(c);
+
+        // act/assert
+        LinecastChecker2D.with(tree)
+            .returns(Vector2D.of(1.5, 1.5), Vector2D.Unit.PLUS_Y)
+            .and(Vector2D.of(1.5, 1.5), Vector2D.Unit.PLUS_X)
+            .whenGiven(Segment.fromPoints(Vector2D.of(0.25, 0.25), Vector2D.of(2, 2), TEST_PRECISION));
+    }
+
+    @Test
     public void testTransform() {
         // arrange
         RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.of(1, 1), Vector2D.of(3, 2), TEST_PRECISION)


[commons-geometry] 08/08: Merge branch 'GEOMETRY-68__Matt'

Posted by er...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

erans pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git

commit 200933a99abd1d34e004a79df3f942505a493be3
Merge: b14888f 6ab4eaf
Author: Gilles Sadowski <gi...@harfang.homelinux.org>
AuthorDate: Tue Jan 7 15:57:51 2020 +0100

    Merge branch 'GEOMETRY-68__Matt'
    
    Closes #50.

 .../geometry/euclidean/AbstractLinecastPoint.java  | 130 ++++++
 .../geometry/euclidean/oned/OrientedPoint.java     |  32 +-
 .../euclidean/threed/BoundarySource3D.java         |  20 +
 .../threed/BoundarySourceLinecastWrapper3D.java    |  89 ++++
 .../geometry/euclidean/threed/ConvexSubPlane.java  |  27 ++
 .../geometry/euclidean/threed/ConvexVolume.java    |  14 +-
 .../commons/geometry/euclidean/threed/Line3D.java  |  13 +
 .../geometry/euclidean/threed/LinecastPoint3D.java | 116 ++++++
 .../geometry/euclidean/threed/Linecastable3D.java  |  71 ++++
 .../commons/geometry/euclidean/threed/Plane.java   |  34 +-
 .../geometry/euclidean/threed/RegionBSPTree3D.java | 166 ++++++--
 .../geometry/euclidean/threed/SubPlane.java        |   2 +-
 .../Parallelepiped.java}                           |  34 +-
 .../euclidean/threed/shapes/package-info.java      |  19 +-
 .../geometry/euclidean/twod/BoundarySource2D.java  |  20 +
 .../twod/BoundarySourceLinecastWrapper2D.java      |  89 ++++
 .../geometry/euclidean/twod/ConvexArea.java        |  14 +-
 .../commons/geometry/euclidean/twod/Line.java      |  34 +-
 .../geometry/euclidean/twod/LinecastPoint2D.java   | 115 ++++++
 .../geometry/euclidean/twod/Linecastable2D.java    |  72 ++++
 .../geometry/euclidean/twod/PolarCoordinates.java  |   2 +-
 .../commons/geometry/euclidean/twod/Polyline.java  |  14 +-
 .../geometry/euclidean/twod/RegionBSPTree2D.java   | 164 +++++++-
 .../commons/geometry/euclidean/twod/SubLine.java   |   2 +-
 .../Parallelogram.java}                            |  37 +-
 .../package-info.java}                             |  21 +-
 .../euclidean/DocumentationExamplesTest.java       |  72 +++-
 .../geometry/euclidean/oned/OrientedPointTest.java |  17 +-
 .../euclidean/threed/BoundarySource3DTest.java     |  98 +++++
 .../BoundarySourceLinecastWrapper3DTest.java       | 246 +++++++++++
 .../euclidean/threed/ConvexSubPlaneTest.java       |  42 ++
 .../euclidean/threed/ConvexVolumeTest.java         |  41 ++
 .../geometry/euclidean/threed/Line3DTest.java      |  32 ++
 .../euclidean/threed/LinecastChecker3D.java        | 191 +++++++++
 .../euclidean/threed/LinecastPoint3DTest.java      | 203 +++++++++
 .../geometry/euclidean/threed/PlaneTest.java       |  14 +-
 .../euclidean/threed/RegionBSPTree3DTest.java      | 456 ++++++++++++++-------
 .../geometry/euclidean/threed/SubPlaneTest.java    |  20 +-
 .../ParallelepipedTest.java}                       |  36 +-
 .../euclidean/twod/BoundarySource2DTest.java       |  93 +++++
 .../twod/BoundarySourceLinecastWrapper2DTest.java  | 235 +++++++++++
 .../geometry/euclidean/twod/ConvexAreaTest.java    |  41 ++
 .../commons/geometry/euclidean/twod/LineTest.java  |  35 +-
 .../geometry/euclidean/twod/LinecastChecker2D.java | 191 +++++++++
 .../euclidean/twod/LinecastPoint2DTest.java        | 203 +++++++++
 .../geometry/euclidean/twod/PolylineTest.java      |  41 ++
 .../euclidean/twod/RegionBSPTree2DTest.java        | 203 ++++++++-
 .../ParallelogramTest.java}                        |  44 +-
 .../commons/geometry/spherical/oned/CutAngle.java  |  30 +-
 .../geometry/spherical/twod/GreatCircle.java       |  24 +-
 .../geometry/spherical/twod/RegionBSPTree2S.java   |  12 +
 .../geometry/spherical/twod/SubGreatCircle.java    |   2 +-
 .../geometry/spherical/oned/CutAngleTest.java      |  16 +-
 .../geometry/spherical/twod/GreatCircleTest.java   |  20 +-
 .../spherical/twod/RegionBSPTree2STest.java        |  31 +-
 src/site/xdoc/index.xml                            |   8 +-
 src/site/xdoc/userguide/index.xml                  |  65 ++-
 57 files changed, 3612 insertions(+), 501 deletions(-)


[commons-geometry] 04/08: moving Equivalency interface out of internal package since it is part of the public API

Posted by er...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

erans pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git

commit 2ee4af0c4fc73a05d2f36ddce078813788835d53
Author: Matt Juntunen <ma...@hotmail.com>
AuthorDate: Wed Jan 1 17:00:41 2020 -0500

    moving Equivalency interface out of internal package since it is part of the public API
---
 .../org/apache/commons/geometry/core/{internal => }/Equivalency.java    | 2 +-
 .../java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java  | 2 +-
 .../main/java/org/apache/commons/geometry/euclidean/threed/Line3D.java  | 2 +-
 .../org/apache/commons/geometry/euclidean/threed/LinecastPoint3D.java   | 2 +-
 .../main/java/org/apache/commons/geometry/euclidean/threed/Plane.java   | 2 +-
 .../src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java  | 2 +-
 .../org/apache/commons/geometry/euclidean/twod/LinecastPoint2D.java     | 2 +-
 .../main/java/org/apache/commons/geometry/spherical/oned/CutAngle.java  | 2 +-
 .../java/org/apache/commons/geometry/spherical/twod/GreatCircle.java    | 2 +-
 9 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/Equivalency.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Equivalency.java
similarity index 96%
rename from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/Equivalency.java
rename to commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Equivalency.java
index cd52a73..64b47de 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/Equivalency.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Equivalency.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.commons.geometry.core.internal;
+package org.apache.commons.geometry.core;
 
 /** Interface for determining equivalency, not exact equality, between
  * two objects. This is performs a function similar to {@link Object#equals(Object)}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java
index 83e4c85..78491a9 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java
@@ -20,9 +20,9 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 
+import org.apache.commons.geometry.core.Equivalency;
 import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
-import org.apache.commons.geometry.core.internal.Equivalency;
 import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
 import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line3D.java
index 8ee21cc..fe16fba 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line3D.java
@@ -19,8 +19,8 @@ package org.apache.commons.geometry.euclidean.threed;
 import java.util.Objects;
 
 import org.apache.commons.geometry.core.Embedding;
+import org.apache.commons.geometry.core.Equivalency;
 import org.apache.commons.geometry.core.Transform;
-import org.apache.commons.geometry.core.internal.Equivalency;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.oned.AffineTransformMatrix1D;
 import org.apache.commons.geometry.euclidean.oned.Interval;
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3D.java
index c9ccda0..e1776e4 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/LinecastPoint3D.java
@@ -22,7 +22,7 @@ import java.util.Comparator;
 import java.util.List;
 import java.util.ListIterator;
 
-import org.apache.commons.geometry.core.internal.Equivalency;
+import org.apache.commons.geometry.core.Equivalency;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.AbstractLinecastPoint;
 
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java
index cf0873b..1ab2ba4 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java
@@ -21,8 +21,8 @@ import java.util.Collection;
 import java.util.Iterator;
 import java.util.Objects;
 
+import org.apache.commons.geometry.core.Equivalency;
 import org.apache.commons.geometry.core.Transform;
-import org.apache.commons.geometry.core.internal.Equivalency;
 import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
 import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java
index 89173ae..adedde9 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java
@@ -18,8 +18,8 @@ package org.apache.commons.geometry.euclidean.twod;
 
 import java.util.Objects;
 
+import org.apache.commons.geometry.core.Equivalency;
 import org.apache.commons.geometry.core.Transform;
-import org.apache.commons.geometry.core.internal.Equivalency;
 import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
 import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2D.java
index 12c1811..de6d6e6 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LinecastPoint2D.java
@@ -22,7 +22,7 @@ import java.util.Comparator;
 import java.util.List;
 import java.util.ListIterator;
 
-import org.apache.commons.geometry.core.internal.Equivalency;
+import org.apache.commons.geometry.core.Equivalency;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.AbstractLinecastPoint;
 
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/CutAngle.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/CutAngle.java
index af54513..0547868 100644
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/CutAngle.java
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/CutAngle.java
@@ -20,9 +20,9 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 
+import org.apache.commons.geometry.core.Equivalency;
 import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
-import org.apache.commons.geometry.core.internal.Equivalency;
 import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
 import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircle.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircle.java
index 1deaf8c..584cd2f 100644
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircle.java
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircle.java
@@ -18,8 +18,8 @@ package org.apache.commons.geometry.spherical.twod;
 
 import java.util.Objects;
 
+import org.apache.commons.geometry.core.Equivalency;
 import org.apache.commons.geometry.core.Transform;
-import org.apache.commons.geometry.core.internal.Equivalency;
 import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
 import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;