You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by ma...@apache.org on 2020/05/27 11:29:24 UTC

[commons-geometry] branch master updated (29858b8 -> 69a1d4f)

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

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


    from 29858b8  Remove dead and unnecessary links in module pages (web site).
     new ed41804  GEOMETRY-94: adding PlaneConvexSubset additional implementations; GEOMETRY-77: adding Bounds2D and Bounds3D classes
     new a70cea9  GEOMETRY-97: using the term 'centroid' to refer to geometric center rather than 'barycenter'
     new 69a1d4f  adding references to hyperplane and hyperplanesubset factory classes in user guide

The 3 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:
 .../org/apache/commons/geometry/core/Region.java   |  15 +-
 .../commons/geometry/core/RegionEmbedding.java     |  11 +-
 .../geometry/core/internal/HyperplaneSubsets.java  |  89 +++
 .../AbstractRegionEmbeddingHyperplaneSubset.java   |  99 ---
 .../core/partitioning/HyperplaneSubset.java        |  34 +-
 .../partitioning/bsp/AbstractRegionBSPTree.java    |  22 +-
 .../commons/geometry/core/EmbeddingTest.java       |   8 +-
 .../commons/geometry/core/RegionEmbeddingTest.java | 116 ----
 .../core/internal/HyperplaneSubsetsTest.java       | 143 ++++
 .../AbstractConvexHyperplaneBoundedRegionTest.java |  12 +-
 .../core/partitioning/AbstractHyperplaneTest.java  |   4 +-
 ...bstractRegionEmbeddingHyperplaneSubsetTest.java | 219 -------
 .../bsp/AbstractBSPTreeMergeOperatorTest.java      |  10 +-
 .../core/partitioning/bsp/AbstractBSPTreeTest.java |  16 +-
 .../bsp/AbstractRegionBSPTreeBooleanTest.java      |  10 +-
 .../bsp/AbstractRegionBSPTreeTest.java             |  22 +-
 .../core/partitioning/bsp/BSPTreeVisitorTest.java  |   8 +-
 .../core/partitioning/bsp/MergeChecker.java        |   6 +-
 .../partitioning/bsp/RegionCutBoundaryTest.java    |   6 +-
 .../test/AttributeBSPTree.java                     |   2 +-
 .../test/PartitionTestUtils.java                   |   2 +-
 .../test/TestBSPTree.java                          |   2 +-
 .../{partition => partitioning}/test/TestLine.java |   2 +-
 .../test/TestLineSegment.java                      |   8 +-
 .../test/TestLineSegmentCollection.java            |  12 +-
 .../test/TestLineSegmentCollectionBuilder.java     |   2 +-
 .../test/TestPoint1D.java                          |   2 +-
 .../test/TestPoint2D.java                          |   2 +-
 .../test/TestRegionBSPTree.java                    |   2 +-
 .../test/TestTransform2D.java                      |   2 +-
 .../euclidean/threed/SphereGenerator.java          |   4 +-
 .../commons/geometry/euclidean/AbstractBounds.java | 159 +++++
 .../geometry/euclidean/AbstractNSphere.java        |   2 +-
 .../commons/geometry/euclidean/oned/Interval.java  |   2 +-
 .../geometry/euclidean/oned/OrientedPoint.java     |  21 +-
 .../geometry/euclidean/oned/RegionBSPTree1D.java   |  24 +-
 .../euclidean/threed/AbstractConvexPolygon3D.java  | 451 +++++++++++++
 .../threed/AbstractEmbeddedRegionPlaneSubset.java  | 137 ++++
 .../euclidean/threed/AbstractPlaneSubset.java      |  99 ++-
 .../euclidean/threed/BoundarySource3D.java         |  21 +
 .../threed/BoundarySourceBoundsBuilder3D.java      |  57 ++
 .../threed/BoundarySourceLinecaster3D.java         |  16 +-
 .../geometry/euclidean/threed/Bounds3D.java        | 290 +++++++++
 .../{package-info.java => ConvexPolygon3D.java}    |  22 +-
 .../geometry/euclidean/threed/ConvexVolume.java    |  40 +-
 .../threed/EmbeddedAreaPlaneConvexSubset.java      | 117 ++++
 .../euclidean/threed/EmbeddedTreePlaneSubset.java  | 122 +++-
 .../geometry/euclidean/threed/EmbeddingPlane.java  | 333 ++++++++++
 .../commons/geometry/euclidean/threed/Plane.java   | 647 +++++++-----------
 .../euclidean/threed/PlaneConvexSubset.java        | 114 +---
 .../geometry/euclidean/threed/PlaneSubset.java     | 259 ++------
 .../commons/geometry/euclidean/threed/Planes.java  | 721 ++++++++++++++++----
 .../geometry/euclidean/threed/RegionBSPTree3D.java |  55 +-
 .../euclidean/threed/SimpleTriangle3D.java         | 110 ++++
 .../geometry/euclidean/threed/Triangle3D.java      |  59 ++
 .../geometry/euclidean/threed/Vector3D.java        | 181 +++++-
 .../threed/VertexListConvexPolygon3D.java          |  84 +++
 .../threed/line/EmbeddedTreeLineSubset3D.java      |  47 +-
 .../threed/line/LineSpanningSubset3D.java          |  19 +
 .../euclidean/threed/line/LineSubset3D.java        |  18 +-
 .../geometry/euclidean/threed/line/Ray3D.java      |  19 +
 .../euclidean/threed/line/ReverseRay3D.java        |  19 +
 .../geometry/euclidean/threed/line/Segment3D.java  |  16 +
 .../euclidean/threed/shape/Parallelepiped.java     |   2 +-
 .../geometry/euclidean/twod/BoundarySource2D.java  |   9 +
 .../twod/BoundarySourceBoundsBuilder2D.java        |  58 ++
 .../euclidean/twod/BoundarySourceLinecaster2D.java |  16 +-
 .../commons/geometry/euclidean/twod/Bounds2D.java  | 271 ++++++++
 .../geometry/euclidean/twod/ConvexArea.java        | 142 ++--
 .../euclidean/twod/EmbeddedTreeLineSubset.java     |  54 ++
 .../commons/geometry/euclidean/twod/Line.java      |   6 +-
 .../euclidean/twod/LineSpanningSubset.java         |  18 +
 .../geometry/euclidean/twod/LineSubset.java        |  28 +-
 .../commons/geometry/euclidean/twod/Lines.java     |  25 +-
 .../commons/geometry/euclidean/twod/Ray.java       |  81 ++-
 .../geometry/euclidean/twod/RegionBSPTree2D.java   |   8 +-
 .../geometry/euclidean/twod/ReverseRay.java        |  99 +--
 .../commons/geometry/euclidean/twod/Segment.java   | 109 ++--
 .../commons/geometry/euclidean/twod/Vector2D.java  | 145 +++++
 .../geometry/euclidean/twod/path/LinePath.java     |  10 +-
 .../euclidean/DocumentationExamplesTest.java       |  59 +-
 .../geometry/euclidean/EuclideanTestUtils.java     |  43 ++
 .../geometry/euclidean/oned/IntervalTest.java      |  16 +-
 .../geometry/euclidean/oned/OrientedPointTest.java |   3 +-
 .../euclidean/oned/RegionBSPTree1DTest.java        |  36 +-
 ...ubsetTest.java => AbstractPlaneSubsetTest.java} | 106 ++-
 .../euclidean/threed/BoundarySource3DTest.java     |  12 +-
 .../threed/BoundarySourceBoundsBuilder3DTest.java  | 155 +++++
 .../threed/BoundarySourceLinecaster3DTest.java     |   8 +-
 .../geometry/euclidean/threed/Bounds3DTest.java    | 538 +++++++++++++++
 .../euclidean/threed/ConvexVolumeTest.java         | 119 +++-
 .../threed/EmbeddedAreaPlaneConvexSubsetTest.java  | 468 +++++++++++++
 .../threed/EmbeddedTreePlaneSubsetTest.java        | 415 +++++++++---
 .../euclidean/threed/EmbeddingPlaneTest.java       | 509 +++++++++++++++
 .../euclidean/threed/PlaneConvexSubsetTest.java    | 362 ++---------
 .../geometry/euclidean/threed/PlaneTest.java       | 442 ++++++-------
 .../geometry/euclidean/threed/PlanesTest.java      | 723 +++++++++++++++++++++
 .../euclidean/threed/RegionBSPTree3DTest.java      | 150 +++--
 .../euclidean/threed/SimpleTriangle3DTest.java     | 336 ++++++++++
 .../geometry/euclidean/threed/Vector3DTest.java    |  79 +++
 .../threed/VertexListConvexPolygon3DTest.java      | 393 +++++++++++
 .../threed/line/EmbeddedTreeLineSubset3DTest.java  | 107 ++-
 .../geometry/euclidean/threed/line/Line3DTest.java |   3 +
 .../geometry/euclidean/threed/line/Ray3DTest.java  |   9 +
 .../euclidean/threed/line/ReverseRay3DTest.java    |   9 +
 .../euclidean/threed/line/Segment3DTest.java       |  31 +
 .../euclidean/threed/shape/ParallelepipedTest.java |  14 +-
 .../euclidean/threed/shape/SphereTest.java         |   9 +-
 .../twod/BoundarySourceBoundsBuilder2DTest.java    | 127 ++++
 .../geometry/euclidean/twod/Bounds2DTest.java      | 507 +++++++++++++++
 .../geometry/euclidean/twod/ConvexAreaTest.java    | 390 +++++------
 .../euclidean/twod/EmbeddedTreeLineSubsetTest.java |  53 +-
 .../euclidean/twod/LineSpanningSubsetTest.java     |   2 +
 .../geometry/euclidean/twod/LineSubsetTest.java    |  21 +
 .../commons/geometry/euclidean/twod/RayTest.java   |  32 +-
 .../euclidean/twod/RegionBSPTree2DTest.java        |  61 +-
 .../geometry/euclidean/twod/ReverseRayTest.java    |  33 +-
 .../geometry/euclidean/twod/SegmentTest.java       |  43 ++
 .../geometry/euclidean/twod/Vector2DTest.java      |  83 ++-
 .../geometry/euclidean/twod/path/LinePathTest.java |   8 +-
 .../geometry/euclidean/twod/shape/CircleTest.java  |   8 +-
 .../euclidean/twod/shape/ParallelogramTest.java    |  16 +-
 .../commons/geometry/examples/io/Format3D.java     |  41 +-
 .../geometry/hull/euclidean/twod/ConvexHull2D.java |   2 +-
 .../geometry/spherical/oned/AngularInterval.java   |   4 +-
 .../commons/geometry/spherical/oned/CutAngle.java  |  19 +-
 .../geometry/spherical/oned/RegionBSPTree1S.java   |  10 +-
 .../geometry/spherical/twod/ConvexArea2S.java      |  26 +-
 .../geometry/spherical/twod/GreatCircleSubset.java |  64 +-
 .../geometry/spherical/twod/RegionBSPTree2S.java   |  10 +-
 .../spherical/DocumentationExamplesTest.java       |   4 +-
 .../spherical/oned/AngularIntervalTest.java        |   4 +-
 .../geometry/spherical/oned/CutAngleTest.java      |   4 +-
 .../spherical/oned/RegionBSPTree1STest.java        |  24 +-
 .../geometry/spherical/twod/ConvexArea2STest.java  |  85 +--
 .../twod/EmbeddedTreeSubGreatCircleTest.java       |   5 +
 .../geometry/spherical/twod/GreatArcPathTest.java  |   4 +-
 .../geometry/spherical/twod/GreatArcTest.java      |   2 +
 .../spherical/twod/GreatCircleSubsetTest.java      |   5 +
 .../spherical/twod/RegionBSPTree2STest.java        |  82 +--
 .../checkstyle/checkstyle-suppressions.xml         |   1 +
 .../resources/spotbugs/spotbugs-exclude-filter.xml |  26 +-
 src/site/xdoc/index.xml                            |   4 +-
 src/site/xdoc/userguide/index.xml                  |  86 ++-
 144 files changed, 10265 insertions(+), 3039 deletions(-)
 create mode 100644 commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/HyperplaneSubsets.java
 delete mode 100644 commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractRegionEmbeddingHyperplaneSubset.java
 delete mode 100644 commons-geometry-core/src/test/java/org/apache/commons/geometry/core/RegionEmbeddingTest.java
 create mode 100644 commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/HyperplaneSubsetsTest.java
 delete mode 100644 commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractRegionEmbeddingHyperplaneSubsetTest.java
 rename commons-geometry-core/src/test/java/org/apache/commons/geometry/core/{partition => partitioning}/test/AttributeBSPTree.java (98%)
 rename commons-geometry-core/src/test/java/org/apache/commons/geometry/core/{partition => partitioning}/test/PartitionTestUtils.java (99%)
 rename commons-geometry-core/src/test/java/org/apache/commons/geometry/core/{partition => partitioning}/test/TestBSPTree.java (98%)
 rename commons-geometry-core/src/test/java/org/apache/commons/geometry/core/{partition => partitioning}/test/TestLine.java (99%)
 rename commons-geometry-core/src/test/java/org/apache/commons/geometry/core/{partition => partitioning}/test/TestLineSegment.java (98%)
 rename commons-geometry-core/src/test/java/org/apache/commons/geometry/core/{partition => partitioning}/test/TestLineSegmentCollection.java (96%)
 rename commons-geometry-core/src/test/java/org/apache/commons/geometry/core/{partition => partitioning}/test/TestLineSegmentCollectionBuilder.java (98%)
 rename commons-geometry-core/src/test/java/org/apache/commons/geometry/core/{partition => partitioning}/test/TestPoint1D.java (97%)
 rename commons-geometry-core/src/test/java/org/apache/commons/geometry/core/{partition => partitioning}/test/TestPoint2D.java (97%)
 rename commons-geometry-core/src/test/java/org/apache/commons/geometry/core/{partition => partitioning}/test/TestRegionBSPTree.java (98%)
 rename commons-geometry-core/src/test/java/org/apache/commons/geometry/core/{partition => partitioning}/test/TestTransform2D.java (97%)
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractBounds.java
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractConvexPolygon3D.java
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractEmbeddedRegionPlaneSubset.java
 copy commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircleSubset.java => commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractPlaneSubset.java (54%)
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceBoundsBuilder3D.java
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Bounds3D.java
 copy commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/{package-info.java => ConvexPolygon3D.java} (69%)
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddedAreaPlaneConvexSubset.java
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddingPlane.java
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SimpleTriangle3D.java
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Triangle3D.java
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/VertexListConvexPolygon3D.java
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceBoundsBuilder2D.java
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Bounds2D.java
 rename commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/{PlaneSubsetTest.java => AbstractPlaneSubsetTest.java} (77%)
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceBoundsBuilder3DTest.java
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Bounds3DTest.java
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedAreaPlaneConvexSubsetTest.java
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddingPlaneTest.java
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlanesTest.java
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SimpleTriangle3DTest.java
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/VertexListConvexPolygon3DTest.java
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceBoundsBuilder2DTest.java
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Bounds2DTest.java


[commons-geometry] 03/03: adding references to hyperplane and hyperplanesubset factory classes in user guide

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

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

commit 69a1d4f6ad6632913a324532ffa30e6bef227bcc
Author: Matt Juntunen <ma...@apache.org>
AuthorDate: Mon May 25 09:01:50 2020 -0400

    adding references to hyperplane and hyperplanesubset factory classes in user guide
---
 src/site/xdoc/userguide/index.xml | 24 ++++++++++++++++++++++--
 1 file changed, 22 insertions(+), 2 deletions(-)

diff --git a/src/site/xdoc/userguide/index.xml b/src/site/xdoc/userguide/index.xml
index 35f195a..de563a8 100644
--- a/src/site/xdoc/userguide/index.xml
+++ b/src/site/xdoc/userguide/index.xml
@@ -388,7 +388,8 @@ Vector2D centroid = tree.getCentroid(); // region centroid = (0.5, 0.5)
         <em>Commons Geometry</em> contains a number of core interfaces that appear throughout the library, generally
         following the same implementation patterns. For each space and dimension, there are interfaces that are always
         implemented with a single class, some that may have more than one implementation, and some that are optional.
-        See the summary below for details.
+        Additionally, each space and dimension has a primary factory class containing static factory methods for
+        producing hyperplanes and common hyperplane subsets. See the summary below for details.
       </p>
 
       <h5>Each supported space and dimension contains...</h5>
@@ -481,6 +482,9 @@ Vector2D centroid = tree.getCentroid(); // region centroid = (0.5, 0.5)
             Hyperplane -
             <a class="code" href="../commons-geometry-euclidean/apidocs/org/apache/commons/geometry/euclidean/oned/OrientedPoint.html">OrientedPoint</a>
           </li>
+          <li>Hyperplane Factory Class -
+            <a class="code" href="../commons-geometry-euclidean/apidocs/org/apache/commons/geometry/euclidean/oned/OrientedPoints.html">OrientedPoints</a>
+          </li>
           <li>
             Region
             <ul>
@@ -501,7 +505,6 @@ Vector2D centroid = tree.getCentroid(); // region centroid = (0.5, 0.5)
                 <a class="code" href="../commons-geometry-euclidean/apidocs/org/apache/commons/geometry/euclidean/oned/AffineTransformMatrix1D.html">AffineTransformMatrix1D</a> -
                 Represents affine transforms using a 2x2 matrix.
               </li>
-
             </ul>
           </li>
         </ul>
@@ -569,6 +572,9 @@ List&lt;Interval&gt; intervals = tree.toIntervals(); // size = 2
               <a class="code" href="../commons-geometry-euclidean/apidocs/org/apache/commons/geometry/euclidean/twod/Ray.html">Ray</a>, and
               <a class="code" href="../commons-geometry-euclidean/apidocs/org/apache/commons/geometry/euclidean/twod/ReverseRay.html">ReverseRay</a>)
           </li>
+          <li>Hyperplane / HyperplaneSubset Factory Class -
+            <a class="code" href="../commons-geometry-euclidean/apidocs/org/apache/commons/geometry/euclidean/twod/Lines.html">Lines</a>
+          </li>
           <li>
             Region
             <ul>
@@ -712,6 +718,9 @@ Vector2D normal = pt.getNormal(); // (1.0, 0.0)
               <a class="code" href="../commons-geometry-euclidean/apidocs/org/apache/commons/geometry/euclidean/threed/ConvexPolygon3D.html">ConvexPolygon3D</a> and
               <a class="code" href="../commons-geometry-euclidean/apidocs/org/apache/commons/geometry/euclidean/threed/Triangle3D.html">Triangle3D</a>)
           </li>
+          <li>Hyperplane / HyperplaneSubset Factory Class -
+            <a class="code" href="../commons-geometry-euclidean/apidocs/org/apache/commons/geometry/euclidean/threed/Planes.html">Planes</a>
+          </li>
           <li>
             Region
             <ul>
@@ -759,6 +768,9 @@ Vector2D normal = pt.getNormal(); // (1.0, 0.0)
                 <a class="code" href="../commons-geometry-euclidean/apidocs/org/apache/commons/geometry/euclidean/threed/line/Ray3D.html">Ray3D</a>, and
                 <a class="code" href="../commons-geometry-euclidean/apidocs/org/apache/commons/geometry/euclidean/threed/line/ReverseRay3D.html">ReverseRay3D</a>.
               </li>
+              <li><a class="code" href="../commons-geometry-euclidean/apidocs/org/apache/commons/geometry/euclidean/threed/line/Lines3D.html">Lines3D</a> -
+                Factory class for 3D lines and line subsets.
+              </li>
             </ul>
           </li>
         </ul>
@@ -913,6 +925,10 @@ Vector3D normal = pts.get(0).getNormal(); // (0.0, 0.0, -1.0)
             <a class="code" href="../commons-geometry-spherical/apidocs/org/apache/commons/geometry/spherical/oned/CutAngle.html">CutAngle</a>
           </li>
           <li>
+            Hyperplane Factory Class -
+            <a class="code" href="../commons-geometry-spherical/apidocs/org/apache/commons/geometry/spherical/oned/CutAngles.html">CutAngles</a>
+          </li>
+          <li>
             Region
             <ul>
               <li>
@@ -997,6 +1013,10 @@ List&lt;AngularInterval&gt; intervals = tree.toIntervals(); //size = 2
             <a class="code" href="../commons-geometry-spherical/apidocs/org/apache/commons/geometry/spherical/twod/GreatArc.html">GreatArc</a>
           </li>
           <li>
+            Hyperplane / HyperplaneSubset Factory Class -
+            <a class="code" href="../commons-geometry-spherical/apidocs/org/apache/commons/geometry/spherical/twod/GreatCircles.html">GreatCircles</a>
+          </li>
+          <li>
             Region
             <ul>
               <li>


[commons-geometry] 01/03: GEOMETRY-94: adding PlaneConvexSubset additional implementations; GEOMETRY-77: adding Bounds2D and Bounds3D classes

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

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

commit ed41804dc3f8273e483ddfabd331732e1ae2c550
Author: Matt Juntunen <ma...@apache.org>
AuthorDate: Sun May 24 12:17:37 2020 -0400

    GEOMETRY-94: adding PlaneConvexSubset additional implementations; GEOMETRY-77: adding Bounds2D and Bounds3D classes
---
 .../org/apache/commons/geometry/core/Region.java   |   2 +-
 .../commons/geometry/core/RegionEmbedding.java     |  11 +-
 .../geometry/core/internal/HyperplaneSubsets.java  |  89 +++
 .../AbstractRegionEmbeddingHyperplaneSubset.java   |  99 ---
 .../core/partitioning/HyperplaneSubset.java        |  27 +-
 .../commons/geometry/core/EmbeddingTest.java       |   8 +-
 .../commons/geometry/core/RegionEmbeddingTest.java | 116 ----
 .../core/internal/HyperplaneSubsetsTest.java       | 143 ++++
 .../AbstractConvexHyperplaneBoundedRegionTest.java |  10 +-
 .../core/partitioning/AbstractHyperplaneTest.java  |   4 +-
 ...bstractRegionEmbeddingHyperplaneSubsetTest.java | 219 -------
 .../bsp/AbstractBSPTreeMergeOperatorTest.java      |  10 +-
 .../core/partitioning/bsp/AbstractBSPTreeTest.java |  16 +-
 .../bsp/AbstractRegionBSPTreeBooleanTest.java      |  10 +-
 .../bsp/AbstractRegionBSPTreeTest.java             |  16 +-
 .../core/partitioning/bsp/BSPTreeVisitorTest.java  |   8 +-
 .../core/partitioning/bsp/MergeChecker.java        |   6 +-
 .../partitioning/bsp/RegionCutBoundaryTest.java    |   6 +-
 .../test/AttributeBSPTree.java                     |   2 +-
 .../test/PartitionTestUtils.java                   |   2 +-
 .../test/TestBSPTree.java                          |   2 +-
 .../{partition => partitioning}/test/TestLine.java |   2 +-
 .../test/TestLineSegment.java                      |   8 +-
 .../test/TestLineSegmentCollection.java            |  12 +-
 .../test/TestLineSegmentCollectionBuilder.java     |   2 +-
 .../test/TestPoint1D.java                          |   2 +-
 .../test/TestPoint2D.java                          |   2 +-
 .../test/TestRegionBSPTree.java                    |   2 +-
 .../test/TestTransform2D.java                      |   2 +-
 .../euclidean/threed/SphereGenerator.java          |   4 +-
 .../commons/geometry/euclidean/AbstractBounds.java | 159 +++++
 .../geometry/euclidean/oned/OrientedPoint.java     |  21 +-
 .../euclidean/threed/AbstractConvexPolygon3D.java  | 451 +++++++++++++
 .../threed/AbstractEmbeddedRegionPlaneSubset.java  | 137 ++++
 .../euclidean/threed/AbstractPlaneSubset.java      |  99 ++-
 .../euclidean/threed/BoundarySource3D.java         |  21 +
 .../threed/BoundarySourceBoundsBuilder3D.java      |  57 ++
 .../threed/BoundarySourceLinecaster3D.java         |  16 +-
 .../geometry/euclidean/threed/Bounds3D.java        | 290 +++++++++
 .../geometry/euclidean/threed/ConvexPolygon3D.java |  29 +-
 .../geometry/euclidean/threed/ConvexVolume.java    |  28 +-
 .../threed/EmbeddedAreaPlaneConvexSubset.java      | 117 ++++
 .../euclidean/threed/EmbeddedTreePlaneSubset.java  | 122 +++-
 .../geometry/euclidean/threed/EmbeddingPlane.java  | 333 ++++++++++
 .../commons/geometry/euclidean/threed/Plane.java   | 647 +++++++-----------
 .../euclidean/threed/PlaneConvexSubset.java        | 114 +---
 .../geometry/euclidean/threed/PlaneSubset.java     | 259 ++------
 .../commons/geometry/euclidean/threed/Planes.java  | 721 ++++++++++++++++----
 .../geometry/euclidean/threed/RegionBSPTree3D.java |  27 +-
 .../euclidean/threed/SimpleTriangle3D.java         | 110 ++++
 .../geometry/euclidean/threed/Triangle3D.java      |  59 ++
 .../geometry/euclidean/threed/Vector3D.java        | 181 +++++-
 .../threed/VertexListConvexPolygon3D.java          |  84 +++
 .../threed/line/EmbeddedTreeLineSubset3D.java      |  47 +-
 .../threed/line/LineSpanningSubset3D.java          |  19 +
 .../euclidean/threed/line/LineSubset3D.java        |  18 +-
 .../geometry/euclidean/threed/line/Ray3D.java      |  19 +
 .../euclidean/threed/line/ReverseRay3D.java        |  19 +
 .../geometry/euclidean/threed/line/Segment3D.java  |  16 +
 .../euclidean/threed/shape/Parallelepiped.java     |   2 +-
 .../geometry/euclidean/twod/BoundarySource2D.java  |   9 +
 .../twod/BoundarySourceBoundsBuilder2D.java        |  58 ++
 .../euclidean/twod/BoundarySourceLinecaster2D.java |  16 +-
 .../commons/geometry/euclidean/twod/Bounds2D.java  | 271 ++++++++
 .../geometry/euclidean/twod/ConvexArea.java        | 138 ++--
 .../euclidean/twod/EmbeddedTreeLineSubset.java     |  54 ++
 .../commons/geometry/euclidean/twod/Line.java      |   6 +-
 .../euclidean/twod/LineSpanningSubset.java         |  18 +
 .../geometry/euclidean/twod/LineSubset.java        |  28 +-
 .../commons/geometry/euclidean/twod/Lines.java     |  25 +-
 .../commons/geometry/euclidean/twod/Ray.java       |  81 ++-
 .../geometry/euclidean/twod/ReverseRay.java        |  99 +--
 .../commons/geometry/euclidean/twod/Segment.java   | 109 ++--
 .../commons/geometry/euclidean/twod/Vector2D.java  | 145 +++++
 .../geometry/euclidean/twod/path/LinePath.java     |  10 +-
 .../euclidean/DocumentationExamplesTest.java       |  37 +-
 .../geometry/euclidean/EuclideanTestUtils.java     |  43 ++
 .../geometry/euclidean/oned/OrientedPointTest.java |   3 +-
 ...ubsetTest.java => AbstractPlaneSubsetTest.java} | 106 ++-
 .../euclidean/threed/BoundarySource3DTest.java     |  12 +-
 .../threed/BoundarySourceBoundsBuilder3DTest.java  | 155 +++++
 .../threed/BoundarySourceLinecaster3DTest.java     |   8 +-
 .../geometry/euclidean/threed/Bounds3DTest.java    | 538 +++++++++++++++
 .../euclidean/threed/ConvexVolumeTest.java         | 105 ++-
 .../threed/EmbeddedAreaPlaneConvexSubsetTest.java  | 468 +++++++++++++
 .../threed/EmbeddedTreePlaneSubsetTest.java        | 415 +++++++++---
 .../euclidean/threed/EmbeddingPlaneTest.java       | 509 +++++++++++++++
 .../euclidean/threed/PlaneConvexSubsetTest.java    | 362 ++---------
 .../geometry/euclidean/threed/PlaneTest.java       | 442 ++++++-------
 .../geometry/euclidean/threed/PlanesTest.java      | 723 +++++++++++++++++++++
 .../euclidean/threed/RegionBSPTree3DTest.java      |  94 ++-
 .../euclidean/threed/SimpleTriangle3DTest.java     | 336 ++++++++++
 .../geometry/euclidean/threed/Vector3DTest.java    |  79 +++
 .../threed/VertexListConvexPolygon3DTest.java      | 393 +++++++++++
 .../threed/line/EmbeddedTreeLineSubset3DTest.java  | 107 ++-
 .../geometry/euclidean/threed/line/Line3DTest.java |   3 +
 .../geometry/euclidean/threed/line/Ray3DTest.java  |   9 +
 .../euclidean/threed/line/ReverseRay3DTest.java    |   9 +
 .../euclidean/threed/line/Segment3DTest.java       |  31 +
 .../twod/BoundarySourceBoundsBuilder2DTest.java    | 127 ++++
 .../geometry/euclidean/twod/Bounds2DTest.java      | 507 +++++++++++++++
 .../geometry/euclidean/twod/ConvexAreaTest.java    | 334 ++++------
 .../euclidean/twod/EmbeddedTreeLineSubsetTest.java |  53 +-
 .../euclidean/twod/LineSpanningSubsetTest.java     |   2 +
 .../geometry/euclidean/twod/LineSubsetTest.java    |  21 +
 .../commons/geometry/euclidean/twod/RayTest.java   |  32 +-
 .../euclidean/twod/RegionBSPTree2DTest.java        |  29 +-
 .../geometry/euclidean/twod/ReverseRayTest.java    |  33 +-
 .../geometry/euclidean/twod/SegmentTest.java       |  43 ++
 .../geometry/euclidean/twod/Vector2DTest.java      |  83 ++-
 .../geometry/euclidean/twod/path/LinePathTest.java |   8 +-
 .../commons/geometry/examples/io/Format3D.java     |  41 +-
 .../geometry/hull/euclidean/twod/ConvexHull2D.java |   2 +-
 .../commons/geometry/spherical/oned/CutAngle.java  |  19 +-
 .../geometry/spherical/twod/GreatCircleSubset.java |  64 +-
 .../geometry/spherical/oned/CutAngleTest.java      |   4 +-
 .../twod/EmbeddedTreeSubGreatCircleTest.java       |   5 +
 .../geometry/spherical/twod/GreatArcTest.java      |   2 +
 .../spherical/twod/GreatCircleSubsetTest.java      |   5 +
 .../checkstyle/checkstyle-suppressions.xml         |   1 +
 .../resources/spotbugs/spotbugs-exclude-filter.xml |  26 +-
 src/site/xdoc/userguide/index.xml                  |  48 +-
 122 files changed, 9891 insertions(+), 2716 deletions(-)

diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Region.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Region.java
index f22e00c..308f83f 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Region.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Region.java
@@ -45,7 +45,7 @@ public interface Region<P extends Point<P>> extends Sized {
     /** Get the barycenter of the region or null if no barycenter exists or
      * one exists but is not unique. A barycenter will not exist for empty or
      * infinite regions.
-     * @return the barycenter of the region or null if none exists
+     * @return the barycenter of the region or null if no unique barycenter exists
      */
     P getBarycenter();
 
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionEmbedding.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionEmbedding.java
index b82b75a..feedb41 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionEmbedding.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionEmbedding.java
@@ -23,16 +23,7 @@ package org.apache.commons.geometry.core;
  * @see Region
  */
 public interface RegionEmbedding<P extends Point<P>, S extends Point<S>>
-    extends Embedding<P, S>, Sized {
-
-    /** Get the size of the instance, which by default is the size of the embedded
-     * subspace region.
-     * @return the size of instance
-     */
-    @Override
-    default double getSize() {
-        return getSubspaceRegion().getSize();
-    }
+    extends Embedding<P, S> {
 
     /** Get the embedded subspace region.
      * @return the embedded subspace region
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/HyperplaneSubsets.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/HyperplaneSubsets.java
new file mode 100644
index 0000000..1f453a6
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/HyperplaneSubsets.java
@@ -0,0 +1,89 @@
+/*
+ * 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.internal;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Region;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
+
+/** Utility methods for {@link org.apache.commons.geometry.core.partitioning.HyperplaneSubset}
+ * implementations.
+ */
+public final class HyperplaneSubsets {
+
+    /** Utility class; no instantiation. */
+    private HyperplaneSubsets() {
+    }
+
+    /** Classify a point against a region embedded in a hyperplane.
+     * @param <P> Point implementation class
+     * @param <S> Subspace point implementation class
+     * @param <H> Hyperplane implementation class
+     * @param <R> Region implementation class
+     * @param pt the point to classify
+     * @param hyperplane hyperplane containing the embedded region
+     * @param embeddedRegion embedded region to classify against
+     * @return the region location of the given point
+     */
+    public static <
+        P extends Point<P>,
+        S extends Point<S>,
+        H extends EmbeddingHyperplane<P, S>,
+        R extends Region<S>> RegionLocation classifyAgainstEmbeddedRegion(final P pt,
+                final H hyperplane, final R embeddedRegion) {
+
+        if (hyperplane.contains(pt)) {
+            final S subPoint = hyperplane.toSubspace(pt);
+
+            return embeddedRegion.classify(subPoint);
+        }
+
+        return RegionLocation.OUTSIDE;
+    }
+
+    /** Return the closest point to a given point in a region embedded in a hyperplane.
+     * @param <P> Point implementation class
+     * @param <S> Subspace point implementation class
+     * @param <H> Hyperplane implementation class
+     * @param <R> Region implementation class
+     * @param pt point to find the closest point to
+     * @param hyperplane hyperplane containing the embedded region
+     * @param embeddedRegion embedded region to find the closest point in
+     * @return the closest point to {@code pt} in the embedded region
+     */
+    public static <
+        P extends Point<P>,
+        S extends Point<S>,
+        H extends EmbeddingHyperplane<P, S>,
+        R extends Region<S>> P closestToEmbeddedRegion(final P pt,
+                final H hyperplane, final R embeddedRegion) {
+
+        final S subPt = hyperplane.toSubspace(pt);
+
+        if (embeddedRegion.contains(subPt)) {
+            return hyperplane.toSpace(subPt);
+        }
+
+        final S subProjected = embeddedRegion.project(subPt);
+        if (subProjected != null) {
+            return hyperplane.toSpace(subProjected);
+        }
+
+        return null;
+    }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractRegionEmbeddingHyperplaneSubset.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractRegionEmbeddingHyperplaneSubset.java
deleted file mode 100644
index e72071a..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractRegionEmbeddingHyperplaneSubset.java
+++ /dev/null
@@ -1,99 +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.partitioning;
-
-import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.Region;
-import org.apache.commons.geometry.core.RegionEmbedding;
-import org.apache.commons.geometry.core.RegionLocation;
-
-/** Abstract base class for hyperplane subset implementations that embed a lower-dimension region through
- * an embedding hyperplane.
- * @param <P> Point implementation type
- * @param <S> Subspace point implementation type
- * @param <H> Hyperplane containing the embedded subspace
- */
-public abstract class AbstractRegionEmbeddingHyperplaneSubset<
-    P extends Point<P>,
-    S extends Point<S>,
-    H extends EmbeddingHyperplane<P, S>> implements HyperplaneSubset<P>, RegionEmbedding<P, S> {
-
-    /** {@inheritDoc} */
-    @Override
-    public boolean isFull() {
-        return getSubspaceRegion().isFull();
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public boolean isEmpty() {
-        return getSubspaceRegion().isEmpty();
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public S toSubspace(final P pt) {
-        return getHyperplane().toSubspace(pt);
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public P toSpace(final S pt) {
-        return getHyperplane().toSpace(pt);
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public RegionLocation classify(P point) {
-        final H hyperplane = getHyperplane();
-
-        if (hyperplane.contains(point)) {
-            final S subPoint = hyperplane.toSubspace(point);
-
-            return getSubspaceRegion().classify(subPoint);
-        }
-        return RegionLocation.OUTSIDE;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public P closest(P point) {
-        final H hyperplane = getHyperplane();
-
-        final P projected = hyperplane.project(point);
-        final S subProjected = hyperplane.toSubspace(projected);
-
-        final Region<S> region = getSubspaceRegion();
-        if (region.contains(subProjected)) {
-            return projected;
-        }
-
-        final S subRegionBoundary = region.project(subProjected);
-        if (subRegionBoundary != null) {
-            return hyperplane.toSpace(subRegionBoundary);
-        }
-        return null;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public abstract H getHyperplane();
-
-    /** {@inheritDoc} */
-    @Override
-    public abstract HyperplaneBoundedRegion<S> getSubspaceRegion();
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneSubset.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneSubset.java
index 8d3f73b..580350c 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneSubset.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneSubset.java
@@ -23,12 +23,26 @@ import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Sized;
 import org.apache.commons.geometry.core.Transform;
 
-/** Interface representing a subset of the points lying on a hyperplane. Examples include
+/** Interface representing a subset of the points lying in a hyperplane. Examples include
  * rays and line segments in Euclidean 2D space and triangular facets in Euclidean 3D space.
  * Hyperplane subsets can have finite or infinite size and can represent contiguous regions
  * of the hyperplane (as in the examples aboves); multiple, disjoint regions; or the
  * {@link Hyperplane#span() entire hyperplane}.
  *
+ * <p>This interface is very similar to the {@link org.apache.commons.geometry.core.Region Region}
+ * interface but has slightly different semantics. Whereas {@code Region} instances represent sets
+ * of points that can expand through all of the dimensions of a space, {@code HyperplaneSubset} instances
+ * are constrained to their containing hyperplane and are more accurately defined as {@code Region}s
+ * of the {@code n-1} dimension subspace defined by the hyperplane. This makes the methods of this interface
+ * have slightly different meanings as compared to their {@code Region} counterparts. For example, consider
+ * a triangular facet in Euclidean 3D space. The {@link #getSize()} method of this hyperplane subset does
+ * not return the <em>volume</em> of the instance (which would be {@code 0}) as a regular 3D region would, but
+ * rather returns the <em>area</em> of the 2D polygon defined by the facet. Similarly, the {@link #classify(Point)}
+ * method returns {@link RegionLocation#INSIDE} for points that lie inside of the 2D polygon defined by the
+ * facet, instead of the {@link RegionLocation#BOUNDARY} value that would be expected if the facet was considered
+ * as a true 3D region with zero thickness.
+ * </p>
+ *
  * @param <P> Point implementation type
  * @see Hyperplane
  */
@@ -51,6 +65,13 @@ public interface HyperplaneSubset<P extends Point<P>> extends Splittable<P, Hype
      */
     boolean isEmpty();
 
+    /** Get the barycenter of the hyperplane subset or null if no barycenter exists or
+     * one exists but is not unique. A barycenter will not exist for empty or
+     * infinite hyperplane subsets.
+     * @return the barycenter of the hyperplane subset or null if no unique barycenter exists
+     */
+    P getBarycenter();
+
     /** Classify a point with respect to the subset region. The point is classified as follows:
      * <ul>
      *  <li>{@link RegionLocation#INSIDE INSIDE} - The point lies on the hyperplane
@@ -99,7 +120,9 @@ public interface HyperplaneSubset<P extends Point<P>> extends Splittable<P, Hype
      */
     HyperplaneSubset<P> transform(Transform<P> transform);
 
-    /** Convert this instance into a list of convex child subsets.
+    /** Convert this instance into a list of convex child subsets representing the same region.
+     * Implementations are not required to return an optimal convex subdivision of the current
+     * instance. They are free to return whatever subdivision is readily available.
      * @return a list of hyperplane convex subsets representing the same subspace
      *      region as this instance
      */
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/EmbeddingTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/EmbeddingTest.java
index 0274232..ec9a1d2 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/EmbeddingTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/EmbeddingTest.java
@@ -20,10 +20,10 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
-import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestPoint1D;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint1D;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
 import org.junit.Assert;
 import org.junit.Test;
 
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/RegionEmbeddingTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/RegionEmbeddingTest.java
deleted file mode 100644
index 667bea3..0000000
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/RegionEmbeddingTest.java
+++ /dev/null
@@ -1,116 +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;
-
-import org.apache.commons.geometry.core.partition.test.TestPoint1D;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class RegionEmbeddingTest {
-
-    private static final double TEST_EPS = 1e-10;
-
-    @Test
-    public void testGetSize() {
-        // arrange
-        StubRegionEmbedding finite = new StubRegionEmbedding(2.0);
-        StubRegionEmbedding infinite = new StubRegionEmbedding(Double.POSITIVE_INFINITY);
-        StubRegionEmbedding nan = new StubRegionEmbedding(Double.NaN);
-
-        // act/assert
-        Assert.assertEquals(2.0, finite.getSize(), TEST_EPS);
-        Assert.assertTrue(finite.isFinite());
-        Assert.assertFalse(finite.isInfinite());
-
-        GeometryTestUtils.assertPositiveInfinity(infinite.getSize());
-        Assert.assertFalse(infinite.isFinite());
-        Assert.assertTrue(infinite.isInfinite());
-
-        Assert.assertTrue(Double.isNaN(nan.getSize()));
-        Assert.assertFalse(nan.isFinite());
-        Assert.assertFalse(nan.isInfinite());
-    }
-
-    private static class StubRegionEmbedding implements RegionEmbedding<TestPoint2D, TestPoint1D> {
-
-        private final StubRegion1D subspaceRegion;
-
-        StubRegionEmbedding(final double size) {
-            subspaceRegion = new StubRegion1D(size);
-        }
-
-        @Override
-        public TestPoint1D toSubspace(TestPoint2D pt) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public TestPoint2D toSpace(TestPoint1D pt) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public Region<TestPoint1D> getSubspaceRegion() {
-            return subspaceRegion;
-        }
-    }
-
-    private static class StubRegion1D implements Region<TestPoint1D> {
-
-        private final double size;
-
-        StubRegion1D(final double size) {
-            this.size = size;
-        }
-
-        @Override
-        public double getSize() {
-            return size;
-        }
-
-        @Override
-        public boolean isFull() {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public boolean isEmpty() {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public double getBoundarySize() {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public TestPoint1D getBarycenter() {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public RegionLocation classify(TestPoint1D pt) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public TestPoint1D project(TestPoint1D pt) {
-            throw new UnsupportedOperationException();
-        }
-    }
-}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/HyperplaneSubsetsTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/HyperplaneSubsetsTest.java
new file mode 100644
index 0000000..5eeeca5
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/HyperplaneSubsetsTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.internal;
+
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint1D;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class HyperplaneSubsetsTest {
+
+    @Test
+    public void testClassify() {
+        // arrange
+        TestLine line = TestLine.X_AXIS;
+        StubRegion1D region = new StubRegion1D();
+
+        // act/assert
+        Assert.assertEquals(RegionLocation.INSIDE,
+                HyperplaneSubsets.classifyAgainstEmbeddedRegion(new TestPoint2D(-1, 0), line, region));
+        Assert.assertEquals(RegionLocation.BOUNDARY,
+                HyperplaneSubsets.classifyAgainstEmbeddedRegion(new TestPoint2D(0, 0), line, region));
+
+        Assert.assertEquals(RegionLocation.OUTSIDE,
+                HyperplaneSubsets.classifyAgainstEmbeddedRegion(new TestPoint2D(0, 1), line, region));
+        Assert.assertEquals(RegionLocation.OUTSIDE,
+                HyperplaneSubsets.classifyAgainstEmbeddedRegion(new TestPoint2D(-1, 1), line, region));
+        Assert.assertEquals(RegionLocation.OUTSIDE,
+                HyperplaneSubsets.classifyAgainstEmbeddedRegion(new TestPoint2D(-1, -1), line, region));
+    }
+
+    @Test
+    public void testClosest() {
+        // arrange
+        TestLine line = TestLine.X_AXIS;
+        StubRegion1D region = new StubRegion1D();
+        StubRegion1D emptyRegion = new StubRegion1D(true);
+
+        // act/assert
+        PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0),
+                HyperplaneSubsets.closestToEmbeddedRegion(new TestPoint2D(-1, 0), line, region));
+
+        PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0),
+                HyperplaneSubsets.closestToEmbeddedRegion(new TestPoint2D(0, 0), line, region));
+        PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0),
+                HyperplaneSubsets.closestToEmbeddedRegion(new TestPoint2D(1, 0), line, region));
+        PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0),
+                HyperplaneSubsets.closestToEmbeddedRegion(new TestPoint2D(1, 1), line, region));
+        PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0),
+                HyperplaneSubsets.closestToEmbeddedRegion(new TestPoint2D(1, -1), line, region));
+
+        PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0),
+                HyperplaneSubsets.closestToEmbeddedRegion(new TestPoint2D(-1, 1), line, region));
+        PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0),
+                HyperplaneSubsets.closestToEmbeddedRegion(new TestPoint2D(-1, -1), line, region));
+
+        Assert.assertNull(HyperplaneSubsets.closestToEmbeddedRegion(TestPoint2D.ZERO, line, emptyRegion));
+    }
+
+    /** Stub region implementation. Negative numbers are on the inside of the region.
+     */
+    private static class StubRegion1D implements HyperplaneBoundedRegion<TestPoint1D> {
+
+        private final boolean empty;
+
+        StubRegion1D() {
+            this(false);
+        }
+
+        StubRegion1D(boolean empty) {
+            this.empty = empty;
+        }
+
+        @Override
+        public boolean isFull() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean isEmpty() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public double getSize() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public double getBoundarySize() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public TestPoint1D getBarycenter() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public RegionLocation classify(TestPoint1D pt) {
+            if (!empty) {
+                int sign = PartitionTestUtils.PRECISION.sign(pt.getX());
+
+                if (sign < 0) {
+                    return RegionLocation.INSIDE;
+                } else if (sign == 0) {
+                    return RegionLocation.BOUNDARY;
+                }
+            }
+            return RegionLocation.OUTSIDE;
+        }
+
+        @Override
+        public TestPoint1D project(TestPoint1D pt) {
+            return empty ? null : new TestPoint1D(0);
+        }
+
+        @Override
+        public Split<? extends HyperplaneBoundedRegion<TestPoint1D>> split(Hyperplane<TestPoint1D> splitter) {
+            throw new UnsupportedOperationException();
+        }
+    }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegionTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegionTest.java
index ccebc49..8c5a6bb 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegionTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegionTest.java
@@ -25,11 +25,11 @@ import org.apache.commons.geometry.core.GeometryTestUtils;
 import org.apache.commons.geometry.core.Region;
 import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
-import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestLineSegment;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
-import org.apache.commons.geometry.core.partition.test.TestTransform2D;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestLineSegment;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.TestTransform2D;
 import org.junit.Assert;
 import org.junit.Test;
 
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractHyperplaneTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractHyperplaneTest.java
index 9b41cf0..54c9a7e 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractHyperplaneTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractHyperplaneTest.java
@@ -17,8 +17,8 @@
 package org.apache.commons.geometry.core.partitioning;
 
 import org.apache.commons.geometry.core.Transform;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
 import org.junit.Assert;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractRegionEmbeddingHyperplaneSubsetTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractRegionEmbeddingHyperplaneSubsetTest.java
deleted file mode 100644
index 4dc0863..0000000
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractRegionEmbeddingHyperplaneSubsetTest.java
+++ /dev/null
@@ -1,219 +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.partitioning;
-
-import java.util.List;
-
-import org.apache.commons.geometry.core.RegionLocation;
-import org.apache.commons.geometry.core.Transform;
-import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestPoint1D;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class AbstractRegionEmbeddingHyperplaneSubsetTest {
-
-    @Test
-    public void testSimpleProperties() {
-        // arrange
-        StubHyperplaneSubset sub = new StubHyperplaneSubset(1.0);
-
-        // act/assert
-        Assert.assertTrue(sub.isFull());
-        Assert.assertTrue(sub.isEmpty());
-        Assert.assertEquals(1.0, sub.getSize(), PartitionTestUtils.EPS);
-
-        Assert.assertTrue(sub.isFinite());
-        Assert.assertFalse(sub.isInfinite());
-    }
-
-    @Test
-    public void testFiniteAndInfinite() {
-        // arrange
-        StubHyperplaneSubset finite = new StubHyperplaneSubset(1.0);
-        StubHyperplaneSubset inf = new StubHyperplaneSubset(Double.POSITIVE_INFINITY);
-        StubHyperplaneSubset nan = new StubHyperplaneSubset(Double.NaN);
-
-        // act/assert
-        Assert.assertTrue(finite.isFinite());
-        Assert.assertFalse(finite.isInfinite());
-
-        Assert.assertFalse(inf.isFinite());
-        Assert.assertTrue(inf.isInfinite());
-
-        Assert.assertFalse(nan.isFinite());
-        Assert.assertFalse(nan.isInfinite());
-    }
-
-    @Test
-    public void testSpaceConversions() {
-        // arrange
-        StubHyperplaneSubset sub = new StubHyperplaneSubset();
-
-        // act/assert
-        Assert.assertEquals(2.0, sub.toSubspace(new TestPoint2D(2.0, 3.0)).getX(), PartitionTestUtils.EPS);
-        PartitionTestUtils.assertPointsEqual(new TestPoint2D(2.0, 0.0), sub.toSpace(new TestPoint1D(2.0)));
-    }
-
-    @Test
-    public void testClassify() {
-        // arrange
-        StubHyperplaneSubset sub = new StubHyperplaneSubset();
-
-        // act/assert
-        Assert.assertEquals(RegionLocation.INSIDE, sub.classify(new TestPoint2D(-1, 0)));
-        Assert.assertEquals(RegionLocation.BOUNDARY, sub.classify(new TestPoint2D(0, 0)));
-
-        Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(new TestPoint2D(0, 1)));
-        Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(new TestPoint2D(-1, 1)));
-        Assert.assertEquals(RegionLocation.OUTSIDE, sub.classify(new TestPoint2D(-1, -1)));
-    }
-
-    @Test
-    public void testClosest() {
-        // arrange
-        StubHyperplaneSubset sub = new StubHyperplaneSubset();
-
-        // act/assert
-        PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0), sub.closest(new TestPoint2D(-1, 0)));
-
-        PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0), sub.closest(new TestPoint2D(0, 0)));
-        PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0), sub.closest(new TestPoint2D(1, 0)));
-        PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0), sub.closest(new TestPoint2D(1, 1)));
-        PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0), sub.closest(new TestPoint2D(1, -1)));
-
-        PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0), sub.closest(new TestPoint2D(-1, 1)));
-        PartitionTestUtils.assertPointsEqual(new TestPoint2D(-1, 0), sub.closest(new TestPoint2D(-1, -1)));
-    }
-
-    @Test
-    public void testClosest_nullSubspaceRegionProjection() {
-        // arrange
-        StubHyperplaneSubset sub = new StubHyperplaneSubset();
-        sub.region.projected = null;
-
-        // act/assert
-        Assert.assertNull(sub.closest(new TestPoint2D(1, 1)));
-    }
-
-    private static class StubHyperplaneSubset
-        extends AbstractRegionEmbeddingHyperplaneSubset<TestPoint2D, TestPoint1D, TestLine> {
-
-        private final StubRegion1D region;
-
-        StubHyperplaneSubset() {
-            this(0);
-        }
-
-        StubHyperplaneSubset(final double size) {
-            this.region = new StubRegion1D(size);
-        }
-
-        @Override
-        public Builder<TestPoint2D> builder() {
-            return null;
-        }
-
-        @Override
-        public List<? extends HyperplaneConvexSubset<TestPoint2D>> toConvex() {
-            return null;
-        }
-
-        @Override
-        public TestLine getHyperplane() {
-            return TestLine.X_AXIS;
-        }
-
-        @Override
-        public HyperplaneBoundedRegion<TestPoint1D> getSubspaceRegion() {
-            return region;
-        }
-
-        @Override
-        public Split<StubHyperplaneSubset> split(Hyperplane<TestPoint2D> splitter) {
-            return null;
-        }
-
-        @Override
-        public HyperplaneSubset<TestPoint2D> transform(Transform<TestPoint2D> transform) {
-            return null;
-        }
-    }
-
-    /** Stub region implementation with some hard-coded values. Negative numbers are
-     * on the inside of the region.
-     */
-    private static class StubRegion1D implements HyperplaneBoundedRegion<TestPoint1D> {
-
-        private TestPoint1D projected = new TestPoint1D(0);
-
-        private final double size;
-
-        StubRegion1D(final double size) {
-            this.size = size;
-        }
-
-        @Override
-        public boolean isFull() {
-            return true;
-        }
-
-        @Override
-        public boolean isEmpty() {
-            return true;
-        }
-
-        @Override
-        public double getSize() {
-            return size;
-        }
-
-        @Override
-        public double getBoundarySize() {
-            return 0;
-        }
-
-        @Override
-        public TestPoint1D getBarycenter() {
-            return null;
-        }
-
-        @Override
-        public RegionLocation classify(TestPoint1D pt) {
-            int sign = PartitionTestUtils.PRECISION.sign(pt.getX());
-
-            if (sign < 0) {
-                return RegionLocation.INSIDE;
-            } else if (sign == 0) {
-                return RegionLocation.BOUNDARY;
-            }
-            return RegionLocation.OUTSIDE;
-        }
-
-        @Override
-        public TestPoint1D project(TestPoint1D pt) {
-            return projected;
-        }
-
-        @Override
-        public Split<? extends HyperplaneBoundedRegion<TestPoint1D>> split(Hyperplane<TestPoint1D> splitter) {
-            throw new UnsupportedOperationException();
-        }
-    }
-}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperatorTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperatorTest.java
index 8d59095..01f9900 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperatorTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperatorTest.java
@@ -18,11 +18,11 @@ package org.apache.commons.geometry.core.partitioning.bsp;
 
 import java.util.stream.StreamSupport;
 
-import org.apache.commons.geometry.core.partition.test.AttributeBSPTree;
-import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
-import org.apache.commons.geometry.core.partition.test.AttributeBSPTree.AttributeNode;
+import org.apache.commons.geometry.core.partitioning.test.AttributeBSPTree;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.AttributeBSPTree.AttributeNode;
 import org.junit.Assert;
 import org.junit.Test;
 
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeTest.java
index a79cab1..d70a1e9 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeTest.java
@@ -27,16 +27,16 @@ import java.util.stream.Collectors;
 import java.util.stream.StreamSupport;
 
 import org.apache.commons.geometry.core.Transform;
-import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestBSPTree;
-import org.apache.commons.geometry.core.partition.test.TestBSPTree.TestNode;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestLineSegment;
-import org.apache.commons.geometry.core.partition.test.TestLineSegmentCollection;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
-import org.apache.commons.geometry.core.partition.test.TestTransform2D;
 import org.apache.commons.geometry.core.partitioning.BoundarySource;
 import org.apache.commons.geometry.core.partitioning.bsp.BSPTree.FindNodeCutRule;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestBSPTree;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestLineSegment;
+import org.apache.commons.geometry.core.partitioning.test.TestLineSegmentCollection;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.TestTransform2D;
+import org.apache.commons.geometry.core.partitioning.test.TestBSPTree.TestNode;
 import org.junit.Assert;
 import org.junit.Test;
 
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeBooleanTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeBooleanTest.java
index 999c79d..dd58d62 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeBooleanTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeBooleanTest.java
@@ -19,11 +19,11 @@ package org.apache.commons.geometry.core.partitioning.bsp;
 import java.util.Arrays;
 import java.util.function.Supplier;
 
-import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestLineSegment;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
-import org.apache.commons.geometry.core.partition.test.TestRegionBSPTree;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestLineSegment;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.TestRegionBSPTree;
 import org.junit.Test;
 
 public class AbstractRegionBSPTreeBooleanTest {
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java
index 3bbf64f..59dd3b0 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java
@@ -25,20 +25,20 @@ import java.util.function.Function;
 import org.apache.commons.geometry.core.GeometryTestUtils;
 import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
-import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestLineSegment;
-import org.apache.commons.geometry.core.partition.test.TestLineSegmentCollection;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
-import org.apache.commons.geometry.core.partition.test.TestRegionBSPTree;
-import org.apache.commons.geometry.core.partition.test.TestRegionBSPTree.TestRegionNode;
-import org.apache.commons.geometry.core.partition.test.TestTransform2D;
 import org.apache.commons.geometry.core.partitioning.BoundarySource;
 import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
 import org.apache.commons.geometry.core.partitioning.Split;
 import org.apache.commons.geometry.core.partitioning.SplitLocation;
 import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
 import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree.RegionSizeProperties;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestLineSegment;
+import org.apache.commons.geometry.core.partitioning.test.TestLineSegmentCollection;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.TestRegionBSPTree;
+import org.apache.commons.geometry.core.partitioning.test.TestTransform2D;
+import org.apache.commons.geometry.core.partitioning.test.TestRegionBSPTree.TestRegionNode;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitorTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitorTest.java
index 4beadd8..73ae682 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitorTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitorTest.java
@@ -16,12 +16,12 @@
  */
 package org.apache.commons.geometry.core.partitioning.bsp;
 
-import org.apache.commons.geometry.core.partition.test.TestBSPTree;
-import org.apache.commons.geometry.core.partition.test.TestBSPTree.TestNode;
-import org.apache.commons.geometry.core.partition.test.TestLine;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
 import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.ClosestFirstVisitor;
 import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.FarthestFirstVisitor;
+import org.apache.commons.geometry.core.partitioning.test.TestBSPTree;
+import org.apache.commons.geometry.core.partitioning.test.TestLine;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.TestBSPTree.TestNode;
 import org.junit.Assert;
 import org.junit.Test;
 
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/MergeChecker.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/MergeChecker.java
index 55c3611..feb91cd 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/MergeChecker.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/MergeChecker.java
@@ -23,9 +23,9 @@ import java.util.function.Consumer;
 import java.util.function.Supplier;
 
 import org.apache.commons.geometry.core.RegionLocation;
-import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
-import org.apache.commons.geometry.core.partition.test.TestRegionBSPTree;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.TestRegionBSPTree;
 import org.junit.Assert;
 
 /** Helper class with a fluent API used to construct assert conditions on tree merge operations.
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/RegionCutBoundaryTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/RegionCutBoundaryTest.java
index 5b305ce..9e2bb90 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/RegionCutBoundaryTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/RegionCutBoundaryTest.java
@@ -16,9 +16,9 @@
  */
 package org.apache.commons.geometry.core.partitioning.bsp;
 
-import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
-import org.apache.commons.geometry.core.partition.test.TestLineSegment;
-import org.apache.commons.geometry.core.partition.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
+import org.apache.commons.geometry.core.partitioning.test.TestLineSegment;
+import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
 import org.junit.Assert;
 import org.junit.Test;
 
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/AttributeBSPTree.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/AttributeBSPTree.java
similarity index 98%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/AttributeBSPTree.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/AttributeBSPTree.java
index 2b83824..c3b43fa 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/AttributeBSPTree.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/AttributeBSPTree.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.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
 
 import org.apache.commons.geometry.core.Point;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/PartitionTestUtils.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/PartitionTestUtils.java
similarity index 99%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/PartitionTestUtils.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/PartitionTestUtils.java
index 2172224..8c4512b 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/PartitionTestUtils.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/PartitionTestUtils.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.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
 
 import java.util.Arrays;
 import java.util.List;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestBSPTree.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestBSPTree.java
similarity index 98%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestBSPTree.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestBSPTree.java
index f976fd1..973fa09 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestBSPTree.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestBSPTree.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.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
 
 import org.apache.commons.geometry.core.partitioning.BoundarySource;
 import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLine.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLine.java
similarity index 99%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLine.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLine.java
index 0f4ba88..f5feb5f 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLine.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLine.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.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
 
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegment.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegment.java
similarity index 98%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegment.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegment.java
index 896b286..ab923b5 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegment.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegment.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.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
 
 import java.util.Arrays;
 import java.util.List;
@@ -141,6 +141,12 @@ public class TestLineSegment implements HyperplaneConvexSubset<TestPoint2D> {
 
     /** {@inheritDoc} */
     @Override
+    public TestPoint2D getBarycenter() {
+        return line.toSpace(0.5 * (end - start));
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public RegionLocation classify(TestPoint2D point) {
         if (line.contains(point)) {
             final double value = line.toSubspaceValue(point);
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollection.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegmentCollection.java
similarity index 96%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollection.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegmentCollection.java
index a036d9d..4918fc3 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollection.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegmentCollection.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.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -22,10 +22,10 @@ import java.util.List;
 
 import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
-import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
 import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
+import org.apache.commons.geometry.core.partitioning.Split;
 
 /** Class containing a collection line segments. This class should only be used for
  * testing purposes.
@@ -110,6 +110,12 @@ public class TestLineSegmentCollection implements HyperplaneSubset<TestPoint2D>
 
     /** {@inheritDoc} */
     @Override
+    public TestPoint2D getBarycenter() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public Split<TestLineSegmentCollection> split(Hyperplane<TestPoint2D> splitter) {
         final List<TestLineSegment> minusList = new ArrayList<>();
         final List<TestLineSegment> plusList = new ArrayList<>();
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollectionBuilder.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegmentCollectionBuilder.java
similarity index 98%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollectionBuilder.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegmentCollectionBuilder.java
index 7589574..0e7adf8 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollectionBuilder.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegmentCollectionBuilder.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.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
 
 import java.util.ArrayList;
 import java.util.LinkedList;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint1D.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestPoint1D.java
similarity index 97%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint1D.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestPoint1D.java
index 59ce9a8..400b149 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint1D.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestPoint1D.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.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
 
 import org.apache.commons.geometry.core.Point;
 
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint2D.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestPoint2D.java
similarity index 97%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint2D.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestPoint2D.java
index f3aebab..190e235 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestPoint2D.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestPoint2D.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.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
 
 import org.apache.commons.geometry.core.Point;
 
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestRegionBSPTree.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestRegionBSPTree.java
similarity index 98%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestRegionBSPTree.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestRegionBSPTree.java
index 5b46c41..651c117 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestRegionBSPTree.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestRegionBSPTree.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.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
 
 import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestTransform2D.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestTransform2D.java
similarity index 97%
rename from commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestTransform2D.java
rename to commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestTransform2D.java
index 738e90b..e12e125 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestTransform2D.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestTransform2D.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.partition.test;
+package org.apache.commons.geometry.core.partitioning.test;
 
 import java.util.function.UnaryOperator;
 
diff --git a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/euclidean/threed/SphereGenerator.java b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/euclidean/threed/SphereGenerator.java
index 246a579..2b60cd4 100644
--- a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/euclidean/threed/SphereGenerator.java
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/euclidean/threed/SphereGenerator.java
@@ -24,7 +24,7 @@ import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.enclosing.EnclosingBall;
 import org.apache.commons.geometry.enclosing.SupportBallGenerator;
 import org.apache.commons.geometry.enclosing.euclidean.twod.DiskGenerator;
-import org.apache.commons.geometry.euclidean.threed.Plane;
+import org.apache.commons.geometry.euclidean.threed.EmbeddingPlane;
 import org.apache.commons.geometry.euclidean.threed.Planes;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 import org.apache.commons.geometry.euclidean.twod.Vector2D;
@@ -62,7 +62,7 @@ public class SphereGenerator implements SupportBallGenerator<Vector3D> {
         }
         final Vector3D vC = support.get(2);
         if (support.size() < 4) {
-            final Plane p = Planes.fromPoints(vA, vB, vC, precision);
+            final EmbeddingPlane p = Planes.fromPoints(vA, vB, vC, precision).getEmbedding();
             final EnclosingBall<Vector2D> disk =
                     new DiskGenerator().ballOnSupport(Arrays.asList(p.toSubspace(vA),
                                                                     p.toSubspace(vB),
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractBounds.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractBounds.java
new file mode 100644
index 0000000..307d98b
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractBounds.java
@@ -0,0 +1,159 @@
+/*
+ * 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 org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+
+/** Base class representing an axis-aligned bounding box with minimum and maximum bounding points.
+ * @param <P> Point implementation type
+ * @param <B> Bounds implementation type
+ */
+public abstract class AbstractBounds<
+    P extends EuclideanVector<P>,
+    B extends AbstractBounds<P, B>> {
+
+    /** Minimum point. */
+    private final P min;
+
+    /** Maximum point. */
+    private final P max;
+
+    /** Simple constructor. Callers are responsible for ensuring that all coordinate values are finite and
+     * that all values in {@code min} are less than or equal to their corresponding values in {@code max}.
+     * No validation is performed.
+     * @param min minimum point
+     * @param max maximum point
+     */
+    protected AbstractBounds(final P min, final P max) {
+        this.min = min;
+        this.max = max;
+    }
+
+    /** Get the minimum point.
+     * @return the minimum point
+     */
+    public P getMin() {
+        return min;
+    }
+
+    /** Get the maximum point.
+     * @return the maximum point
+     */
+    public P getMax() {
+        return max;
+    }
+
+    /** Get the diagonal of the bounding box. The return value is a vector pointing from
+     * {@code min} to {@code max} and contains the size of the box along each coordinate axis.
+     * @return the diagonal vector of the bounding box
+     */
+    public P getDiagonal() {
+        return min.vectorTo(max);
+    }
+
+    /** Return the center of the bounding box.
+     * @return the center of the bounding box
+     */
+    public P getBarycenter() {
+        return min.lerp(max, 0.5);
+    }
+
+    /** Return true if the bounding box has non-zero size along each coordinate axis, as
+     * evaluated by the given precision context.
+     * @param precision precision context used for floating point comparisons
+     * @return true if the bounding box has non-zero size along each coordinate axis
+     */
+    public abstract boolean hasSize(DoublePrecisionContext precision);
+
+    /** Return true if the given point is strictly within or on the boundary of the bounding box.
+     * In other words, true if returned if <code>p<sub>t</sub> &gt;= min<sub>t</sub></code> and
+     * <code>p<sub>t</sub> &lt;= max<sub>t</sub></code> for each coordinate value <code>t</code>.
+     * Floating point comparisons are strict; values are considered equal only if they match exactly.
+     * @param pt the point to check
+     * @return true if the given point is strictly within or on the boundary of the instance
+     * @see #contains(EuclideanVector, DoublePrecisionContext)
+     */
+    public abstract boolean contains(P pt);
+
+    /** Return true if the given point is within or on the boundary of the bounding box, using the given
+     * precision context for floating point comparisons. This is similar to {@link #contains(EuclideanVector)}
+     * but allows points that may be strictly outside of the box due to floating point errors to be considered
+     * inside.
+     * @param pt the point to check
+     * @param precision precision context used to compare floating point values
+     * @return if the given point is within or on the boundary of the bounds, as determined
+     *      by the given precision context
+     * @see #contains(EuclideanVector, DoublePrecisionContext)
+     */
+    public abstract boolean contains(P pt, DoublePrecisionContext precision);
+
+    /** Return true if any point on the interior or boundary of this instance is also considered to be
+     * on the interior or boundary of the argument. Specifically, true is returned if
+     * <code>aMin<sub>t</sub> &lt;= bMax<sub>t</sub></code> and <code>aMax<sub>t</sub> &gt;= bMin<sub>t</sub></code>
+     * for all coordinate values {@code t}, where {@code a} is the current instance and {@code b} is the argument.
+     * Floating point comparisons are strict; values are considered equal only if they match exactly.
+     * @param other bounding box to intersect with
+     * @return true if the bounds intersect
+     */
+    public abstract boolean intersects(B other);
+
+    /** Return the intersection of this bounding box and the argument, or null if no intersection exists.
+     * Floating point comparisons are strict; values are considered equal only if they match exactly. Note
+     * this this method may return bounding boxes with zero size in one or more coordinate axes.
+     * @param other bounding box to intersect with
+     * @return the intersection of this instance and the argument, or null if no such intersection
+     *      exists
+     * @see #intersects(AbstractBounds)
+     */
+    public abstract B intersection(B other);
+
+    /** Return a hyperplane-bounded region containing the same points as this instance.
+     * @param precision precision context used for floating point comparisons in the returned
+     *      region instance
+     * @return a hyperplane-bounded region containing the same points as this instance
+     */
+    public abstract HyperplaneBoundedRegion<P> toRegion(DoublePrecisionContext precision);
+
+    /** Return true if the current instance and argument are considered equal as evaluated by the
+     * given precision context. Bounds are considered equal if they contain equivalent min and max
+     * points.
+     * @param other bounds to compare with
+     * @param precision precision context to compare floating point numbers
+     * @return true if this instance is equivalent to the argument, as evaluated by the given
+     *      precision context
+     * @see EuclideanVector#eq(EuclideanVector, DoublePrecisionContext)
+     */
+    public boolean eq(final B other, final DoublePrecisionContext precision) {
+        return min.eq(other.getMin(), precision) &&
+                max.eq(other.getMax(), precision);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName())
+            .append("[min= ")
+            .append(min)
+            .append(", max= ")
+            .append(max)
+            .append(']');
+
+        return sb.toString();
+    }
+}
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 0463139..1e641b8 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
@@ -80,7 +80,7 @@ public final class OrientedPoint extends AbstractHyperplane<Vector1D>
     /** Get the direction of the hyperplane's plus side.
      * @return the hyperplane direction
      */
-    public Vector1D getDirection() {
+    public Vector1D.Unit getDirection() {
         return positiveFacing ? Vector1D.Unit.PLUS : Vector1D.Unit.MINUS;
     }
 
@@ -272,7 +272,7 @@ public final class OrientedPoint extends AbstractHyperplane<Vector1D>
 
         /** {@inheritDoc}
         *
-        * <p>This method simply returns false.</p>
+        * <p>This method always returns {@code false}.</p>
         */
         @Override
         public boolean isFull() {
@@ -281,7 +281,7 @@ public final class OrientedPoint extends AbstractHyperplane<Vector1D>
 
         /** {@inheritDoc}
         *
-        * <p>This method simply returns false.</p>
+        * <p>This method always returns {@code false}.</p>
         */
         @Override
         public boolean isEmpty() {
@@ -290,7 +290,7 @@ public final class OrientedPoint extends AbstractHyperplane<Vector1D>
 
         /** {@inheritDoc}
          *
-         * <p>This method simply returns false.</p>
+         * <p>This method always returns {@code false}.</p>
          */
         @Override
         public boolean isInfinite() {
@@ -299,7 +299,7 @@ public final class OrientedPoint extends AbstractHyperplane<Vector1D>
 
         /** {@inheritDoc}
         *
-        * <p>This method simply returns true.</p>
+        * <p>This method always returns {@code true}.</p>
         */
         @Override
         public boolean isFinite() {
@@ -308,7 +308,7 @@ public final class OrientedPoint extends AbstractHyperplane<Vector1D>
 
         /** {@inheritDoc}
          *
-         *  <p>This method simply returns {@code 0}.</p>
+         *  <p>This method always returns {@code 0}.</p>
          */
         @Override
         public double getSize() {
@@ -316,6 +316,15 @@ public final class OrientedPoint extends AbstractHyperplane<Vector1D>
         }
 
         /** {@inheritDoc}
+        *
+        *  <p>This method returns the point for the defining hyperplane.</p>
+        */
+        @Override
+        public Vector1D getBarycenter() {
+            return hyperplane.getPoint();
+        }
+
+        /** {@inheritDoc}
          *
          * <p>This method returns {@link RegionLocation#BOUNDARY} if the
          * point is on the hyperplane and {@link RegionLocation#OUTSIDE}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractConvexPolygon3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractConvexPolygon3D.java
new file mode 100644
index 0000000..8d69812
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractConvexPolygon3D.java
@@ -0,0 +1,451 @@
+/*
+ * 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.Iterator;
+import java.util.List;
+
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.internal.Vectors;
+import org.apache.commons.geometry.euclidean.threed.line.Lines3D;
+import org.apache.commons.geometry.euclidean.twod.ConvexArea;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+
+/** Abstract base class for {@link ConvexPolygon3D} implementations.
+ */
+abstract class AbstractConvexPolygon3D extends AbstractPlaneSubset implements ConvexPolygon3D {
+
+    /** Plane containing the convex polygon. */
+    private final Plane plane;
+
+    /** Simple constructor.
+     * @param plane the plane containing the convex polygon
+     */
+    AbstractConvexPolygon3D(final Plane plane) {
+        this.plane = plane;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Plane getPlane() {
+        return plane;
+    }
+
+    /** {@inheritDoc}
+     *
+     *  <p>This method always returns {@code false}.</p>
+     */
+    @Override
+    public boolean isFull() {
+        return false;
+    }
+
+    /** {@inheritDoc}
+     *
+     *  <p>This method always returns {@code false}.</p>
+     */
+    @Override
+    public boolean isEmpty() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getSize() {
+        // see http://geomalgorithms.com/a01-_area.html#3D-Planar-Polygons
+        final List<Vector3D> vertices = getVertices();
+
+        double crossSumX = 0.0;
+        double crossSumY = 0.0;
+        double crossSumZ = 0.0;
+
+        Vector3D prevPt = vertices.get(vertices.size() - 1);
+        Vector3D cross;
+        for (final Vector3D curPt : vertices) {
+            cross = prevPt.cross(curPt);
+
+            crossSumX += cross.getX();
+            crossSumY += cross.getY();
+            crossSumZ += cross.getZ();
+
+            prevPt = curPt;
+        }
+
+        return 0.5 * plane.getNormal().dot(Vector3D.of(crossSumX, crossSumY, crossSumZ));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Vector3D getBarycenter() {
+        final List<Vector3D> vertices = getVertices();
+
+        double areaSum = 0.0;
+        double scaledBarycenterSumX = 0.0;
+        double scaledBarycenterSumY = 0.0;
+        double scaledBarycenterSumZ = 0.0;
+
+        Iterator<Vector3D> it = vertices.iterator();
+
+        Vector3D startPt = it.next();
+
+        Vector3D prevPt = it.next();
+        Vector3D curPt;
+
+        Vector3D prevVec = startPt.vectorTo(prevPt);
+        Vector3D curVec = null;
+
+        double triArea;
+        Vector3D triBarycenter;
+        while (it.hasNext()) {
+            curPt = it.next();
+            curVec = startPt.vectorTo(curPt);
+
+            triArea = 0.5 * prevVec.cross(curVec).norm();
+            triBarycenter = Vector3D.centroid(startPt, prevPt, curPt);
+
+            areaSum += triArea;
+
+            scaledBarycenterSumX += triArea * triBarycenter.getX();
+            scaledBarycenterSumY += triArea * triBarycenter.getY();
+            scaledBarycenterSumZ += triArea * triBarycenter.getZ();
+
+            prevPt = curPt;
+            prevVec = curVec;
+        }
+
+        if (areaSum > 0.0) {
+            final double scale = 1 / areaSum;
+            return Vector3D.of(
+                        scale * scaledBarycenterSumX,
+                        scale * scaledBarycenterSumY,
+                        scale * scaledBarycenterSumZ
+                    );
+        }
+
+        // zero area, which means that the points are all linear; return the point midway between the
+        // min and max points
+        final Vector3D min = Vector3D.min(vertices);
+        final Vector3D max = Vector3D.max(vertices);
+
+        return min.lerp(max, 0.5);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Bounds3D getBounds() {
+        return Bounds3D.from(getVertices());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public RegionLocation classify(final Vector3D pt) {
+        if (plane.contains(pt)) {
+            final List<Vector3D> vertices = getVertices();
+            final DoublePrecisionContext precision = plane.getPrecision();
+
+            final Vector3D normal = plane.getNormal();
+            Vector3D edgeVec;
+            Vector3D edgePlusVec;
+            Vector3D testVec;
+
+            Vector3D offsetVec;
+            double offsetSign;
+            double offset;
+            int cmp;
+
+            boolean onBoundary = false;
+
+            Vector3D startVertex = vertices.get(vertices.size() - 1);
+            for (final Vector3D nextVertex : vertices) {
+
+                edgeVec = startVertex.vectorTo(nextVertex);
+                edgePlusVec = edgeVec.cross(normal);
+
+                testVec = startVertex.vectorTo(pt);
+
+                offsetVec = testVec.reject(edgeVec);
+                offsetSign = Math.signum(offsetVec.dot(edgePlusVec));
+                offset = offsetSign * offsetVec.norm();
+
+                cmp = precision.compare(offset, 0.0);
+                if (cmp > 0) {
+                    // the point is on the plus side (outside) of a boundary
+                    return RegionLocation.OUTSIDE;
+                } else if (cmp == 0) {
+                    onBoundary = true;
+                }
+
+                startVertex = nextVertex;
+            }
+
+            if (onBoundary) {
+                // the point is not on the outside of any boundaries and is directly on at least one
+                return RegionLocation.BOUNDARY;
+            }
+
+            // the point is on the inside of all boundaries
+            return RegionLocation.INSIDE;
+        }
+
+        // the point is not on the plane
+        return RegionLocation.OUTSIDE;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Vector3D closest(final Vector3D pt) {
+        final Vector3D normal = plane.getNormal();
+        final DoublePrecisionContext precision = plane.getPrecision();
+
+        final List<Vector3D> vertices = getVertices();
+
+        final Vector3D projPt = plane.project(pt);
+
+        Vector3D edgeVec;
+        Vector3D edgePlusVec;
+        Vector3D testVec;
+
+        Vector3D offsetVec;
+        double offsetSign;
+        double offset;
+        int cmp;
+
+        Vector3D boundaryVec;
+        double boundaryPointT;
+        Vector3D boundaryPoint = null;
+        double boundaryPointDistSq;
+
+        double closestBoundaryPointDistSq = Double.POSITIVE_INFINITY;
+        Vector3D closestBoundaryPoint = null;
+
+        Vector3D startVertex = vertices.get(vertices.size() - 1);
+        for (final Vector3D nextVertex : vertices) {
+
+            edgeVec = startVertex.vectorTo(nextVertex);
+            edgePlusVec = edgeVec.cross(normal);
+
+            testVec = startVertex.vectorTo(projPt);
+
+            offsetVec = testVec.reject(edgeVec);
+            offsetSign = Math.signum(offsetVec.dot(edgePlusVec));
+            offset = offsetSign * offsetVec.norm();
+
+            cmp = precision.compare(offset, 0.0);
+            if (cmp >= 0) {
+                // the point is on directly on the boundary or on its plus side; project the point onto the
+                // boundary, taking care to restrict the point to the actual extent of the boundary,
+                // and select the point with the shortest distance
+                boundaryVec = testVec.subtract(offsetVec);
+                boundaryPointT =
+                        Math.signum(boundaryVec.dot(edgeVec)) * (boundaryVec.norm() / Vectors.checkedNorm(edgeVec));
+                boundaryPointT = Math.max(0, Math.min(1, boundaryPointT));
+
+                boundaryPoint = startVertex.lerp(nextVertex, boundaryPointT);
+
+                boundaryPointDistSq = boundaryPoint.distanceSq(projPt);
+                if (boundaryPointDistSq < closestBoundaryPointDistSq) {
+                    closestBoundaryPointDistSq = boundaryPointDistSq;
+                    closestBoundaryPoint = boundaryPoint;
+                }
+            }
+
+            startVertex = nextVertex;
+        }
+
+        if (closestBoundaryPoint != null) {
+            // the point is on the outside of the polygon; return the closest point on the boundary
+            return closestBoundaryPoint;
+        }
+
+        // the projected point is on the inside of all boundaries and therefore on the inside of the subset
+        return projPt;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public PlaneConvexSubset.Embedded getEmbedded() {
+        final EmbeddingPlane embeddingPlane = plane.getEmbedding();
+        final List<Vector2D> subspaceVertices = embeddingPlane.toSubspace(getVertices());
+        final ConvexArea area = ConvexArea.convexPolygonFromVertices(subspaceVertices,
+                embeddingPlane.getPrecision());
+
+        return new EmbeddedAreaPlaneConvexSubset(embeddingPlane, area);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Split<PlaneConvexSubset> split(final Hyperplane<Vector3D> splitter) {
+        final Plane splitterPlane = (Plane) splitter;
+        final List<Vector3D> vertices = getVertices();
+
+        final int size = vertices.size();
+
+        int minusPlusTransitionIdx = -1;
+        Vector3D minusPlusInsertVertex = null;
+
+        int plusMinusTransitionIdx = -1;
+        Vector3D plusMinusInsertVertex = null;
+
+        int transitionCount = 0;
+
+        Vector3D curVertex;
+        HyperplaneLocation curLoc;
+
+        int lastSideIdx = -1;
+        Vector3D lastSideVertex = null;
+        HyperplaneLocation lastSideLoc = null;
+
+        int lastBoundaryIdx = -1;
+
+        for (int i = 0; i <= size || transitionCount == 1; ++i) {
+
+            curVertex = vertices.get(i % size);
+            curLoc = splitter.classify(curVertex);
+
+            if (lastSideLoc == HyperplaneLocation.MINUS && curLoc == HyperplaneLocation.PLUS) {
+                // transitioned from minus side to plus side
+                minusPlusTransitionIdx = Math.max(lastSideIdx, lastBoundaryIdx);
+                ++transitionCount;
+
+                if (lastBoundaryIdx < 0) {
+                    // no shared boundary point; compute a new vertex
+                    minusPlusInsertVertex = splitterPlane.intersection(
+                            Lines3D.fromPoints(lastSideVertex, curVertex, splitterPlane.getPrecision()));
+                }
+            } else if (lastSideLoc == HyperplaneLocation.PLUS && curLoc == HyperplaneLocation.MINUS) {
+                // transitioned from plus side to minus side
+                plusMinusTransitionIdx = Math.max(lastSideIdx, lastBoundaryIdx);
+                ++transitionCount;
+
+                if (lastBoundaryIdx < 0) {
+                    // no shared boundary point; compute a new vertex
+                    plusMinusInsertVertex = splitterPlane.intersection(
+                            Lines3D.fromPoints(lastSideVertex, curVertex, splitterPlane.getPrecision()));
+                }
+            }
+
+            if (curLoc == HyperplaneLocation.ON) {
+                lastBoundaryIdx = i;
+            } else {
+                lastBoundaryIdx = -1;
+
+                lastSideIdx = i;
+                lastSideVertex = curVertex;
+                lastSideLoc = curLoc;
+            }
+        }
+
+        if (minusPlusTransitionIdx > -1 && plusMinusTransitionIdx > -1) {
+            // we've split; compute the vertex list for each side
+            final List<Vector3D> minusVertices =  buildPolygonSplitVertexList(
+                    plusMinusTransitionIdx, plusMinusInsertVertex,
+                    minusPlusTransitionIdx, minusPlusInsertVertex, vertices);
+            final List<Vector3D> plusVertices = buildPolygonSplitVertexList(
+                    minusPlusTransitionIdx, minusPlusInsertVertex,
+                    plusMinusTransitionIdx, plusMinusInsertVertex, vertices);
+
+            // delegate back to the Planes factory methods to determine the concrete types
+            // for each side of the split
+            return new Split<>(
+                    Planes.fromConvexPlanarVertices(plane, minusVertices),
+                    Planes.fromConvexPlanarVertices(plane, plusVertices));
+
+        } else if (lastSideLoc == HyperplaneLocation.PLUS) {
+            // we lie entirely on the plus side of the splitter
+            return new Split<>(null, this);
+        } else if (lastSideLoc == HyperplaneLocation.MINUS) {
+            // we lie entirely on the minus side of the splitter
+            return new Split<>(this, null);
+        }
+
+        // we lie entirely on the splitter
+        return new Split<>(null, null);
+    }
+
+    /** Internal method for building a vertex list for one side of a split result. The method is
+     * designed to make the fewest allocations possible.
+     * @param enterIdx the index of the vertex from {@code vertices} immediately before the polygon transitioned
+     *      to being fully entered into this side of the split result. If no point from {@code vertices} lay
+     *      directly on the splitting plane while entering this side and a new vertex had to be computed for the
+     *      split result, then this index will be the last vertex on the opposite side of the split. If a vertex
+     *      did lie directly on the splitting plane, then this index will point to that vertex.
+     * @param newEnterPt the newly-computed point to be added as the first vertex in the split result; may
+     *      be null if no such point exists
+     * @param exitIdx the index of the vertex from {@code vertices} immediately before the polygon transitioned
+     *      to being fully exited from this side of the split result. If no point from {@code vertices} lay
+     *      directly on the splitting plane while exiting this side and a new vertex had to be computed for the
+     *      split result, then this index will be the last vertex on the this side of the split. If a vertex did
+     *      lie directly on the splitting plane, then this index will point to that vertex.
+     * @param newExitPt the newly-computed point to be added as the last vertex in the split result; may
+     *      be null if no such point exists
+     * @param vertices the original list of vertices that this split result originated from; this list is
+     *      not modified by this operation
+     * @return the list of vertices for the split result
+     */
+    private List<Vector3D> buildPolygonSplitVertexList(final int enterIdx, final Vector3D newEnterPt,
+            final int exitIdx, final Vector3D newExitPt, final List<Vector3D> vertices) {
+
+        final int size = vertices.size();
+
+        final boolean hasNewEnterPt = newEnterPt != null;
+        final boolean hasNewExitPt = newExitPt != null;
+
+        final int startIdx = (hasNewEnterPt ? enterIdx + 1 : enterIdx) % size;
+        final int endIdx = exitIdx % size;
+
+        final boolean hasWrappedIndices = endIdx < startIdx;
+
+        final int resultSize = (hasWrappedIndices ? endIdx + size : endIdx) - startIdx + 1;
+        final List<Vector3D> result = new ArrayList<>(resultSize);
+
+        if (hasNewEnterPt) {
+            result.add(newEnterPt);
+        }
+
+        if (hasWrappedIndices) {
+            result.addAll(vertices.subList(startIdx, size));
+            result.addAll(vertices.subList(0, endIdx + 1));
+        } else {
+            result.addAll(vertices.subList(startIdx, endIdx + 1));
+        }
+
+        if (hasNewExitPt) {
+            result.add(newExitPt);
+        }
+
+        return result;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName())
+            .append("[normal= ")
+            .append(getPlane().getNormal())
+            .append(", vertices= ")
+            .append(getVertices())
+            .append(']');
+
+        return sb.toString();
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractEmbeddedRegionPlaneSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractEmbeddedRegionPlaneSubset.java
new file mode 100644
index 0000000..b19f3c4
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractEmbeddedRegionPlaneSubset.java
@@ -0,0 +1,137 @@
+/*
+ * 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.RegionLocation;
+import org.apache.commons.geometry.core.internal.HyperplaneSubsets;
+import org.apache.commons.geometry.euclidean.twod.BoundarySource2D;
+import org.apache.commons.geometry.euclidean.twod.Bounds2D;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+
+/** Base class for {@link PlaneSubset} implementations that use an embedded subspace region
+ * to define their plane subsets.
+ */
+abstract class AbstractEmbeddedRegionPlaneSubset extends AbstractPlaneSubset implements PlaneSubset.Embedded {
+
+    /** The plane containing the embedded region. */
+    private final EmbeddingPlane plane;
+
+    /** Construct a new instance in the given plane.
+     * @param plane plane containing the subset
+     */
+    AbstractEmbeddedRegionPlaneSubset(final EmbeddingPlane plane) {
+        this.plane = plane;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public EmbeddingPlane getPlane() {
+        return plane;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public EmbeddingPlane getHyperplane() {
+        return plane;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isFull() {
+        return getSubspaceRegion().isFull();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isEmpty() {
+        return getSubspaceRegion().isEmpty();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getSize() {
+        return getSubspaceRegion().getSize();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Vector3D getBarycenter() {
+        final Vector2D subspaceBarycenter = getSubspaceRegion().getBarycenter();
+        if (subspaceBarycenter != null) {
+            return getPlane().toSpace(subspaceBarycenter);
+        }
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Vector3D toSpace(final Vector2D pt) {
+        return plane.toSpace(pt);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Vector2D toSubspace(final Vector3D pt) {
+        return plane.toSubspace(pt);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public RegionLocation classify(final Vector3D pt) {
+        return HyperplaneSubsets.classifyAgainstEmbeddedRegion(pt, plane, getSubspaceRegion());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Vector3D closest(final Vector3D pt) {
+        return HyperplaneSubsets.closestToEmbeddedRegion(pt, plane, getSubspaceRegion());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName())
+            .append("[plane= ")
+            .append(getPlane())
+            .append(", subspaceRegion= ")
+            .append(getSubspaceRegion())
+            .append(']');
+
+        return sb.toString();
+    }
+
+    /** Compute 3D bounds from a subspace boundary source.
+     * @param src subspace boundary source
+     * @return 3D bounds from the given embedded subspace boundary source or null
+     *      if no valid bounds could be determined
+     */
+    protected Bounds3D getBoundsFromSubspace(final BoundarySource2D src) {
+        final Bounds2D subspaceBounds = src.getBounds();
+        if (subspaceBounds != null) {
+            final Vector3D min = plane.toSpace(subspaceBounds.getMin());
+            final Vector3D max = plane.toSpace(subspaceBounds.getMax());
+
+            return Bounds3D.builder()
+                    .add(min)
+                    .add(max)
+                    .build();
+        }
+
+        return null;
+    }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircleSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractPlaneSubset.java
similarity index 54%
copy from commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircleSubset.java
copy to commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractPlaneSubset.java
index 5fbf0f5..6e9d017 100644
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircleSubset.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractPlaneSubset.java
@@ -14,102 +14,81 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.commons.geometry.spherical.twod;
+package org.apache.commons.geometry.euclidean.threed;
 
-import java.util.List;
 import java.util.Objects;
 
-import org.apache.commons.geometry.core.partitioning.AbstractRegionEmbeddingHyperplaneSubset;
 import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
 import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.spherical.oned.Point1S;
+import org.apache.commons.geometry.euclidean.threed.line.Line3D;
+import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
 
-/** Class representing a subset of the points in a great circle.
- * @see GreatCircles
+/** Abstract base class for {@link PlaneSubset} implementations.
  */
-public abstract class GreatCircleSubset
-    extends AbstractRegionEmbeddingHyperplaneSubset<Point2S, Point1S, GreatCircle> {
-    /** The great circle defining this instance. */
-    private final GreatCircle circle;
-
-    /** Simple constructor.
-     * @param circle great circle defining this instance
-     */
-    GreatCircleSubset(final GreatCircle circle) {
-        this.circle = circle;
-    }
-
-    /** Get the great circle defining this instance.
-     * @return the great circle defining this instance
-     * @see #getHyperplane()
-     */
-    public GreatCircle getCircle() {
-        return circle;
-    }
+abstract class AbstractPlaneSubset implements PlaneSubset {
 
     /** {@inheritDoc} */
     @Override
-    public GreatCircle getHyperplane() {
-        return getCircle();
+    public Plane getHyperplane() {
+        return getPlane();
     }
 
     /** {@inheritDoc} */
     @Override
-    public abstract List<GreatArc> toConvex();
+    public HyperplaneSubset.Builder<Vector3D> builder() {
+        return new Builder(getPlane());
+    }
 
     /** {@inheritDoc} */
     @Override
-    public HyperplaneSubset.Builder<Point2S> builder() {
-        return new Builder(circle);
+    public Vector3D intersection(final Line3D line) {
+        return Planes.intersection(this, line);
     }
 
-    /** Return the object used to perform floating point comparisons, which is the
-     * same object used by the underlying {@link GreatCircle}.
-     * @return precision object used to perform floating point comparisons.
-     */
-    public DoublePrecisionContext getPrecision() {
-        return circle.getPrecision();
+    /** {@inheritDoc} */
+    @Override
+    public Vector3D intersection(LineConvexSubset3D lineSubset) {
+        return Planes.intersection(this, lineSubset);
     }
 
     /** Internal implementation of the {@link HyperplaneSubset.Builder} interface. In cases where only a single
      * convex subset is given to the builder, this class returns the convex subset instance directly. In all other
-     * cases, an {@link EmbeddedTreeGreatCircleSubset} is used to construct the final subset.
+     * cases, an {@link EmbeddedTreePlaneSubset} is used to construct the final subset.
      */
-    private static final class Builder implements HyperplaneSubset.Builder<Point2S> {
-        /** Great circle that a subset is being constructed for. */
-        private final GreatCircle circle;
+    private static final class Builder implements HyperplaneSubset.Builder<Vector3D> {
+        /** Plane that a subset is being constructed for. */
+        private final Plane plane;
 
         /** Embedded tree subset. */
-        private EmbeddedTreeGreatCircleSubset treeSubset;
+        private EmbeddedTreePlaneSubset treeSubset;
 
         /** Convex subset added as the first subset to the builder. This is returned directly if
          * no other subsets are added.
          */
-        private GreatArc convexSubset;
+        private PlaneConvexSubset convexSubset;
 
-        /** Create a new subset builder for the given great circle.
-         * @param circle great circle to build a subset for
+        /** Create a new subset builder for the given plane.
+         * @param plane plane to build a subset for
          */
-        Builder(final GreatCircle circle) {
-            this.circle = circle;
+        Builder(final Plane plane) {
+            this.plane = plane;
         }
 
         /** {@inheritDoc} */
         @Override
-        public void add(final HyperplaneSubset<Point2S> sub) {
+        public void add(final HyperplaneSubset<Vector3D> sub) {
             addInternal(sub);
         }
 
         /** {@inheritDoc} */
         @Override
-        public void add(final HyperplaneConvexSubset<Point2S> sub) {
+        public void add(final HyperplaneConvexSubset<Vector3D> sub) {
             addInternal(sub);
         }
 
         /** {@inheritDoc} */
         @Override
-        public GreatCircleSubset build() {
+        public PlaneSubset build() {
             // return the convex subset directly if that was all we were given
             if (convexSubset != null) {
                 return convexSubset;
@@ -120,13 +99,13 @@ public abstract class GreatCircleSubset
         /** Internal method for adding hyperplane subsets to this builder.
          * @param sub the hyperplane subset to add; may be either convex or non-convex
          */
-        private void addInternal(final HyperplaneSubset<Point2S> sub) {
+        private void addInternal(final HyperplaneSubset<Vector3D> sub) {
             Objects.requireNonNull(sub, "Hyperplane subset must not be null");
 
-            if (sub instanceof GreatArc) {
-                addConvexSubset((GreatArc) sub);
-            } else if (sub instanceof EmbeddedTreeGreatCircleSubset) {
-                addTreeSubset((EmbeddedTreeGreatCircleSubset) sub);
+            if (sub instanceof PlaneConvexSubset) {
+                addConvexSubset((PlaneConvexSubset) sub);
+            } else if (sub instanceof EmbeddedTreePlaneSubset) {
+                addTreeSubset((EmbeddedTreePlaneSubset) sub);
             } else {
                 throw new IllegalArgumentException("Unsupported hyperplane subset type: " + sub.getClass().getName());
             }
@@ -135,8 +114,8 @@ public abstract class GreatCircleSubset
         /** Add a convex subset to the builder.
          * @param convex convex subset to add
          */
-        private void addConvexSubset(final GreatArc convex) {
-            GreatCircles.validateGreatCirclesEquivalent(circle, convex.getCircle());
+        private void addConvexSubset(final PlaneConvexSubset convex) {
+            Planes.validatePlanesEquivalent(plane, convex.getPlane());
 
             if (treeSubset == null && convexSubset == null) {
                 convexSubset = convex;
@@ -148,7 +127,7 @@ public abstract class GreatCircleSubset
         /** Add an embedded tree subset to the builder.
          * @param tree embedded tree subset to add
          */
-        private void addTreeSubset(final EmbeddedTreeGreatCircleSubset tree) {
+        private void addTreeSubset(final EmbeddedTreePlaneSubset tree) {
             // no need to validate the line here since the add() method does that for us
             getTreeSubset().add(tree);
         }
@@ -156,9 +135,9 @@ public abstract class GreatCircleSubset
         /** Get the tree subset for the builder, creating it if needed.
          * @return the tree subset for the builder
          */
-        private EmbeddedTreeGreatCircleSubset getTreeSubset() {
+        private EmbeddedTreePlaneSubset getTreeSubset() {
             if (treeSubset == null) {
-                treeSubset = new EmbeddedTreeGreatCircleSubset(circle);
+                treeSubset = new EmbeddedTreePlaneSubset(plane.getEmbedding());
 
                 if (convexSubset != null) {
                     treeSubset.add(convexSubset);
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 791d7f4..7a4eb19 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
@@ -19,6 +19,7 @@ package org.apache.commons.geometry.euclidean.threed;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
+import java.util.stream.Stream;
 
 import org.apache.commons.geometry.core.partitioning.BoundarySource;
 import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
@@ -41,6 +42,17 @@ public interface BoundarySource3D extends BoundarySource<PlaneConvexSubset>, Lin
         return tree;
     }
 
+    /** Return the boundaries of this instance as a stream of {@link Triangle3D}
+     * instances. An {@link IllegalStateException} exception is thrown while reading
+     * from the stream if any boundary cannot be converted to a triangle (i.e. if it
+     * has infinite size).
+     * @return a stream of triangles representing the instance boundaries
+     * @see org.apache.commons.geometry.euclidean.threed.PlaneSubset#toTriangles()
+     */
+    default Stream<Triangle3D> triangleStream() {
+        return boundaryStream().flatMap(b -> b.toTriangles().stream());
+    }
+
     /** {@inheritDoc} */
     @Override
     default List<LinecastPoint3D> linecast(final LineConvexSubset3D subset) {
@@ -53,6 +65,15 @@ public interface BoundarySource3D extends BoundarySource<PlaneConvexSubset>, Lin
         return new BoundarySourceLinecaster3D(this).linecastFirst(subset);
     }
 
+    /** Get a {@link Bounds3D} object defining the axis-aligned box containing all vertices
+     * in the boundaries for this instance. Null is returned if any boundaries are infinite
+     * or no vertices were found.
+     * @return the bounding box for this instance or null if no valid bounds could be determined
+     */
+    default Bounds3D getBounds() {
+        return new BoundarySourceBoundsBuilder3D().getBounds(this);
+    }
+
     /** Return a {@link BoundarySource3D} instance containing the given boundaries.
      * @param boundaries boundaries to include in the boundary source
      * @return a boundary source containing the given boundaries
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceBoundsBuilder3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceBoundsBuilder3D.java
new file mode 100644
index 0000000..b0164dd
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceBoundsBuilder3D.java
@@ -0,0 +1,57 @@
+/*
+ * 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.Iterator;
+import java.util.stream.Stream;
+
+
+/** Class used to construct {@link Bounds3D} instances representing the min and
+ * max points present in a {@link BoundarySource3D}. The implementation examines
+ * the vertices of each boundary in turn. Null is returned if any boundaries are
+ * infinite or no vertices are present.
+ */
+class BoundarySourceBoundsBuilder3D {
+
+    /** Get a {@link Bounds3D} instance containing all vertices in the given boundary source.
+     * Null is returned if any encountered boundaries were not finite or no vertices were found.
+     * @param src the boundary source to compute the bounds of
+     * @return the bounds of the argument or null if no valid bounds could be determined
+     */
+    public Bounds3D getBounds(final BoundarySource3D src) {
+        final Bounds3D.Builder builder = Bounds3D.builder();
+
+        try (Stream<PlaneConvexSubset> stream = src.boundaryStream()) {
+            final Iterator<PlaneConvexSubset> it = stream.iterator();
+
+            PlaneConvexSubset boundary;
+            while (it.hasNext()) {
+                boundary = it.next();
+
+                if (!boundary.isFinite()) {
+                    return null;
+                }
+
+                builder.addAll(boundary.getVertices());
+            }
+        }
+
+        return builder.containsBounds() ?
+                builder.build() :
+                null;
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecaster3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecaster3D.java
index 56caf89..3129756 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecaster3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecaster3D.java
@@ -46,20 +46,22 @@ final class BoundarySourceLinecaster3D implements Linecastable3D {
     /** {@inheritDoc} */
     @Override
     public List<LinecastPoint3D> linecast(final LineConvexSubset3D subset) {
-        final List<LinecastPoint3D> results =  getIntersectionStream(subset)
-                .collect(Collectors.toCollection(ArrayList::new));
+        try (Stream<LinecastPoint3D> stream = getIntersectionStream(subset)) {
 
-        LinecastPoint3D.sortAndFilter(results);
+            final List<LinecastPoint3D> results = stream.collect(Collectors.toCollection(ArrayList::new));
+            LinecastPoint3D.sortAndFilter(results);
 
-        return results;
+            return results;
+        }
     }
 
     /** {@inheritDoc} */
     @Override
     public LinecastPoint3D linecastFirst(final LineConvexSubset3D subset) {
-        return getIntersectionStream(subset)
-                .min(LinecastPoint3D.ABSCISSA_ORDER)
-                .orElse(null);
+        try (Stream<LinecastPoint3D> stream = getIntersectionStream(subset)) {
+            return stream.min(LinecastPoint3D.ABSCISSA_ORDER)
+                    .orElse(null);
+        }
     }
 
     /** Return a stream containing intersections between the boundary source and the
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Bounds3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Bounds3D.java
new file mode 100644
index 0000000..d6cae6e
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Bounds3D.java
@@ -0,0 +1,290 @@
+/*
+ * 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.Arrays;
+import java.util.Objects;
+
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.AbstractBounds;
+import org.apache.commons.geometry.euclidean.threed.shape.Parallelepiped;
+
+/** Class containing minimum and maximum points defining a 3D axis-aligned bounding box. Unless otherwise
+ * noted, floating point comparisons used in this class are strict, meaning that values are considered equal
+ * if and only if they match exactly.
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public final class Bounds3D extends AbstractBounds<Vector3D, Bounds3D> {
+
+    /** Simple constructor. Callers are responsible for ensuring the min is not greater than max.
+     * @param min minimum point
+     * @param max maximum point
+     */
+    private Bounds3D(final Vector3D min, final Vector3D max) {
+        super(min, max);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean hasSize(final DoublePrecisionContext precision) {
+        final Vector3D diag = getDiagonal();
+
+        return !precision.eqZero(diag.getX()) &&
+                !precision.eqZero(diag.getY()) &&
+                !precision.eqZero(diag.getZ());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean contains(final Vector3D pt) {
+        final double x = pt.getX();
+        final double y = pt.getY();
+        final double z = pt.getZ();
+
+        final Vector3D min = getMin();
+        final Vector3D max = getMax();
+
+        return x >= min.getX() && x <= max.getX() &&
+                y >= min.getY() && y <= max.getY() &&
+                z >= min.getZ() && z <= max.getZ();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean contains(final Vector3D pt, final DoublePrecisionContext precision) {
+        final double x = pt.getX();
+        final double y = pt.getY();
+        final double z = pt.getZ();
+
+        final Vector3D min = getMin();
+        final Vector3D max = getMax();
+
+        return precision.gte(x, min.getX()) && precision.lte(x, max.getX()) &&
+                precision.gte(y, min.getY()) && precision.lte(y, max.getY()) &&
+                precision.gte(z, min.getZ()) && precision.lte(z, max.getZ());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean intersects(final Bounds3D other) {
+        final Vector3D aMin = getMin();
+        final Vector3D aMax = getMax();
+
+        final Vector3D bMin = other.getMin();
+        final Vector3D bMax = other.getMax();
+
+        return aMin.getX() <= bMax.getX() && aMax.getX() >= bMin.getX() &&
+                aMin.getY() <= bMax.getY() && aMax.getY() >= bMin.getY() &&
+                aMin.getZ() <= bMax.getZ() && aMax.getZ() >= bMin.getZ();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Bounds3D intersection(final Bounds3D other) {
+        if (intersects(other)) {
+            final Vector3D aMin = getMin();
+            final Vector3D aMax = getMax();
+
+            final Vector3D bMin = other.getMin();
+            final Vector3D bMax = other.getMax();
+
+            // get the max of the mins and the mins of the maxes
+            final double minX = Math.max(aMin.getX(), bMin.getX());
+            final double minY = Math.max(aMin.getY(), bMin.getY());
+            final double minZ = Math.max(aMin.getZ(), bMin.getZ());
+
+            final double maxX = Math.min(aMax.getX(), bMax.getX());
+            final double maxY = Math.min(aMax.getY(), bMax.getY());
+            final double maxZ = Math.min(aMax.getZ(), bMax.getZ());
+
+            return new Bounds3D(
+                    Vector3D.of(minX, minY, minZ),
+                    Vector3D.of(maxX, maxY, maxZ));
+        }
+
+        return null; // no intersection
+    }
+
+    /** {@inheritDoc}
+     *
+     * @throws IllegalArgumentException if any dimension of the bounding box is zero
+     *      as evaluated by the given precision context
+     */
+    @Override
+    public Parallelepiped toRegion(final DoublePrecisionContext precision) {
+        return Parallelepiped.axisAligned(getMin(), getMax(), precision);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(getMin(), getMax());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        } else if (!(obj instanceof Bounds3D)) {
+            return false;
+        }
+
+        final Bounds3D other = (Bounds3D) obj;
+
+        return getMin().equals(other.getMin()) &&
+                getMax().equals(other.getMax());
+    }
+
+    /** Construct a new instance from the given points.
+     * @param first first point
+     * @param more additional points
+     * @return a new instance containing the min and max coordinates values from the input points
+     */
+    public static Bounds3D from(final Vector3D first, final Vector3D... more) {
+        final Builder builder = builder();
+
+        builder.add(first);
+        builder.addAll(Arrays.asList(more));
+
+        return builder.build();
+    }
+
+    /** Construct a new instance from the given points.
+     * @param points input points
+     * @return a new instance containing the min and max coordinates values from the input points
+     */
+    public static Bounds3D from(final Iterable<Vector3D> points) {
+        final Builder builder = builder();
+
+        builder.addAll(points);
+
+        return builder.build();
+    }
+
+    /** Construct a new {@link Builder} instance for creating bounds.
+     * @return a new builder instance for creating bounds
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /** Class used to construct {@link Bounds3D} instances.
+     */
+    public static final class Builder {
+
+        /** Minimum x coordinate. */
+        private double minX = Double.POSITIVE_INFINITY;
+
+        /** Minimum y coordinate. */
+        private double minY = Double.POSITIVE_INFINITY;
+
+        /** Minimum z coordinate. */
+        private double minZ = Double.POSITIVE_INFINITY;
+
+        /** Maximum x coordinate. */
+        private double maxX = Double.NEGATIVE_INFINITY;
+
+        /** Maximum y coordinate. */
+        private double maxY = Double.NEGATIVE_INFINITY;
+
+        /** Maximum z coordinate. */
+        private double maxZ = Double.NEGATIVE_INFINITY;
+
+        /** Private constructor; instantiate through factory method. */
+        private Builder() { }
+
+        /** Add a point to this instance.
+         * @param pt point to add
+         * @return this instance
+         */
+        public Builder add(final Vector3D pt) {
+            final double x = pt.getX();
+            final double y = pt.getY();
+            final double z = pt.getZ();
+
+            minX = Math.min(x, minX);
+            minY = Math.min(y, minY);
+            minZ = Math.min(z, minZ);
+
+            maxX = Math.max(x, maxX);
+            maxY = Math.max(y, maxY);
+            maxZ = Math.max(z, maxZ);
+
+            return this;
+        }
+
+        /** Add a collection of points to this instance.
+         * @param pts points to add
+         * @return this instance
+         */
+        public Builder addAll(final Iterable<Vector3D> pts) {
+            for (final Vector3D pt : pts) {
+                add(pt);
+            }
+
+            return this;
+        }
+
+        /** Add the min and max points from the given bounds to this instance.
+         * @param bounds bounds containing the min and max points to add
+         * @return this instance
+         */
+        public Builder add(final Bounds3D bounds) {
+            add(bounds.getMin());
+            add(bounds.getMax());
+
+            return this;
+        }
+
+        /** Return true if this builder contains valid min and max coordinate values.
+         * @return true if this builder contains valid min and max coordinate values
+         */
+        public boolean containsBounds() {
+            return Double.isFinite(minX) &&
+                    Double.isFinite(minY) &&
+                    Double.isFinite(minZ) &&
+                    Double.isFinite(maxX) &&
+                    Double.isFinite(maxY) &&
+                    Double.isFinite(maxZ);
+        }
+
+        /** Create a new {@link Bounds3D} instance from the values in this builder.
+         * The builder can continue to be used to create other instances.
+         * @return a new bounds instance
+         * @throws IllegalStateException if no points were given to the builder or any of the computed
+         *      min and max coordinate values are NaN or infinite
+         * @see #containsBounds()
+         */
+        public Bounds3D build() {
+            final Vector3D min = Vector3D.of(minX, minY, minZ);
+            final Vector3D max = Vector3D.of(maxX, maxY, maxZ);
+
+            if (!containsBounds()) {
+                if (Double.isInfinite(minX) && minX > 0 &&
+                        Double.isInfinite(maxX) && maxX < 0) {
+                    throw new IllegalStateException("Cannot construct bounds: no points given");
+                }
+
+                throw new IllegalStateException("Invalid bounds: min= " + min + ", max= " + max);
+            }
+
+            return new Bounds3D(min, max);
+        }
+    }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionEmbedding.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexPolygon3D.java
similarity index 53%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionEmbedding.java
copy to commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexPolygon3D.java
index b82b75a..3c7b365 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionEmbedding.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexPolygon3D.java
@@ -14,28 +14,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.commons.geometry.core;
+package org.apache.commons.geometry.euclidean.threed;
 
-/** Interface representing a geometric element that embeds a region in a subspace.
- * @param <P> Point type defining the embedding space.
- * @param <S> Point type defining the embedded subspace.
- * @see Embedding
- * @see Region
+import org.apache.commons.geometry.core.Transform;
+
+/** Interface representing a closed, finite convex polygon in Euclidean 3D space.
  */
-public interface RegionEmbedding<P extends Point<P>, S extends Point<S>>
-    extends Embedding<P, S>, Sized {
+public interface ConvexPolygon3D extends PlaneConvexSubset {
 
-    /** Get the size of the instance, which by default is the size of the embedded
-     * subspace region.
-     * @return the size of instance
-     */
+    /** {@inheritDoc} */
     @Override
-    default double getSize() {
-        return getSubspaceRegion().getSize();
-    }
+    ConvexPolygon3D reverse();
 
-    /** Get the embedded subspace region.
-     * @return the embedded subspace region
-     */
-    Region<S> getSubspaceRegion();
+    /** {@inheritDoc} */
+    @Override
+    ConvexPolygon3D transform(Transform<Vector3D> transform);
 }
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 57949c3..fb9e940 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
@@ -26,7 +26,6 @@ import org.apache.commons.geometry.core.partitioning.AbstractConvexHyperplaneBou
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
 import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
 import org.apache.commons.geometry.core.partitioning.Split;
-import org.apache.commons.geometry.euclidean.twod.ConvexArea;
 
 /** Class representing a finite or infinite convex volume in Euclidean 3D space.
  * The boundaries of this area, if any, are composed of plane convex subsets.
@@ -65,14 +64,11 @@ public class ConvexVolume extends AbstractConvexHyperplaneBoundedRegion<Vector3D
                 return Double.POSITIVE_INFINITY;
             }
 
-            final Plane plane = boundary.getPlane();
-            final ConvexArea subarea = boundary.getSubspaceRegion();
+            final Plane boundaryPlane = boundary.getPlane();
+            final double boundaryArea = boundary.getSize();
+            final Vector3D boundaryBarycenter = boundary.getBarycenter();
 
-            final Vector3D facetBarycenter = boundary.getHyperplane().toSpace(
-                    subarea.getBarycenter());
-
-
-            volumeSum += subarea.getSize() * facetBarycenter.dot(plane.getNormal());
+            volumeSum += boundaryArea * boundaryBarycenter.dot(boundaryPlane.getNormal());
         }
 
         return volumeSum / 3.0;
@@ -92,19 +88,17 @@ public class ConvexVolume extends AbstractConvexHyperplaneBoundedRegion<Vector3D
                 return null;
             }
 
-            final Plane plane = boundary.getPlane();
-            final ConvexArea subarea = boundary.getSubspaceRegion();
-
-            final Vector3D facetBarycenter = boundary.getHyperplane().toSpace(
-                    subarea.getBarycenter());
+            final Plane boundaryPlane = boundary.getPlane();
+            final double boundaryArea = boundary.getSize();
+            final Vector3D boundaryBarycenter = boundary.getBarycenter();
 
-            final double scaledVolume = subarea.getSize() * facetBarycenter.dot(plane.getNormal());
+            final double scaledVolume = boundaryArea * boundaryBarycenter.dot(boundaryPlane.getNormal());
 
             volumeSum += scaledVolume;
 
-            sumX += scaledVolume * facetBarycenter.getX();
-            sumY += scaledVolume * facetBarycenter.getY();
-            sumZ += scaledVolume * facetBarycenter.getZ();
+            sumX += scaledVolume * boundaryBarycenter.getX();
+            sumY += scaledVolume * boundaryBarycenter.getY();
+            sumZ += scaledVolume * boundaryBarycenter.getZ();
         }
 
         if (volumeSum > 0) {
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddedAreaPlaneConvexSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddedAreaPlaneConvexSubset.java
new file mode 100644
index 0000000..2a11ce2
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddedAreaPlaneConvexSubset.java
@@ -0,0 +1,117 @@
+/*
+ * 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 org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
+import org.apache.commons.geometry.euclidean.twod.ConvexArea;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+
+/** Internal implementation of {@link PlaneConvexSubset} that uses an embedded
+ * {@link ConvexArea} to represent the subspace region. This class is capable of
+ * representing regions of infinite size.
+ */
+final class EmbeddedAreaPlaneConvexSubset extends AbstractEmbeddedRegionPlaneSubset
+    implements PlaneConvexSubset, PlaneConvexSubset.Embedded {
+
+    /** The embedded 2D area. */
+    private final ConvexArea area;
+
+    /** Create a new instance from its component parts.
+     * @param plane plane the the convex area is embedded in
+     * @param area the embedded convex area
+     */
+    EmbeddedAreaPlaneConvexSubset(final EmbeddingPlane plane, final ConvexArea area) {
+        super(plane);
+
+        this.area = area;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public PlaneConvexSubset.Embedded getEmbedded() {
+        return this;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public ConvexArea getSubspaceRegion() {
+        return area;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public List<Vector3D> getVertices() {
+        return getPlane().toSpace(area.getVertices());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Bounds3D getBounds() {
+        return getBoundsFromSubspace(area);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public List<Triangle3D> toTriangles() {
+        if (isInfinite()) {
+            throw new IllegalStateException("Cannot convert infinite plane subset to triangles: " + this);
+        }
+
+        final EmbeddingPlane plane = getPlane();
+        final List<Vector3D> vertices = plane.toSpace(area.getVertices());
+
+        return Planes.convexPolygonToTriangleFan(plane, vertices);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public EmbeddedAreaPlaneConvexSubset transform(final Transform<Vector3D> transform) {
+        final EmbeddingPlane.SubspaceTransform st = getPlane().subspaceTransform(transform);
+        final ConvexArea tArea = area.transform(st.getTransform());
+
+        return new EmbeddedAreaPlaneConvexSubset(st.getPlane().getEmbedding(), tArea);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public EmbeddedAreaPlaneConvexSubset reverse() {
+        final EmbeddingPlane plane = getPlane();
+        final EmbeddingPlane rPlane = plane.reverse();
+
+        final Vector2D rU = rPlane.toSubspace(plane.toSpace(Vector2D.Unit.PLUS_X));
+        final Vector2D rV = rPlane.toSubspace(plane.toSpace(Vector2D.Unit.PLUS_Y));
+
+        final AffineTransformMatrix2D transform =
+                AffineTransformMatrix2D.fromColumnVectors(rU, rV);
+
+        return new EmbeddedAreaPlaneConvexSubset(rPlane, area.transform(transform));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Split<PlaneConvexSubset> split(final Hyperplane<Vector3D> splitter) {
+        // delegate back to the Planes factory method so that it has a chance to decide
+        // on the best possible implementation for the given area
+        return Planes.subspaceSplit((Plane) splitter, this,
+            (p, r) -> Planes.subsetFromConvexArea(p, (ConvexArea) r));
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubset.java
index 7b15ef6..f1d27e1 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubset.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubset.java
@@ -22,41 +22,45 @@ import java.util.List;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
 import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.twod.ConvexArea;
 import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+import org.apache.commons.geometry.euclidean.twod.rotation.Rotation2D;
 
 /** Class representing an arbitrary subset of a plane using a {@link RegionBSPTree2D}.
  * This class can represent convex, non-convex, finite, infinite, and empty regions.
  *
  * <p>This class is mutable and <em>not</em> thread safe.</p>
  */
-public final class EmbeddedTreePlaneSubset extends PlaneSubset {
+public final class EmbeddedTreePlaneSubset extends AbstractEmbeddedRegionPlaneSubset {
+
     /** The 2D region representing the area on the plane. */
     private final RegionBSPTree2D region;
 
     /** Construct a new, empty plane subset for the given plane.
-     * @param plane plane defining the subset
+     * @param plane plane containing the subset
      */
-    public EmbeddedTreePlaneSubset(final Plane plane) {
+    public EmbeddedTreePlaneSubset(final EmbeddingPlane plane) {
         this(plane, false);
     }
 
     /** Construct a new subset for the given plane. If {@code full}
      * is true, then the subset will cover the entire plane; otherwise,
      * it will be empty.
-     * @param plane plane defining the subset
+     * @param plane plane containing the subset
      * @param full if true, the subset will cover the entire space;
      *      otherwise it will be empty
      */
-    public EmbeddedTreePlaneSubset(final Plane plane, boolean full) {
+    public EmbeddedTreePlaneSubset(final EmbeddingPlane plane, boolean full) {
         this(plane, new RegionBSPTree2D(full));
     }
 
     /** Construct a new instance from its defining plane and subspace region.
-     * @param plane plane defining the subset
+     * @param plane plane containing the subset
      * @param region subspace region for the plane subset
      */
-    public EmbeddedTreePlaneSubset(final Plane plane, final RegionBSPTree2D region) {
+    public EmbeddedTreePlaneSubset(final EmbeddingPlane plane, final RegionBSPTree2D region) {
         super(plane);
 
         this.region = region;
@@ -64,19 +68,56 @@ public final class EmbeddedTreePlaneSubset extends PlaneSubset {
 
     /** {@inheritDoc} */
     @Override
+    public PlaneSubset.Embedded getEmbedded() {
+        return this;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public RegionBSPTree2D getSubspaceRegion() {
+        return region;
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public List<PlaneConvexSubset> toConvex() {
         final List<ConvexArea> areas = region.toConvex();
 
-        final Plane plane = getPlane();
         final List<PlaneConvexSubset> facets = new ArrayList<>(areas.size());
 
         for (final ConvexArea area : areas) {
-            facets.add(Planes.subsetFromConvexArea(plane, area));
+            facets.add(Planes.subsetFromConvexArea(getPlane(), area));
         }
 
         return facets;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public List<Triangle3D> toTriangles() {
+        final EmbeddingPlane plane = getPlane();
+        final List<Triangle3D> triangles = new ArrayList<>();
+
+        List<Vector3D> vertices;
+        for (final ConvexArea area : region.toConvex()) {
+            if (area.isInfinite()) {
+                throw new IllegalStateException("Cannot convert infinite plane subset to triangles: " + this);
+            }
+
+            vertices = plane.toSpace(area.getVertices());
+
+            triangles.addAll(Planes.convexPolygonToTriangleFan(plane, vertices));
+        }
+
+        return triangles;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Bounds3D getBounds() {
+        return getBoundsFromSubspace(region);
+    }
+
     /** {@inheritDoc}
      *
      * <p>In all cases, the current instance is not modified. However, In order to avoid
@@ -89,19 +130,15 @@ public final class EmbeddedTreePlaneSubset extends PlaneSubset {
      */
     @Override
     public Split<EmbeddedTreePlaneSubset> split(final Hyperplane<Vector3D> splitter) {
-        return splitInternal(splitter, this, (p, r) -> new EmbeddedTreePlaneSubset(p, (RegionBSPTree2D) r));
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public RegionBSPTree2D getSubspaceRegion() {
-        return region;
+        return Planes.subspaceSplit((Plane) splitter, this,
+            (p, r) -> new EmbeddedTreePlaneSubset(p, (RegionBSPTree2D) r));
     }
 
     /** {@inheritDoc} */
     @Override
     public EmbeddedTreePlaneSubset transform(final Transform<Vector3D> transform) {
-        final Plane.SubspaceTransform subTransform = getPlane().subspaceTransform(transform);
+        final EmbeddingPlane.SubspaceTransform subTransform =
+                getPlane().getEmbedding().subspaceTransform(transform);
 
         final RegionBSPTree2D tRegion = RegionBSPTree2D.empty();
         tRegion.copy(region);
@@ -118,7 +155,16 @@ public final class EmbeddedTreePlaneSubset extends PlaneSubset {
     public void add(final PlaneConvexSubset subset) {
         Planes.validatePlanesEquivalent(getPlane(), subset.getPlane());
 
-        region.add(subset.getSubspaceRegion());
+        final PlaneConvexSubset.Embedded embedded = subset.getEmbedded();
+        final Rotation2D rot = getEmbeddedRegionRotation(embedded);
+
+        final ConvexArea subspaceArea = embedded.getSubspaceRegion();
+
+        final ConvexArea toAdd = rot != null ?
+                subspaceArea.transform(rot) :
+                subspaceArea;
+
+        region.add(toAdd);
     }
 
     /** Add a plane subset to this instance.
@@ -129,6 +175,44 @@ public final class EmbeddedTreePlaneSubset extends PlaneSubset {
     public void add(final EmbeddedTreePlaneSubset subset) {
         Planes.validatePlanesEquivalent(getPlane(), subset.getPlane());
 
-        region.union(subset.getSubspaceRegion());
+        final RegionBSPTree2D otherTree = subset.getSubspaceRegion();
+        final Rotation2D rot = getEmbeddedRegionRotation(subset);
+
+        RegionBSPTree2D regionToAdd;
+        if (rot != null) {
+            // we need to transform the subspace region before adding
+            regionToAdd = otherTree.copy();
+            regionToAdd.transform(rot);
+        } else {
+            regionToAdd = otherTree;
+        }
+
+        region.union(regionToAdd);
+    }
+
+    /** Construct a rotation transform used to transform the subspace of the given embedded region plane
+     * subset into the subspace of this instance. Returns null if no transform is needed. This method must only
+     * be called with embedded regions that share an equivalent plane with this instance, meaning that the
+     * planes have the same origin point and normal
+     * @param embedded the embedded region plane subset to compare with the current instance
+     * @return a rotation transform to convert from the subspace of the argument into the current subspace; returns
+     *      null if no such transform is needed
+     */
+    private Rotation2D getEmbeddedRegionRotation(final PlaneSubset.Embedded embedded) {
+        // check if we need to apply a rotation to the given embedded subspace
+        final EmbeddingPlane thisPlane = getPlane();
+        final EmbeddingPlane otherPlane = embedded.getPlane();
+
+        final DoublePrecisionContext precision = thisPlane.getPrecision();
+
+        final double uDot = thisPlane.getU().dot(otherPlane.getU());
+        if (!precision.eq(uDot, 1.0)) {
+            final Vector2D otherPlaneU = thisPlane.toSubspace(otherPlane.getOrigin().add(otherPlane.getU()));
+            final double angle = Math.atan2(otherPlaneU.getY(), otherPlaneU.getX());
+
+            return Rotation2D.of(angle);
+        }
+
+        return null;
     }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddingPlane.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddingPlane.java
new file mode 100644
index 0000000..055c050
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/EmbeddingPlane.java
@@ -0,0 +1,333 @@
+/*
+ * 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.Objects;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
+import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+
+/** Extension of the {@link Plane} class that supports embedding of 2D subspaces in the plane.
+ * This is accomplished by defining two additional vectors, {@link #getU() u} and {@link #getV() v},
+ * that define the {@code x} and {@code y} axes respectively of the embedded subspace. For completeness,
+ * an additional vector {@link #getW()} is defined, which is simply an alias for the plane normal.
+ * Together, the vectors {@code u}, {@code v}, and {@code w} form a right-handed orthonormal basis.
+ *
+ * <p>The additional {@code u} and {@code v} vectors are not required to fulfill the contract of
+ * {@link org.apache.commons.geometry.core.partitioning.Hyperplane Hyperplane}. Therefore, they
+ * are not considered when using instances of this type purely as a hyperplane. For example, the
+ * {@link Plane#eq(Plane, DoublePrecisionContext) eq} and
+ * {@link Plane#similarOrientation(org.apache.commons.geometry.core.partitioning.Hyperplane) similiarOrientation}
+ * methods do not consider them.</p>
+ */
+public final class EmbeddingPlane extends Plane implements EmbeddingHyperplane<Vector3D, Vector2D> {
+    /** First normalized vector of the plane frame (in plane). */
+    private final Vector3D.Unit u;
+
+    /** Second normalized vector of the plane frame (in plane). */
+    private final Vector3D.Unit v;
+
+    /** Construct a new instance from an orthonormal set of basis vectors and an origin offset.
+     * @param u first vector of the basis (in plane)
+     * @param v second vector of the basis (in plane)
+     * @param w third vector of the basis (plane normal)
+     * @param originOffset offset of the origin with respect to the plane.
+     * @param precision precision context used for floating point comparisons
+     */
+    EmbeddingPlane(final Vector3D.Unit u, final Vector3D.Unit v, final Vector3D.Unit w, double originOffset,
+            final DoublePrecisionContext precision) {
+        super(w, originOffset, precision);
+
+        this.u = u;
+        this.v = v;
+    }
+
+    /** Get the plane first canonical vector.
+     * <p>
+     * The frame defined by ({@link #getU u}, {@link #getV v},
+     * {@link #getW w}) is a right-handed orthonormalized frame).
+     * </p>
+     * @return normalized first canonical vector
+     * @see #getV
+     * @see #getW
+     * @see #getNormal
+     */
+    public Vector3D.Unit getU() {
+        return u;
+    }
+
+    /** Get the plane second canonical vector.
+     * <p>
+     * The frame defined by ({@link #getU u}, {@link #getV v},
+     * {@link #getW w}) is a right-handed orthonormalized frame).
+     * </p>
+     * @return normalized second canonical vector
+     * @see #getU
+     * @see #getW
+     * @see #getNormal
+     */
+    public Vector3D.Unit getV() {
+        return v;
+    }
+
+    /** Get the plane third canonical vector, ie, the plane normal. This
+     * method is simply an alias for {@link #getNormal()}.
+     * <p>
+     * The frame defined by {@link #getU() u}, {@link #getV() v},
+     * {@link #getW() w} is a right-handed orthonormalized frame.
+     * </p>
+     * @return normalized normal vector
+     * @see #getU()
+     * @see #getV()
+     * @see #getNormal()
+     */
+    public Vector3D.Unit getW() {
+        return getNormal();
+    }
+
+    /** Return the current instance.
+     */
+    @Override
+    public EmbeddingPlane getEmbedding() {
+        return this;
+    }
+
+    /** Transform a 3D space point into an in-plane point.
+     * @param point point of the space
+     * @return in-plane point
+     * @see #toSpace
+     */
+    @Override
+    public Vector2D toSubspace(final Vector3D point) {
+        return Vector2D.of(point.dot(u), point.dot(v));
+    }
+
+    /** Transform an in-plane point into a 3D space point.
+     * @param point in-plane point
+     * @return 3D space point
+     * @see #toSubspace(Vector3D)
+     */
+    @Override
+    public Vector3D toSpace(final Vector2D point) {
+        return Vector3D.linearCombination(
+                point.getX(), u,
+                point.getY(), v,
+                -getOriginOffset(), getNormal());
+    }
+
+    /** Get one point from the 3D-space.
+     * @param inPlane desired in-plane coordinates for the point in the plane
+     * @param offset  desired offset for the point
+     * @return one point in the 3D-space, with given coordinates and offset relative
+     *         to the plane
+     */
+    public Vector3D pointAt(final Vector2D inPlane, final double offset) {
+        return Vector3D.linearCombination(
+                inPlane.getX(), u,
+                inPlane.getY(), v,
+                offset - getOriginOffset(), getNormal());
+    }
+
+    /** Build a new reversed version of this plane, with opposite orientation.
+     * <p>
+     * The new plane frame is chosen in such a way that a 3D point that had
+     * {@code (x, y)} in-plane coordinates and {@code z} offset with respect to the
+     * plane and is unaffected by the change will have {@code (y, x)} in-plane
+     * coordinates and {@code -z} offset with respect to the new plane. This means
+     * that the {@code u} and {@code v} vectors returned by the {@link #getU} and
+     * {@link #getV} methods are exchanged, and the {@code w} vector returned by the
+     * {@link #getNormal} method is reversed.
+     * </p>
+     * @return a new reversed plane
+     */
+    @Override
+    public EmbeddingPlane reverse() {
+        return new EmbeddingPlane(v, u, getNormal().negate(), -getOriginOffset(), getPrecision());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public EmbeddingPlane transform(final Transform<Vector3D> transform) {
+        final Vector3D origin = getOrigin();
+        final Vector3D plusU = origin.add(u);
+        final Vector3D plusV = origin.add(v);
+
+        final Vector3D tOrigin = transform.apply(origin);
+        final Vector3D tPlusU = transform.apply(plusU);
+        final Vector3D tPlusV = transform.apply(plusV);
+
+        final Vector3D.Unit tU = tOrigin.directionTo(tPlusU);
+        final Vector3D.Unit tV = tOrigin.directionTo(tPlusV);
+        final Vector3D.Unit tW = tU.cross(tV).normalize();
+
+        final double tOriginOffset = -tOrigin.dot(tW);
+
+        return new EmbeddingPlane(tU, tV, tW, tOriginOffset, getPrecision());
+    }
+
+    /** Translate the plane by the specified amount.
+     * @param translation translation to apply
+     * @return a new plane
+     */
+    @Override
+    public EmbeddingPlane translate(final Vector3D translation) {
+        final Vector3D tOrigin = getOrigin().add(translation);
+
+        return Planes.fromPointAndPlaneVectors(tOrigin, u, v, getPrecision());
+    }
+
+    /** Rotate the plane around the specified point.
+     * @param center rotation center
+     * @param rotation 3-dimensional rotation
+     * @return a new rotated plane
+     */
+    @Override
+    public EmbeddingPlane rotate(final Vector3D center, final QuaternionRotation rotation) {
+        final Vector3D delta = getOrigin().subtract(center);
+        final Vector3D tOrigin = center.add(rotation.apply(delta));
+        final Vector3D.Unit tU = rotation.apply(u).normalize();
+        final Vector3D.Unit tV = rotation.apply(v).normalize();
+
+        return Planes.fromPointAndPlaneVectors(tOrigin, tU, tV, getPrecision());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(getNormal(), getOriginOffset(), u, v, getPrecision());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        } else if (obj == null || obj.getClass() != EmbeddingPlane.class) {
+            return false;
+        }
+
+        final EmbeddingPlane other = (EmbeddingPlane) obj;
+
+        return Objects.equals(this.getNormal(), other.getNormal()) &&
+                Double.compare(this.getOriginOffset(), other.getOriginOffset()) == 0 &&
+                Objects.equals(this.u, other.u) &&
+                Objects.equals(this.v, other.v) &&
+                Objects.equals(this.getPrecision(), other.getPrecision());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName())
+            .append("[origin= ")
+            .append(getOrigin())
+            .append(", u= ")
+            .append(u)
+            .append(", v= ")
+            .append(v)
+            .append(", w= ")
+            .append(getNormal())
+            .append(']');
+
+        return sb.toString();
+    }
+
+    /** Get an object containing the current plane transformed by the argument along with a
+     * 2D transform that can be applied to subspace points. The subspace transform transforms
+     * subspace points such that their 3D location in the transformed plane is the same as their
+     * 3D location in the original plane after the 3D transform is applied. For example, consider
+     * the code below:
+     * <pre>
+     *      SubspaceTransform st = plane.subspaceTransform(transform);
+     *
+     *      Vector2D subPt = Vector2D.of(1, 1);
+     *
+     *      Vector3D a = transform.apply(plane.toSpace(subPt)); // transform in 3D space
+     *      Vector3D b = st.getPlane().toSpace(st.getTransform().apply(subPt)); // transform in 2D space
+     * </pre>
+     * At the end of execution, the points {@code a} (which was transformed using the original
+     * 3D transform) and {@code b} (which was transformed in 2D using the subspace transform)
+     * are equivalent.
+     *
+     * @param transform the transform to apply to this instance
+     * @return an object containing the transformed plane along with a transform that can be applied
+     *      to subspace points
+     * @see #transform(Transform)
+     */
+    public SubspaceTransform subspaceTransform(final Transform<Vector3D> transform) {
+        final Vector3D origin = getOrigin();
+
+        final Vector3D tOrigin = transform.apply(origin);
+        final Vector3D tPlusU = transform.apply(origin.add(u));
+        final Vector3D tPlusV = transform.apply(origin.add(v));
+
+        final EmbeddingPlane tPlane = Planes.fromPointAndPlaneVectors(
+                tOrigin,
+                tOrigin.vectorTo(tPlusU),
+                tOrigin.vectorTo(tPlusV),
+                getPrecision());
+
+        final Vector2D tSubspaceOrigin = tPlane.toSubspace(tOrigin);
+        final Vector2D tSubspaceU = tSubspaceOrigin.vectorTo(tPlane.toSubspace(tPlusU));
+        final Vector2D tSubspaceV = tSubspaceOrigin.vectorTo(tPlane.toSubspace(tPlusV));
+
+        final AffineTransformMatrix2D subspaceTransform =
+                AffineTransformMatrix2D.fromColumnVectors(tSubspaceU, tSubspaceV, tSubspaceOrigin);
+
+        return new SubspaceTransform(tPlane, subspaceTransform);
+    }
+
+    /** Class containing a transformed plane instance along with a subspace (2D) transform. The subspace
+     * transform produces the equivalent of the 3D transform in 2D.
+     */
+    public static final class SubspaceTransform {
+        /** The transformed plane. */
+        private final EmbeddingPlane plane;
+
+        /** The subspace transform instance. */
+        private final AffineTransformMatrix2D transform;
+
+        /** Simple constructor.
+         * @param plane the transformed plane
+         * @param transform 2D transform that can be applied to subspace points
+         */
+        public SubspaceTransform(final EmbeddingPlane plane, final AffineTransformMatrix2D transform) {
+            this.plane = plane;
+            this.transform = transform;
+        }
+
+        /** Get the transformed plane instance.
+         * @return the transformed plane instance
+         */
+        public EmbeddingPlane getPlane() {
+            return plane;
+        }
+
+        /** Get the 2D transform that can be applied to subspace points. This transform can be used
+         * to perform the equivalent of the 3D transform in 2D space.
+         * @return the subspace transform instance
+         */
+        public AffineTransformMatrix2D getTransform() {
+            return transform;
+        }
+    }
+}
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 4601680..ae08eb6 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
@@ -20,344 +20,219 @@ import java.util.Objects;
 
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.partitioning.AbstractHyperplane;
-import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.oned.Vector1D;
 import org.apache.commons.geometry.euclidean.threed.line.Line3D;
 import org.apache.commons.geometry.euclidean.threed.line.Lines3D;
 import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
-import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
 import org.apache.commons.geometry.euclidean.twod.ConvexArea;
-import org.apache.commons.geometry.euclidean.twod.Vector2D;
 
-/** Class representing a plane in 3 dimensional Euclidean space.
+/** Class representing a plane in 3 dimensional Euclidean space. Each plane is defined by a
+ * {@link #getNormal() normal} and an {@link #getOriginOffset() origin offset}. If \(\vec{n}\) is the plane normal,
+ * \(d\) is the origin offset, and \(p\) and \(q\) are any points in the plane, then the following are true:
+ * <ul>
+ *  <li>\(\lVert \vec{n} \rVert\) = 1</li>
+ *  <li>\(\vec{n} \cdot (p - q) = 0\)</li>
+ *  <li>\(d = - (\vec{n} \cdot q)\)</li>
+ *  </ul>
+ *  In other words, the normal is a unit vector such that the dot product of the normal and the difference of
+ *  any two points in the plane is always equal to \(0\). Similarly, the {@code origin offset} is equal to the
+ *  negation of the dot product of the normal and any point in the plane. The projection of the origin onto the
+ *  plane (given by {@link #getOrigin()}), is computed as \(-d \vec{n}\).
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
  * @see Planes
  */
-public final class Plane extends AbstractHyperplane<Vector3D>
-    implements EmbeddingHyperplane<Vector3D, Vector2D> {
-    /** First normalized vector of the plane frame (in plane). */
-    private final Vector3D u;
-
-    /** Second normalized vector of the plane frame (in plane). */
-    private final Vector3D v;
+public class Plane extends AbstractHyperplane<Vector3D> implements Hyperplane<Vector3D> {
 
-    /** Normalized plane normal. */
-    private final Vector3D w;
+    /** Plane normal. */
+    private final Vector3D.Unit normal;
 
     /** Offset of the origin with respect to the plane. */
     private final double originOffset;
 
-    /**
-     * Constructor to build a new plane with the given values.
-     * Made private to prevent inheritance.
-     * @param u u vector (on plane)
-     * @param v v vector (on plane)
-     * @param w unit normal vector
-     * @param originOffset offset of the origin with respect to the plane.
+    /** Construct a plane from its component parts.
+     * @param normal unit normal vector
+     * @param originOffset offset of the origin with respect to the plane
      * @param precision precision context used to compare floating point values
      */
-    Plane(final Vector3D u, final Vector3D v, final Vector3D w, double originOffset,
+    Plane(final Vector3D.Unit normal, double originOffset,
             final DoublePrecisionContext precision) {
 
         super(precision);
 
-        this.u = u;
-        this.v = v;
-        this.w = w;
-
+        this.normal = normal;
         this.originOffset = originOffset;
     }
 
-    /**
-     * Get the orthogonal projection of the 3D-space origin in the plane.
+    /** Get the orthogonal projection of the 3D-space origin in the plane.
      * @return the origin point of the plane frame (point closest to the 3D-space
      *         origin)
      */
     public Vector3D getOrigin() {
-        return w.multiply(-originOffset);
+        return normal.multiply(-originOffset);
     }
 
-    /**
-     *  Get the offset of the spatial origin ({@code 0, 0, 0}) with respect to the plane.
-     *
-     *  @return the offset of the origin with respect to the plane.
+    /** Get the offset of the spatial origin ({@code 0, 0, 0}) with respect to the plane.
+     * @return the offset of the origin with respect to the plane.
      */
     public double getOriginOffset() {
         return originOffset;
     }
 
-    /**
-     * Get the plane first canonical vector.
-     * <p>
-     * The frame defined by ({@link #getU getU}, {@link #getV getV},
-     * {@link #getNormal getNormal}) is a right-handed orthonormalized frame).
-     * </p>
-     *
-     * @return normalized first canonical vector
-     * @see #getV
-     * @see #getNormal
+    /** Get the plane normal vector.
+     * @return plane normal vector
      */
-    public Vector3D getU() {
-        return u;
+    public Vector3D.Unit getNormal() {
+        return normal;
     }
 
-    /**
-     * Get the plane second canonical vector.
-     * <p>
-     * The frame defined by ({@link #getU getU}, {@link #getV getV},
-     * {@link #getNormal getNormal}) is a right-handed orthonormalized frame).
-     * </p>
-     *
-     * @return normalized second canonical vector
-     * @see #getU
-     * @see #getNormal
+    /** Return an {@link EmbeddingPlane} instance suitable for embedding 2D geometric objects
+     * into this plane. Returned instances are guaranteed to be equal between invocations.
+     * @return a plane instance suitable for embedding 2D subspaces
      */
-    public Vector3D getV() {
-        return v;
-    }
+    public EmbeddingPlane getEmbedding() {
+        final Vector3D.Unit u = normal.orthogonal();
+        final Vector3D.Unit v = normal.cross(u).normalize();
 
-    /**
-     * Get the normalized normal vector.
-     * <p>
-     * The frame defined by {@link #getU()}, {@link #getV()},
-     * {@link #getW()} is a right-handed orthonormalized frame.
-     * </p>
-     *
-     * @return normalized normal vector
-     * @see #getU()
-     * @see #getV()
-     * @see #getNormal()
-     */
-    public Vector3D getW() {
-        return w;
-    }
-
-    /**
-     * Get the normalized normal vector. This method is an alias
-     * for {@link #getW()}.
-     * <p>
-     * The frame defined by {@link #getU()}, {@link #getV()},
-     * {@link #getW()} is a right-handed orthonormalized frame.
-     * </p>
-     *
-     * @return normalized normal vector
-     * @see #getU()
-     * @see #getV()
-     * @see #getW()
-     */
-    public Vector3D getNormal() {
-        return getW();
+        return new EmbeddingPlane(u, v, normal, originOffset, getPrecision());
     }
 
     /** {@inheritDoc} */
     @Override
-    public Vector3D project(final Vector3D point) {
-        return toSpace(toSubspace(point));
+    public double offset(final Vector3D point) {
+        return point.dot(normal) + originOffset;
     }
 
-    /**
-     * Project a 3D line onto the plane.
-     * @param line the line to project
-     * @return the projection of the given line onto the plane.
+    /** Get the offset (oriented distance) of the given line with respect to the plane. The value
+     * closest to zero is returned, which will always be zero if the line is not parallel to the plane.
+     * @param line line to calculate the offset of
+     * @return the offset of the line with respect to the plane or 0.0 if the line
+     *      is not parallel to the plane.
      */
-    public Line3D project(final Line3D line) {
-        final Vector3D direction = line.getDirection();
-        final Vector3D projection = w.multiply(direction.dot(w) * (1 / w.normSq()));
-
-        final Vector3D projectedLineDirection = direction.subtract(projection);
-        final Vector3D p1 = project(line.getOrigin());
-        final Vector3D p2 = p1.add(projectedLineDirection);
+    public double offset(final Line3D line) {
+        if (!isParallel(line)) {
+            return 0.0;
+        }
+        return offset(line.getOrigin());
+    }
 
-        return Lines3D.fromPoints(p1, p2, getPrecision());
+    /** Get the offset (oriented distance) of the given plane with respect to this instance. The value
+     * closest to zero is returned, which will always be zero if the planes are not parallel.
+     * @param plane plane to calculate the offset of
+     * @return the offset of the plane with respect to this instance or 0.0 if the planes
+     *      are not parallel.
+     */
+    public double offset(final Plane plane) {
+        if (!isParallel(plane)) {
+            return 0.0;
+        }
+        return originOffset + (similarOrientation(plane) ? -plane.originOffset : plane.originOffset);
     }
 
-    /**
-     * Build a new reversed version of this plane, with opposite orientation.
-     * <p>
-     * The new plane frame is chosen in such a way that a 3D point that had
-     * {@code (x, y)} in-plane coordinates and {@code z} offset with respect to the
-     * plane and is unaffected by the change will have {@code (y, x)} in-plane
-     * coordinates and {@code -z} offset with respect to the new plane. This means
-     * that the {@code u} and {@code v} vectors returned by the {@link #getU} and
-     * {@link #getV} methods are exchanged, and the {@code w} vector returned by the
-     * {@link #getNormal} method is reversed.
-     * </p>
-     * @return a new reversed plane
+    /** Check if the instance contains a point.
+     * @param p point to check
+     * @return true if p belongs to the plane
      */
     @Override
-    public Plane reverse() {
-        return new Plane(v, u, w.negate(), -originOffset, getPrecision());
+    public boolean contains(final Vector3D p) {
+        return getPrecision().eqZero(offset(p));
     }
 
-    /**
-     * Transform a 3D space point into an in-plane point.
-     *
-     * @param point point of the space (must be a {@link Vector3D} instance)
-     * @return in-plane point
-     * @see #toSpace
+    /** Check if the instance contains a line.
+     * @param line line to check
+     * @return true if line is contained in this plane
      */
-    @Override
-    public Vector2D toSubspace(final Vector3D point) {
-        return Vector2D.of(point.dot(u), point.dot(v));
+    public boolean contains(final Line3D line) {
+        return isParallel(line) && contains(line.getOrigin());
     }
 
-    /**
-     * Transform an in-plane point into a 3D space point.
-     *
-     * @param point in-plane point (must be a {@link Vector2D} instance)
-     * @return 3D space point
-     * @see #toSubspace(Vector3D)
+    /** Check if the instance contains another plane. Planes are considered similar if they contain
+     * the same points. This does not mean they are equal since they can have opposite normals.
+     * @param plane plane to which the instance is compared
+     * @return true if the planes are similar
      */
-    @Override
-    public Vector3D toSpace(final Vector2D point) {
-        return Vector3D.linearCombination(point.getX(), u, point.getY(), v, -originOffset, w);
+    public boolean contains(final Plane plane) {
+        final double angle = normal.angle(plane.normal);
+        final DoublePrecisionContext precision = getPrecision();
+
+        return ((precision.eqZero(angle)) && precision.eq(originOffset, plane.originOffset)) ||
+                ((precision.eq(angle, Math.PI)) && precision.eq(originOffset, -plane.originOffset));
     }
 
     /** {@inheritDoc} */
     @Override
-    public Plane transform(final Transform<Vector3D> transform) {
-        final Vector3D origin = getOrigin();
-
-        final Vector3D p1 = transform.apply(origin);
-        final Vector3D p2 = transform.apply(origin.add(u));
-        final Vector3D p3 = transform.apply(origin.add(v));
-
-        return Planes.fromPoints(p1, p2, p3, getPrecision());
+    public Vector3D project(final Vector3D point) {
+        return getOrigin().add(point.reject(normal));
     }
 
-    /** Get an object containing the current plane transformed by the argument along with a
-     * 2D transform that can be applied to subspace points. The subspace transform transforms
-     * subspace points such that their 3D location in the transformed plane is the same as their
-     * 3D location in the original plane after the 3D transform is applied. For example, consider
-     * the code below:
-     * <pre>
-     *      SubspaceTransform st = plane.subspaceTransform(transform);
-     *
-     *      Vector2D subPt = Vector2D.of(1, 1);
-     *
-     *      Vector3D a = transform.apply(plane.toSpace(subPt)); // transform in 3D space
-     *      Vector3D b = st.getPlane().toSpace(st.getTransform().apply(subPt)); // transform in 2D space
-     * </pre>
-     * At the end of execution, the points {@code a} (which was transformed using the original
-     * 3D transform) and {@code b} (which was transformed in 2D using the subspace transform)
-     * are equivalent.
-     *
-     * @param transform the transform to apply to this instance
-     * @return an object containing the transformed plane along with a transform that can be applied
-     *      to subspace points
-     * @see #transform(Transform)
+    /** Project a 3D line onto the plane.
+     * @param line the line to project
+     * @return the projection of the given line onto the plane.
      */
-    public SubspaceTransform subspaceTransform(final Transform<Vector3D> transform) {
-        final Vector3D origin = getOrigin();
-
-        final Vector3D p1 = transform.apply(origin);
-        final Vector3D p2 = transform.apply(origin.add(u));
-        final Vector3D p3 = transform.apply(origin.add(v));
-
-        final Plane tPlane = Planes.fromPoints(p1, p2, p3, getPrecision());
-
-        final Vector2D tSubspaceOrigin = tPlane.toSubspace(p1);
-        final Vector2D tSubspaceU = tSubspaceOrigin.vectorTo(tPlane.toSubspace(p2));
-        final Vector2D tSubspaceV = tSubspaceOrigin.vectorTo(tPlane.toSubspace(p3));
+    public Line3D project(final Line3D line) {
+        final Vector3D direction = line.getDirection();
+        final Vector3D projection = normal.multiply(direction.dot(normal) * (1 / normal.normSq()));
 
-        final AffineTransformMatrix2D subspaceTransform =
-                AffineTransformMatrix2D.fromColumnVectors(tSubspaceU, tSubspaceV, tSubspaceOrigin);
+        final Vector3D projectedLineDirection = direction.subtract(projection);
+        final Vector3D p1 = project(line.getOrigin());
+        final Vector3D p2 = p1.add(projectedLineDirection);
 
-        return new SubspaceTransform(tPlane, subspaceTransform);
+        return Lines3D.fromPoints(p1, p2, getPrecision());
     }
 
-    /**
-     * Rotate the plane around the specified point.
-     * <p>
-     * The instance is not modified, a new instance is created.
-     * </p>
-     *
-     * @param center   rotation center
-     * @param rotation 3-dimensional rotation
-     * @return a new plane
-     */
-    public Plane rotate(final Vector3D center, final QuaternionRotation rotation) {
-        final Vector3D delta = getOrigin().subtract(center);
-        final Vector3D p = center.add(rotation.apply(delta));
-        final Vector3D normal = rotation.apply(this.w);
-        final Vector3D wTmp = normal.normalize();
-
-        final double originOffsetTmp = -p.dot(wTmp);
-        final Vector3D uTmp = rotation.apply(this.u);
-        final Vector3D vTmp = rotation.apply(this.v);
-
-        return new Plane(uTmp, vTmp, wTmp, originOffsetTmp, getPrecision());
+    /** {@inheritDoc} */
+    @Override
+    public PlaneConvexSubset span() {
+        return Planes.subsetFromConvexArea(getEmbedding(), ConvexArea.full());
     }
 
-    /**
-     * Translate the plane by the specified amount.
-     * <p>
-     * The instance is not modified, a new instance is created.
-     * </p>
-     *
-     * @param translation translation to apply
-     * @return a new plane
+    /** Check if the line is parallel to the instance.
+     * @param line line to check.
+     * @return true if the line is parallel to the instance, false otherwise.
      */
-    public Plane translate(final Vector3D translation) {
-        final Vector3D p = getOrigin().add(translation);
-        final Vector3D normal = this.w;
-        final Vector3D wTmp = normal.normalize();
-        final double originOffsetTmp = -p.dot(wTmp);
+    public boolean isParallel(final Line3D line) {
+        final double dot = normal.dot(line.getDirection());
 
-        return new Plane(this.u, this.v, wTmp, originOffsetTmp, getPrecision());
+        return getPrecision().eqZero(dot);
     }
 
-    /**
-     * Get one point from the 3D-space.
-     *
-     * @param inPlane desired in-plane coordinates for the point in the plane
-     * @param offset  desired offset for the point
-     * @return one point in the 3D-space, with given coordinates and offset relative
-     *         to the plane
+    /** Check if the plane is parallel to the instance.
+     * @param plane plane to check.
+     * @return true if the plane is parallel to the instance, false otherwise.
      */
-    public Vector3D pointAt(final Vector2D inPlane, final double offset) {
-        return Vector3D.linearCombination(inPlane.getX(), u, inPlane.getY(), v, offset - originOffset, w);
+    public boolean isParallel(final Plane plane) {
+        return getPrecision().eqZero(normal.cross(plane.normal).norm());
     }
 
-    /**
-     * Check if the instance contains another plane.
-     * <p>
-     * Planes are considered similar if they contain the same points. This does not
-     * mean they are equal since they can have opposite normals.
-     * </p>
-     *
-     * @param plane plane to which the instance is compared
-     * @return true if the planes are similar
-     */
-    public boolean contains(final Plane plane) {
-        final double angle = w.angle(plane.w);
-        final DoublePrecisionContext precision = getPrecision();
-
-        return ((precision.eqZero(angle)) && precision.eq(originOffset, plane.originOffset)) ||
-                ((precision.eq(angle, Math.PI)) && precision.eq(originOffset, -plane.originOffset));
+    /** {@inheritDoc} */
+    @Override
+    public boolean similarOrientation(final Hyperplane<Vector3D> other) {
+        return (((Plane) other).normal).dot(normal) > 0;
     }
 
-    /**
-     * Get the intersection of a line with the instance.
-     *
+    /** Get the intersection of a line with this plane.
      * @param line line intersecting the instance
      * @return intersection point between between the line and the instance (null if
      *         the line is parallel to the instance)
      */
     public Vector3D intersection(final Line3D line) {
         final Vector3D direction = line.getDirection();
-        final double dot = w.dot(direction);
+        final double dot = normal.dot(direction);
+
         if (getPrecision().eqZero(dot)) {
             return null;
         }
-        final Vector3D point = line.toSpace(Vector1D.ZERO);
-        final double k = -(originOffset + w.dot(point)) / dot;
-        return Vector3D.linearCombination(1.0, point, k, direction);
+
+        final Vector3D point = line.pointAt(0);
+        final double k = -(originOffset + normal.dot(point)) / dot;
+
+        return Vector3D.linearCombination(
+                1.0, point,
+                k, direction);
     }
 
-    /**
-     * Get the line formed by the intersection of this instance with the given plane.
+    /** Get the line formed by the intersection of this instance with the given plane.
      * The returned line lies in both planes and points in the direction of
      * the cross product <code>n<sub>1</sub> x n<sub>2</sub></code>, where <code>n<sub>1</sub></code>
      * is the normal of the current instance and <code>n<sub>2</sub></code> is the normal
@@ -370,148 +245,113 @@ public final class Plane extends AbstractHyperplane<Vector3D>
      *      if no such line exists
      */
     public Line3D intersection(final Plane other) {
-        final Vector3D direction = w.cross(other.w);
+        final Vector3D direction = normal.cross(other.normal);
+
         if (getPrecision().eqZero(direction.norm())) {
             return null;
         }
+
         final Vector3D point = intersection(this, other, Planes.fromNormal(direction, getPrecision()));
+
         return Lines3D.fromPointAndDirection(point, direction, getPrecision());
     }
 
-    /**
-     * Get the intersection point of three planes. Returns null if no unique intersection point
-     * exists (ie, there are no intersection points or an infinite number).
-     *
-     * @param plane1 first plane1
-     * @param plane2 second plane2
-     * @param plane3 third plane2
-     * @return intersection point of the three planes or null if no unique intersection point exists
+    /** Build a new reversed version of this plane, with opposite orientation.
+     * @return a new reversed plane
      */
-    public static Vector3D intersection(final Plane plane1, final Plane plane2, final Plane plane3) {
-
-        // coefficients of the three planes linear equations
-        final double a1 = plane1.w.getX();
-        final double b1 = plane1.w.getY();
-        final double c1 = plane1.w.getZ();
-        final double d1 = plane1.originOffset;
-
-        final double a2 = plane2.w.getX();
-        final double b2 = plane2.w.getY();
-        final double c2 = plane2.w.getZ();
-        final double d2 = plane2.originOffset;
-
-        final double a3 = plane3.w.getX();
-        final double b3 = plane3.w.getY();
-        final double c3 = plane3.w.getZ();
-        final double d3 = plane3.originOffset;
-
-        // direct Cramer resolution of the linear system
-        // (this is still feasible for a 3x3 system)
-        final double a23 = (b2 * c3) - (b3 * c2);
-        final double b23 = (c2 * a3) - (c3 * a2);
-        final double c23 = (a2 * b3) - (a3 * b2);
-        final double determinant = (a1 * a23) + (b1 * b23) + (c1 * c23);
-
-        // use the precision context of the first plane to determine equality
-        if (plane1.getPrecision().eqZero(determinant)) {
-            return null;
-        }
-
-        final double r = 1.0 / determinant;
-        return Vector3D.of((-a23 * d1 - (c1 * b3 - c3 * b1) * d2 - (c2 * b1 - c1 * b2) * d3) * r,
-                (-b23 * d1 - (c3 * a1 - c1 * a3) * d2 - (c1 * a2 - c2 * a1) * d3) * r,
-                (-c23 * d1 - (b1 * a3 - b3 * a1) * d2 - (b2 * a1 - b1 * a2) * d3) * r);
-
-    }
-
-    /** {@inheritDoc} */
     @Override
-    public PlaneConvexSubset span() {
-        return Planes.subsetFromConvexArea(this, ConvexArea.full());
+    public Plane reverse() {
+        return new Plane(normal.negate(), -originOffset, getPrecision());
     }
 
-    /**
-     * Check if the instance contains a point.
+    /** {@inheritDoc}
      *
-     * @param p point to check
-     * @return true if p belongs to the plane
+     * <p>Instances are transformed by selecting 3 representative points from the
+     * plane, transforming them, and constructing a new plane from the transformed points.
+     * Since the normal is not transformed directly, but rather is constructed new from the
+     * transformed points, the relative orientations of points in the plane are preserved,
+     * even for transforms that do not
+     * {@link Transform#preservesOrientation() preserve orientation}. The example below shows
+     * a plane being transformed by a non-orientation-preserving transform. The normal of the
+     * transformed plane retains its counterclockwise relationship to the points in the plane,
+     * in contrast with the normal that is transformed directly by the transform.
+     * </p>
+     * <pre>
+     * // construct a plane from 3 points; the normal will be selected such that the
+     * // points are ordered counterclockwise when looking down the plane normal.
+     * Vector3D p1 = Vector3D.of(0, 0, 0);
+     * Vector3D p2 = Vector3D.of(+1, 0, 0);
+     * Vector3D p3 = Vector3D.of(0, +1, 0);
+     *
+     * Plane plane = Planes.fromPoints(p1, p2, p3, precision); // normal is (0, 0, +1)
+     *
+     * // create a transform that negates all x-values; this transform does not
+     * // preserve orientation, i.e. it will convert a right-handed system into a left-handed
+     * // system and vice versa
+     * AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(-1, 1,  1);
+     *
+     * // transform the plane
+     * Plane transformedPlane = plane.transform(transform);
+     *
+     * // the plane normal is oriented such that transformed points are still ordered
+     * // counterclockwise when looking down the plane normal; since the point (1, 0, 0) has
+     * // now become (-1, 0, 0), the normal has flipped to (0, 0, -1)
+     * transformedPlane.getNormal();
+     *
+     * // directly transform the original plane normal; the normal is unchanged by the transform
+     * // since the target space of the transform is left-handed
+     * AffineTransformMatrix3D normalTransform = transform.normalTransform();
+     * Vector3D directlyTransformedNormal = normalTransform.apply(plane.getNormal()); // (0, 0, +1)
+     * </pre>
      */
     @Override
-    public boolean contains(final Vector3D p) {
-        return getPrecision().eqZero(offset(p));
-    }
+    public Plane transform(final Transform<Vector3D> transform) {
+        // create 3 representation points lying on the plane, transform them,
+        // and use the transformed points to create a new plane
 
-    /**
-     * Check if the instance contains a line.
-     * @param line line to check
-     * @return true if line is contained in this plane
-     */
-    public boolean contains(final Line3D line) {
-        return isParallel(line) && contains(line.getOrigin());
-    }
+        final Vector3D u = normal.orthogonal();
+        final Vector3D v = normal.cross(u);
 
-    /** Check if the line is parallel to the instance.
-     * @param line line to check.
-     * @return true if the line is parallel to the instance, false otherwise.
-     */
-    public boolean isParallel(final Line3D line) {
-        final double dot = w.dot(line.getDirection());
+        final Vector3D p1 = getOrigin();
+        final Vector3D p2 = p1.add(u);
+        final Vector3D p3 = p1.add(v);
 
-        return getPrecision().eqZero(dot);
-    }
+        final Vector3D t1 = transform.apply(p1);
+        final Vector3D t2 = transform.apply(p2);
+        final Vector3D t3 = transform.apply(p3);
 
-    /** Check, if the plane is parallel to the instance.
-     * @param plane plane to check.
-     * @return true if the plane is parallel to the instance, false otherwise.
-     */
-    public boolean isParallel(final Plane plane) {
-        return getPrecision().eqZero(w.cross(plane.w).norm());
+        return Planes.fromPoints(t1, t2, t3, getPrecision());
     }
 
-    /**
-     * Get the offset (oriented distance) of the given plane with respect to this instance. The value
-     * closest to zero is returned, which will always be zero if the planes are not parallel.
-     * @param plane plane to calculate the offset of
-     * @return the offset of the plane with respect to this instance or 0.0 if the planes
-     *      are not parallel.
+    /** Translate the plane by the specified amount.
+     * @param translation translation to apply
+     * @return a new plane
      */
-    public double offset(final Plane plane) {
-        if (!isParallel(plane)) {
-            return 0.0;
-        }
-        return originOffset + (similarOrientation(plane) ? -plane.originOffset : plane.originOffset);
+    public Plane translate(final Vector3D translation) {
+        final Vector3D tOrigin = getOrigin().add(translation);
+
+        return Planes.fromPointAndNormal(tOrigin, normal, getPrecision());
     }
 
-    /**
-     * Get the offset (oriented distance) of the given line with respect to the plane. The value
-     * closest to zero is returned, which will always be zero if the line is not parallel to the plane.
-     * @param line line to calculate the offset of
-     * @return the offset of the line with respect to the plane or 0.0 if the line
-     *      is not parallel to the plane.
+    /** Rotate the plane around the specified point.
+     * @param center rotation center
+     * @param rotation 3-dimensional rotation
+     * @return a new plane
      */
-    public double offset(final Line3D line) {
-        if (!isParallel(line)) {
-            return 0.0;
-        }
-        return offset(line.getOrigin());
-    }
+    public Plane rotate(final Vector3D center, final QuaternionRotation rotation) {
+        final Vector3D delta = getOrigin().subtract(center);
+        final Vector3D tOrigin = center.add(rotation.apply(delta));
 
-    /** {@inheritDoc} */
-    @Override
-    public double offset(final Vector3D point) {
-        return point.dot(w) + originOffset;
-    }
+        // we can directly apply the rotation to the normal since it will transform
+        // it properly (there is no translation or scaling involved)
+        final Vector3D.Unit tNormal = rotation.apply(normal).normalize();
 
-    /** {@inheritDoc} */
-    @Override
-    public boolean similarOrientation(final Hyperplane<Vector3D> other) {
-        return (((Plane) other).w).dot(w) > 0;
+        return Planes.fromPointAndNormal(tOrigin, tNormal, getPrecision());
     }
 
-
     /** 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.
+     * given precision context for comparison. Instances are considered equivalent if they contain
+     * the same points, which is determined by comparing the plane {@code origins} and {@code normals}.
      * @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
@@ -519,14 +359,13 @@ public final class Plane extends AbstractHyperplane<Vector3D>
      */
     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);
+                normal.eq(other.normal, precision);
     }
 
     /** {@inheritDoc} */
     @Override
     public int hashCode() {
-        return Objects.hash(u, v, w, originOffset, getPrecision());
+        return Objects.hash(normal, originOffset, getPrecision());
     }
 
     /** {@inheritDoc} */
@@ -534,15 +373,13 @@ public final class Plane extends AbstractHyperplane<Vector3D>
     public boolean equals(Object obj) {
         if (this == obj) {
             return true;
-        } else if (!(obj instanceof Plane)) {
+        } else if (obj == null || obj.getClass() != this.getClass()) {
             return false;
         }
 
         final Plane other = (Plane) obj;
 
-        return Objects.equals(this.u, other.u) &&
-                Objects.equals(this.v, other.v) &&
-                Objects.equals(this.w, other.w) &&
+        return Objects.equals(this.normal, other.normal) &&
                 Double.compare(this.originOffset, other.originOffset) == 0 &&
                 Objects.equals(this.getPrecision(), other.getPrecision());
     }
@@ -554,49 +391,53 @@ public final class Plane extends AbstractHyperplane<Vector3D>
         sb.append(getClass().getSimpleName())
             .append("[origin= ")
             .append(getOrigin())
-            .append(", u= ")
-            .append(u)
-            .append(", v= ")
-            .append(v)
-            .append(", w= ")
-            .append(w)
+            .append(", normal= ")
+            .append(normal)
             .append(']');
 
         return sb.toString();
     }
 
-    /** Class containing a transformed plane instance along with a subspace (2D) transform. The subspace
-     * transform produces the equivalent of the 3D transform in 2D.
+    /** Get the intersection point of three planes. Returns null if no unique intersection point
+     * exists (ie, there are no intersection points or an infinite number).
+     * @param plane1 first plane1
+     * @param plane2 second plane2
+     * @param plane3 third plane2
+     * @return intersection point of the three planes or null if no unique intersection point exists
      */
-    public static final class SubspaceTransform {
-        /** The transformed plane. */
-        private final Plane plane;
-
-        /** The subspace transform instance. */
-        private final AffineTransformMatrix2D transform;
-
-        /** Simple constructor.
-         * @param plane the transformed plane
-         * @param transform 2D transform that can be applied to subspace points
-         */
-        public SubspaceTransform(final Plane plane, final AffineTransformMatrix2D transform) {
-            this.plane = plane;
-            this.transform = transform;
-        }
+    public static Vector3D intersection(final Plane plane1, final Plane plane2, final Plane plane3) {
 
-        /** Get the transformed plane instance.
-         * @return the transformed plane instance
-         */
-        public Plane getPlane() {
-            return plane;
-        }
+        // coefficients of the three planes linear equations
+        final double a1 = plane1.normal.getX();
+        final double b1 = plane1.normal.getY();
+        final double c1 = plane1.normal.getZ();
+        final double d1 = plane1.originOffset;
+
+        final double a2 = plane2.normal.getX();
+        final double b2 = plane2.normal.getY();
+        final double c2 = plane2.normal.getZ();
+        final double d2 = plane2.originOffset;
 
-        /** Get the 2D transform that can be applied to subspace points. This transform can be used
-         * to perform the equivalent of the 3D transform in 2D space.
-         * @return the subspace transform instance
-         */
-        public AffineTransformMatrix2D getTransform() {
-            return transform;
+        final double a3 = plane3.normal.getX();
+        final double b3 = plane3.normal.getY();
+        final double c3 = plane3.normal.getZ();
+        final double d3 = plane3.originOffset;
+
+        // direct Cramer resolution of the linear system
+        // (this is still feasible for a 3x3 system)
+        final double a23 = (b2 * c3) - (b3 * c2);
+        final double b23 = (c2 * a3) - (c3 * a2);
+        final double c23 = (a2 * b3) - (a3 * b2);
+        final double determinant = (a1 * a23) + (b1 * b23) + (c1 * c23);
+
+        // use the precision context of the first plane to determine equality
+        if (plane1.getPrecision().eqZero(determinant)) {
+            return null;
         }
+
+        final double r = 1.0 / determinant;
+        return Vector3D.of((-a23 * d1 - (c1 * b3 - c3 * b1) * d2 - (c2 * b1 - c1 * b2) * d3) * r,
+                (-b23 * d1 - (c3 * a1 - c1 * a3) * d2 - (c1 * a2 - c2 * a1) * d3) * r,
+                (-c23 * d1 - (b1 * a3 - b3 * a1) * d2 - (b2 * a1 - b1 * a2) * d3) * r);
     }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PlaneConvexSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PlaneConvexSubset.java
index 82573a5..26e9169 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PlaneConvexSubset.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PlaneConvexSubset.java
@@ -23,106 +23,60 @@ import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
 import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
 import org.apache.commons.geometry.core.partitioning.Split;
-import org.apache.commons.geometry.euclidean.threed.Plane.SubspaceTransform;
-import org.apache.commons.geometry.euclidean.threed.line.Line3D;
-import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
-import org.apache.commons.geometry.euclidean.twod.AffineTransformMatrix2D;
 import org.apache.commons.geometry.euclidean.twod.ConvexArea;
-import org.apache.commons.geometry.euclidean.twod.Vector2D;
 
-/** Class representing a convex subset of points in a plane. The subset may be finite
- * or infinite.
- * @see Planes
+/** Interface representing a finite or infinite convex subset of points in a plane in Euclidean 3D
+ * space.
  */
-public final class PlaneConvexSubset extends PlaneSubset
-    implements HyperplaneConvexSubset<Vector3D>  {
-    /** The embedded 2D area. */
-    private final ConvexArea area;
-
-    /** Create a new instance from its component parts.
-     * @param plane plane the the convex area is embedded in
-     * @param area the embedded convex area
-     */
-    PlaneConvexSubset(final Plane plane, final ConvexArea area) {
-        super(plane);
-
-        this.area = area;
-    }
+public interface PlaneConvexSubset extends PlaneSubset, HyperplaneConvexSubset<Vector3D> {
 
     /** {@inheritDoc} */
     @Override
-    public List<PlaneConvexSubset> toConvex() {
-        return Collections.singletonList(this);
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public PlaneConvexSubset reverse() {
-        final Plane plane = getPlane();
-        final Plane rPlane = plane.reverse();
-
-        final Vector2D rU = rPlane.toSubspace(plane.toSpace(Vector2D.Unit.PLUS_X));
-        final Vector2D rV = rPlane.toSubspace(plane.toSpace(Vector2D.Unit.PLUS_Y));
-
-        final AffineTransformMatrix2D transform =
-                AffineTransformMatrix2D.fromColumnVectors(rU, rV);
-
-        return new PlaneConvexSubset(rPlane, area.transform(transform));
-    }
+    PlaneConvexSubset reverse();
 
     /** {@inheritDoc} */
     @Override
-    public PlaneConvexSubset transform(final Transform<Vector3D> transform) {
-        final SubspaceTransform st = getPlane().subspaceTransform(transform);
-        final ConvexArea tArea = area.transform(st.getTransform());
-
-        return Planes.subsetFromConvexArea(st.getPlane(), tArea);
-    }
+    PlaneConvexSubset transform(Transform<Vector3D> transform);
 
     /** {@inheritDoc} */
     @Override
-    public ConvexArea getSubspaceRegion() {
-        return area;
-    }
+    Split<PlaneConvexSubset> split(Hyperplane<Vector3D> splitter);
 
     /** {@inheritDoc} */
     @Override
-    public Split<PlaneConvexSubset> split(final Hyperplane<Vector3D> splitter) {
-        return splitInternal(splitter, this, (p, r) -> new PlaneConvexSubset(p, (ConvexArea) r));
-    }
-
-    /** Get the unique intersection of this plane subset 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 plane subset.
-     * @param line line to intersect with this plane subset
-     * @return the unique intersection point between the line and this plane subset
-     *      or null if no such point exists.
-     * @see Plane#intersection(Line3D)
+    PlaneConvexSubset.Embedded getEmbedded();
+
+    /** Get the vertices for the convex subset in a counter-clockwise order as viewed looking down the plane
+     * normal. Each vertex in the returned list is unique. If the boundary of the subset is closed, the start
+     * vertex is <em>not</em> repeated at the end of the list.
+     *
+     * <p>It is important to note that, in general, the list of vertices returned by this method
+     * is not sufficient to completely characterize the subset. For example, a simple triangle
+     * has 3 vertices, but an infinite area constructed from two parallel lines and two lines that
+     * intersect between them will also have 3 vertices. It is also possible for non-empty subsets to
+     * contain no vertices at all. For example, a subset with no boundaries (representing the full
+     * plane), a subset with a single boundary (ie, a half-plane), or a subset with two parallel boundaries will
+     * not contain any vertices.</p>
+     * @return the list of vertices for the plane convex subset in a counter-clockwise order as viewed looking
+     *      down the plane normal
      */
-    public Vector3D intersection(final Line3D line) {
-        final Vector3D pt = getPlane().intersection(line);
-        return (pt != null && contains(pt)) ? pt : null;
-    }
+    List<Vector3D> getVertices();
 
-    /** Get the unique intersection of this plane subset with the given line subset. 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 is not contained in both the line subset and plane subset.
-     * @param lineSubset line subset to intersect with
-     * @return the unique intersection point between this plane subset and the argument or
-     *      null if no such point exists.
-     * @see Plane#intersection(Line3D)
+    /** {@inheritDoc}
+     *
+     * <p>This method simply returns a singleton list containing this object.</p>
      */
-    public Vector3D intersection(final LineConvexSubset3D lineSubset) {
-        final Vector3D pt = intersection(lineSubset.getLine());
-        return (pt != null && lineSubset.contains(pt)) ? pt : null;
+    @Override
+    default List<PlaneConvexSubset> toConvex() {
+        return Collections.singletonList(this);
     }
 
-    /** Get the vertices for the plane subset. The vertices lie at the intersections of the
-     * 2D area bounding lines.
-     * @return the vertices for the plane subset
+    /** Interface used to represent plane convex subsets as embedded 2D subspace regions.
      */
-    public List<Vector3D> getVertices() {
-        return getPlane().toSpace(area.getVertices());
+    interface Embedded extends PlaneSubset.Embedded {
+
+        /** {@inheritDoc} */
+        @Override
+        ConvexArea getSubspaceRegion();
     }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PlaneSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PlaneSubset.java
index 945f87d..0583cf0 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PlaneSubset.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PlaneSubset.java
@@ -17,236 +17,87 @@
 package org.apache.commons.geometry.euclidean.threed;
 
 import java.util.List;
-import java.util.Objects;
-import java.util.function.BiFunction;
 
-import org.apache.commons.geometry.core.partitioning.AbstractRegionEmbeddingHyperplaneSubset;
-import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.RegionEmbedding;
 import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
-import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
 import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
-import org.apache.commons.geometry.core.partitioning.Split;
-import org.apache.commons.geometry.core.partitioning.SplitLocation;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.threed.line.Line3D;
-import org.apache.commons.geometry.euclidean.twod.Line;
-import org.apache.commons.geometry.euclidean.twod.Lines;
+import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
 import org.apache.commons.geometry.euclidean.twod.Vector2D;
 
-/** Class representing a subset of points in a 3D Euclidean space. For example, triangles
- * and other polygons in 3D are plane subsets. Instances may be finite or infinite.
+/** Interface representing a subset of points in a plane in Euclidean 3D space. Instances
+ * may represent finite, infinite, convex, non-convex, and/or disjoint regions of the plane.
  */
-public abstract class PlaneSubset
-    extends AbstractRegionEmbeddingHyperplaneSubset<Vector3D, Vector2D, Plane> {
-    /** The plane defining this instance. */
-    private final Plane plane;
+public interface PlaneSubset extends HyperplaneSubset<Vector3D> {
 
-    /** Construct a new instance based on the given plane.
-     * @param plane the plane defining the subset
-     */
-    PlaneSubset(final Plane plane) {
-        this.plane = plane;
-    }
-
-    /** Get the plane that this subset lies on. This method is an alias
-     * for {@link #getHyperplane()}.
-     * @return the plane that this subset lies on
+    /** Get the plane containing this subset. This is equivalent to {@link #getHyperplane()}.
+     * @return the plane containing this subset
      * @see #getHyperplane()
      */
-    public Plane getPlane() {
-        return getHyperplane();
-    }
+    Plane getPlane();
 
     /** {@inheritDoc} */
     @Override
-    public Plane getHyperplane() {
-        return plane;
-    }
+    Plane getHyperplane();
 
     /** {@inheritDoc} */
     @Override
-    public abstract List<PlaneConvexSubset> toConvex();
-
-    /** {@inheritDoc} */
-    @Override
-    public HyperplaneSubset.Builder<Vector3D> builder() {
-        return new Builder(plane);
-    }
-
-    /** Return the object used to perform floating point comparisons, which is the
-     * same object used by the underlying {@link Plane}).
-     * @return precision object used to perform floating point comparisons.
+    List<PlaneConvexSubset> toConvex();
+
+    /** Return a list of triangles representing the same subset region as this instance. An
+     * {@link IllegalStateException} is thrown if the subset has infinite size and therefore
+     * cannot be converted to triangles. If the subset has zero size (is empty), an empty list is
+     * returned.
+     * @return a list of triangles representing the same subset region as this instance
+     * @throws IllegalStateException if the subset has infinite size and therefore cannot
+     *      be converted to triangles
      */
-    public DoublePrecisionContext getPrecision() {
-        return plane.getPrecision();
-    }
+    List<Triangle3D> toTriangles();
 
-    /** {@inheritDoc} */
-    @Override
-    public String toString() {
-        final StringBuilder sb = new StringBuilder();
-        sb.append(getClass().getSimpleName())
-            .append("[plane= ")
-            .append(getPlane())
-            .append(", subspaceRegion= ")
-            .append(getSubspaceRegion())
-            .append(']');
-
-
-        return sb.toString();
-    }
-
-    /** Generic, internal split method. Subclasses should call this from their
-     * {@link #split(Hyperplane)} methods.
-     * @param splitter splitting hyperplane
-     * @param thisInstance a reference to the current instance; this is passed as
-     *      an argument in order to allow it to be a generic type
-     * @param factory function used to create new hyperplane subset instances
-     * @param <T> Plane subset implementation type
-     * @return the result of the split operation
+    /** Get a {@link Bounds3D} object defining an axis-aligned bounding box containing all
+     * vertices for this subset. Null is returned if the subset is infinite or does not
+     * contain any vertices.
+     * @return the bounding box for this instance or null if no valid bounds could be determined
      */
-    protected <T extends PlaneSubset> Split<T> splitInternal(final Hyperplane<Vector3D> splitter,
-                    final T thisInstance, final BiFunction<Plane, HyperplaneBoundedRegion<Vector2D>, T> factory) {
-
-        final Plane thisPlane = thisInstance.getPlane();
-        final Plane splitterPlane = (Plane) splitter;
-        final DoublePrecisionContext precision = thisInstance.getPrecision();
-
-        final Line3D intersection = thisPlane.intersection(splitterPlane);
-        if (intersection == null) {
-            // the planes are parallel or coincident; check which side of
-            // the splitter we lie on
-            final double offset = splitterPlane.offset(thisPlane);
-            final int comp = precision.compare(offset, 0.0);
-
-            if (comp < 0) {
-                return new Split<>(thisInstance, null);
-            } else if (comp > 0) {
-                return new Split<>(null, thisInstance);
-            } else {
-                return new Split<>(null, null);
-            }
-        } else {
-            // the lines intersect; split the subregion
-            final Vector3D intersectionOrigin = intersection.getOrigin();
-            final Vector2D subspaceP1 = thisPlane.toSubspace(intersectionOrigin);
-            final Vector2D subspaceP2 = thisPlane.toSubspace(intersectionOrigin.add(intersection.getDirection()));
-
-            final Line subspaceSplitter = Lines.fromPoints(subspaceP1, subspaceP2, getPrecision());
-
-            final Split<? extends HyperplaneBoundedRegion<Vector2D>> split =
-                    thisInstance.getSubspaceRegion().split(subspaceSplitter);
-            final SplitLocation subspaceSplitLoc = split.getLocation();
-
-            if (SplitLocation.MINUS == subspaceSplitLoc) {
-                return new Split<>(thisInstance, null);
-            } else if (SplitLocation.PLUS == subspaceSplitLoc) {
-                return new Split<>(null, thisInstance);
-            }
-
-            final T minus = (split.getMinus() != null) ? factory.apply(getPlane(), split.getMinus()) : null;
-            final T plus = (split.getPlus() != null) ? factory.apply(getPlane(), split.getPlus()) : null;
-
-            return new Split<>(minus, plus);
-        }
-    }
+    Bounds3D getBounds();
 
-    /** Internal implementation of the {@link HyperplaneSubset.Builder} interface. In cases where only a single
-     * convex subset is given to the builder, this class returns the convex subset instance directly. In all other
-     * cases, an {@link EmbeddedTreePlaneSubset} is used to construct the final subset.
+    /** Return an object containing the plane subset as an embedded 2D subspace region.
+     * @return an object containing the plane subset as an embedded 2D subspace region
      */
-    private static final class Builder implements HyperplaneSubset.Builder<Vector3D> {
-        /** Plane that a subset is being constructed for. */
-        private final Plane plane;
-
-        /** Embedded tree subset. */
-        private EmbeddedTreePlaneSubset treeSubset;
+    PlaneSubset.Embedded getEmbedded();
+
+    /** Get the unique intersection of this plane subset 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 plane subset.
+     * @param line line to intersect with this plane subset
+     * @return the unique intersection point between the line and this plane subset
+     *      or null if no such point exists.
+     * @see Plane#intersection(Line3D)
+     */
+    Vector3D intersection(Line3D line);
+
+    /** Get the unique intersection of this plane subset with the given line subset. 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 is not contained in both the line subset and plane subset.
+     * @param lineSubset line subset to intersect with
+     * @return the unique intersection point between this plane subset and the argument or
+     *      null if no such point exists.
+     * @see Plane#intersection(Line3D)
+     */
+    Vector3D intersection(LineConvexSubset3D lineSubset);
 
-        /** Convex subset added as the first subset to the builder. This is returned directly if
-         * no other subsets are added.
-         */
-        private PlaneConvexSubset convexSubset;
+    /** Interface used to represent plane subsets as embedded 2D subspace regions.
+     */
+    interface Embedded extends RegionEmbedding<Vector3D, Vector2D> {
 
-        /** Create a new subset builder for the given plane.
-         * @param plane plane to build a subset for
+        /** Get the plane embedding the subspace region.
+         * @return the plane embedding the subspace region
          */
-        Builder(final Plane plane) {
-            this.plane = plane;
-        }
+        EmbeddingPlane getPlane();
 
         /** {@inheritDoc} */
         @Override
-        public void add(final HyperplaneSubset<Vector3D> sub) {
-            addInternal(sub);
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public void add(final HyperplaneConvexSubset<Vector3D> sub) {
-            addInternal(sub);
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public PlaneSubset build() {
-            // return the convex subset directly if that was all we were given
-            if (convexSubset != null) {
-                return convexSubset;
-            }
-            return getTreeSubset();
-        }
-
-        /** Internal method for adding hyperplane subsets to this builder.
-         * @param sub the hyperplane subset to add; may be either convex or non-convex
-         */
-        private void addInternal(final HyperplaneSubset<Vector3D> sub) {
-            Objects.requireNonNull(sub, "Hyperplane subset must not be null");
-
-            if (sub instanceof PlaneConvexSubset) {
-                addConvexSubset((PlaneConvexSubset) sub);
-            } else if (sub instanceof EmbeddedTreePlaneSubset) {
-                addTreeSubset((EmbeddedTreePlaneSubset) sub);
-            } else {
-                throw new IllegalArgumentException("Unsupported hyperplane subset type: " + sub.getClass().getName());
-            }
-        }
-
-        /** Add a convex subset to the builder.
-         * @param convex convex subset to add
-         */
-        private void addConvexSubset(final PlaneConvexSubset convex) {
-            Planes.validatePlanesEquivalent(plane, convex.getPlane());
-
-            if (treeSubset == null && convexSubset == null) {
-                convexSubset = convex;
-            } else {
-                getTreeSubset().add(convex);
-            }
-        }
-
-        /** Add an embedded tree subset to the builder.
-         * @param tree embedded tree subset to add
-         */
-        private void addTreeSubset(final EmbeddedTreePlaneSubset tree) {
-            // no need to validate the line here since the add() method does that for us
-            getTreeSubset().add(tree);
-        }
-
-        /** Get the tree subset for the builder, creating it if needed.
-         * @return the tree subset for the builder
-         */
-        private EmbeddedTreePlaneSubset getTreeSubset() {
-            if (treeSubset == null) {
-                treeSubset = new EmbeddedTreePlaneSubset(plane);
-
-                if (convexSubset != null) {
-                    treeSubset.add(convexSubset);
-
-                    convexSubset = null;
-                }
-            }
-
-            return treeSubset;
-        }
+        HyperplaneBoundedRegion<Vector2D> getSubspaceRegion();
     }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Planes.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Planes.java
index fcba291..9da6697 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Planes.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Planes.java
@@ -16,13 +16,23 @@
  */
 package org.apache.commons.geometry.euclidean.threed;
 
+import java.text.MessageFormat;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
+import java.util.function.BiFunction;
 
+import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.line.Line3D;
+import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
 import org.apache.commons.geometry.euclidean.twod.ConvexArea;
+import org.apache.commons.geometry.euclidean.twod.Line;
+import org.apache.commons.geometry.euclidean.twod.Lines;
 import org.apache.commons.geometry.euclidean.twod.Vector2D;
 
 /** Class containing factory methods for constructing {@link Plane} and {@link PlaneSubset} instances.
@@ -33,8 +43,7 @@ public final class Planes {
     private Planes() {
     }
 
-    /**
-     * Build a plane from a point and two (on plane) vectors.
+    /** Build a plane from a point and two (on plane) vectors.
      * @param p the provided point (on plane)
      * @param u u vector (on plane)
      * @param v v vector (on plane)
@@ -42,18 +51,17 @@ public final class Planes {
      * @return a new plane
      * @throws IllegalArgumentException if the norm of the given values is zero, NaN, or infinite.
      */
-    public static Plane fromPointAndPlaneVectors(final Vector3D p, final Vector3D u, final Vector3D v,
+    public static EmbeddingPlane fromPointAndPlaneVectors(final Vector3D p, final Vector3D u, final Vector3D v,
             final DoublePrecisionContext precision) {
-        final Vector3D uNorm = u.normalize();
-        final Vector3D vNorm = uNorm.orthogonal(v);
-        final Vector3D wNorm = uNorm.cross(vNorm).normalize();
+        final Vector3D.Unit uNorm = u.normalize();
+        final Vector3D.Unit vNorm = uNorm.orthogonal(v);
+        final Vector3D.Unit wNorm = uNorm.cross(vNorm).normalize();
         final double originOffset = -p.dot(wNorm);
 
-        return new Plane(uNorm, vNorm, wNorm, originOffset, precision);
+        return new EmbeddingPlane(uNorm, vNorm, wNorm, originOffset, precision);
     }
 
-    /**
-     * Build a plane from a normal.
+    /** Build a plane from a normal.
      * Chooses origin as point on plane.
      * @param normal normal direction to the plane
      * @param precision precision context used to compare floating point values
@@ -64,8 +72,7 @@ public final class Planes {
         return fromPointAndNormal(Vector3D.ZERO, normal, precision);
     }
 
-    /**
-     * Build a plane from a point and a normal.
+    /** Build a plane from a point and a normal.
      *
      * @param p point belonging to the plane
      * @param normal normal direction to the plane
@@ -75,17 +82,13 @@ public final class Planes {
      */
     public static Plane fromPointAndNormal(final Vector3D p, final Vector3D normal,
             final DoublePrecisionContext precision) {
-        final Vector3D w = normal.normalize();
-        final double originOffset = -p.dot(w);
+        final Vector3D.Unit unitNormal = normal.normalize();
+        final double originOffset = -p.dot(unitNormal);
 
-        final Vector3D u = w.orthogonal();
-        final Vector3D v = w.cross(u);
-
-        return new Plane(u, v, w, originOffset, precision);
+        return new Plane(unitNormal, originOffset, precision);
     }
 
-    /**
-     * Build a plane from three points.
+    /** Build a plane from three points.
      * <p>
      * The plane is oriented in the direction of {@code (p2-p1) ^ (p3-p1)}
      * </p>
@@ -116,161 +119,607 @@ public final class Planes {
      *      points do not define a unique plane
      */
     public static Plane fromPoints(final Collection<Vector3D> pts, final DoublePrecisionContext precision) {
+        return new PlaneBuilder(pts, precision).build();
+    }
 
-        if (pts.size() < 3) {
-            throw new IllegalArgumentException("At least 3 points are required to define a plane; " +
-                    "argument contains only " + pts.size() + ".");
+    /** Create a new plane subset from a plane and an embedded convex subspace area.
+     * @param plane embedding plane for the area
+     * @param area area embedded in the plane
+     * @return a new convex sub plane instance
+     */
+    public static PlaneConvexSubset subsetFromConvexArea(final EmbeddingPlane plane, final ConvexArea area) {
+        if (area.isFinite()) {
+            // prefer a vertex-based representation for finite areas
+            final List<Vector3D> vertices = plane.toSpace(area.getVertices());
+
+            return fromConvexPlanarVertices(plane, vertices);
         }
 
-        final Iterator<Vector3D> it = pts.iterator();
+        return new EmbeddedAreaPlaneConvexSubset(plane, area);
+    }
 
-        final Vector3D startPt = it.next();
+    /** Create a new convex polygon from the given sequence of vertices. The vertices must define a unique
+     * plane, meaning that at least 3 unique vertices must be given. The given sequence is assumed to be closed,
+     * ie that an edge exists between the last vertex and the first.
+     * @param pts collection of points defining the convex polygon
+     * @param precision precision context used to compare floating point values
+     * @return a new convex polygon defined by the given sequence of vertices
+     * @throws IllegalArgumentException if fewer than 3 vertices are given or the vertices do not define a
+     *       unique plane
+     * @see #fromPoints(Collection, DoublePrecisionContext)
+     */
+    public static ConvexPolygon3D convexPolygonFromVertices(final Collection<Vector3D> pts,
+            final DoublePrecisionContext precision) {
+        final List<Vector3D> vertices = new ArrayList<>(pts.size());
+        final Plane plane = new PlaneBuilder(pts, precision).buildForConvexPolygon(vertices);
+
+        // make sure that the first point is not repeated at the end
+        final Vector3D firstPt = vertices.get(0);
+        final Vector3D lastPt = vertices.get(vertices.size() - 1);
+        if (firstPt.eq(lastPt, precision)) {
+            vertices.remove(vertices.size() - 1);
+        }
 
-        Vector3D u = null;
-        Vector3D w = null;
+        if (vertices.size() == 3) {
+            return new SimpleTriangle3D(plane, vertices.get(0), vertices.get(1), vertices.get(2));
+        }
+        return new VertexListConvexPolygon3D(plane, vertices);
+    }
 
-        Vector3D currentPt;
-        Vector3D prevPt = startPt;
+    /** Construct a triangle from three vertices. The triangle plane is oriented such that the points
+     * are arranged in a counter-clockwise order when looking down the plane normal.
+     * @param p1 first vertex
+     * @param p2 second vertex
+     * @param p3 third vertex
+     * @param precision precision context used for floating point comparisons
+     * @return a triangle constructed from the three vertices
+     * @throws IllegalArgumentException if the points do not define a unique plane
+     */
+    public static Triangle3D triangleFromVertices(final Vector3D p1, final Vector3D p2, final Vector3D p3,
+            final DoublePrecisionContext precision) {
+        final Plane plane = fromPoints(p1, p2, p3, precision);
+        return new SimpleTriangle3D(plane, p1, p2, p3);
+    }
 
-        Vector3D currentVector = null;
-        Vector3D prevVector = null;
+    /** Construct a list of {@link Triangle3D} instances from a set of vertices and arrays of face indices.
+     * For example, the following code constructs a list of triangles forming a square pyramid.
+     * <pre>
+     * DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-10);
+     *
+     * Vector3D[] vertices = {
+     *      Vector3D.ZERO,
+     *      Vector3D.of(1, 0, 0),
+     *      Vector3D.of(1, 1, 0),
+     *      Vector3D.of(0, 1, 0),
+     *      Vector3D.of(0.5, 0.5, 4)
+     * };
+     *
+     * int[][] faceIndices = {
+     *      {0, 2, 1},
+     *      {0, 3, 2},
+     *      {0, 1, 4},
+     *      {1, 2, 4},
+     *      {2, 3, 4},
+     *      {3, 0, 4}
+     * };
+     *
+     * List&lt;Triangle3D&gt; triangles = Planes.indexedTriangles(vertices, faceIndices, TEST_PRECISION);
+     * </pre>
+     * @param vertices vertices available for use in triangle construction
+     * @param faceIndices array of indices for each triangular face; each entry in the array is an array of
+     *      3 index values into {@code vertices}, defining the 3 vertices that will be used to construct the
+     *      triangle
+     * @param precision precision context used for floating point comparisons
+     * @return a list of triangles constructed from the set of vertices and face indices
+     * @throws IllegalArgumentException if any face index array does not contain exactly 3 elements or a set
+     *      of 3 vertices do not define a plane
+     * @throws IndexOutOfBoundsException if any index into {@code vertices} is out of bounds
+     */
+    public static List<Triangle3D> indexedTriangles(final Vector3D[] vertices, final int[][] faceIndices,
+            final DoublePrecisionContext precision) {
+        return indexedTriangles(Arrays.asList(vertices), faceIndices, precision);
+    }
 
-        Vector3D cross = null;
-        double crossNorm;
-        double crossSumX = 0.0;
-        double crossSumY = 0.0;
-        double crossSumZ = 0.0;
+    /** Construct a list of {@link Triangle3D} instances from a set of vertices and arrays of face indices.
+     * @param vertices vertices available for use in triangle construction
+     * @param faceIndices array of indices for each triangular face; each entry in the array is an array of
+     *      3 index values into {@code vertices}, defining the 3 vertices that will be used to construct the
+     *      triangle
+     * @param precision precision context used for floating point comparisons
+     * @return a list of triangles constructed from the set of vertices and face indices
+     * @throws IllegalArgumentException if any face index array does not contain exactly 3 elements or a set
+     *      of 3 vertices do not define a plane
+     * @throws IndexOutOfBoundsException if any index into {@code vertices} is out of bounds
+     * @see #indexedTriangles(Vector3D[], int[][], DoublePrecisionContext)
+     */
+    public static List<Triangle3D> indexedTriangles(final List<Vector3D> vertices, final int[][] faceIndices,
+            final DoublePrecisionContext precision) {
 
-        boolean nonPlanar = false;
+        final int numFaces = faceIndices.length;
+        final List<Triangle3D> triangles = new ArrayList<>(numFaces);
 
-        while (it.hasNext()) {
-            currentPt = it.next();
+        int[] face;
+        for (int i = 0; i < numFaces; ++i) {
+            face = faceIndices[i];
+            if (face.length != 3) {
+                throw new IllegalArgumentException(MessageFormat.format(
+                        "Invalid number of vertex indices for face at index {0}: expected 3 but found {1}",
+                        i, face.length));
+            }
 
-            if (!currentPt.eq(prevPt, precision)) {
-                currentVector = startPt.vectorTo(currentPt);
+            triangles.add(triangleFromVertices(
+                        vertices.get(face[0]),
+                        vertices.get(face[1]),
+                        vertices.get(face[2]),
+                        precision
+                    ));
+        }
 
-                if (u == null) {
-                    // save the first non-zero vector as our u vector
-                    u = currentVector.normalize();
-                }
-                if (prevVector != null) {
-                    cross = prevVector.cross(currentVector);
-
-                    crossSumX += cross.getX();
-                    crossSumY += cross.getY();
-                    crossSumZ += cross.getZ();
-
-                    crossNorm = cross.norm();
-
-                    if (!precision.eqZero(crossNorm)) {
-                        // the cross product has non-zero magnitude
-                        if (w == null) {
-                            // save the first non-zero cross product as our normal
-                            w = cross.normalize();
-                        } else if (!precision.eq(1.0, Math.abs(w.dot(cross) / crossNorm))) {
-                            // if the normalized dot product is not either +1 or -1, then
-                            // the points are not coplanar
-                            nonPlanar = true;
-                            break;
-                        }
-                    }
-                }
+        return triangles;
+    }
 
-                prevVector = currentVector;
-                prevPt = currentPt;
+    /** Construct a list of {@link ConvexPolygon3D} instances from a set of vertices and arrays of face indices. Each
+     * face must contain at least 3 vertices but the number of vertices per face does not need to be constant.
+     * For example, the following code constructs a list of convex polygons forming a square pyramid.
+     * Note that the first face (the pyramid base) uses a different number of vertices than the other faces.
+     * <pre>
+     * DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-10);
+     *
+     * Vector3D[] vertices = {
+     *      Vector3D.ZERO,
+     *      Vector3D.of(1, 0, 0),
+     *      Vector3D.of(1, 1, 0),
+     *      Vector3D.of(0, 1, 0),
+     *      Vector3D.of(0.5, 0.5, 4)
+     * };
+     *
+     * int[][] faceIndices = {
+     *      {0, 3, 2, 1}, // square base
+     *      {0, 1, 4},
+     *      {1, 2, 4},
+     *      {2, 3, 4},
+     *      {3, 0, 4}
+     * };
+     *
+     * List&lt;ConvexPolygon3D&gt; polygons = Planes.indexedConvexPolygons(vertices, faceIndices, precision);
+     * </pre>
+     * @param vertices vertices available for use in convex polygon construction
+     * @param faceIndices array of indices for each triangular face; each entry in the array is an array of
+     *      at least 3 index values into {@code vertices}, defining the vertices that will be used to construct the
+     *      convex polygon
+     * @param precision precision context used for floating point comparisons
+     * @return a list of convex polygons constructed from the set of vertices and face indices
+     * @throws IllegalArgumentException if any face index array does not contain at least 3 elements or a set
+     *      of vertices do not define a planar convex polygon
+     * @throws IndexOutOfBoundsException if any index into {@code vertices} is out of bounds
+     */
+    public static List<ConvexPolygon3D> indexedConvexPolygons(final Vector3D[] vertices, final int[][] faceIndices,
+            final DoublePrecisionContext precision) {
+        return indexedConvexPolygons(Arrays.asList(vertices), faceIndices, precision);
+    }
+
+    /** Construct a list of {@link ConvexPolygon3D} instances from a set of vertices and arrays of face indices. Each
+     * face must contain at least 3 vertices but the number of vertices per face does not need to be constant.
+     * @param vertices vertices available for use in convex polygon construction
+     * @param faceIndices array of indices for each triangular face; each entry in the array is an array of
+     *      at least 3 index values into {@code vertices}, defining the vertices that will be used to construct the
+     *      convex polygon
+     * @param precision precision context used for floating point comparisons
+     * @return a list of convex polygons constructed from the set of vertices and face indices
+     * @throws IllegalArgumentException if any face index array does not contain at least 3 elements or a set
+     *      of vertices do not define a planar convex polygon
+     * @throws IndexOutOfBoundsException if any index into {@code vertices} is out of bounds
+     * @see #indexedConvexPolygons(Vector3D[], int[][], DoublePrecisionContext)
+     */
+    public static List<ConvexPolygon3D> indexedConvexPolygons(final List<Vector3D> vertices, final int[][] faceIndices,
+            final DoublePrecisionContext precision) {
+        final int numFaces = faceIndices.length;
+        final List<ConvexPolygon3D> polygons = new ArrayList<>(numFaces);
+        final List<Vector3D> faceVertices = new ArrayList<>();
+
+        int[] face;
+        for (int i = 0; i < numFaces; ++i) {
+            face = faceIndices[i];
+            if (face.length < 3) {
+                throw new IllegalArgumentException(MessageFormat.format(
+                        "Invalid number of vertex indices for face at index {0}: required at least 3 but found {1}",
+                        i, face.length));
+            }
+
+            for (int j = 0; j < face.length; ++j) {
+                faceVertices.add(vertices.get(face[j]));
             }
-        }
 
-        if (u == null || w == null || nonPlanar) {
-            throw new IllegalArgumentException("Points do not define a plane: " + pts);
+            polygons.add(convexPolygonFromVertices(
+                        faceVertices,
+                        precision
+                    ));
+
+            faceVertices.clear();
         }
 
-        if (w.dot(Vector3D.of(crossSumX, crossSumY, crossSumZ)) < 0) {
-            w = w.negate();
+        return polygons;
+    }
+
+    /** Get the unique intersection of the plane subset 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 plane subset.
+     * @param planeSubset plane subset to intersect with
+     * @param line line to intersect with this plane subset
+     * @return the unique intersection point between the line and this plane subset
+     *      or null if no such point exists.
+     */
+    static Vector3D intersection(final PlaneSubset planeSubset, final Line3D line) {
+        final Vector3D pt = planeSubset.getPlane().intersection(line);
+        return (pt != null && planeSubset.contains(pt)) ? pt : null;
+    }
+
+    /** Get the unique intersection of the plane subset with the given line subset. 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 is not contained in both the line subset and plane subset.
+     * @param planeSubset plane subset to intersect with
+     * @param lineSubset line subset to intersect with
+     * @return the unique intersection point between this plane subset and the argument or
+     *      null if no such point exists.
+     */
+    static Vector3D intersection(final PlaneSubset planeSubset, final LineConvexSubset3D lineSubset) {
+        final Vector3D pt = intersection(planeSubset, lineSubset.getLine());
+        return (pt != null && lineSubset.contains(pt)) ? pt : null;
+    }
+
+    /** Validate that the actual plane contains the same points as the expected plane, throwing an exception if not.
+     * The subspace orientations of embedding planes are not considered.
+     * @param expected the expected plane
+     * @param actual the actual plane
+     * @throws IllegalArgumentException if the actual plane is not equivalent to the expected plane
+     */
+    static void validatePlanesEquivalent(final Plane expected, final Plane actual) {
+        if (!expected.eq(actual, expected.getPrecision())) {
+            throw new IllegalArgumentException("Arguments do not represent the same plane. Expected " +
+                    expected + " but was " + actual + ".");
         }
+    }
+
+    /** Generic split method that uses performs the split using the subspace region of the plane subset.
+     * @param splitter splitting hyperplane
+     * @param subset the plane subset being split
+     * @param factory function used to create new plane subset instances
+     * @param <T> Plane subset implementation type
+     * @return the result of the split operation
+     */
+    static <T extends PlaneSubset> Split<T> subspaceSplit(final Plane splitter, final T subset,
+            final BiFunction<EmbeddingPlane, HyperplaneBoundedRegion<Vector2D>, T> factory) {
+
+        final EmbeddingPlane thisPlane = subset.getPlane().getEmbedding();
+
+        final Line3D intersection = thisPlane.intersection(splitter);
+        if (intersection == null) {
+            return getNonIntersectingSplitResult(splitter, subset);
+        } else {
+            final EmbeddingPlane embeddingPlane = subset.getPlane().getEmbedding();
+
+            // the lines intersect; split the subregion
+            final Vector3D intersectionOrigin = intersection.getOrigin();
+            final Vector2D subspaceP1 = embeddingPlane.toSubspace(intersectionOrigin);
+            final Vector2D subspaceP2 = embeddingPlane.toSubspace(intersectionOrigin.add(intersection.getDirection()));
+
+            final Line subspaceSplitter = Lines.fromPoints(subspaceP1, subspaceP2, thisPlane.getPrecision());
+
+            final Split<? extends HyperplaneBoundedRegion<Vector2D>> split =
+                    subset.getEmbedded().getSubspaceRegion().split(subspaceSplitter);
+            final SplitLocation subspaceSplitLoc = split.getLocation();
+
+            if (SplitLocation.MINUS == subspaceSplitLoc) {
+                return new Split<>(subset, null);
+            } else if (SplitLocation.PLUS == subspaceSplitLoc) {
+                return new Split<>(null, subset);
+            }
 
-        final Vector3D v = w.cross(u);
-        final double originOffset = -startPt.dot(w);
+            final T minus = (split.getMinus() != null) ? factory.apply(thisPlane, split.getMinus()) : null;
+            final T plus = (split.getPlus() != null) ? factory.apply(thisPlane, split.getPlus()) : null;
 
-        return new Plane(u, v, w, originOffset, precision);
+            return new Split<>(minus, plus);
+        }
     }
 
-    /** Create a new plane subset from a plane and an embedded convex subspace area.
-     * @param plane embedding plane for the area
-     * @param area area embedded in the plane
-     * @return a new convex sub plane instance
+    /** Get a split result for cases where the splitting plane and the plane containing the subset being split
+     * do not intersect. Callers are responsible for ensuring that the planes involved do not actually intersect.
+     * @param <T> Plane subset implementation type
+     * @param splitter plane performing the splitting
+     * @param subset subset being split
+     * @return the split result for the non-intersecting split
      */
-    public static PlaneConvexSubset subsetFromConvexArea(final Plane plane, final ConvexArea area) {
-        return new PlaneConvexSubset(plane, area);
+    private static <T extends PlaneSubset> Split<T> getNonIntersectingSplitResult(
+            final Plane splitter, final T subset) {
+        final Plane plane = subset.getPlane();
+
+        final double offset = splitter.offset(plane);
+        final int comp = plane.getPrecision().compare(offset, 0.0);
+
+        if (comp < 0) {
+            return new Split<>(subset, null);
+        } else if (comp > 0) {
+            return new Split<>(null, subset);
+        } else {
+            return new Split<>(null, null);
+        }
     }
 
-    /** Create a new plane subset from the given sequence of points. The points must define a unique plane,
-     * meaning that at least 3 unique vertices must be given. In contrast with the
-     * {@link #subsetFromVertices(Collection, DoublePrecisionContext)} method, the first point in the sequence
-     * is included at the end if needed, in order to form a closed loop.
-     * @param pts collection of points defining the plane subset
-     * @param precision precision context used to compare floating point values
-     * @return a new plane subset defined by the given sequence of vertices
-     * @throws IllegalArgumentException if fewer than 3 vertices are given or the vertices do not define a
-     *       unique plane
-     * @see #subsetFromVertices(Collection, boolean, DoublePrecisionContext)
-     * @see #fromPoints(Collection, DoublePrecisionContext)
+    /** Construct a convex polygon 3D from a plane and a list of vertices lying in the plane. Callers are
+     * responsible for ensuring that the vertices lie in the plane and define a convex polygon.
+     * @param plane the plane containing the convex polygon
+     * @param vertices vertices defining the closed, convex polygon. The must must contain at least 3 unique
+     *      vertices and should not include the start vertex at the end of the list.
+     * @return a new convex polygon instance
+     * @throws IllegalArgumentException if the size of {@code vertices} if less than 3
      */
-    public static PlaneConvexSubset subsetFromVertexLoop(final Collection<Vector3D> pts,
-            final DoublePrecisionContext precision) {
-        return subsetFromVertices(pts, true, precision);
+    static ConvexPolygon3D fromConvexPlanarVertices(final Plane plane, final List<Vector3D> vertices) {
+        final int size = vertices.size();
+
+        if (size == 3) {
+            return new SimpleTriangle3D(plane, vertices.get(0), vertices.get(1), vertices.get(2));
+        }
+
+        return new VertexListConvexPolygon3D(plane, vertices);
     }
 
-    /** Create a new plane subset from the given sequence of points. The points must define a unique plane,
-     * meaning that at least 3 unique vertices must be given.
-     * @param pts collection of points defining the plane subset
-     * @param precision precision context used to compare floating point values
-     * @return a new plane subset defined by the given sequence of vertices
-     * @throws IllegalArgumentException if fewer than 3 vertices are given or the vertices do not define a
-     *      unique plane
-     * @see #subsetFromVertexLoop(Collection, DoublePrecisionContext)
-     * @see #subsetFromVertices(Collection, boolean, DoublePrecisionContext)
-     * @see #fromPoints(Collection, DoublePrecisionContext)
+    /** Convert a convex polygon defined by a plane and list of points into a triangle fan.
+     * @param plane plane containing the convex polygon
+     * @param vertices vertices defining the convex polygon
+     * @return a triangle fan representing the same area as the convex polygon
+     * @throws IllegalArgumentException if fewer than 3 vertices are given
      */
-    public static PlaneConvexSubset subsetFromVertices(final Collection<Vector3D> pts,
-            final DoublePrecisionContext precision) {
-        return subsetFromVertices(pts, false, precision);
+    static List<Triangle3D> convexPolygonToTriangleFan(final Plane plane, final List<Vector3D> vertices) {
+        final int size = vertices.size();
+        if (size < 3) {
+            throw new IllegalArgumentException("Cannot create triangle fan: 3 or more vertices are required " +
+                    "but found only " + vertices.size());
+        }
+
+        final List<Triangle3D> triangles = new ArrayList<>(size - 2);
+
+        int fanIdx = findBestTriangleFanIndex(vertices);
+        int vertexIdx = (fanIdx + 1) % size;
+
+        Vector3D fanBase = vertices.get(fanIdx);
+        Vector3D vertexA = vertices.get(vertexIdx);
+        Vector3D vertexB;
+
+        vertexIdx = (vertexIdx + 1) % size;
+        while (vertexIdx != fanIdx) {
+            vertexB = vertices.get(vertexIdx);
+
+            // add directly as a triangle instance to avoid computation of the plane again
+            triangles.add(new SimpleTriangle3D(plane, fanBase, vertexA, vertexB));
+
+            vertexA = vertexB;
+            vertexIdx = (vertexIdx + 1) % size;
+        }
+
+        return triangles;
     }
 
-    /** Create a new plane subset from the given sequence of points. The points must define a unique plane,
-     * meaning that at least 3 unique vertices must be given. If {@code close} is true, the vertices are made
-     * into a closed loop by including the start point at the end if needed.
-     * @param pts collection of points
-     * @param close if true, the point sequence will implicitly include the start point again at the end; otherwise
-     *      the vertex sequence is taken as-is
-     * @param precision precision context used to compare floating point values
-     * @return a new plane subset instance
-     * @throws IllegalArgumentException if fewer than 3 vertices are given or the vertices do not define a
-     *      unique plane
-     * @see #subsetFromVertexLoop(Collection, DoublePrecisionContext)
-     * @see #subsetFromVertices(Collection, boolean, DoublePrecisionContext)
-     * @see #fromPoints(Collection, DoublePrecisionContext)
+    /** Find the index of the best vertex to use as the base for a triangle fan split of the convex polygon
+     * defined by the given vertices. The best vertex is the one that forms the largest interior angle in the
+     * polygon since a split at that point will help prevent the creation of very thin triangles.
+     * @param vertices vertices defining the convex polygon; must not be empty
+     * @return the index of the best vertex to use as the base for a triangle fan split of the convex polygon
      */
-    public static PlaneConvexSubset subsetFromVertices(final Collection<Vector3D> pts, final boolean close,
-            final DoublePrecisionContext precision) {
+    private static int findBestTriangleFanIndex(final List<Vector3D> vertices) {
+        final Iterator<Vector3D> it = vertices.iterator();
+
+        Vector3D curPt = it.next();
+        Vector3D nextPt;
 
-        final Plane plane = Planes.fromPoints(pts, precision);
+        Vector3D lastVec = vertices.get(vertices.size() - 1).directionTo(curPt);
+        Vector3D incomingVec = lastVec;
+        Vector3D outgoingVec;
 
-        final List<Vector2D> subspacePts = plane.toSubspace(pts);
-        final ConvexArea area = ConvexArea.fromVertices(subspacePts, close, precision);
+        int bestIdx = 0;
+        double bestDot = -1.0;
 
-        return new PlaneConvexSubset(plane, area);
+        int idx = 0;
+        double dot;
+        while (it.hasNext()) {
+            nextPt = it.next();
+            outgoingVec = curPt.directionTo(nextPt);
+
+            dot = incomingVec.dot(outgoingVec);
+            if (dot > bestDot) {
+                bestIdx = idx;
+                bestDot = dot;
+            }
+
+            curPt = nextPt;
+            incomingVec = outgoingVec;
+
+            ++idx;
+        }
+
+        // handle the last vertex on its own
+        dot = incomingVec.dot(lastVec);
+        if (dot > bestDot) {
+            bestIdx = idx;
+        }
+
+        return bestIdx;
     }
 
-    /** Validate that the actual plane is equivalent to the expected plane, throwing an exception if not.
-     * @param expected the expected plane
-     * @param actual the actual plane
-     * @throws IllegalArgumentException if the actual plane is not equivalent to the expected plane
+    /** Internal helper class used to construct planes from sequences of points. Instances can be also be
+     * configured to collect lists of unique points found during plane construction and validate that the
+     * defined region is convex.
      */
-    static void validatePlanesEquivalent(final Plane expected, final Plane actual) {
-        if (!expected.eq(actual, expected.getPrecision())) {
-            throw new IllegalArgumentException("Arguments do not represent the same plane. Expected " +
-                    expected + " but was " + actual + ".");
+    private static final class PlaneBuilder {
+
+        /** The point sequence to build a plane for. */
+        private final Collection<Vector3D> pts;
+
+        /** Precision context used for floating point comparisons. */
+        private final DoublePrecisionContext precision;
+
+        /** The start point from the point sequence. */
+        private Vector3D startPt;
+
+        /** The previous point from the point sequence. */
+        private Vector3D prevPt;
+
+        /** The previous vector from the point sequence, preceeding from the {@code startPt} to {@code prevPt}. */
+        private Vector3D prevVector;
+
+        /** The computed {@code normal} vector for the plane. */
+        private Vector3D.Unit normal;
+
+        /** The x component of the sum of all cross products from adjacent vectors in the point sequence. */
+        private double crossSumX;
+
+        /** The y component of the sum of all cross products from adjacent vectors in the point sequence. */
+        private double crossSumY;
+
+        /** The z component of the sum of all cross products from adjacent vectors in the point sequence. */
+        private double crossSumZ;
+
+        /** If true, an exception will be thrown if the point sequence is discovered to be non-convex. */
+        private boolean requireConvex = false;
+
+        /** List that unique vertices discovered in the input sequence will be added to. */
+        private List<Vector3D> uniqueVertexOutput;
+
+        /** Construct a new build instance for the given point sequence and precision context.
+         * @param pts point sequence
+         * @param precision precision context used to perform floating point comparisons
+         */
+        PlaneBuilder(final Collection<Vector3D> pts, final DoublePrecisionContext precision) {
+            this.pts = pts;
+            this.precision = precision;
+        }
+
+        /** Build a plane from the configured point sequence.
+         * @return a plane built from the configured point sequence
+         * @throw IllegalArgumentException if the points do not define a plane
+         */
+        Plane build() {
+            if (pts.size() < 3) {
+                throw nonPlanar();
+            }
+
+            pts.forEach(this::processPoint);
+
+            return createPlane();
+        }
+
+        /** Build a plane from the configured point sequence, validating that the points form a convex region
+         * and adding all discovered unique points to the given list.
+         * @param vertexOutput list that unique points discovered in the point sequence will be added to
+         * @return a plane created from the configured point sequence
+         * @throw IllegalArgumentException if the points do not define a plane or the {@code requireConvex}
+         *      flag is true and the points do not define a convex area
+         */
+        Plane buildForConvexPolygon(final List<Vector3D> vertexOutput) {
+            this.requireConvex = true;
+            this.uniqueVertexOutput = vertexOutput;
+
+            Plane plane = build();
+
+            return plane;
+        }
+
+        /** Process a point from the point sequence.
+         * @param pt
+         * @throw IllegalArgumentException if the points do not define a plane or the {@code requireConvex}
+         *      flag is true and the points do not define a convex area
+         */
+        private void processPoint(final Vector3D pt) {
+            if (prevPt == null) {
+                startPt = pt;
+                prevPt = pt;
+
+                if (uniqueVertexOutput != null) {
+                    uniqueVertexOutput.add(pt);
+                }
+
+            } else if (!prevPt.eq(pt, precision)) { // skip duplicate points
+                final Vector3D vec = startPt.vectorTo(pt);
+
+                if (prevVector != null) {
+                    processCrossProduct(prevVector.cross(vec));
+                }
+
+                if (uniqueVertexOutput != null) {
+                    uniqueVertexOutput.add(pt);
+                }
+
+                prevPt = pt;
+                prevVector = vec;
+            }
+        }
+
+        /** Process the computed cross product of two vectors from the input point sequence. The vectors
+         * start at the first point in the sequence and point to adjacent points later in the sequence.
+         * @param cross the cross product of two vectors from the input point sequence
+         * @throw IllegalArgumentException if the points do not define a plane or the {@code requireConvex}
+         *      flag is true and the points do not define a convex area
+         */
+        private void processCrossProduct(final Vector3D cross) {
+            crossSumX += cross.getX();
+            crossSumY += cross.getY();
+            crossSumZ += cross.getZ();
+
+            final double crossNorm = cross.norm();
+
+            if (!precision.eqZero(crossNorm)) {
+                // the cross product has non-zero magnitude
+                if (normal == null) {
+                    // save the first non-zero cross product as our normal
+                    normal = cross.normalize();
+                } else {
+                    final double crossDot = normal.dot(cross) / crossNorm;
+
+                    // check non-planar before non-convex since the former is a more general type
+                    // of issue
+                    if (!precision.eq(1.0, Math.abs(crossDot))) {
+                        throw nonPlanar();
+                    } else if (requireConvex && crossDot < 0) {
+                        throw nonConvex();
+                    }
+                }
+            }
+        }
+
+        /** Construct the plane instance using the value gathered during point processing.
+         * @return the created plane instance
+         * @throw IllegalArgumentException if the point do not define a plane
+         */
+        private Plane createPlane() {
+            if (normal == null) {
+                throw nonPlanar();
+            }
+
+            // flip the normal if needed to match the overall orientation of the points
+            if (normal.dot(Vector3D.of(crossSumX, crossSumY, crossSumZ)) < 0) {
+                normal = normal.negate();
+            }
+
+            // construct the plane
+            final double originOffset = -startPt.dot(normal);
+
+            return new Plane(normal, originOffset, precision);
+        }
+
+        /** Return an exception with a message stating that the points given to this builder do not
+         * define a plane.
+         * @return an exception stating that the points do not define a plane
+         */
+        private IllegalArgumentException nonPlanar() {
+            return new IllegalArgumentException("Points do not define a plane: " + pts);
+        }
+
+        /** Return an exception with a message stating that the points given to this builder do not
+         * define a convex region.
+         * @return an exception stating that the points do not define a plane
+         */
+        private IllegalArgumentException nonConvex() {
+            return new IllegalArgumentException("Points do not define a convex region: " + pts);
         }
     }
 }
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 0739d67..66126fe 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
@@ -22,7 +22,6 @@ import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
 import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
 import org.apache.commons.geometry.core.partitioning.Split;
 import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
@@ -32,7 +31,6 @@ import org.apache.commons.geometry.core.partitioning.bsp.RegionCutBoundary;
 import org.apache.commons.geometry.euclidean.threed.line.Line3D;
 import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
 import org.apache.commons.geometry.euclidean.threed.line.LinecastPoint3D;
-import org.apache.commons.geometry.euclidean.twod.Vector2D;
 
 /** Binary space partitioning (BSP) tree representing a region in three dimensional
  * Euclidean space.
@@ -205,7 +203,7 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
      * @return a new tree instance constructed from the given boundaries
      * @see #from(Iterable, boolean)
      */
-    public static RegionBSPTree3D from(final Iterable<PlaneConvexSubset> boundaries) {
+    public static RegionBSPTree3D from(final Iterable<? extends PlaneConvexSubset> boundaries) {
         return from(boundaries, false);
     }
 
@@ -216,7 +214,7 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
      * @param full if true, the initial tree will contain the entire space
      * @return a new tree instance constructed from the given boundaries
      */
-    public static RegionBSPTree3D from(final Iterable<PlaneConvexSubset> boundaries, final boolean full) {
+    public static RegionBSPTree3D from(final Iterable<? extends PlaneConvexSubset> boundaries, final boolean full) {
         final RegionBSPTree3D tree = new RegionBSPTree3D(full);
         tree.insert(boundaries);
 
@@ -354,29 +352,26 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
          */
         private void addBoundaryContribution(final HyperplaneSubset<Vector3D> boundary, boolean reverse) {
             final PlaneSubset boundarySubset = (PlaneSubset) boundary;
-            final HyperplaneBoundedRegion<Vector2D> base = boundarySubset.getSubspaceRegion();
 
-            final double area = base.getSize();
-            final Vector2D baseBarycenter = base.getBarycenter();
+            final Plane boundaryPlane = boundarySubset.getPlane();
+            final double boundaryArea = boundarySubset.getSize();
+            final Vector3D boundaryBarycenter = boundarySubset.getBarycenter();
 
-            if (Double.isInfinite(area)) {
+            if (Double.isInfinite(boundaryArea)) {
                 volumeSum = Double.POSITIVE_INFINITY;
-            } else if (baseBarycenter != null) {
-                final Plane plane = boundarySubset.getPlane();
-                final Vector3D facetBarycenter = plane.toSpace(base.getBarycenter());
-
+            } else if (boundaryBarycenter != null) {
                 // the volume here is actually 3x the actual pyramid volume; we'll apply
                 // the final scaling all at once at the end
-                double scaledVolume = area * facetBarycenter.dot(plane.getNormal());
+                double scaledVolume = boundaryArea * boundaryBarycenter.dot(boundaryPlane.getNormal());
                 if (reverse) {
                     scaledVolume = -scaledVolume;
                 }
 
                 volumeSum += scaledVolume;
 
-                sumX += scaledVolume * facetBarycenter.getX();
-                sumY += scaledVolume * facetBarycenter.getY();
-                sumZ += scaledVolume * facetBarycenter.getZ();
+                sumX += scaledVolume * boundaryBarycenter.getX();
+                sumY += scaledVolume * boundaryBarycenter.getY();
+                sumZ += scaledVolume * boundaryBarycenter.getZ();
             }
         }
     }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SimpleTriangle3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SimpleTriangle3D.java
new file mode 100644
index 0000000..e5e8b39
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SimpleTriangle3D.java
@@ -0,0 +1,110 @@
+/*
+ * 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.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Transform;
+
+/** Simple implementation of {@link Triangle3D}.
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+final class SimpleTriangle3D extends AbstractConvexPolygon3D implements Triangle3D {
+
+    /** First point in the triangle. */
+    private final Vector3D p1;
+
+    /** Second point in the triangle. */
+    private final Vector3D p2;
+
+    /** Third point in the triangle. */
+    private final Vector3D p3;
+
+    /** Construct a new instance from a plane and 3 points. Callers are responsible for ensuring that
+     * the points lie on the plane and define a triangle. No validation is performed.
+     * @param plane the plane containing the triangle
+     * @param p1 first point in the triangle
+     * @param p2 second point in the triangle
+     * @param p3 third point in the triangle
+     */
+    SimpleTriangle3D(final Plane plane, final Vector3D p1, final Vector3D p2, final Vector3D p3) {
+        super(plane);
+
+        this.p1 = p1;
+        this.p2 = p2;
+        this.p3 = p3;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Vector3D getPoint1() {
+        return p1;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Vector3D getPoint2() {
+        return p2;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Vector3D getPoint3() {
+        return p3;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public List<Vector3D> getVertices() {
+        return Arrays.asList(p1, p2, p3);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getSize() {
+        final Vector3D v1 = p1.vectorTo(p2);
+        final Vector3D v2 = p1.vectorTo(p3);
+        return 0.5 * v1.cross(v2).norm();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Vector3D getBarycenter() {
+        return Vector3D.centroid(p1, p2, p3);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public SimpleTriangle3D reverse() {
+        final Plane rPlane = getPlane().reverse();
+
+        return new SimpleTriangle3D(rPlane, p1, p3, p2); // reverse point ordering
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public SimpleTriangle3D transform(final Transform<Vector3D> transform) {
+        final Plane tPlane = getPlane().transform(transform);
+        final Vector3D t1 = transform.apply(p1);
+        final Vector3D t2 = transform.apply(p2);
+        final Vector3D t3 = transform.apply(p3);
+
+        return new SimpleTriangle3D(tPlane, t1, t2, t3);
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Triangle3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Triangle3D.java
new file mode 100644
index 0000000..bf49a72
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Triangle3D.java
@@ -0,0 +1,59 @@
+/*
+ * 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.Collections;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Transform;
+
+/** Interface representing a triangle in Euclidean 3D space.
+ */
+public interface Triangle3D extends ConvexPolygon3D {
+
+    /** The first point in the triangle.
+     * @return the first point in the triangle
+     */
+    Vector3D getPoint1();
+
+    /** The second point in the triangle.
+     * @return the second point in the triangle
+     */
+    Vector3D getPoint2();
+
+    /** The third point in the triangle.
+     * @return the third point in the triangle
+     */
+    Vector3D getPoint3();
+
+    /** {@inheritDoc} */
+    @Override
+    Triangle3D reverse();
+
+    /** {@inheritDoc} */
+    @Override
+    Triangle3D transform(Transform<Vector3D> transform);
+
+    /** {@inheritDoc}
+    *
+    * <p>This method simply returns a singleton list containing this object.</p>
+    */
+    @Override
+    default List<Triangle3D> toTriangles() {
+        return Collections.singletonList(this);
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java
index 2982ea8..388eb98 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java
@@ -16,7 +16,9 @@
  */
 package org.apache.commons.geometry.euclidean.threed;
 
+import java.util.Arrays;
 import java.util.Comparator;
+import java.util.Iterator;
 import java.util.function.UnaryOperator;
 
 import org.apache.commons.geometry.core.internal.DoubleFunction3N;
@@ -71,20 +73,20 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
         return cmp;
     };
 
-    /** Abscissa (first coordinate value). */
+    /** X coordinate value (abscissa). */
     private final double x;
 
-    /** Ordinate (second coordinate value). */
+    /** Y coordinate value (ordinate). */
     private final double y;
 
-    /** Height (third coordinate value). */
+    /** Z coordinate value (height). */
     private final double z;
 
     /** Simple constructor.
      * Build a vector from its coordinates
-     * @param x abscissa
-     * @param y ordinate
-     * @param z height
+     * @param x x coordinate value
+     * @param y y coordinate value
+     * @param z z coordinate value
      */
     private Vector3D(double x, double y, double z) {
         this.x = x;
@@ -92,22 +94,22 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
         this.z = z;
     }
 
-    /** Returns the abscissa (first coordinate) value of the instance.
-     * @return the abscissa
+    /** Return the x coordinate value (abscissa) of the instance.
+     * @return the x coordinate value
      */
     public double getX() {
         return x;
     }
 
-    /** Returns the ordinate (second coordinate) value of the instance.
-     * @return the ordinate
+    /** Return the y coordinate value (ordinate) of the instance.
+     * @return the y coordinate value
      */
     public double getY() {
         return y;
     }
 
-    /** Returns the height (third coordinate) value of the instance.
-     * @return the height
+    /** Returns the z coordinate value (height) of the instance.
+     * @return the z coordinate value
      */
     public double getZ() {
         return z;
@@ -481,9 +483,9 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
     }
 
     /** Returns a vector with the given coordinate values.
-     * @param x abscissa (first coordinate value)
-     * @param y abscissa (second coordinate value)
-     * @param z height (third coordinate value)
+     * @param x x coordinate value
+     * @param y y coordinate value
+     * @param z z coordinate value
      * @return vector instance
      */
     public static Vector3D of(final double x, final double y, final double z) {
@@ -512,6 +514,149 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
         return SimpleTupleFormat.getDefault().parse(str, Vector3D::new);
     }
 
+    /** Return a vector containing the maximum component values from all input vectors.
+     * @param first first vector
+     * @param more additional vectors
+     * @return a vector containing the maximum component values from all input vectors
+     */
+    public static Vector3D max(final Vector3D first, final Vector3D... more) {
+        return computeMax(first, Arrays.asList(more).iterator());
+    }
+
+    /** Return a vector containing the maximum component values from all input vectors.
+     * @param vecs input vectors
+     * @return a vector containing the maximum component values from all input vectors
+     * @throws IllegalArgumentException if the argument does not contain any vectors
+     */
+    public static Vector3D max(final Iterable<Vector3D> vecs) {
+        final Iterator<Vector3D> it = vecs.iterator();
+        if (!it.hasNext()) {
+            throw new IllegalArgumentException("Cannot compute vector max: no vectors given");
+        }
+
+        return computeMax(it.next(), it);
+    }
+
+    /** Internal method for computing a max vector.
+     * @param first first vector
+     * @param more iterator with additional vectors
+     * @return vector containing the maximum component values of all input vectors
+     */
+    private static Vector3D computeMax(final Vector3D first, final Iterator<Vector3D> more) {
+        double x = first.getX();
+        double y = first.getY();
+        double z = first.getZ();
+
+        Vector3D vec;
+        while (more.hasNext()) {
+            vec = more.next();
+
+            x = Math.max(x, vec.getX());
+            y = Math.max(y, vec.getY());
+            z = Math.max(z, vec.getZ());
+        }
+
+        return Vector3D.of(x, y, z);
+    }
+
+    /** Return a vector containing the minimum component values from all input vectors.
+     * @param first first vector
+     * @param more additional vectors
+     * @return a vector containing the minimum component values from all input vectors
+     */
+    public static Vector3D min(final Vector3D first, final Vector3D... more) {
+        return computeMin(first, Arrays.asList(more).iterator());
+    }
+
+    /** Return a vector containing the minimum component values from all input vectors.
+     * @param vecs input vectors
+     * @return a vector containing the minimum component values from all input vectors
+     * @throws IllegalArgumentException if the argument does not contain any vectors
+     */
+    public static Vector3D min(final Iterable<Vector3D> vecs) {
+        final Iterator<Vector3D> it = vecs.iterator();
+        if (!it.hasNext()) {
+            throw new IllegalArgumentException("Cannot compute vector min: no vectors given");
+        }
+
+        return computeMin(it.next(), it);
+    }
+
+    /** Internal method for computing a min vector.
+     * @param first first vector
+     * @param more iterator with additional vectors
+     * @return vector containing the minimum component values of all input vectors
+     */
+    private static Vector3D computeMin(final Vector3D first, final Iterator<Vector3D> more) {
+        double x = first.getX();
+        double y = first.getY();
+        double z = first.getZ();
+
+        Vector3D vec;
+        while (more.hasNext()) {
+            vec = more.next();
+
+            x = Math.min(x, vec.getX());
+            y = Math.min(y, vec.getY());
+            z = Math.min(z, vec.getZ());
+        }
+
+        return Vector3D.of(x, y, z);
+    }
+
+    /** Compute the centroid of the given points. The centroid is the arithmetic mean position of a set
+     * of points.
+     * @param first first point
+     * @param more additional points
+     * @return the centroid of the given points
+     */
+    public static Vector3D centroid(final Vector3D first, final Vector3D... more) {
+        return computeCentroid(first, Arrays.asList(more).iterator());
+    }
+
+    /** Compute the centroid of the given points. The centroid is the arithmetic mean position of a set
+     * of points.
+     * @param pts the points to compute the centroid of
+     * @return the centroid of the given points
+     * @throws IllegalArgumentException if the argument contains no points
+     */
+    public static Vector3D centroid(final Iterable<Vector3D> pts) {
+        final Iterator<Vector3D> it = pts.iterator();
+        if (!it.hasNext()) {
+            throw new IllegalArgumentException("Cannot compute centroid: no points given");
+        }
+
+        return computeCentroid(it.next(), it);
+    }
+
+    /** Internal method for computing the centroid of a set of points.
+     * @param first first point
+     * @param more iterator with additional points
+     * @return the centroid of the point set
+     */
+    private static Vector3D computeCentroid(final Vector3D first, final Iterator<Vector3D> more) {
+        double x = first.getX();
+        double y = first.getY();
+        double z = first.getZ();
+
+        int count = 1;
+
+        Vector3D pt;
+        while (more.hasNext()) {
+            pt = more.next();
+
+            x += pt.getX();
+            y += pt.getY();
+            z += pt.getZ();
+
+            ++count;
+        }
+
+        double invCount = 1.0 / count;
+
+        return new Vector3D(invCount * x, invCount * y, invCount * z);
+    }
+
     /** Returns a vector consisting of the linear combination of the inputs.
      * <p>
      * A linear combination is the sum of all of the inputs multiplied by their
@@ -615,9 +760,9 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
 
         /** Simple constructor. Callers are responsible for ensuring that the given
          * values represent a normalized vector.
-         * @param x abscissa (first coordinate value)
-         * @param y ordinate (second coordinate value)
-         * @param z height (third coordinate value)
+         * @param x x coordinate value
+         * @param y x coordinate value
+         * @param z x coordinate value
          */
         private Unit(final double x, final double y, final double z) {
             super(x, y, z);
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/VertexListConvexPolygon3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/VertexListConvexPolygon3D.java
new file mode 100644
index 0000000..218561f
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/VertexListConvexPolygon3D.java
@@ -0,0 +1,84 @@
+/*
+ * 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.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.geometry.core.Transform;
+
+/** Internal {@link ConvexPolygon3D} implementation class that uses a list of vertices
+ * to represent the plane subset.
+ */
+final class VertexListConvexPolygon3D extends AbstractConvexPolygon3D implements ConvexPolygon3D {
+
+    /** Vertex loop defining the convex polygon. */
+    private final List<Vector3D> vertices;
+
+    /** Construct a new instance with the given plane and list of vertices. Callers are responsible
+     * for ensuring that the given vertices form a convex subset lying in {@code plane}. The list of
+     * vertices should not contain the duplicated first endpoint. No validation is performed.
+     * @param plane plane containing convex polygon
+     * @param vertices vertices defining the convex polygon
+     * @throw IllegalArgumentException if fewer than 3 vertices are given
+     */
+    VertexListConvexPolygon3D(final Plane plane, final List<Vector3D> vertices) {
+        super(plane);
+
+        // sanity check
+        if (vertices.size() < 3) {
+            throw new IllegalArgumentException("Convex polygon requires at least 3 points; found " + vertices.size());
+        }
+
+        this.vertices = Collections.unmodifiableList(vertices);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public List<Vector3D> getVertices() {
+        return vertices;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public List<Triangle3D> toTriangles() {
+        return Planes.convexPolygonToTriangleFan(getPlane(), vertices);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public VertexListConvexPolygon3D transform(final Transform<Vector3D> transform) {
+        final Plane tPlane = getPlane().transform(transform);
+        final List<Vector3D> tVertices = vertices.stream()
+                .map(transform)
+                .collect(Collectors.toList());
+
+        return new VertexListConvexPolygon3D(tPlane, tVertices);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public VertexListConvexPolygon3D reverse() {
+        final Plane rPlane = getPlane().reverse();
+        final List<Vector3D> rVertices = new ArrayList<>(vertices);
+        Collections.reverse(rVertices);
+
+        return new VertexListConvexPolygon3D(rPlane, rVertices);
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3D.java
index 8d5188c..71e2fa1 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3D.java
@@ -22,6 +22,8 @@ import java.util.List;
 import org.apache.commons.geometry.core.Transform;
 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.Bounds3D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 import org.apache.commons.geometry.euclidean.threed.line.Line3D.SubspaceTransform;
 
@@ -63,6 +65,45 @@ public final class EmbeddedTreeLineSubset3D extends LineSubset3D {
         this.region = region;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public double getSize() {
+        return region.getSize();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public RegionBSPTree1D getSubspaceRegion() {
+        return region;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Vector3D getBarycenter() {
+        final Vector1D subcenter = region.getBarycenter();
+        return subcenter != null ?
+                getLine().toSpace(subcenter) :
+                null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Bounds3D getBounds() {
+        final double min = region.getMin();
+        final double max = region.getMax();
+
+        if (Double.isFinite(min) && Double.isFinite(max)) {
+            final Line3D line = getLine();
+
+            return Bounds3D.builder()
+                    .add(line.toSpace(min))
+                    .add(line.toSpace(max))
+                    .build();
+        }
+
+        return null;
+    }
+
     /** Transform this instance.
      * @param transform the transform to apply
      * @return a new, transformed instance
@@ -97,12 +138,6 @@ public final class EmbeddedTreeLineSubset3D extends LineSubset3D {
 
     /** {@inheritDoc} */
     @Override
-    public RegionBSPTree1D getSubspaceRegion() {
-        return region;
-    }
-
-    /** {@inheritDoc} */
-    @Override
     public String toString() {
         final Line3D line = getLine();
 
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSpanningSubset3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSpanningSubset3D.java
index 373e74b..0c75a9a 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSpanningSubset3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSpanningSubset3D.java
@@ -19,6 +19,7 @@ package org.apache.commons.geometry.euclidean.threed.line;
 import java.text.MessageFormat;
 
 import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.euclidean.threed.Bounds3D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 
 /** Class representing the span of a line in 3D Euclidean space. This is the set of all points
@@ -98,6 +99,24 @@ final class LineSpanningSubset3D extends LineConvexSubset3D {
         return Double.POSITIVE_INFINITY;
     }
 
+    /** {@inheritDoc}
+     *
+     * <p>This method always returns {@code null}.</p>
+     */
+    @Override
+    public Vector3D getBarycenter() {
+        return null; // infinite; no center
+    }
+
+    /** {@inheritDoc}
+    *
+    * <p>This method always returns {@code null}.</p>
+    */
+    @Override
+    public Bounds3D getBounds() {
+        return null; // infinite; no bounds
+    }
+
     /** {@inheritDoc} */
     @Override
     public LineSpanningSubset3D transform(final Transform<Vector3D> transform) {
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSubset3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSubset3D.java
index 0a27e98..0c3c110 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSubset3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSubset3D.java
@@ -17,14 +17,16 @@
 package org.apache.commons.geometry.euclidean.threed.line;
 
 import org.apache.commons.geometry.core.RegionEmbedding;
+import org.apache.commons.geometry.core.Sized;
 import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
 import org.apache.commons.geometry.euclidean.oned.Vector1D;
+import org.apache.commons.geometry.euclidean.threed.Bounds3D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 
 /** Class representing a subset of a line in 3D Euclidean space. For example, line segments,
  * rays, and disjoint combinations of the two are line subsets. Line subsets may be finite or infinite.
  */
-public abstract class LineSubset3D implements RegionEmbedding<Vector3D, Vector1D> {
+public abstract class LineSubset3D implements RegionEmbedding<Vector3D, Vector1D>, Sized {
     /** The line containing this instance. */
     private final Line3D line;
 
@@ -54,6 +56,20 @@ public abstract class LineSubset3D implements RegionEmbedding<Vector3D, Vector1D
         return line.toSubspace(pt);
     }
 
+    /** Get the center of the line subset, or null if the subset is empty of
+     * infinite.
+     * @return the center of the line subset, or null if the subset is empty of
+     *      infinite
+     */
+    public abstract Vector3D getBarycenter();
+
+    /** Get the 3D bounding box of the line subset or null if the subset is
+     * empty or infinite.
+     * @return the 3D bounding box the line subset or null if the subset is
+     *      empty or infinite
+     */
+    public abstract Bounds3D getBounds();
+
     /** Get the subspace region for the instance.
      * @return the subspace region for the instance
      */
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Ray3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Ray3D.java
index 4975d50..1448efd 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Ray3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Ray3D.java
@@ -17,6 +17,7 @@
 package org.apache.commons.geometry.euclidean.threed.line;
 
 import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.euclidean.threed.Bounds3D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 
 /** Class representing a ray in 3D Euclidean space. A ray is a portion of a line consisting of
@@ -107,6 +108,24 @@ public final class Ray3D extends LineConvexSubset3D {
         return Double.POSITIVE_INFINITY;
     }
 
+    /** {@inheritDoc}
+     *
+     * <p>This method always returns {@code null}.</p>
+     */
+    @Override
+    public Vector3D getBarycenter() {
+        return null; // infinite; no center
+    }
+
+   /** {@inheritDoc}
+    *
+    * <p>This method always returns {@code null}.</p>
+    */
+    @Override
+    public Bounds3D getBounds() {
+        return null; // infinite; no bounds
+    }
+
     /** Get the direction of the ray. This is a convenience method for {@code ray.getLine().getDirection()}.
      * @return the direction of the ray
      */
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/ReverseRay3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/ReverseRay3D.java
index 480efec..3fb8aa2 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/ReverseRay3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/ReverseRay3D.java
@@ -17,6 +17,7 @@
 package org.apache.commons.geometry.euclidean.threed.line;
 
 import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.euclidean.threed.Bounds3D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 
 /** Class representing a portion of a line in 3D Euclidean space that starts at infinity and
@@ -108,6 +109,24 @@ public final class ReverseRay3D extends LineConvexSubset3D {
         return end;
     }
 
+    /** {@inheritDoc}
+     *
+     * <p>This method always returns {@code null}.</p>
+     */
+    @Override
+    public Vector3D getBarycenter() {
+        return null; // infinite; no center
+    }
+
+   /** {@inheritDoc}
+    *
+    * <p>This method always returns {@code null}.</p>
+    */
+    @Override
+    public Bounds3D getBounds() {
+        return null; // infinite; no bounds
+    }
+
     /** {@inheritDoc} */
     @Override
     public ReverseRay3D transform(final Transform<Vector3D> transform) {
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Segment3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Segment3D.java
index c351c43..62f0a4b 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Segment3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Segment3D.java
@@ -18,6 +18,7 @@ package org.apache.commons.geometry.euclidean.threed.line;
 
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.threed.Bounds3D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 
 /** Class representing a line segment in 3D Euclidean space. A line segment is a portion of
@@ -108,6 +109,21 @@ public final class Segment3D extends LineConvexSubset3D {
 
     /** {@inheritDoc} */
     @Override
+    public Vector3D getBarycenter() {
+        return getLine().toSpace((0.5 * (end - start)) + start);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Bounds3D getBounds() {
+        return Bounds3D.builder()
+                .add(getStartPoint())
+                .add(getEndPoint())
+                .build();
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public Segment3D transform(final Transform<Vector3D> transform) {
         final Vector3D t1 = transform.apply(getStartPoint());
         final Vector3D t2 = transform.apply(getEndPoint());
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/shape/Parallelepiped.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/shape/Parallelepiped.java
index fc99ec2..a2b5888 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/shape/Parallelepiped.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/shape/Parallelepiped.java
@@ -200,7 +200,7 @@ public final class Parallelepiped extends ConvexVolume {
                 Arrays.asList(pd, pc, pb, pa) :
                 Arrays.asList(pa, pb, pc, pd);
 
-        return Planes.subsetFromVertexLoop(loop, precision);
+        return Planes.convexPolygonFromVertices(loop, precision);
     }
 
     /** Ensure that the given points defining one side of a parallelepiped face are separated by a non-zero
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 45cfe7e..ffca9a1 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
@@ -50,6 +50,15 @@ public interface BoundarySource2D extends BoundarySource<LineConvexSubset>, Line
         return new BoundarySourceLinecaster2D(this).linecastFirst(subset);
     }
 
+    /** Get a {@link Bounds2D} object defining the axis-aligned box containing all vertices
+     * in the boundaries for this instance. Null is returned if any boundaries are infinite
+     * or no vertices were found.
+     * @return the bounding box for this instance or null if no valid bounds could be determined
+     */
+    default Bounds2D getBounds() {
+        return new BoundarySourceBoundsBuilder2D().getBounds(this);
+    }
+
     /** Return a {@link BoundarySource2D} instance containing the given boundaries.
      * @param boundaries line subsets to include in the boundary source
      * @return a boundary source containing the given boundaries
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceBoundsBuilder2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceBoundsBuilder2D.java
new file mode 100644
index 0000000..44f2e6d
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceBoundsBuilder2D.java
@@ -0,0 +1,58 @@
+/*
+ * 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.Iterator;
+import java.util.stream.Stream;
+
+/** Class used to construct {@link Bounds2D} instances representing the min and
+ * max points present in a {@link BoundarySource2D}. The implementation examines
+ * the vertices of each boundary in turn. Null is returned if any boundaries are
+ * infinite or no vertices are present.
+ */
+class BoundarySourceBoundsBuilder2D {
+
+    /** Get a {@link Bounds3D} instance containing all vertices in the given boundary source.
+     * Null is returned if any encountered boundaries were not finite or no vertices were found.
+     * @param src boundary source to compute the bounds of
+     * @return the bounds of the argument or null if no valid bounds could be determined
+     */
+    public Bounds2D getBounds(final BoundarySource2D src) {
+
+        final Bounds2D.Builder builder = Bounds2D.builder();
+
+        try (Stream<LineConvexSubset> stream = src.boundaryStream()) {
+            final Iterator<LineConvexSubset> it = stream.iterator();
+
+            LineConvexSubset boundary;
+            while (it.hasNext()) {
+                boundary = it.next();
+
+                if (boundary.isInfinite()) {
+                    return null; // break out early
+                }
+
+                builder.add(boundary.getStartPoint());
+                builder.add(boundary.getEndPoint());
+            }
+        }
+
+        return builder.containsBounds() ?
+                builder.build() :
+                null;
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecaster2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecaster2D.java
index 69a265c..92b25d2 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecaster2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySourceLinecaster2D.java
@@ -42,20 +42,22 @@ final class BoundarySourceLinecaster2D implements Linecastable2D {
     /** {@inheritDoc} */
     @Override
     public List<LinecastPoint2D> linecast(final LineConvexSubset subset) {
-        final List<LinecastPoint2D> results = getIntersectionStream(subset)
-                .collect(Collectors.toCollection(ArrayList::new));
+        try (Stream<LinecastPoint2D> stream = getIntersectionStream(subset)) {
 
-        LinecastPoint2D.sortAndFilter(results);
+            final List<LinecastPoint2D> results = stream.collect(Collectors.toCollection(ArrayList::new));
+            LinecastPoint2D.sortAndFilter(results);
 
-        return results;
+            return results;
+        }
     }
 
     /** {@inheritDoc} */
     @Override
     public LinecastPoint2D linecastFirst(final LineConvexSubset subset) {
-        return getIntersectionStream(subset)
-                .min(LinecastPoint2D.ABSCISSA_ORDER)
-                .orElse(null);
+        try (Stream<LinecastPoint2D> stream = getIntersectionStream(subset)) {
+            return stream.min(LinecastPoint2D.ABSCISSA_ORDER)
+                    .orElse(null);
+        }
     }
 
     /** Return a stream containing intersections between the boundary source and the
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Bounds2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Bounds2D.java
new file mode 100644
index 0000000..0613d7f
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Bounds2D.java
@@ -0,0 +1,271 @@
+/*
+ * 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.Arrays;
+import java.util.Objects;
+
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.AbstractBounds;
+import org.apache.commons.geometry.euclidean.twod.shape.Parallelogram;
+
+/** Class containing minimum and maximum points defining a 2D axis-aligned bounding box. Unless otherwise
+ * noted, floating point comparisons used in this class are strict, meaning that values are considered equal
+ * if and only if they match exactly.
+ *
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public final class Bounds2D extends AbstractBounds<Vector2D, Bounds2D> {
+
+    /** Simple constructor. Callers are responsible for ensuring the min is not greater than max.
+     * @param min minimum point
+     * @param max maximum point
+     */
+    private Bounds2D(final Vector2D min, final Vector2D max) {
+        super(min, max);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean hasSize(final DoublePrecisionContext precision) {
+        final Vector2D diag = getDiagonal();
+
+        return !precision.eqZero(diag.getX()) &&
+                !precision.eqZero(diag.getY());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean contains(final Vector2D pt) {
+        final double x = pt.getX();
+        final double y = pt.getY();
+
+        final Vector2D min = getMin();
+        final Vector2D max = getMax();
+
+        return x >= min.getX() && x <= max.getX() &&
+                y >= min.getY() && y <= max.getY();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean contains(final Vector2D pt, final DoublePrecisionContext precision) {
+        final double x = pt.getX();
+        final double y = pt.getY();
+
+        final Vector2D min = getMin();
+        final Vector2D max = getMax();
+
+        return precision.gte(x, min.getX()) && precision.lte(x, max.getX()) &&
+                precision.gte(y, min.getY()) && precision.lte(y, max.getY());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean intersects(final Bounds2D other) {
+        final Vector2D aMin = getMin();
+        final Vector2D aMax = getMax();
+
+        final Vector2D bMin = other.getMin();
+        final Vector2D bMax = other.getMax();
+
+        return aMin.getX() <= bMax.getX() && aMax.getX() >= bMin.getX() &&
+                aMin.getY() <= bMax.getY() && aMax.getY() >= bMin.getY();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Bounds2D intersection(final Bounds2D other) {
+        if (intersects(other)) {
+            final Vector2D aMin = getMin();
+            final Vector2D aMax = getMax();
+
+            final Vector2D bMin = other.getMin();
+            final Vector2D bMax = other.getMax();
+
+            // get the max of the mins and the mins of the maxes
+            final double minX = Math.max(aMin.getX(), bMin.getX());
+            final double minY = Math.max(aMin.getY(), bMin.getY());
+
+            final double maxX = Math.min(aMax.getX(), bMax.getX());
+            final double maxY = Math.min(aMax.getY(), bMax.getY());
+
+            return new Bounds2D(
+                    Vector2D.of(minX, minY),
+                    Vector2D.of(maxX, maxY));
+        }
+
+        return null; // no intersection
+    }
+
+    /** {@inheritDoc}
+     *
+     * @throws IllegalArgumentException if any dimension of the bounding box is zero
+     *      as evaluated by the given precision context
+     */
+    @Override
+    public Parallelogram toRegion(final DoublePrecisionContext precision) {
+        return Parallelogram.axisAligned(getMin(), getMax(), precision);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(getMin(), getMax());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        } else if (!(obj instanceof Bounds2D)) {
+            return false;
+        }
+
+        final Bounds2D other = (Bounds2D) obj;
+
+        return getMin().equals(other.getMin()) &&
+                getMax().equals(other.getMax());
+    }
+
+    /** Construct a new instance from the given points.
+     * @param first first point
+     * @param more additional points
+     * @return a new instance containing the min and max coordinates values from the input points
+     */
+    public static Bounds2D from(final Vector2D first, final Vector2D... more) {
+        final Builder builder = builder();
+
+        builder.add(first);
+        builder.addAll(Arrays.asList(more));
+
+        return builder.build();
+    }
+
+    /** Construct a new instance from the given points.
+     * @param points input points
+     * @return a new instance containing the min and max coordinates values from the input points
+     */
+    public static Bounds2D from(final Iterable<Vector2D> points) {
+        final Builder builder = builder();
+
+        builder.addAll(points);
+
+        return builder.build();
+    }
+
+    /** Construct a new {@link Builder} instance for creating bounds.
+     * @return a new builder instance for creating bounds
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /** Class used to construct {@link Bounds2D} instances.
+     */
+    public static final class Builder {
+
+        /** Minimum x coordinate. */
+        private double minX = Double.POSITIVE_INFINITY;
+
+        /** Minimum y coordinate. */
+        private double minY = Double.POSITIVE_INFINITY;
+
+        /** Maximum x coordinate. */
+        private double maxX = Double.NEGATIVE_INFINITY;
+
+        /** Maximum y coordinate. */
+        private double maxY = Double.NEGATIVE_INFINITY;
+
+        /** Private constructor; instantiate through factory method. */
+        private Builder() { }
+
+        /** Add a point to this instance.
+         * @param pt point to add
+         * @return this instance
+         */
+        public Builder add(final Vector2D pt) {
+            final double x = pt.getX();
+            final double y = pt.getY();
+
+            minX = Math.min(x, minX);
+            minY = Math.min(y, minY);
+
+            maxX = Math.max(x, maxX);
+            maxY = Math.max(y, maxY);
+
+            return this;
+        }
+
+        /** Add a collection of points to this instance.
+         * @param pts points to add
+         * @return this instance
+         */
+        public Builder addAll(final Iterable<Vector2D> pts) {
+            for (final Vector2D pt : pts) {
+                add(pt);
+            }
+
+            return this;
+        }
+
+        /** Add the min and max points from the given bounds to this instance.
+         * @param bounds bounds containing the min and max points to add
+         * @return this instance
+         */
+        public Builder add(final Bounds2D bounds) {
+            add(bounds.getMin());
+            add(bounds.getMax());
+
+            return this;
+        }
+
+        /** Return true if this builder contains valid min and max coordinate values.
+         * @return true if this builder contains valid min and max coordinate values
+         */
+        public boolean containsBounds() {
+            return Double.isFinite(minX) &&
+                    Double.isFinite(minY) &&
+                    Double.isFinite(maxX) &&
+                    Double.isFinite(maxY);
+        }
+
+        /** Create a new {@link Bounds2D} instance from the values in this builder.
+         * The builder can continue to be used to create other instances.
+         * @return a new bounds instance
+         * @throws IllegalStateException if no points were given to the builder or any of the computed
+         *      min and max coordinate values are NaN or infinite
+         * @see #containsBounds()
+         */
+        public Bounds2D build() {
+            final Vector2D min = Vector2D.of(minX, minY);
+            final Vector2D max = Vector2D.of(maxX, maxY);
+
+            if (!containsBounds()) {
+                if (Double.isInfinite(minX) && minX > 0 &&
+                        Double.isInfinite(maxX) && maxX < 0) {
+                    throw new IllegalStateException("Cannot construct bounds: no points given");
+                }
+
+                throw new IllegalStateException("Invalid bounds: min= " + min + ", max= " + max);
+            }
+
+            return new Bounds2D(min, max);
+        }
+    }
+}
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 67d0566..0aecbeb 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
@@ -16,12 +16,10 @@
  */
 package org.apache.commons.geometry.euclidean.twod;
 
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import org.apache.commons.geometry.core.Transform;
@@ -39,6 +37,9 @@ import org.apache.commons.geometry.euclidean.twod.path.LinePath;
 public class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vector2D, LineConvexSubset>
     implements BoundarySource2D {
 
+    /** Error message used when attempting to construct a convex polygon from a non-convex line path. */
+    private static final String NON_CONVEX_PATH_ERROR = "Cannot construct convex polygon from non-convex path: ";
+
     /** Instance representing the full 2D plane. */
     private static final ConvexArea FULL = new ConvexArea(Collections.emptyList());
 
@@ -74,12 +75,18 @@ public class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vector2D,
         return InteriorAngleLinePathConnector.connectMinimized(getBoundaries());
     }
 
-    /** Get the vertices defining the area. The vertices lie at the intersections of the
-     * area bounding lines. Note that it is possible for areas to contain no vertices at
-     * all. For example, an area with no boundaries (representing the full space), an area
-     * with a single boundary, or an area one with two parallel boundaries will not contain
-     * vertices.
-     * @return the vertices defining the area
+    /** Get the vertices for the area in a counter-clockwise order. Each vertex in the
+     * returned list is unique. If the boundary of the area is closed, the start vertex is
+     * <em>not</em> repeated at the end of the list.
+     *
+     * <p>It is important to note that, in general, the list of vertices returned by this method
+     * is not sufficient to completely characterize the area. For example, a simple triangle
+     * has 3 vertices, but an infinite area constructed from two parallel lines and two lines that
+     * intersect between them will also have 3 vertices. It is also possible for non-empty areas to
+     * contain no vertices at all. For example, an area with no boundaries (representing the full
+     * space), an area with a single boundary, or an area with two parallel boundaries will not
+     * contain any vertices.</p>
+     * @return the list of vertices for the area in a counter-clockwise order
      */
     public List<Vector2D> getVertices() {
         final List<LinePath> paths = getBoundaryPaths();
@@ -191,89 +198,74 @@ public class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vector2D,
         return FULL;
     }
 
-    /** Construct a convex area by creating lines between adjacent vertices. The vertices must be given in a
-     * counter-clockwise around order the interior of the shape. If the area is intended to be closed, the
-     * beginning point must be repeated at the end of the path.
-     * @param vertices vertices to use to construct the area
-     * @param precision precision context used to create new line instances
-     * @return a convex area constructed using lines between adjacent vertices
-     * @see #fromVertexLoop(Collection, DoublePrecisionContext)
-     * @see #fromVertices(Collection, boolean, DoublePrecisionContext)
+    /** Construct a convex polygon from the given vertices.
+     * @param vertices vertices to use to construct the polygon
+     * @param precision precision context used for floating point comparisons
+     * @return a convex polygon constructed using the given vertices
+     * @throws IllegalStateException if {@code vertices} contains only a single unique vertex
+     * @throws IllegalArgumentException if the constructed path does not define a closed, convex polygon
+     * @see LinePath#fromVertexLoop(Collection, DoublePrecisionContext)
      */
-    public static ConvexArea fromVertices(final Collection<Vector2D> vertices,
+    public static ConvexArea convexPolygonFromVertices(final Collection<Vector2D> vertices,
             final DoublePrecisionContext precision) {
-        return fromVertices(vertices, false, precision);
+        return convexPolygonFromPath(LinePath.fromVertexLoop(vertices, precision));
     }
 
-    /** Construct a convex area by creating lines between adjacent vertices. An implicit line is created between the
-     * last vertex given and the first one. The vertices must be given in a counter-clockwise around order the interior
-     * of the shape.
-     * @param vertices vertices to use to construct the area
-     * @param precision precision context used to create new line instances
-     * @return a convex area constructed using lines between adjacent vertices
-     * @see #fromVertices(Collection, DoublePrecisionContext)
-     * @see #fromVertices(Collection, boolean, DoublePrecisionContext)
+    /** Construct a convex polygon from a line path.
+     * @param path path to construct the polygon from
+     * @return a convex polygon constructed from the given line path
+     * @throws IllegalArgumentException if the path does not define a closed, convex polygon
      */
-    public static ConvexArea fromVertexLoop(final Collection<Vector2D> vertices,
-            final DoublePrecisionContext precision) {
-        return fromVertices(vertices, true, precision);
-    }
+    public static ConvexArea convexPolygonFromPath(final LinePath path) {
+        // ensure that the path is closed; this also ensures that we do not have any infinite elements
+        if (!path.isClosed()) {
+            throw new IllegalArgumentException("Cannot construct convex polygon from unclosed path: " + path);
+        }
 
-    /** Construct a convex area from lines between adjacent vertices.
-     * @param vertices vertices to use to construct the area
-     * @param close if true, an additional line will be created between the last and first vertex
-     * @param precision precision context used to create new line instances
-     * @return a convex area constructed using lines between adjacent vertices
-     */
-    public static ConvexArea fromVertices(final Collection<Vector2D> vertices, boolean close,
-            final DoublePrecisionContext precision) {
-        if (vertices.isEmpty()) {
-            return full();
+        final List<LineConvexSubset> elements = path.getElements();
+        if (elements.size() < 3) {
+            throw new IllegalArgumentException(
+                    "Cannot construct convex polygon from path with less than 3 elements: " + path);
         }
 
-        final List<Line> lines = new ArrayList<>();
+        // go through the elements and validate that the produced area is convex and finite;
+        // use the precision context from the first path element
+        LineConvexSubset startElement = elements.get(0);
+        Vector2D startVertex = startElement.getStartPoint();
+        DoublePrecisionContext precision = startElement.getPrecision();
 
-        Vector2D first = null;
-        Vector2D prev = null;
-        Vector2D cur = null;
+        Vector2D curVector;
+        Vector2D prevVector = null;
 
-        for (final Vector2D vertex : vertices) {
-            cur = vertex;
+        double signedArea;
+        double totalSignedArea = 0.0;
 
-            if (first == null) {
-                first = cur;
-            }
+        LineConvexSubset element;
 
-            if (prev != null && !cur.eq(prev, precision)) {
-                lines.add(Lines.fromPoints(prev, cur, precision));
-            }
+        // we can skip the last element since the we know that the path is closed, meaning that the
+        // last element's end point is equal to our start point
+        for (int i = 0; i < elements.size() - 1; ++i) {
+            element = elements.get(i);
 
-            prev = cur;
-        }
+            curVector = startVertex.vectorTo(element.getEndPoint());
 
-        if (close && cur != null && !cur.eq(first, precision)) {
-            lines.add(Lines.fromPoints(cur, first, precision));
-        }
+            if (prevVector != null) {
+                signedArea = prevVector.signedArea(curVector);
+                if (precision.lt(signedArea, 0.0)) {
+                    throw new IllegalArgumentException(NON_CONVEX_PATH_ERROR + path);
+                }
 
-        if (!vertices.isEmpty() && lines.isEmpty()) {
-            throw new IllegalStateException("Unable to create convex area: only a single unique vertex provided");
-        }
+                totalSignedArea += signedArea;
+            }
 
-        return fromBounds(lines);
-    }
+            prevVector = curVector;
+        }
 
-    /** Construct a convex area from a line subset path. The area represents the intersection of all of the
-     * negative half-spaces of the lines in the path. The boundaries of the returned area may therefore not
-     * match the line subsets in the path.
-     * @param path path to construct the area from
-     * @return a convex area constructed from the line subsets in the given path
-     */
-    public static ConvexArea fromPath(final LinePath path) {
-        final List<Line> lines = path.boundaryStream()
-                .map(LineConvexSubset::getLine)
-                .collect(Collectors.toList());
+        if (precision.lte(totalSignedArea, 0.0)) {
+            throw new IllegalArgumentException(NON_CONVEX_PATH_ERROR + path);
+        }
 
-        return fromBounds(lines);
+        return new ConvexArea(elements);
     }
 
     /** Create a convex area formed by the intersection of the negative half-spaces of the
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/EmbeddedTreeLineSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/EmbeddedTreeLineSubset.java
index ab557dd..8a56114 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/EmbeddedTreeLineSubset.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/EmbeddedTreeLineSubset.java
@@ -21,6 +21,7 @@ import java.util.List;
 
 import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.internal.HyperplaneSubsets;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
 import org.apache.commons.geometry.core.partitioning.Split;
 import org.apache.commons.geometry.core.partitioning.SplitLocation;
@@ -29,6 +30,7 @@ import org.apache.commons.geometry.euclidean.oned.Interval;
 import org.apache.commons.geometry.euclidean.oned.OrientedPoint;
 import org.apache.commons.geometry.euclidean.oned.OrientedPoints;
 import org.apache.commons.geometry.euclidean.oned.RegionBSPTree1D;
+import org.apache.commons.geometry.euclidean.oned.Vector1D;
 import org.apache.commons.geometry.euclidean.twod.Line.SubspaceTransform;
 
 /** Class representing an arbitrary subset of a line using a {@link RegionBSPTree1D}.
@@ -71,6 +73,58 @@ public final class EmbeddedTreeLineSubset extends LineSubset {
 
     /** {@inheritDoc} */
     @Override
+    public boolean isFull() {
+        return region.isFull();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isEmpty() {
+        return region.isEmpty();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getSize() {
+        return region.getSize();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Vector2D getBarycenter() {
+        final Vector1D subspaceBarycenter = region.getBarycenter();
+        if (subspaceBarycenter != null) {
+            return getLine().toSpace(subspaceBarycenter);
+        }
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Bounds2D getBounds() {
+        final double min = region.getMin();
+        final double max = region.getMax();
+
+        if (Double.isFinite(min) && Double.isFinite(max)) {
+            final Line line = getLine();
+
+            return Bounds2D.builder()
+                    .add(line.toSpace(min))
+                    .add(line.toSpace(max))
+                    .build();
+        }
+
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Vector2D closest(final Vector2D pt) {
+        return HyperplaneSubsets.closestToEmbeddedRegion(pt, getLine(), region);
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public EmbeddedTreeLineSubset transform(final Transform<Vector2D> transform) {
         final SubspaceTransform st = getLine().subspaceTransform(transform);
 
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 b7a0d93..7593089 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
@@ -63,7 +63,7 @@ public final class Line extends AbstractHyperplane<Vector2D>
     static final String TO_STRING_FORMAT = "{0}[origin= {1}, direction= {2}]";
 
     /** The direction of the line as a normalized vector. */
-    private final Vector2D direction;
+    private final Vector2D.Unit direction;
 
     /** The distance between the origin and the line. */
     private final double originOffset;
@@ -73,7 +73,7 @@ public final class Line extends AbstractHyperplane<Vector2D>
      * @param originOffset The signed distance between the line and the origin.
      * @param precision Precision context used to compare floating point numbers.
      */
-    Line(final Vector2D direction, final double originOffset, final DoublePrecisionContext precision) {
+    Line(final Vector2D.Unit direction, final double originOffset, final DoublePrecisionContext precision) {
         super(precision);
 
         this.direction = direction;
@@ -93,7 +93,7 @@ public final class Line extends AbstractHyperplane<Vector2D>
     /** Get the direction of the line.
      * @return the direction of the line
      */
-    public Vector2D getDirection() {
+    public Vector2D.Unit getDirection() {
         return direction;
     }
 
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSpanningSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSpanningSubset.java
index ab16ae3..8c504f5 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSpanningSubset.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSpanningSubset.java
@@ -73,6 +73,15 @@ final class LineSpanningSubset extends LineConvexSubset {
     }
 
     /** {@inheritDoc}
+     *
+     * <p>This method always returns {@code null}.</p>
+     */
+    @Override
+    public Vector2D getBarycenter() {
+        return null;
+    }
+
+    /** {@inheritDoc}
     *
     * <p>This method always returns {@code null}.</p>
     */
@@ -108,6 +117,15 @@ final class LineSpanningSubset extends LineConvexSubset {
         return Double.POSITIVE_INFINITY;
     }
 
+    /** {@inheritDoc}
+    *
+    * <p>This method always returns {@code null}.</p>
+    */
+    @Override
+    public Bounds2D getBounds() {
+        return null; // infinite; no bounds
+    }
+
     /** {@inheritDoc} */
     @Override
     public LineSpanningSubset transform(final Transform<Vector2D> transform) {
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSubset.java
index ba05313..2e8a2c3 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSubset.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSubset.java
@@ -19,8 +19,9 @@ package org.apache.commons.geometry.euclidean.twod;
 import java.util.List;
 import java.util.Objects;
 
+import org.apache.commons.geometry.core.RegionEmbedding;
 import org.apache.commons.geometry.core.RegionLocation;
-import org.apache.commons.geometry.core.partitioning.AbstractRegionEmbeddingHyperplaneSubset;
+import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
 import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
 import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
 import org.apache.commons.geometry.core.partitioning.Split;
@@ -30,7 +31,7 @@ import org.apache.commons.geometry.euclidean.oned.Vector1D;
 /** Class representing a subset of points on a line in 2D Euclidean space. For example, line segments
  * and rays are line subsets. Line subsets may be finite or infinite.
  */
-public abstract class LineSubset extends AbstractRegionEmbeddingHyperplaneSubset<Vector2D, Vector1D, Line> {
+public abstract class LineSubset implements HyperplaneSubset<Vector2D>, RegionEmbedding<Vector2D, Vector1D> {
     /** The line containing this instance. */
     private final Line line;
 
@@ -58,8 +59,31 @@ public abstract class LineSubset extends AbstractRegionEmbeddingHyperplaneSubset
 
     /** {@inheritDoc} */
     @Override
+    public Vector1D toSubspace(final Vector2D pt) {
+        return line.toSubspace(pt);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Vector2D toSpace(final Vector1D pt) {
+        return line.toSpace(pt);
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public abstract List<LineConvexSubset> toConvex();
 
+    /** Get a {@link Bounds2D} object defining an axis-aligned bounding box containing all
+     * vertices for this subset. Null is returned if the subset is infinite or does not
+     * contain any vertices.
+     * @return the bounding box for this instance or null if no valid bounds could be determined
+     */
+    public abstract Bounds2D getBounds();
+
+    /** {@inheritDoc} */
+    @Override
+    public abstract HyperplaneBoundedRegion<Vector1D> getSubspaceRegion();
+
     /** {@inheritDoc} */
     @Override
     public HyperplaneSubset.Builder<Vector2D> builder() {
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Lines.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Lines.java
index 1ac2ebe..f8d5251 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Lines.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Lines.java
@@ -57,7 +57,7 @@ public final class Lines {
             throw new IllegalArgumentException("Line direction cannot be zero");
         }
 
-        final Vector2D normalizedDir = dir.normalize();
+        final Vector2D.Unit normalizedDir = dir.normalize();
         final double originOffset = normalizedDir.signedArea(pt);
 
         return new Line(normalizedDir, originOffset, precision);
@@ -102,7 +102,10 @@ public final class Lines {
      * @throws IllegalArgumentException if any coordinate in {@code startPoint} is NaN or infinite
      */
     public static Ray rayFromPoint(final Line line, final Vector2D startPoint) {
-        return rayFromLocation(line, line.abscissa(startPoint));
+        if (!startPoint.isFinite()) {
+            throw new IllegalArgumentException("Invalid ray start point: " + startPoint);
+        }
+        return new Ray(line, line.project(startPoint));
     }
 
     /** Construct a ray starting at the given 1D location on {@code line} and continuing in the
@@ -117,8 +120,7 @@ public final class Lines {
         if (!Double.isFinite(startLocation)) {
             throw new IllegalArgumentException("Invalid ray start location: " + Double.toString(startLocation));
         }
-
-        return new Ray(line, startLocation);
+        return new Ray(line, line.toSpace(startLocation));
     }
 
     /** Construct a reverse ray from an end point and a line direction.
@@ -145,7 +147,10 @@ public final class Lines {
      * @throws IllegalArgumentException if any coordinate in {@code endPoint} is NaN or infinite
      */
     public static ReverseRay reverseRayFromPoint(final Line line, final Vector2D endPoint) {
-        return reverseRayFromLocation(line, line.abscissa(endPoint));
+        if (!endPoint.isFinite()) {
+            throw new IllegalArgumentException("Invalid reverse ray end point: " + endPoint);
+        }
+        return new ReverseRay(line, line.project(endPoint));
     }
 
     /** Construct a reverse ray starting at infinity and continuing in the direction of {@code line}
@@ -161,7 +166,7 @@ public final class Lines {
             throw new IllegalArgumentException("Invalid reverse ray end location: " + Double.toString(endLocation));
         }
 
-        return new ReverseRay(line, endLocation);
+        return new ReverseRay(line, line.toSpace(endLocation));
     }
 
     /** Construct a new line segment from two points. A new line is created for the segment and points in the
@@ -212,7 +217,7 @@ public final class Lines {
             final double min = Math.min(a, b);
             final double max = Math.max(a, b);
 
-            return new Segment(line, min, max);
+            return new Segment(line, line.toSpace(min), line.toSpace(max));
         }
 
         throw new IllegalArgumentException(
@@ -260,13 +265,13 @@ public final class Lines {
         if (hasMin) {
             if (hasMax) {
                 // has both
-                return new Segment(line, min, max);
+                return new Segment(line, line.toSpace(min), line.toSpace(max));
             }
             // min only
-            return new Ray(line, min);
+            return new Ray(line, line.toSpace(min));
         } else if (hasMax) {
             // max only
-            return new ReverseRay(line, max);
+            return new ReverseRay(line, line.toSpace(max));
         } else if (Double.isInfinite(min) && Double.isInfinite(max) && Double.compare(min, max) < 0) {
             return new LineSpanningSubset(line);
         }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Ray.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Ray.java
index c817727..304b8a7 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Ray.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Ray.java
@@ -19,6 +19,7 @@ package org.apache.commons.geometry.euclidean.twod;
 import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 
 /** Class representing a ray in 2D Euclidean space. A ray is a portion of a line consisting of
  * a single start point and extending to infinity along the direction of the line.
@@ -29,26 +30,18 @@ import org.apache.commons.geometry.core.partitioning.Split;
  */
 public final class Ray extends LineConvexSubset {
 
-    /** The start abscissa value for the ray. */
-    private final double start;
+    /** The start point for the ray. */
+    private final Vector2D startPoint;
 
-    /** Construct a ray from a line and a start point. The start point is projected
-     * onto the line. No validation is performed.
+    /** Construct a ray from a line and a start point. Callers are responsible for ensuring that the
+     * given point lies on the line. No validation is performed.
      * @param line line for the ray
      * @param startPoint start point for the ray
      */
     Ray(final Line line, final Vector2D startPoint) {
-        this(line, line.abscissa(startPoint));
-    }
-
-    /** Construct a ray from a line and a 1D start location. No validation is performed.
-     * @param line line for the ray
-     * @param start 1D start location
-     */
-    Ray(final Line line, final double start) {
         super(line);
 
-        this.start = start;
+        this.startPoint = startPoint;
     }
 
     /** {@inheritDoc}
@@ -87,15 +80,24 @@ public final class Ray extends LineConvexSubset {
         return Double.POSITIVE_INFINITY;
     }
 
+    /** {@inheritDoc}
+     *
+     * <p>This method always returns {@code null}.</p>
+     */
+    @Override
+    public Vector2D getBarycenter() {
+        return null;
+    }
+
     @Override
     public Vector2D getStartPoint() {
-        return getLine().toSpace(start);
+        return startPoint;
     }
 
     /** {@inheritDoc} */
     @Override
     public double getSubspaceStart() {
-        return start;
+        return getLine().abscissa(startPoint);
     }
 
     /** {@inheritDoc}
@@ -108,14 +110,23 @@ public final class Ray extends LineConvexSubset {
     }
 
     /** {@inheritDoc}
-    *
-    * <p>This method always returns {@link Double#POSITIVE_INFINITY}.</p>
-    */
+     *
+     * <p>This method always returns {@link Double#POSITIVE_INFINITY}.</p>
+     */
     @Override
     public double getSubspaceEnd() {
         return Double.POSITIVE_INFINITY;
     }
 
+    /** {@inheritDoc}
+     *
+     * <p>This method always returns {@code null}.</p>
+     */
+    @Override
+    public Bounds2D getBounds() {
+        return null; // infinite; no bounds
+    }
+
     /** Get the direction of the ray. This is a convenience method for {@code ray.getLine().getDirection()}.
      * @return the direction of the ray
      */
@@ -135,7 +146,7 @@ public final class Ray extends LineConvexSubset {
     /** {@inheritDoc} */
     @Override
     public ReverseRay reverse() {
-        return new ReverseRay(getLine().reverse(), -start);
+        return new ReverseRay(getLine().reverse(), startPoint);
     }
 
     /** {@inheritDoc} */
@@ -155,7 +166,7 @@ public final class Ray extends LineConvexSubset {
     /** {@inheritDoc} */
     @Override
     RegionLocation classifyAbscissa(double abscissa) {
-        int cmp = getPrecision().compare(abscissa, start);
+        int cmp = getPrecision().compare(abscissa, getSubspaceStart());
         if (cmp > 0) {
             return RegionLocation.INSIDE;
         } else if (cmp == 0) {
@@ -168,27 +179,33 @@ public final class Ray extends LineConvexSubset {
     /** {@inheritDoc} */
     @Override
     double closestAbscissa(double abscissa) {
-        return Math.max(start, abscissa);
+        return Math.max(getSubspaceStart(), abscissa);
     }
 
     /** {@inheritDoc} */
     @Override
     Split<LineConvexSubset> splitOnIntersection(final Line splitter, final Vector2D intersection) {
-
         final Line line = getLine();
-        final double splitAbscissa = line.abscissa(intersection);
+        final DoublePrecisionContext splitterPrecision = splitter.getPrecision();
 
-        LineConvexSubset low = null;
-        LineConvexSubset high = null;
+        final int startCmp = splitterPrecision.compare(splitter.offset(startPoint), 0.0);
+        final boolean pointsTowardPlus = splitter.getOffsetDirection().dot(line.getDirection()) >= 0.0;
 
-        int cmp = getPrecision().compare(splitAbscissa, start);
-        if (cmp > 0) {
-            low = new Segment(line, start, splitAbscissa);
-            high = new Ray(line, splitAbscissa);
-        } else {
-            high = this;
+        if (pointsTowardPlus && startCmp > -1) {
+            // entirely on plus side
+            return new Split<>(null, this);
+        } else if (!pointsTowardPlus && startCmp < 1) {
+            // entirely on minus side
+            return new Split<>(this, null);
         }
 
-        return createSplitResult(splitter, low, high);
+        // we're going to be split
+        final Segment splitSeg = new Segment(line, startPoint, intersection);
+        final Ray splitRay = new Ray(line, intersection);
+
+        final LineConvexSubset minus = (startCmp > 0) ? splitRay : splitSeg;
+        final LineConvexSubset plus = (startCmp > 0) ? splitSeg : splitRay;
+
+        return new Split<>(minus, plus);
     }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ReverseRay.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ReverseRay.java
index 766b912..6abec6a 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ReverseRay.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ReverseRay.java
@@ -19,6 +19,7 @@ package org.apache.commons.geometry.euclidean.twod;
 import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 
 /** Class representing a portion of a line in 2D Euclidean space that starts at infinity and
  * continues in the direction of the line up to a single end point. This is equivalent to taking a
@@ -30,26 +31,18 @@ import org.apache.commons.geometry.core.partitioning.Split;
  */
 public final class ReverseRay extends LineConvexSubset {
 
-    /** The abscissa of the endpoint. */
-    private final double end;
+    /** The end point of the reverse ray. */
+    private final Vector2D endPoint;
 
-    /** Construct a new instance from the given line and end point. The end point is projected onto
-     * the line. No validation is performed.
+    /** Construct a new instance from the given line and end point. Callers are responsible for ensuring that
+     * the given end point lies on the line. No validation is performed.
      * @param line line for the instance
      * @param endPoint end point for the instance
      */
     ReverseRay(final Line line, final Vector2D endPoint) {
-        this(line, line.abscissa(endPoint));
-    }
-
-    /** Construct a new instance from the given line and 1D end location. No validation is performed.
-     * @param line line for the instance
-     * @param end end location for the instance
-     */
-    ReverseRay(final Line line, final double end) {
         super(line);
 
-        this.end = end;
+        this.endPoint = endPoint;
     }
 
     /** {@inheritDoc}
@@ -62,36 +55,45 @@ public final class ReverseRay extends LineConvexSubset {
     }
 
     /** {@inheritDoc}
-    *
-    * <p>This method always returns {@code true}.</p>
-    */
+     *
+     * <p>This method always returns {@code true}.</p>
+     */
     @Override
     public boolean isInfinite() {
         return true;
     }
 
     /** {@inheritDoc}
-    *
-    * <p>This method always returns {@code false}.</p>
-    */
+     *
+     * <p>This method always returns {@code false}.</p>
+     */
     @Override
     public boolean isFinite() {
         return false;
     }
 
     /** {@inheritDoc}
-    *
-    * <p>This method always returns {@link Double#POSITIVE_INFINITY}.</p>
-    */
+     *
+     * <p>This method always returns {@link Double#POSITIVE_INFINITY}.</p>
+     */
     @Override
     public double getSize() {
         return Double.POSITIVE_INFINITY;
     }
 
     /** {@inheritDoc}
-    *
-    * <p>This method always returns {@code null}.</p>
-    */
+     *
+     * <p>This method always returns {@code null}.</p>
+     */
+    @Override
+    public Vector2D getBarycenter() {
+        return null;
+    }
+
+    /** {@inheritDoc}
+     *
+     * <p>This method always returns {@code null}.</p>
+     */
     @Override
     public Vector2D getStartPoint() {
         return null;
@@ -109,13 +111,22 @@ public final class ReverseRay extends LineConvexSubset {
     /** {@inheritDoc} */
     @Override
     public Vector2D getEndPoint() {
-        return getLine().toSpace(end);
+        return endPoint;
     }
 
     /** {@inheritDoc} */
     @Override
     public double getSubspaceEnd() {
-        return end;
+        return getLine().abscissa(endPoint);
+    }
+
+    /** {@inheritDoc}
+    *
+    * <p>This method always returns {@code null}.</p>
+    */
+    @Override
+    public Bounds2D getBounds() {
+        return null; // infinite; no bounds
     }
 
     /** {@inheritDoc} */
@@ -130,7 +141,7 @@ public final class ReverseRay extends LineConvexSubset {
     /** {@inheritDoc} */
     @Override
     public Ray reverse() {
-        return new Ray(getLine().reverse(), -end);
+        return new Ray(getLine().reverse(), endPoint);
     }
 
     /** {@inheritDoc} */
@@ -150,7 +161,7 @@ public final class ReverseRay extends LineConvexSubset {
     /** {@inheritDoc} */
     @Override
     RegionLocation classifyAbscissa(double abscissa) {
-        int cmp = getPrecision().compare(abscissa, end);
+        int cmp = getPrecision().compare(abscissa, getSubspaceEnd());
         if (cmp < 0) {
             return RegionLocation.INSIDE;
         } else if (cmp == 0) {
@@ -163,27 +174,33 @@ public final class ReverseRay extends LineConvexSubset {
     /** {@inheritDoc} */
     @Override
     double closestAbscissa(double abscissa) {
-        return Math.min(end, abscissa);
+        return Math.min(getSubspaceEnd(), abscissa);
     }
 
     /** {@inheritDoc} */
     @Override
     protected Split<LineConvexSubset> splitOnIntersection(final Line splitter, final Vector2D intersection) {
-
         final Line line = getLine();
-        final double splitAbscissa = line.abscissa(intersection);
+        final DoublePrecisionContext splitterPrecision = splitter.getPrecision();
 
-        LineConvexSubset low = null;
-        LineConvexSubset high = null;
+        final int endCmp = splitterPrecision.compare(splitter.offset(endPoint), 0.0);
+        final boolean pointsTowardPlus = splitter.getOffsetDirection().dot(line.getDirection()) >= 0.0;
 
-        int cmp = getPrecision().compare(splitAbscissa, end);
-        if (cmp < 0) {
-            low = new ReverseRay(line, splitAbscissa);
-            high = new Segment(line, splitAbscissa, end);
-        } else {
-            low = this;
+        if (pointsTowardPlus && endCmp < 1) {
+            // entirely on minus side
+            return new Split<>(this, null);
+        } else if (!pointsTowardPlus && endCmp > -1) {
+            // entirely on plus side
+            return new Split<>(null, this);
         }
 
-        return createSplitResult(splitter, low, high);
+        // we're going to be split
+        final Segment splitSeg = new Segment(line, intersection, endPoint);
+        final ReverseRay splitRevRay = new ReverseRay(line, intersection);
+
+        final LineConvexSubset minus = (endCmp > 0) ? splitRevRay : splitSeg;
+        final LineConvexSubset plus = (endCmp > 0) ? splitSeg : splitRevRay;
+
+        return new Split<>(minus, plus);
     }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java
index 0464ccd..8fa77b0 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java
@@ -30,48 +30,39 @@ import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
  */
 public final class Segment extends LineConvexSubset {
 
-    /** Start abscissa for the segment. */
-    private final double start;
+    /** Start point for the segment. */
+    private final Vector2D startPoint;
 
-    /** End abscissa for the segment. */
-    private final double end;
+    /** End point for the segment. */
+    private final Vector2D endPoint;
 
-    /** Construct a new instance from a line and two points on the line. The points are projected onto
-     * the line and must be in order of increasing abscissa. No validation is performed.
+    /** Construct a new instance from a line and two points on the line. Callers are responsible for
+     * ensuring that the given points lie on the line and are in order of increasing abscissa.
+     * No validation is performed.
      * @param line line for the segment
      * @param startPoint segment start point
      * @param endPoint segment end point
      */
     Segment(final Line line, final Vector2D startPoint, final Vector2D endPoint) {
-        this(line, line.abscissa(startPoint), line.abscissa(endPoint));
-    }
-
-    /** Construct a new instance from a line and two abscissa locations on the line.
-     * The abscissa locations must be in increasing order. No validation is performed.
-     * @param line line for the segment
-     * @param start abscissa start location
-     * @param end abscissa end location
-     */
-    Segment(final Line line, final double start, final double end) {
         super(line);
 
-        this.start = start;
-        this.end = end;
+        this.startPoint = startPoint;
+        this.endPoint = endPoint;
     }
 
     /** {@inheritDoc}
-    *
-    * <p>This method always returns {@code false}.</p>
-    */
+     *
+     * <p>This method always returns {@code false}.</p>
+     */
     @Override
     public boolean isFull() {
         return false;
     }
 
     /** {@inheritDoc}
-    *
-    * <p>This method always returns {@code false}.</p>
-    */
+     *
+     * <p>This method always returns {@code false}.</p>
+     */
     @Override
     public boolean isInfinite() {
         return false;
@@ -89,31 +80,46 @@ public final class Segment extends LineConvexSubset {
     /** {@inheritDoc} */
     @Override
     public double getSize() {
-        return end - start;
+        return startPoint.distance(endPoint);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Vector2D getBarycenter() {
+        return startPoint.lerp(endPoint, 0.5);
     }
 
     /** {@inheritDoc} */
     @Override
     public Vector2D getStartPoint() {
-        return getLine().toSpace(start);
+        return startPoint;
     }
 
     /** {@inheritDoc} */
     @Override
     public double getSubspaceStart() {
-        return start;
+        return getLine().abscissa(startPoint);
     }
 
     /** {@inheritDoc} */
     @Override
     public Vector2D getEndPoint() {
-        return getLine().toSpace(end);
+        return endPoint;
     }
 
     /** {@inheritDoc} */
     @Override
     public double getSubspaceEnd() {
-        return end;
+        return getLine().abscissa(endPoint);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Bounds2D getBounds() {
+        return Bounds2D.builder()
+                .add(startPoint)
+                .add(endPoint)
+                .build();
     }
 
     /** {@inheritDoc} */
@@ -130,7 +136,7 @@ public final class Segment extends LineConvexSubset {
     /** {@inheritDoc} */
     @Override
     public Segment reverse() {
-        return new Segment(getLine().reverse(), -end, -start);
+        return new Segment(getLine().reverse(), endPoint, startPoint);
     }
 
     /** {@inheritDoc} */
@@ -151,9 +157,9 @@ public final class Segment extends LineConvexSubset {
     @Override
     RegionLocation classifyAbscissa(final double abscissa) {
         final DoublePrecisionContext precision = getPrecision();
-        int startCmp = precision.compare(abscissa, start);
+        int startCmp = precision.compare(abscissa, getSubspaceStart());
         if (startCmp > 0) {
-            int endCmp = precision.compare(abscissa, end);
+            int endCmp = precision.compare(abscissa, getSubspaceEnd());
             if (endCmp < 0) {
                 return RegionLocation.INSIDE;
             } else if (endCmp == 0) {
@@ -169,32 +175,37 @@ public final class Segment extends LineConvexSubset {
     /** {@inheritDoc} */
     @Override
     double closestAbscissa(final double abscissa) {
-        return Math.max(start, Math.min(end, abscissa));
+        return Math.max(getSubspaceStart(), Math.min(getSubspaceEnd(), abscissa));
     }
 
     /** {@inheritDoc} */
     @Override
     Split<LineConvexSubset> splitOnIntersection(final Line splitter, final Vector2D intersection) {
         final Line line = getLine();
-        final double splitAbscissa = line.abscissa(intersection);
 
-        Segment low = null;
-        Segment high = null;
+        final DoublePrecisionContext splitterPrecision = splitter.getPrecision();
 
-        final DoublePrecisionContext precision = getPrecision();
-        int startCmp = precision.compare(splitAbscissa, start);
-        if (startCmp <= 0) {
-            high = this;
-        }  else {
-            int endCmp = precision.compare(splitAbscissa, end);
-            if (endCmp >= 0) {
-                low = this;
-            } else {
-                low = new Segment(line, start, splitAbscissa);
-                high = new Segment(line, splitAbscissa, end);
-            }
+        final int startCmp = splitterPrecision.compare(splitter.offset(startPoint), 0.0);
+        final int endCmp = splitterPrecision.compare(splitter.offset(endPoint), 0.0);
+
+        if (startCmp == 0 && endCmp == 0) {
+            // the entire segment is directly on the splitter line
+            return new Split<>(null, null);
+        } else if (startCmp < 1 && endCmp < 1) {
+            // the entire segment is on the minus side
+            return new Split<>(this, null);
+        } else if (startCmp > -1 && endCmp > -1) {
+            // the entire segment is on the plus side
+            return new Split<>(null, this);
         }
 
-        return createSplitResult(splitter, low, high);
+        // we need to split the line
+        final Segment startSegment = new Segment(line, startPoint, intersection);
+        final Segment endSegment = new Segment(line, intersection, endPoint);
+
+        final Segment minus = (startCmp > 0) ? endSegment : startSegment;
+        final Segment plus = (startCmp > 0) ? startSegment : endSegment;
+
+        return new Split<>(minus, plus);
     }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java
index 369a90e..2d9df38 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java
@@ -16,7 +16,9 @@
  */
 package org.apache.commons.geometry.euclidean.twod;
 
+import java.util.Arrays;
 import java.util.Comparator;
+import java.util.Iterator;
 import java.util.function.UnaryOperator;
 
 import org.apache.commons.geometry.core.internal.DoubleFunction2N;
@@ -453,6 +455,143 @@ public class Vector2D extends MultiDimensionalEuclideanVector<Vector2D> {
         return SimpleTupleFormat.getDefault().parse(str, Vector2D::new);
     }
 
+    /** Return a vector containing the maximum component values from all input vectors.
+     * @param first first vector
+     * @param more additional vectors
+     * @return a vector containing the maximum component values from all input vectors
+     */
+    public static Vector2D max(final Vector2D first, final Vector2D... more) {
+        return computeMax(first, Arrays.asList(more).iterator());
+    }
+
+    /** Return a vector containing the maximum component values from all input vectors.
+     * @param vecs input vectors
+     * @return a vector containing the maximum component values from all input vectors
+     * @throws IllegalArgumentException if the argument does not contain any vectors
+     */
+    public static Vector2D max(final Iterable<Vector2D> vecs) {
+        final Iterator<Vector2D> it = vecs.iterator();
+        if (!it.hasNext()) {
+            throw new IllegalArgumentException("Cannot compute vector max: no vectors given");
+        }
+
+        return computeMax(it.next(), it);
+    }
+
+    /** Internal method for computing a max vector.
+     * @param first first vector
+     * @param more iterator with additional vectors
+     * @return vector containing the maximum component values of all input vectors
+     */
+    private static Vector2D computeMax(final Vector2D first, final Iterator<Vector2D> more) {
+        double x = first.getX();
+        double y = first.getY();
+
+        Vector2D vec;
+        while (more.hasNext()) {
+            vec = more.next();
+
+            x = Math.max(x, vec.getX());
+            y = Math.max(y, vec.getY());
+        }
+
+        return Vector2D.of(x, y);
+    }
+
+    /** Return a vector containing the minimum component values from all input vectors.
+     * @param first first vector
+     * @param more more vectors
+     * @return a vector containing the minimum component values from all input vectors
+     */
+    public static Vector2D min(final Vector2D first, final Vector2D... more) {
+        return computeMin(first, Arrays.asList(more).iterator());
+    }
+
+    /** Return a vector containing the minimum component values from all input vectors.
+     * @param vecs input vectors
+     * @return a vector containing the minimum component values from all input vectors
+     * @throws IllegalArgumentException if the argument does not contain any vectors
+     */
+    public static Vector2D min(final Iterable<Vector2D> vecs) {
+        final Iterator<Vector2D> it = vecs.iterator();
+        if (!it.hasNext()) {
+            throw new IllegalArgumentException("Cannot compute vector min: no vectors given");
+        }
+
+        return computeMin(it.next(), it);
+    }
+
+    /** Internal method for computing a min vector.
+     * @param first first vector
+     * @param more iterator with additional vectors
+     * @return vector containing the minimum component values of all input vectors
+     */
+    private static Vector2D computeMin(final Vector2D first, final Iterator<Vector2D> more) {
+        double x = first.getX();
+        double y = first.getY();
+
+        Vector2D vec;
+        while (more.hasNext()) {
+            vec = more.next();
+
+            x = Math.min(x, vec.getX());
+            y = Math.min(y, vec.getY());
+        }
+
+        return Vector2D.of(x, y);
+    }
+
+    /** Compute the centroid of the given points. The centroid is the arithmetic mean position of a set
+     * of points.
+     * @param first first point
+     * @param more additional points
+     * @return the centroid of the given points
+     */
+    public static Vector2D centroid(final Vector2D first, final Vector2D... more) {
+        return computeCentroid(first, Arrays.asList(more).iterator());
+    }
+
+    /** Compute the centroid of the given points. The centroid is the arithmetic mean position of a set
+     * of points.
+     * @param pts the points to compute the centroid of
+     * @return the centroid of the given points
+     * @throws IllegalArgumentException if the argument contains no points
+     */
+    public static Vector2D centroid(final Iterable<Vector2D> pts) {
+        final Iterator<Vector2D> it = pts.iterator();
+        if (!it.hasNext()) {
+            throw new IllegalArgumentException("Cannot compute centroid: no points given");
+        }
+
+        return computeCentroid(it.next(), it);
+    }
+
+    /** Internal method for computing the centroid of a set of points.
+     * @param first first point
+     * @param more iterator with additional points
+     * @return the centroid of the point set
+     */
+    private static Vector2D computeCentroid(final Vector2D first, final Iterator<Vector2D> more) {
+        double x = first.getX();
+        double y = first.getY();
+
+        int count = 1;
+
+        Vector2D pt;
+        while (more.hasNext()) {
+            pt = more.next();
+
+            x += pt.getX();
+            y += pt.getY();
+
+            ++count;
+        }
+
+        double invCount = 1.0 / count;
+
+        return new Vector2D(invCount * x, invCount * y);
+    }
+
     /** Returns a vector consisting of the linear combination of the inputs.
      * <p>
      * A linear combination is the sum of all of the inputs multiplied by their
@@ -602,6 +741,12 @@ public class Vector2D extends MultiDimensionalEuclideanVector<Vector2D> {
 
         /** {@inheritDoc} */
         @Override
+        public Vector2D.Unit orthogonal() {
+            return new Unit(-getY(), getX());
+        }
+
+        /** {@inheritDoc} */
+        @Override
         public Vector2D withNorm(final double mag) {
             return multiply(mag);
         }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/path/LinePath.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/path/LinePath.java
index 14c09d0..0b78b40 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/path/LinePath.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/path/LinePath.java
@@ -324,7 +324,7 @@ public class LinePath implements BoundarySource2D, Sized {
                     .append(", ");
             }
 
-            sb.append("vertices= ")
+            sb.append("vertexSequence= ")
                 .append(getVertexSequence());
 
             final LineConvexSubset endElement = getEnd();
@@ -392,6 +392,7 @@ public class LinePath implements BoundarySource2D, Sized {
      * @param precision precision context used to construct the line segment
      *      instances for the path
      * @return new closed path constructed from the given vertices
+     * @throws IllegalStateException if {@code vertices} contains only a single unique vertex
      * @see #fromVertices(Collection, boolean, DoublePrecisionContext)
      */
     public static LinePath fromVertexLoop(final Collection<Vector2D> vertices,
@@ -408,6 +409,7 @@ public class LinePath implements BoundarySource2D, Sized {
      * @param precision precision context used to construct the line segment
      *      instances for the path
      * @return new path constructed from the given vertices
+     * @throws IllegalStateException if {@code vertices} contains only a single unique vertex
      * @see #fromVertices(Collection, boolean, DoublePrecisionContext)
      */
     public static LinePath fromVertices(final Collection<Vector2D> vertices,
@@ -424,6 +426,7 @@ public class LinePath implements BoundarySource2D, Sized {
      * @param precision precision context used to construct the line segment
      *      instances for the path
      * @return new path constructed from the given vertices
+     * @throws IllegalStateException if {@code vertices} contains only a single unique vertex
      */
     public static LinePath fromVertices(final Collection<Vector2D> vertices,
             final boolean close, final DoublePrecisionContext precision) {
@@ -666,6 +669,7 @@ public class LinePath implements BoundarySource2D, Sized {
         /** Close the current path and build a new {@link LinePath} instance.  This method is equivalent
          * to {@code builder.build(true)}.
          * @return new closed path instance
+         * @throws IllegalStateException if the builder was given only a single unique vertex
          */
         public LinePath close() {
             return build(true);
@@ -674,6 +678,7 @@ public class LinePath implements BoundarySource2D, Sized {
         /** Build a {@link LinePath} instance from the configured path. This method is equivalent
          * to {@code builder.build(false)}.
          * @return new path instance
+         * @throws IllegalStateException if the builder was given only a single unique vertex
          */
         public LinePath build() {
             return build(false);
@@ -683,6 +688,7 @@ public class LinePath implements BoundarySource2D, Sized {
          * @param close if true, the path will be closed by adding an end point equivalent to the
          *      start point
          * @return new path instance
+         * @throws IllegalStateException if the builder was given only a single unique vertex
          */
         public LinePath build(final boolean close) {
             if (close) {
@@ -711,7 +717,7 @@ public class LinePath implements BoundarySource2D, Sized {
 
             if (result.isEmpty() && startVertex != null) {
                 throw new IllegalStateException(
-                        MessageFormat.format("Unable to create line path; only a single vertex provided: {0} ",
+                        MessageFormat.format("Unable to create line path; only a single unique vertex provided: {0} ",
                                 startVertex));
             }
 
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 9476733..0051704 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
@@ -29,6 +29,7 @@ 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.ConvexPolygon3D;
 import org.apache.commons.geometry.euclidean.threed.Plane;
 import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
 import org.apache.commons.geometry.euclidean.threed.Planes;
@@ -407,27 +408,27 @@ public class DocumentationExamplesTest {
     public void testRegionBSPTree3DExample() {
         DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
 
-        // create the faces of a pyrmaid with a square base and its apex pointing along the
+        // create the faces of a pyramid with a square base and its apex pointing along the
         // positive z axis
-        Vector3D a1 = Vector3D.Unit.PLUS_Z;
-        Vector3D b1 = Vector3D.of(0.5, 0.5, 0.0);
-        Vector3D b2 = Vector3D.of(0.5, -0.5, 0.0);
-        Vector3D b3 = Vector3D.of(-0.5, -0.5, 0.0);
-        Vector3D b4 = Vector3D.of(-0.5, 0.5, 0.0);
-
-        Vector3D[][] faceIndices = {
-            {b1, a1, b2},
-            {b2, a1, b3},
-            {b3, a1, b4},
-            {b4, a1, b1},
-            {b1, b2, b3, b4}
+        Vector3D[] vertices = {
+            Vector3D.Unit.PLUS_Z,
+            Vector3D.of(0.5, 0.5, 0.0),
+            Vector3D.of(0.5, -0.5, 0.0),
+            Vector3D.of(-0.5, -0.5, 0.0),
+            Vector3D.of(-0.5, 0.5, 0.0)
         };
 
-        // convert the vertices to convex subplanes and insert into a bsp tree
-        RegionBSPTree3D tree = RegionBSPTree3D.empty();
-        Arrays.stream(faceIndices)
-            .map(vertices -> Planes.subsetFromVertexLoop(Arrays.asList(vertices), precision))
-            .forEach(tree::insert);
+        int[][] faceIndices = {
+            {1, 0, 2},
+            {2, 0, 3},
+            {3, 0, 4},
+            {4, 0, 1},
+            {1, 2, 3, 4}
+        };
+
+        // convert the vertices and faces to convex polygons and use to construct a BSP tree
+        List<ConvexPolygon3D> faces = Planes.indexedConvexPolygons(vertices, faceIndices, precision);
+        RegionBSPTree3D tree = RegionBSPTree3D.from(faces);
 
         // split the region through its barycenter along a diagonal of the base
         Plane cutter = Planes.fromPointAndNormal(tree.getBarycenter(), Vector3D.Unit.from(1, 1, 0), precision);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java
index 99d8f0e..33fa04c 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java
@@ -16,9 +16,12 @@
  */
 package org.apache.commons.geometry.euclidean;
 
+import java.util.List;
+
 import org.apache.commons.geometry.core.Region;
 import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.oned.Vector1D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 import org.apache.commons.geometry.euclidean.twod.Vector2D;
@@ -186,6 +189,46 @@ public final class EuclideanTestUtils {
     }
 
     /**
+     * Assert that the given lists represent equivalent vertex loops. The loops must contain the same sequence
+     * of vertices but do not need to start at the same point.
+     * @param expected
+     * @param actual
+     * @param precision
+     */
+    public static void assertVertexLoopSequence(List<Vector3D> expected, List<Vector3D> actual,
+            DoublePrecisionContext precision) {
+        Assert.assertEquals("Vertex sequences have different sizes", expected.size(), actual.size());
+
+        if (expected.size() > 0) {
+
+            int offset = -1;
+            Vector3D start = expected.get(0);
+            for (int i = 0; i < actual.size(); ++i) {
+                if (actual.get(i).eq(start, precision)) {
+                    offset = i;
+                    break;
+                }
+            }
+
+            if (offset < 0) {
+                Assert.fail("Vertex loops do not share any points: expected " + expected + " but was " + actual);
+            }
+
+            Vector3D expectedVertex;
+            Vector3D actualVertex;
+            for (int i = 0; i < expected.size(); ++i) {
+                expectedVertex = expected.get(i);
+                actualVertex = actual.get((i + offset) % actual.size());
+
+                if (!expectedVertex.eq(actualVertex, precision)) {
+                    Assert.fail("Unexpected vertex at index " + i + ": expected " + expectedVertex +
+                            " but was " + actualVertex);
+                }
+            }
+        }
+    }
+
+    /**
      * Asserts that the given value is negative infinity..
      *
      * @param value
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 e9b19f9..249d89b 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
@@ -424,7 +424,7 @@ public class OrientedPointTest {
     @Test
     public void testSubset_simpleMethods() {
         // arrange
-        OrientedPoint pt = OrientedPoints.createPositiveFacing(0, TEST_PRECISION);
+        OrientedPoint pt = OrientedPoints.createPositiveFacing(2, TEST_PRECISION);
         HyperplaneConvexSubset<Vector1D> sub = pt.span();
 
         // act/assert
@@ -434,6 +434,7 @@ public class OrientedPointTest {
         Assert.assertFalse(sub.isInfinite());
         Assert.assertTrue(sub.isFinite());
         Assert.assertEquals(0.0, sub.getSize(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(2), sub.getBarycenter(), TEST_EPS);
 
         List<? extends HyperplaneConvexSubset<Vector1D>> list = sub.toConvex();
         Assert.assertEquals(1, list.size());
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlaneSubsetTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AbstractPlaneSubsetTest.java
similarity index 77%
rename from commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlaneSubsetTest.java
rename to commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AbstractPlaneSubsetTest.java
index 12308fe..846a95e 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlaneSubsetTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AbstractPlaneSubsetTest.java
@@ -23,25 +23,25 @@ import org.apache.commons.geometry.core.GeometryTestUtils;
 import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
-import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
 import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
 import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
 import org.apache.commons.geometry.core.partitioning.Split;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.twod.ConvexArea;
+import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
 import org.apache.commons.geometry.euclidean.twod.Vector2D;
 import org.junit.Assert;
 import org.junit.Test;
 
-public class PlaneSubsetTest {
+public class AbstractPlaneSubsetTest {
 
     private static final double TEST_EPS = 1e-10;
 
     private static final DoublePrecisionContext TEST_PRECISION =
             new EpsilonDoublePrecisionContext(TEST_EPS);
 
-    private static final Plane XY_PLANE = Planes.fromPointAndPlaneVectors(Vector3D.ZERO,
+    private static final EmbeddingPlane XY_PLANE = Planes.fromPointAndPlaneVectors(Vector3D.ZERO,
             Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
 
     @Test
@@ -67,7 +67,7 @@ public class PlaneSubsetTest {
     @Test
     public void testBuilder_addSingleConvex_returnsSameInstance() {
         // arrange
-        PlaneConvexSubset convex = Planes.subsetFromVertexLoop(Arrays.asList(
+        PlaneConvexSubset convex = Planes.convexPolygonFromVertices(Arrays.asList(
                     Vector3D.ZERO,
                     Vector3D.of(1, 0, 0),
                     Vector3D.of(0, 1, 0)
@@ -87,7 +87,7 @@ public class PlaneSubsetTest {
     @Test
     public void testBuilder_addSingleTreeSubset() {
         // arrange
-        ConvexArea area = ConvexArea.fromVertexLoop(Arrays.asList(
+        ConvexArea area = ConvexArea.convexPolygonFromVertices(Arrays.asList(
                     Vector2D.ZERO,
                     Vector2D.of(1, 0),
                     Vector2D.of(0, 1)
@@ -122,17 +122,17 @@ public class PlaneSubsetTest {
     @Test
     public void testBuilder_addMixed_convexFirst() {
         // arrange
-        Plane mainPlane = Planes.fromPointAndPlaneVectors(
+        EmbeddingPlane mainPlane = Planes.fromPointAndPlaneVectors(
                 Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
 
-        ConvexArea a = ConvexArea.fromVertexLoop(
+        ConvexArea a = ConvexArea.convexPolygonFromVertices(
                 Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(1, 1).normalize()), TEST_PRECISION);
-        ConvexArea b = ConvexArea.fromVertexLoop(
+        ConvexArea b = ConvexArea.convexPolygonFromVertices(
                 Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 1).normalize(), Vector2D.of(0, 1)), TEST_PRECISION);
-        ConvexArea c = ConvexArea.fromVertexLoop(
+        ConvexArea c = ConvexArea.convexPolygonFromVertices(
                 Arrays.asList(Vector2D.Unit.PLUS_X, Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y), TEST_PRECISION);
 
-        Plane closePlane = Planes.fromPointAndPlaneVectors(
+        EmbeddingPlane closePlane = Planes.fromPointAndPlaneVectors(
                 Vector3D.of(1e-16, 0, 1), Vector3D.of(1, 1e-16, 0), Vector3D.Unit.PLUS_Y, TEST_PRECISION);
 
         // act
@@ -161,18 +161,18 @@ public class PlaneSubsetTest {
 
     @Test
     public void testBuilder_addMixed_treeSubsetFirst() {
-     // arrange
-        Plane mainPlane = Planes.fromPointAndPlaneVectors(
+        // arrange
+        EmbeddingPlane mainPlane = Planes.fromPointAndPlaneVectors(
                 Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
 
-        ConvexArea a = ConvexArea.fromVertexLoop(
+        ConvexArea a = ConvexArea.convexPolygonFromVertices(
                 Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(1, 1).normalize()), TEST_PRECISION);
-        ConvexArea b = ConvexArea.fromVertexLoop(
+        ConvexArea b = ConvexArea.convexPolygonFromVertices(
                 Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 1).normalize(), Vector2D.of(0, 1)), TEST_PRECISION);
-        ConvexArea c = ConvexArea.fromVertexLoop(
+        ConvexArea c = ConvexArea.convexPolygonFromVertices(
                 Arrays.asList(Vector2D.Unit.PLUS_X, Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y), TEST_PRECISION);
 
-        Plane closePlane = Planes.fromPointAndPlaneVectors(
+        EmbeddingPlane closePlane = Planes.fromPointAndPlaneVectors(
                 Vector3D.of(1e-16, 0, 1), Vector3D.of(1, 1e-16, 0), Vector3D.Unit.PLUS_Y, TEST_PRECISION);
 
         // act
@@ -200,7 +200,7 @@ public class PlaneSubsetTest {
     }
 
     @Test
-    public void testBuilder_nullArguments() {
+    public void testBuilder_add_nullArguments() {
         // arrange
         HyperplaneSubset.Builder<Vector3D> builder = XY_PLANE.span().builder();
 
@@ -215,9 +215,9 @@ public class PlaneSubsetTest {
     }
 
     @Test
-    public void testBuilder_argumentsFromDifferentPlanes() {
+    public void testBuilder_add_argumentsFromDifferentPlanes() {
         // arrange
-        PlaneConvexSubset convex = Planes.subsetFromVertexLoop(Arrays.asList(
+        ConvexPolygon3D convex = Planes.convexPolygonFromVertices(Arrays.asList(
                 Vector3D.ZERO,
                 Vector3D.of(1, 0, 1),
                 Vector3D.of(0, 1, 1)
@@ -225,24 +225,27 @@ public class PlaneSubsetTest {
 
         HyperplaneSubset.Builder<Vector3D> builder = XY_PLANE.span().builder();
 
+        RegionBSPTree2D tree = RegionBSPTree2D.empty();
+        tree.add(convex.getEmbedded().getSubspaceRegion());
+
         // act/assert
         GeometryTestUtils.assertThrows(() -> {
             builder.add(convex);
         }, IllegalArgumentException.class);
 
         GeometryTestUtils.assertThrows(() -> {
-            builder.add(new EmbeddedTreePlaneSubset(convex.getPlane(), convex.getSubspaceRegion().toTree()));
+            builder.add(new EmbeddedTreePlaneSubset(convex.getPlane().getEmbedding(), tree));
         }, IllegalArgumentException.class);
     }
 
     @Test
-    public void testBuilder_addUnknownType() {
+    public void testBuilder_add_addUnknownType() {
         // arrange
         HyperplaneSubset.Builder<Vector3D> builder = XY_PLANE.span().builder();
 
         // act/assert
         GeometryTestUtils.assertThrows(() -> {
-            builder.add(new StubSubPlane(Planes.fromNormal(Vector3D.Unit.PLUS_Y, TEST_PRECISION)));
+            builder.add(new StubPlaneSubset(XY_PLANE));
         }, IllegalArgumentException.class);
     }
 
@@ -252,14 +255,46 @@ public class PlaneSubsetTest {
         }
     }
 
-    private static class StubSubPlane extends PlaneSubset implements HyperplaneSubset<Vector3D> {
+    private static class StubPlaneSubset extends AbstractPlaneSubset {
+
+        private final Plane plane;
 
-        StubSubPlane(Plane plane) {
-            super(plane);
+        StubPlaneSubset(final Plane plane) {
+            this.plane = plane;
         }
 
         @Override
-        public Split<? extends HyperplaneSubset<Vector3D>> split(Hyperplane<Vector3D> splitter) {
+        public Plane getPlane() {
+            return plane;
+        }
+
+        @Override
+        public List<PlaneConvexSubset> toConvex() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public List<Triangle3D> toTriangles() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean isFull() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean isEmpty() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public RegionLocation classify(Vector3D pt) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Vector3D closest(Vector3D pt) {
             throw new UnsupportedOperationException();
         }
 
@@ -269,12 +304,27 @@ public class PlaneSubsetTest {
         }
 
         @Override
-        public List<PlaneConvexSubset> toConvex() {
+        public Split<? extends HyperplaneSubset<Vector3D>> split(Hyperplane<Vector3D> splitter) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public double getSize() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Vector3D getBarycenter() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public PlaneSubset.Embedded getEmbedded() {
             throw new UnsupportedOperationException();
         }
 
         @Override
-        public HyperplaneBoundedRegion<Vector2D> getSubspaceRegion() {
+        public Bounds3D getBounds() {
             throw new UnsupportedOperationException();
         }
     }
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
index 55cc960..7c22343 100644
--- 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
@@ -36,9 +36,9 @@ public class BoundarySource3DTest {
     @Test
     public void testToTree() {
         // act
-        PlaneConvexSubset a = Planes.subsetFromVertexLoop(
+        PlaneConvexSubset a = Planes.convexPolygonFromVertices(
                 Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION);
-        PlaneConvexSubset b = Planes.subsetFromVertexLoop(
+        PlaneConvexSubset b = Planes.convexPolygonFromVertices(
                 Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_Z), TEST_PRECISION);
 
         BoundarySource3D src = BoundarySource3D.from(a, b);
@@ -79,9 +79,9 @@ public class BoundarySource3DTest {
     @Test
     public void testFrom_varargs() {
         // act
-        PlaneConvexSubset a = Planes.subsetFromVertexLoop(
+        PlaneConvexSubset a = Planes.convexPolygonFromVertices(
                 Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION);
-        PlaneConvexSubset b = Planes.subsetFromVertexLoop(
+        PlaneConvexSubset b = Planes.convexPolygonFromVertices(
                 Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_Z), TEST_PRECISION);
 
         BoundarySource3D src = BoundarySource3D.from(a, b);
@@ -110,9 +110,9 @@ public class BoundarySource3DTest {
     @Test
     public void testFrom_list() {
         // act
-        PlaneConvexSubset a = Planes.subsetFromVertexLoop(
+        PlaneConvexSubset a = Planes.convexPolygonFromVertices(
                 Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION);
-        PlaneConvexSubset b = Planes.subsetFromVertexLoop(
+        PlaneConvexSubset b = Planes.convexPolygonFromVertices(
                 Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_Z), TEST_PRECISION);
 
         List<PlaneConvexSubset> input = new ArrayList<>();
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceBoundsBuilder3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceBoundsBuilder3DTest.java
new file mode 100644
index 0000000..30f86d5
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceBoundsBuilder3DTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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 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 BoundarySourceBoundsBuilder3DTest {
+
+    private static final double TEST_EPS = 1e-10;
+
+    private static final DoublePrecisionContext TEST_PRECISION =
+            new EpsilonDoublePrecisionContext(TEST_EPS);
+
+    @Test
+    public void testGetBounds_noBoundaries() {
+        // arrange
+        BoundarySource3D src = BoundarySource3D.from(new ArrayList<>());
+        BoundarySourceBoundsBuilder3D builder = new BoundarySourceBoundsBuilder3D();
+
+        // act
+        Bounds3D b = builder.getBounds(src);
+
+        // assert
+        Assert.assertNull(b);
+    }
+
+    @Test
+    public void testGetBounds_singleFiniteBoundary() {
+        // arrange
+        ConvexPolygon3D poly = Planes.convexPolygonFromVertices(Arrays.asList(
+                Vector3D.of(1, 1, 1),
+                Vector3D.of(1, 0, 2),
+                Vector3D.of(3, 4, 5)), TEST_PRECISION);
+
+        BoundarySource3D src = BoundarySource3D.from(poly);
+        BoundarySourceBoundsBuilder3D builder = new BoundarySourceBoundsBuilder3D();
+
+        // act
+        Bounds3D b = builder.getBounds(src);
+
+        // assert
+        checkBounds(b, Vector3D.of(1, 0, 1), Vector3D.of(3, 4, 5));
+        for (Vector3D pt : poly.getVertices()) {
+            Assert.assertTrue(b.contains(pt));
+        }
+    }
+
+    @Test
+    public void testGetBounds_multipleFiniteBoundaries() {
+        // arrange
+        ConvexPolygon3D poly1 = Planes.convexPolygonFromVertices(Arrays.asList(
+                Vector3D.of(1, 1, 1),
+                Vector3D.of(1, 0, 2),
+                Vector3D.of(3, 4, 5)), TEST_PRECISION);
+
+        ConvexPolygon3D poly2 = Planes.convexPolygonFromVertices(Arrays.asList(
+                Vector3D.of(-1, 1, 1),
+                Vector3D.of(1, 4, 4),
+                Vector3D.of(7, 4, 5)), TEST_PRECISION);
+
+        ConvexPolygon3D poly3 = Planes.convexPolygonFromVertices(Arrays.asList(
+                Vector3D.of(-2, 1, 1),
+                Vector3D.of(1, 7, 2),
+                Vector3D.of(5, 4, 10)), TEST_PRECISION);
+
+        BoundarySource3D src = BoundarySource3D.from(poly1, poly2, poly3);
+        BoundarySourceBoundsBuilder3D builder = new BoundarySourceBoundsBuilder3D();
+
+        // act
+        Bounds3D b = builder.getBounds(src);
+
+        // assert
+        checkBounds(b, Vector3D.of(-2, 0, 1), Vector3D.of(7, 7, 10));
+
+        src.boundaryStream().forEach(boundary -> {
+            for (Vector3D pt : boundary.getVertices()) {
+                Assert.assertTrue(b.contains(pt));
+            }
+        });
+    }
+
+    @Test
+    public void testGetBounds_singleInfiniteBoundary() {
+        // arrange
+        PlaneConvexSubset boundary = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION)
+                .span();
+        BoundarySource3D src = BoundarySource3D.from(boundary);
+        BoundarySourceBoundsBuilder3D builder = new BoundarySourceBoundsBuilder3D();
+
+        // act
+        Bounds3D b = builder.getBounds(src);
+
+        // assert
+        Assert.assertNull(b);
+    }
+
+    @Test
+    public void testGetBounds_mixedFiniteAndInfiniteBoundaries() {
+        // arrange
+        PlaneConvexSubset inf = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Z, TEST_PRECISION)
+                .span()
+                .split(Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, TEST_PRECISION))
+                .getMinus();
+
+        ConvexPolygon3D poly1 = Planes.convexPolygonFromVertices(Arrays.asList(
+                Vector3D.of(1, 1, 1),
+                Vector3D.of(1, 0, 2),
+                Vector3D.of(3, 4, 5)), TEST_PRECISION);
+
+        ConvexPolygon3D poly2 = Planes.convexPolygonFromVertices(Arrays.asList(
+                Vector3D.of(-1, 1, 1),
+                Vector3D.of(1, 4, 4),
+                Vector3D.of(7, 4, 5)), TEST_PRECISION);
+
+        ConvexPolygon3D poly3 = Planes.convexPolygonFromVertices(Arrays.asList(
+                Vector3D.of(-2, 1, 1),
+                Vector3D.of(1, 7, 2),
+                Vector3D.of(5, 4, 10)), TEST_PRECISION);
+
+        BoundarySource3D src = BoundarySource3D.from(poly1, poly2, inf, poly3);
+        BoundarySourceBoundsBuilder3D builder = new BoundarySourceBoundsBuilder3D();
+
+        // act
+        Bounds3D b = builder.getBounds(src);
+
+        // assert
+        Assert.assertNull(b);
+    }
+
+    private static void checkBounds(Bounds3D b, Vector3D min, Vector3D max) {
+        EuclideanTestUtils.assertCoordinatesEqual(min, b.getMin(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(max, b.getMax(), TEST_EPS);
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecaster3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecaster3DTest.java
index a38b244..c62bacd 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecaster3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/BoundarySourceLinecaster3DTest.java
@@ -102,8 +102,8 @@ public class BoundarySourceLinecaster3DTest {
     public void testLinecast_line_removesDuplicatePoints() {
         // arrange
         BoundarySource3D src = BoundarySource3D.from(
-                    Planes.subsetFromVertexLoop(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION),
-                    Planes.subsetFromVertexLoop(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X), TEST_PRECISION)
+                    Planes.convexPolygonFromVertices(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION),
+                    Planes.convexPolygonFromVertices(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X), TEST_PRECISION)
                 );
         BoundarySourceLinecaster3D linecaster = new BoundarySourceLinecaster3D(src);
 
@@ -235,8 +235,8 @@ public class BoundarySourceLinecaster3DTest {
     public void testLinecast_segment_removesDuplicatePoints() {
         // arrange
         BoundarySource3D src = BoundarySource3D.from(
-                    Planes.subsetFromVertexLoop(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION),
-                    Planes.subsetFromVertexLoop(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X), TEST_PRECISION)
+                    Planes.convexPolygonFromVertices(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y), TEST_PRECISION),
+                    Planes.convexPolygonFromVertices(Arrays.asList(Vector3D.ZERO, Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X), TEST_PRECISION)
                 );
         BoundarySourceLinecaster3D linecaster = new BoundarySourceLinecaster3D(src);
 
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Bounds3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Bounds3DTest.java
new file mode 100644
index 0000000..7693870
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Bounds3DTest.java
@@ -0,0 +1,538 @@
+/*
+ * 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.function.BiFunction;
+import java.util.function.ToDoubleFunction;
+import java.util.regex.Pattern;
+
+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.shape.Parallelepiped;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class Bounds3DTest {
+
+    private static final double TEST_EPS = 1e-10;
+
+    private static final DoublePrecisionContext TEST_PRECISION =
+            new EpsilonDoublePrecisionContext(TEST_EPS);
+
+    private static final String NO_POINTS_MESSAGE = "Cannot construct bounds: no points given";
+
+    private static final Pattern INVALID_BOUNDS_PATTERN =
+            Pattern.compile("^Invalid bounds: min= \\([^\\)]+\\), max= \\([^\\)]+\\)");
+
+    @Test
+    public void testFrom_varargs_singlePoint() {
+        // arrange
+        Vector3D p1 = Vector3D.of(-1, 2, -3);
+
+        // act
+        Bounds3D b = Bounds3D.from(p1);
+
+        // assert
+        EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMin(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMax(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, b.getDiagonal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(p1, b.getBarycenter(), TEST_EPS);
+    }
+
+    @Test
+    public void testFrom_varargs_multiplePoints() {
+        // arrange
+        Vector3D p1 = Vector3D.of(1, 6, 7);
+        Vector3D p2 = Vector3D.of(0, 5, 11);
+        Vector3D p3 = Vector3D.of(3, 6, 8);
+
+        // act
+        Bounds3D b = Bounds3D.from(p1, p2, p3);
+
+        // assert
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 5, 7), b.getMin(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 6, 11), b.getMax(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 1, 4), b.getDiagonal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.5, 5.5, 9), b.getBarycenter(), TEST_EPS);
+    }
+
+    @Test
+    public void testFrom_iterable_singlePoint() {
+        // arrange
+        Vector3D p1 = Vector3D.of(-1, 2, -3);
+
+        // act
+        Bounds3D b = Bounds3D.from(Arrays.asList(p1));
+
+        // assert
+        EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMin(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMax(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, b.getDiagonal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(p1, b.getBarycenter(), TEST_EPS);
+    }
+
+    @Test
+    public void testFrom_iterable_multiplePoints() {
+        // arrange
+        Vector3D p1 = Vector3D.of(1, 6, 7);
+        Vector3D p2 = Vector3D.of(2, 5, 9);
+        Vector3D p3 = Vector3D.of(3, 4, 8);
+
+        // act
+        Bounds3D b = Bounds3D.from(Arrays.asList(p1, p2, p3));
+
+        // assert
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 4, 7), b.getMin(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 6, 9), b.getMax(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 2, 2), b.getDiagonal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 5, 8), b.getBarycenter(), TEST_EPS);
+    }
+
+    @Test
+    public void testFrom_iterable_noPoints() {
+        // act/assert
+        GeometryTestUtils.assertThrows(() -> {
+            Bounds3D.from(new ArrayList<>());
+        }, IllegalStateException.class, NO_POINTS_MESSAGE);
+    }
+
+    @Test
+    public void testFrom_invalidBounds() {
+        // arrange
+        Vector3D good = Vector3D.of(1, 1, 1);
+
+        Vector3D nan = Vector3D.of(Double.NaN, 1, 1);
+        Vector3D posInf = Vector3D.of(1, Double.POSITIVE_INFINITY, 1);
+        Vector3D negInf = Vector3D.of(1, 1, Double.NEGATIVE_INFINITY);
+
+        // act/assert
+        GeometryTestUtils.assertThrows(() -> {
+            Bounds3D.from(Vector3D.NaN);
+        }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
+
+        GeometryTestUtils.assertThrows(() -> {
+            Bounds3D.from(Vector3D.POSITIVE_INFINITY);
+        }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
+
+        GeometryTestUtils.assertThrows(() -> {
+            Bounds3D.from(Vector3D.NEGATIVE_INFINITY);
+        }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
+
+        GeometryTestUtils.assertThrows(() -> {
+            Bounds3D.from(good, nan);
+        }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
+
+        GeometryTestUtils.assertThrows(() -> {
+            Bounds3D.from(posInf, good);
+        }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
+
+        GeometryTestUtils.assertThrows(() -> {
+            Bounds3D.from(good, negInf, good);
+        }, IllegalStateException.class, INVALID_BOUNDS_PATTERN);
+    }
+
+    @Test
+    public void testHasSize() {
+        // arrange
+        DoublePrecisionContext low = new EpsilonDoublePrecisionContext(1e-2);
+        DoublePrecisionContext high = new EpsilonDoublePrecisionContext(1e-10);
+
+        Vector3D p1 = Vector3D.ZERO;
+
+        Vector3D p2 = Vector3D.of(1e-5, 1, 1);
+        Vector3D p3 = Vector3D.of(1, 1e-5, 1);
+        Vector3D p4 = Vector3D.of(1, 1, 1e-5);
+
+        Vector3D p5 = Vector3D.of(1, 1, 1);
+
+        // act/assert
+        Assert.assertFalse(Bounds3D.from(p1).hasSize(high));
+        Assert.assertFalse(Bounds3D.from(p1).hasSize(low));
+
+        Assert.assertTrue(Bounds3D.from(p1, p2).hasSize(high));
+        Assert.assertFalse(Bounds3D.from(p1, p2).hasSize(low));
+
+        Assert.assertTrue(Bounds3D.from(p1, p3).hasSize(high));
+        Assert.assertFalse(Bounds3D.from(p1, p3).hasSize(low));
+
+        Assert.assertTrue(Bounds3D.from(p1, p4).hasSize(high));
+        Assert.assertFalse(Bounds3D.from(p1, p4).hasSize(low));
+
+        Assert.assertTrue(Bounds3D.from(p1, p5).hasSize(high));
+        Assert.assertTrue(Bounds3D.from(p1, p5).hasSize(low));
+    }
+
+    @Test
+    public void testContains_strict() {
+        // arrange
+        Bounds3D b = Bounds3D.from(
+                Vector3D.of(0, 4, 8),
+                Vector3D.of(2, 6, 10));
+
+        // act/assert
+        assertContainsStrict(b, true,
+                b.getBarycenter(),
+                Vector3D.of(0, 4, 8), Vector3D.of(2, 6, 10),
+                Vector3D.of(1, 5, 9),
+                Vector3D.of(0, 5, 9), Vector3D.of(2, 5, 9),
+                Vector3D.of(1, 4, 9), Vector3D.of(1, 6, 9),
+                Vector3D.of(1, 5, 8), Vector3D.of(1, 5, 10));
+
+        assertContainsStrict(b, false,
+                Vector3D.ZERO,
+                Vector3D.of(-1, 5, 9), Vector3D.of(3, 5, 9),
+                Vector3D.of(1, 3, 9), Vector3D.of(1, 7, 9),
+                Vector3D.of(1, 5, 7), Vector3D.of(1, 5, 11),
+                Vector3D.of(-1e-15, 4, 8), Vector3D.of(2, 6 + 1e-15, 10), Vector3D.of(0, 4, 10 + 1e-15));
+    }
+
+    @Test
+    public void testContains_precision() {
+        // arrange
+        Bounds3D b = Bounds3D.from(
+                Vector3D.of(0, 4, 8),
+                Vector3D.of(2, 6, 10));
+
+        // act/assert
+        assertContainsWithPrecision(b, true,
+                b.getBarycenter(),
+                Vector3D.of(0, 4, 8), Vector3D.of(2, 6, 10),
+                Vector3D.of(1, 5, 9),
+                Vector3D.of(0, 5, 9), Vector3D.of(2, 5, 9),
+                Vector3D.of(1, 4, 9), Vector3D.of(1, 6, 9),
+                Vector3D.of(1, 5, 8), Vector3D.of(1, 5, 10),
+                Vector3D.of(-1e-15, 4, 8), Vector3D.of(2, 6 + 1e-15, 10), Vector3D.of(0, 4, 10 + 1e-15));
+
+        assertContainsWithPrecision(b, false,
+                Vector3D.ZERO,
+                Vector3D.of(-1, 5, 9), Vector3D.of(3, 5, 9),
+                Vector3D.of(1, 3, 9), Vector3D.of(1, 7, 9),
+                Vector3D.of(1, 5, 7), Vector3D.of(1, 5, 11));
+    }
+
+    @Test
+    public void testIntersects() {
+        // arrange
+        Bounds3D b = Bounds3D.from(Vector3D.ZERO, Vector3D.of(1, 1, 1));
+
+        // act/assert
+        checkIntersects(b, Vector3D::getX, (v, x) -> Vector3D.of(x, v.getY(), v.getZ()));
+        checkIntersects(b, Vector3D::getY, (v, y) -> Vector3D.of(v.getX(), y, v.getZ()));
+        checkIntersects(b, Vector3D::getZ, (v, z) -> Vector3D.of(v.getX(), v.getY(), z));
+    }
+
+    private void checkIntersects(Bounds3D b, ToDoubleFunction<Vector3D> getter,
+            BiFunction<Vector3D, Double, Vector3D> setter) {
+
+        Vector3D min = b.getMin();
+        Vector3D max = b.getMax();
+
+        double minValue = getter.applyAsDouble(min);
+        double maxValue = getter.applyAsDouble(max);
+        double midValue = (0.5 * (maxValue - minValue)) + minValue;
+
+        // check all possible interval relationships
+
+        // start below minValue
+        Assert.assertFalse(b.intersects(Bounds3D.from(
+                setter.apply(min, minValue - 2), setter.apply(max, minValue - 1))));
+
+        Assert.assertTrue(b.intersects(Bounds3D.from(
+                setter.apply(min, minValue - 2), setter.apply(max, minValue))));
+        Assert.assertTrue(b.intersects(Bounds3D.from(
+                setter.apply(min, minValue - 2), setter.apply(max, midValue))));
+        Assert.assertTrue(b.intersects(Bounds3D.from(
+                setter.apply(min, minValue - 2), setter.apply(max, maxValue))));
+        Assert.assertTrue(b.intersects(Bounds3D.from(
+                setter.apply(min, minValue - 2), setter.apply(max, maxValue + 1))));
+
+        // start on minValue
+        Assert.assertTrue(b.intersects(Bounds3D.from(
+                setter.apply(min, minValue), setter.apply(max, minValue))));
+        Assert.assertTrue(b.intersects(Bounds3D.from(
+                setter.apply(min, minValue), setter.apply(max, midValue))));
+        Assert.assertTrue(b.intersects(Bounds3D.from(
+                setter.apply(min, minValue), setter.apply(max, maxValue))));
+        Assert.assertTrue(b.intersects(Bounds3D.from(
+                setter.apply(min, minValue), setter.apply(max, maxValue + 1))));
+
+        // start on midValue
+        Assert.assertTrue(b.intersects(Bounds3D.from(
+                setter.apply(min, midValue), setter.apply(max, midValue))));
+        Assert.assertTrue(b.intersects(Bounds3D.from(
+                setter.apply(min, midValue), setter.apply(max, maxValue))));
+        Assert.assertTrue(b.intersects(Bounds3D.from(
+                setter.apply(min, midValue), setter.apply(max, maxValue + 1))));
+
+        // start on maxValue
+        Assert.assertTrue(b.intersects(Bounds3D.from(
+                setter.apply(min, maxValue), setter.apply(max, maxValue))));
+        Assert.assertTrue(b.intersects(Bounds3D.from(
+                setter.apply(min, maxValue), setter.apply(max, maxValue + 1))));
+
+        // start above maxValue
+        Assert.assertFalse(b.intersects(Bounds3D.from(
+                setter.apply(min, maxValue + 1), setter.apply(max, maxValue + 2))));
+    }
+
+    @Test
+    public void testIntersection() {
+        // -- arrange
+        Bounds3D b = Bounds3D.from(Vector3D.ZERO, Vector3D.of(1, 1, 1));
+
+        // -- act/assert
+
+        // move along x-axis
+        Assert.assertNull(b.intersection(Bounds3D.from(Vector3D.of(-2, 0, 0), Vector3D.of(-1, 1, 1))));
+        checkIntersection(b, Vector3D.of(-1, 0, 0), Vector3D.of(0, 1, 1),
+                Vector3D.of(0, 0, 0), Vector3D.of(0, 1, 1));
+        checkIntersection(b, Vector3D.of(-1, 0, 0), Vector3D.of(0.5, 1, 1),
+                Vector3D.of(0, 0, 0), Vector3D.of(0.5, 1, 1));
+        checkIntersection(b, Vector3D.of(-1, 0, 0), Vector3D.of(1, 1, 1),
+                Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+        checkIntersection(b, Vector3D.of(-1, 0, 0), Vector3D.of(2, 1, 1),
+                Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+        checkIntersection(b, Vector3D.of(0, 0, 0), Vector3D.of(2, 1, 1),
+                Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+        checkIntersection(b, Vector3D.of(0.5, 0, 0), Vector3D.of(2, 1, 1),
+                Vector3D.of(0.5, 0, 0), Vector3D.of(1, 1, 1));
+        checkIntersection(b, Vector3D.of(1, 0, 0), Vector3D.of(2, 1, 1),
+                Vector3D.of(1, 0, 0), Vector3D.of(1, 1, 1));
+        Assert.assertNull(b.intersection(Bounds3D.from(Vector3D.of(2, 0, 0), Vector3D.of(3, 1, 1))));
+
+        // move along y-axis
+        Assert.assertNull(b.intersection(Bounds3D.from(Vector3D.of(0, -2, 0), Vector3D.of(1, -1, 1))));
+        checkIntersection(b, Vector3D.of(0, -1, 0), Vector3D.of(1, 0, 1),
+                Vector3D.of(0, 0, 0), Vector3D.of(1, 0, 1));
+        checkIntersection(b, Vector3D.of(0, -1, 0), Vector3D.of(1, 0.5, 1),
+                Vector3D.of(0, 0, 0), Vector3D.of(1, 0.5, 1));
+        checkIntersection(b, Vector3D.of(0, -1, 0), Vector3D.of(1, 1, 1),
+                Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+        checkIntersection(b, Vector3D.of(0, -1, 0), Vector3D.of(1, 2, 1),
+                Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+        checkIntersection(b, Vector3D.of(0, 0, 0), Vector3D.of(1, 2, 1),
+                Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+        checkIntersection(b, Vector3D.of(0, 0.5, 0), Vector3D.of(1, 2, 1),
+                Vector3D.of(0, 0.5, 0), Vector3D.of(1, 1, 1));
+        checkIntersection(b, Vector3D.of(0, 1, 0), Vector3D.of(1, 2, 1),
+                Vector3D.of(0, 1, 0), Vector3D.of(1, 1, 1));
+        Assert.assertNull(b.intersection(Bounds3D.from(Vector3D.of(0, 2, 0), Vector3D.of(1, 3, 1))));
+
+        // move along z-axis
+        Assert.assertNull(b.intersection(Bounds3D.from(Vector3D.of(0, 0, -2), Vector3D.of(1, 1, -1))));
+        checkIntersection(b, Vector3D.of(0, 0, -1), Vector3D.of(1, 1, 0),
+                Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 0));
+        checkIntersection(b, Vector3D.of(0, 0, -1), Vector3D.of(1, 1, 0.5),
+                Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 0.5));
+        checkIntersection(b, Vector3D.of(0, 0, -1), Vector3D.of(1, 1, 1),
+                Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+        checkIntersection(b, Vector3D.of(0, 0, -1), Vector3D.of(1, 1, 2),
+                Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+        checkIntersection(b, Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 2),
+                Vector3D.of(0, 0, 0), Vector3D.of(1, 1, 1));
+        checkIntersection(b, Vector3D.of(0, 0, 0.5), Vector3D.of(1, 1, 2),
+                Vector3D.of(0, 0, 0.5), Vector3D.of(1, 1, 1));
+        checkIntersection(b, Vector3D.of(0, 0, 1), Vector3D.of(1, 1, 2),
+                Vector3D.of(0, 0, 1), Vector3D.of(1, 1, 1));
+        Assert.assertNull(b.intersection(Bounds3D.from(Vector3D.of(0, 0, 2), Vector3D.of(1, 1, 3))));
+    }
+
+    private void checkIntersection(Bounds3D b, Vector3D a1, Vector3D a2, Vector3D r1, Vector3D r2) {
+        Bounds3D a = Bounds3D.from(a1, a2);
+        Bounds3D result = b.intersection(a);
+
+        checkBounds(result, r1, r2);
+    }
+
+    @Test
+    public void toRegion() {
+        // arrange
+        Bounds3D b = Bounds3D.from(
+                Vector3D.of(0, 4, 8),
+                Vector3D.of(2, 6, 10));
+
+        // act
+        Parallelepiped p = b.toRegion(TEST_PRECISION);
+
+        // assert
+        Assert.assertEquals(8, p.getSize(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 5, 9), p.getBarycenter(), TEST_EPS);
+    }
+
+    @Test
+    public void toRegion_boundingBoxTooSmall() {
+        // act/assert
+        GeometryTestUtils.assertThrows(() -> {
+            Bounds3D.from(Vector3D.ZERO, Vector3D.of(1e-12, 1e-12, 1e-12))
+                .toRegion(TEST_PRECISION);
+        }, IllegalArgumentException.class);
+    }
+
+    @Test
+    public void testEq() {
+        // arrange
+        DoublePrecisionContext low = new EpsilonDoublePrecisionContext(1e-2);
+        DoublePrecisionContext high = new EpsilonDoublePrecisionContext(1e-10);
+
+        Bounds3D b1 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
+
+        Bounds3D b2 = Bounds3D.from(Vector3D.of(1.1, 1, 1), Vector3D.of(2, 2, 2));
+        Bounds3D b3 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(1.9, 2, 2));
+
+        Bounds3D b4 = Bounds3D.from(Vector3D.of(1.001, 1.001, 1.001), Vector3D.of(2.001, 2.001, 2.001));
+
+        // act/assert
+        Assert.assertTrue(b1.eq(b1, low));
+
+        Assert.assertFalse(b1.eq(b2, low));
+        Assert.assertFalse(b1.eq(b3, low));
+
+        Assert.assertTrue(b1.eq(b4, low));
+        Assert.assertTrue(b4.eq(b1, low));
+
+        Assert.assertFalse(b1.eq(b4, high));
+        Assert.assertFalse(b4.eq(b1, high));
+    }
+
+    @Test
+    public void testHashCode() {
+        // arrange
+        Bounds3D b1 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
+
+        Bounds3D b2 = Bounds3D.from(Vector3D.of(-2, 1, 1), Vector3D.of(2, 2, 2));
+        Bounds3D b3 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(3, 2, 2));
+        Bounds3D b4 = Bounds3D.from(Vector3D.of(1 + 1e-15, 1, 1), Vector3D.of(2, 2, 2));
+        Bounds3D b5 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2 + 1e-15, 2, 2));
+
+        Bounds3D b6 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
+
+        // act
+        int hash = b1.hashCode();
+
+        // assert
+        Assert.assertEquals(hash, b1.hashCode());
+
+        Assert.assertNotEquals(hash, b2.hashCode());
+        Assert.assertNotEquals(hash, b3.hashCode());
+        Assert.assertNotEquals(hash, b4.hashCode());
+        Assert.assertNotEquals(hash, b5.hashCode());
+
+        Assert.assertEquals(hash, b6.hashCode());
+    }
+
+    @Test
+    public void testEquals() {
+        // arrange
+        Bounds3D b1 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
+
+        Bounds3D b2 = Bounds3D.from(Vector3D.of(-1, 1, 1), Vector3D.of(2, 2, 2));
+        Bounds3D b3 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(3, 2, 2));
+        Bounds3D b4 = Bounds3D.from(Vector3D.of(1 + 1e-15, 1, 1), Vector3D.of(2, 2, 2));
+        Bounds3D b5 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2 + 1e-15, 2, 2));
+
+        Bounds3D b6 = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
+
+        // act/assert
+        Assert.assertTrue(b1.equals(b1));
+
+        Assert.assertFalse(b1.equals(null));
+        Assert.assertFalse(b1.equals(new Object()));
+
+        Assert.assertFalse(b1.equals(b2));
+        Assert.assertFalse(b1.equals(b3));
+        Assert.assertFalse(b1.equals(b4));
+        Assert.assertFalse(b1.equals(b5));
+
+        Assert.assertTrue(b1.equals(b6));
+    }
+
+    @Test
+    public void testToString() {
+        // arrange
+        Bounds3D b = Bounds3D.from(Vector3D.of(1, 1, 1), Vector3D.of(2, 2, 2));
+
+        // act
+        String str = b.toString();
+
+        // assert
+        GeometryTestUtils.assertContains("Bounds3D[min= (1", str);
+        GeometryTestUtils.assertContains(", max= (2", str);
+    }
+
+    @Test
+    public void testBuilder_addMethods() {
+        // arrange
+        Vector3D p1 = Vector3D.of(1, 10, 11);
+        Vector3D p2 = Vector3D.of(2, 9, 12);
+        Vector3D p3 = Vector3D.of(3, 8, 13);
+        Vector3D p4 = Vector3D.of(4, 7, 14);
+        Vector3D p5 = Vector3D.of(5, 6, 15);
+
+        // act
+        Bounds3D b = Bounds3D.builder()
+                .add(p1)
+                .addAll(Arrays.asList(p2, p3))
+                .add(Bounds3D.from(p4, p5))
+                .build();
+
+        // assert
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 6, 11), b.getMin(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(5, 10, 15), b.getMax(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 8, 13), b.getBarycenter(), TEST_EPS);
+    }
+
+    @Test
+    public void testBuilder_containsBounds() {
+        // act/assert
+        Assert.assertFalse(Bounds3D.builder().containsBounds());
+
+        Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(Double.NaN, 1, 1)).containsBounds());
+        Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(1, Double.NaN, 1)).containsBounds());
+        Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(1, 1, Double.NaN)).containsBounds());
+
+        Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(Double.POSITIVE_INFINITY, 1, 1)).containsBounds());
+        Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(1, Double.POSITIVE_INFINITY, 1)).containsBounds());
+        Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(1, 1, Double.POSITIVE_INFINITY)).containsBounds());
+
+        Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(Double.NEGATIVE_INFINITY, 1, 1)).containsBounds());
+        Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(1, Double.NEGATIVE_INFINITY, 1)).containsBounds());
+        Assert.assertFalse(Bounds3D.builder().add(Vector3D.of(1, 1, Double.NEGATIVE_INFINITY)).containsBounds());
+
+        Assert.assertTrue(Bounds3D.builder().add(Vector3D.ZERO).containsBounds());
+    }
+
+    private static void checkBounds(Bounds3D b, Vector3D min, Vector3D max) {
+        EuclideanTestUtils.assertCoordinatesEqual(min, b.getMin(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(max, b.getMax(), TEST_EPS);
+    }
+
+    private static void assertContainsStrict(Bounds3D bounds, boolean contains, Vector3D... pts) {
+        for (Vector3D pt : pts) {
+            Assert.assertEquals("Unexpected location for point " + pt, contains, bounds.contains(pt));
+        }
+    }
+
+    private static void assertContainsWithPrecision(Bounds3D bounds, boolean contains, Vector3D... pts) {
+        for (Vector3D pt : pts) {
+            Assert.assertEquals("Unexpected location for point " + pt, contains, bounds.contains(pt, TEST_PRECISION));
+        }
+    }
+}
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 b6220de..a0a83c1 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
@@ -18,6 +18,7 @@ package org.apache.commons.geometry.euclidean.threed;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 import org.apache.commons.geometry.core.GeometryTestUtils;
@@ -69,8 +70,9 @@ public class ConvexVolumeTest {
         Assert.assertEquals(1, boundaries.size());
 
         PlaneConvexSubset sp = boundaries.get(0);
-        Assert.assertEquals(0, sp.getSubspaceRegion().getBoundaries().size());
-        Assert.assertSame(plane, sp.getPlane());
+        Assert.assertEquals(0, sp.getEmbedded().getSubspaceRegion().getBoundaries().size());
+        EuclideanTestUtils.assertCoordinatesEqual(plane.getOrigin(), sp.getPlane().getOrigin(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(plane.getNormal(), sp.getPlane().getNormal(), TEST_EPS);
     }
 
     @Test
@@ -86,6 +88,97 @@ public class ConvexVolumeTest {
     }
 
     @Test
+    public void testTriangleStream_noBoundaries() {
+        // arrange
+        ConvexVolume full = ConvexVolume.full();
+
+        // act
+        List<Triangle3D> tris = full.triangleStream().collect(Collectors.toList());
+
+        // act/assert
+        Assert.assertEquals(0, tris.size());
+    }
+
+    @Test
+    public void testTriangleStream_infinite() {
+        // arrange
+        Pattern pattern = Pattern.compile("^Cannot convert infinite plane subset to triangles: .*");
+
+        ConvexVolume half = ConvexVolume.fromBounds(
+                Planes.fromNormal(Vector3D.Unit.MINUS_X, TEST_PRECISION)
+            );
+
+        ConvexVolume quadrant = ConvexVolume.fromBounds(
+                    Planes.fromNormal(Vector3D.Unit.MINUS_X, TEST_PRECISION),
+                    Planes.fromNormal(Vector3D.Unit.MINUS_Y, TEST_PRECISION),
+                    Planes.fromNormal(Vector3D.Unit.MINUS_Z, TEST_PRECISION)
+                );
+
+        // act/assert
+        GeometryTestUtils.assertThrows(() -> {
+            half.triangleStream().collect(Collectors.toList());
+        }, IllegalStateException.class, pattern);
+
+        GeometryTestUtils.assertThrows(() -> {
+            quadrant.triangleStream().collect(Collectors.toList());
+        }, IllegalStateException.class, pattern);
+    }
+
+    @Test
+    public void testTriangleStream_finite() {
+        // arrange
+        Vector3D min = Vector3D.ZERO;
+        Vector3D max = Vector3D.of(1, 1, 1);
+
+        ConvexVolume box = ConvexVolume.fromBounds(
+                    Planes.fromPointAndNormal(min, Vector3D.Unit.MINUS_X, TEST_PRECISION),
+                    Planes.fromPointAndNormal(min, Vector3D.Unit.MINUS_Y, TEST_PRECISION),
+                    Planes.fromPointAndNormal(min, Vector3D.Unit.MINUS_Z, TEST_PRECISION),
+
+                    Planes.fromPointAndNormal(max, Vector3D.Unit.PLUS_X, TEST_PRECISION),
+                    Planes.fromPointAndNormal(max, Vector3D.Unit.PLUS_Y, TEST_PRECISION),
+                    Planes.fromPointAndNormal(max, Vector3D.Unit.PLUS_Z, TEST_PRECISION)
+                );
+
+        // act
+        List<Triangle3D> tris = box.triangleStream().collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(12, tris.size());
+
+        Bounds3D.Builder boundsBuilder = Bounds3D.builder();
+        tris.forEach(t -> boundsBuilder.addAll(t.getVertices()));
+
+        Bounds3D bounds = boundsBuilder.build();
+        EuclideanTestUtils.assertCoordinatesEqual(min, bounds.getMin(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(max, bounds.getMax(), TEST_EPS);
+    }
+
+    @Test
+    public void testGetBounds_noBounds() {
+        // arrange
+        ConvexVolume full = ConvexVolume.full();
+        ConvexVolume halfFull = ConvexVolume.fromBounds(Planes.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION));
+
+        // act/assert
+        Assert.assertNull(full.getBounds());
+        Assert.assertNull(halfFull.getBounds());
+    }
+
+    @Test
+    public void testGetBounds_hasBounds() {
+        // arrange
+        ConvexVolume vol = rect(Vector3D.of(1, 1, 1), 0.5, 1, 2);
+
+        // act
+        Bounds3D bounds = vol.getBounds();
+
+        // assert
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0, -1), bounds.getMin(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.5, 2, 3), bounds.getMax(), TEST_EPS);
+    }
+
+    @Test
     public void testToTree_full() {
         // arrange
         ConvexVolume volume = ConvexVolume.full();
@@ -188,7 +281,7 @@ public class ConvexVolumeTest {
         ConvexVolume vol = rect(Vector3D.ZERO, 0.5, 0.5, 0.5);
 
         PlaneConvexSubset subplane = Planes.subsetFromConvexArea(
-                Planes.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION), ConvexArea.full());
+                Planes.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION).getEmbedding(), ConvexArea.full());
 
         // act
         PlaneConvexSubset trimmed = vol.trim(subplane);
@@ -196,15 +289,13 @@ public class ConvexVolumeTest {
         // assert
         Assert.assertEquals(1, trimmed.getSize(), TEST_EPS);
 
-        List<Vector3D> vertices = trimmed.getPlane().toSpace(
-                trimmed.getSubspaceRegion().getBoundaryPaths().get(0).getVertexSequence());
+        List<Vector3D> vertices = trimmed.getVertices();
 
-        Assert.assertEquals(5, vertices.size());
+        Assert.assertEquals(4, vertices.size());
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0.5, -0.5), vertices.get(0), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0.5, 0.5), vertices.get(1), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, -0.5, 0.5), vertices.get(2), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, -0.5, -0.5), vertices.get(3), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0.5, -0.5), vertices.get(4), TEST_EPS);
     }
 
     @Test
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedAreaPlaneConvexSubsetTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedAreaPlaneConvexSubsetTest.java
new file mode 100644
index 0000000..e40c52c
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedAreaPlaneConvexSubsetTest.java
@@ -0,0 +1,468 @@
+/*
+ * 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.Arrays;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+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.ConvexArea;
+import org.apache.commons.geometry.euclidean.twod.Lines;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+import org.apache.commons.geometry.euclidean.twod.path.LinePath;
+import org.apache.commons.geometry.euclidean.twod.shape.Parallelogram;
+import org.apache.commons.numbers.angle.PlaneAngleRadians;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class EmbeddedAreaPlaneConvexSubsetTest {
+
+    private static final double TEST_EPS = 1e-10;
+
+    private static final DoublePrecisionContext TEST_PRECISION =
+            new EpsilonDoublePrecisionContext(TEST_EPS);
+
+    private static final EmbeddingPlane XY_PLANE_Z1 = Planes.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1),
+            Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+
+    @Test
+    public void testSpaceConversion() {
+        // arrange
+        EmbeddingPlane plane = Planes.fromPointAndPlaneVectors(Vector3D.of(1, 0, 0),
+                Vector3D.Unit.PLUS_Y, Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+
+        EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(plane, ConvexArea.full());
+
+        // act/assert
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2), ps.toSubspace(Vector3D.of(-5, 1, 2)), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, -2, 4), ps.toSpace(Vector2D.of(-2, 4)), TEST_EPS);
+    }
+
+    @Test
+    public void testProperties_infinite() {
+        // arrange
+        ConvexArea area = ConvexArea.full();
+
+        // act
+        EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1, area);
+
+        // assert
+        Assert.assertTrue(ps.isFull());
+        Assert.assertFalse(ps.isEmpty());
+        Assert.assertFalse(ps.isFinite());
+        Assert.assertTrue(ps.isInfinite());
+
+        GeometryTestUtils.assertPositiveInfinity(ps.getSize());
+
+        Assert.assertSame(XY_PLANE_Z1, ps.getPlane());
+        Assert.assertSame(area, ps.getSubspaceRegion());
+
+        Assert.assertEquals(0, ps.getVertices().size());
+    }
+
+    @Test
+    public void testProperties_finite() {
+        // arrange
+        ConvexArea area = ConvexArea.convexPolygonFromPath(LinePath.builder(TEST_PRECISION)
+                .appendVertices(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(0, 1))
+                .build(true));
+
+        // act
+        EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1, area);
+
+        // assert
+        Assert.assertFalse(ps.isFull());
+        Assert.assertFalse(ps.isEmpty());
+        Assert.assertTrue(ps.isFinite());
+        Assert.assertFalse(ps.isInfinite());
+
+        Assert.assertEquals(0.5, ps.getSize(), TEST_EPS);
+
+        Assert.assertSame(XY_PLANE_Z1, ps.getPlane());
+        Assert.assertSame(area, ps.getSubspaceRegion());
+
+        EuclideanTestUtils.assertVertexLoopSequence(
+                Arrays.asList(Vector3D.of(0, 0, 1), Vector3D.of(1, 0, 1), Vector3D.of(0, 1, 1)),
+                ps.getVertices(), TEST_PRECISION);
+    }
+
+    @Test
+    public void testGetVertices_twoParallelLines() {
+        // arrange
+        EmbeddingPlane plane = Planes.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION).getEmbedding();
+        PlaneConvexSubset sp = new EmbeddedAreaPlaneConvexSubset(plane, ConvexArea.fromBounds(
+                    Lines.fromPointAndAngle(Vector2D.of(0, 1), PlaneAngleRadians.PI, TEST_PRECISION),
+                    Lines.fromPointAndAngle(Vector2D.of(0, -1), 0.0, TEST_PRECISION)
+                ));
+
+        // act
+        List<Vector3D> vertices = sp.getVertices();
+
+        // assert
+        Assert.assertEquals(0, vertices.size());
+    }
+
+    @Test
+    public void testGetVertices_infiniteWithVertices() {
+        // arrange
+        EmbeddingPlane plane = Planes.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+        PlaneConvexSubset sp = new EmbeddedAreaPlaneConvexSubset(plane, ConvexArea.fromBounds(
+                    Lines.fromPointAndAngle(Vector2D.of(0, 1), PlaneAngleRadians.PI, TEST_PRECISION),
+                    Lines.fromPointAndAngle(Vector2D.of(0, -1), 0.0, TEST_PRECISION),
+                    Lines.fromPointAndAngle(Vector2D.of(1, 0), PlaneAngleRadians.PI_OVER_TWO, TEST_PRECISION)
+                ));
+
+        // act
+        List<Vector3D> vertices = sp.getVertices();
+
+        // assert
+        Assert.assertEquals(2, vertices.size());
+
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, -1, 1), vertices.get(0), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, 1), vertices.get(1), TEST_EPS);
+    }
+    @Test
+    public void testToTriangles_infinite() {
+        // arrange
+        Pattern pattern = Pattern.compile("^Cannot convert infinite plane subset to triangles: .*");
+
+        // act/assert
+        GeometryTestUtils.assertThrows(() -> {
+            new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1, ConvexArea.full()).toTriangles();
+        }, IllegalStateException.class, pattern);
+
+        GeometryTestUtils.assertThrows(() -> {
+            ConvexArea area = ConvexArea.fromBounds(Lines.fromPointAndAngle(Vector2D.ZERO, 0, TEST_PRECISION));
+            EmbeddedAreaPlaneConvexSubset halfSpace = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1, area);
+
+            halfSpace.toTriangles();
+        }, IllegalStateException.class, pattern);
+
+        GeometryTestUtils.assertThrows(() -> {
+            ConvexArea area = ConvexArea.fromBounds(
+                    Lines.fromPointAndAngle(Vector2D.ZERO, 0, TEST_PRECISION),
+                    Lines.fromPointAndAngle(Vector2D.ZERO, 0.5 * Math.PI, TEST_PRECISION));
+
+            EmbeddedAreaPlaneConvexSubset halfSpaceWithVertices = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1, area);
+
+            halfSpaceWithVertices.toTriangles();
+        }, IllegalStateException.class, pattern);
+    }
+
+    @Test
+    public void testToTriangles_finite() {
+        // arrange
+        Vector3D p1 = Vector3D.of(0, 0, 1);
+        Vector3D p2 = Vector3D.of(1, 0, 1);
+        Vector3D p3 = Vector3D.of(2, 1, 1);
+        Vector3D p4 = Vector3D.of(1.5, 1, 1);
+
+        List<Vector2D> subPts = XY_PLANE_Z1.toSubspace(Arrays.asList(p1, p2, p3, p4));
+
+        EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+                ConvexArea.convexPolygonFromVertices(subPts, TEST_PRECISION));
+
+        // act
+        List<Triangle3D> tris = ps.toTriangles();
+
+        // assert
+        Assert.assertEquals(2, tris.size());
+
+        EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p4, p1, p2),
+                tris.get(0).getVertices(), TEST_PRECISION);
+        EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p4, p2, p3),
+                tris.get(1).getVertices(), TEST_PRECISION);
+    }
+
+    @Test
+    public void testClassify() {
+        // arrange
+        EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+                Parallelogram.builder(TEST_PRECISION)
+                    .setPosition(Vector2D.of(2, 3))
+                    .setScale(2, 2)
+                    .build());
+
+        // act/assert
+        checkPoints(ps, RegionLocation.INSIDE, Vector3D.of(2, 3, 1));
+        checkPoints(ps, RegionLocation.BOUNDARY,
+                Vector3D.of(1, 3, 1), Vector3D.of(3, 3, 1),
+                Vector3D.of(2, 2, 1), Vector3D.of(2, 4, 1));
+        checkPoints(ps, RegionLocation.OUTSIDE,
+                Vector3D.of(2, 3, 0), Vector3D.of(2, 3, 2),
+                Vector3D.of(0, 3, 1), Vector3D.of(4, 3, 1),
+                Vector3D.of(2, 1, 1), Vector3D.of(2, 5, 1));
+    }
+
+    @Test
+    public void testClosest() {
+        // arrange
+        EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+                Parallelogram.builder(TEST_PRECISION)
+                    .setPosition(Vector2D.of(2, 3))
+                    .setScale(2, 2)
+                    .build());
+
+        // act/assert
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 3, 1), ps.closest(Vector3D.of(2, 3, 1)), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 3, 1), ps.closest(Vector3D.of(2, 3, 100)), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 1),
+                ps.closest(Vector3D.of(-100, -100, -100)), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 3.5, 1),
+                ps.closest(Vector3D.of(100, 3.5, 100)), TEST_EPS);
+    }
+
+    @Test
+    public void testGetBounds_noBounds() {
+        // arrange
+        EmbeddingPlane plane = Planes.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1),
+                Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X, TEST_PRECISION);
+
+        EmbeddedAreaPlaneConvexSubset full = new EmbeddedAreaPlaneConvexSubset(plane, ConvexArea.full());
+        EmbeddedAreaPlaneConvexSubset halfPlane = new EmbeddedAreaPlaneConvexSubset(plane,
+                ConvexArea.fromBounds(Lines.fromPointAndAngle(Vector2D.ZERO, 0, TEST_PRECISION)));
+
+        // act/assert
+        Assert.assertNull(full.getBounds());
+        Assert.assertNull(halfPlane.getBounds());
+    }
+
+    @Test
+    public void testGetBounds_hasBounds() {
+        // arrange
+        EmbeddingPlane plane = Planes.fromPointAndPlaneVectors(Vector3D.of(0, 0, 1),
+                Vector3D.Unit.PLUS_Y, Vector3D.Unit.MINUS_X, TEST_PRECISION);
+
+        EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(plane,
+                ConvexArea.convexPolygonFromVertices(Arrays.asList(
+                    Vector2D.of(1, 1), Vector2D.of(2, 1), Vector2D.of(1, 2)
+                ), TEST_PRECISION));
+
+        // act
+        Bounds3D bounds = ps.getBounds();
+
+        // assert
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-2, 1, 1), bounds.getMin(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 2, 1), bounds.getMax(), TEST_EPS);
+    }
+
+    @Test
+    public void testTransform() {
+        // arrange
+        AffineTransformMatrix3D t = AffineTransformMatrix3D.identity()
+                .rotate(QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, -PlaneAngleRadians.PI_OVER_TWO))
+                .scale(1, 1, 2)
+                .translate(Vector3D.of(1, 0, 0));
+
+        EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+                Parallelogram.builder(TEST_PRECISION)
+                    .setPosition(Vector2D.of(2, 3))
+                    .setScale(2, 2)
+                    .build());
+
+        // act
+        EmbeddedAreaPlaneConvexSubset result = ps.transform(t);
+
+        // assert
+        Assert.assertFalse(result.isFull());
+        Assert.assertFalse(result.isEmpty());
+        Assert.assertTrue(result.isFinite());
+        Assert.assertFalse(result.isInfinite());
+
+        Assert.assertEquals(8, result.getSize(), TEST_EPS);
+
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.MINUS_X, result.getPlane().getNormal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_Z, result.getPlane().getU(), TEST_EPS);
+
+        EuclideanTestUtils.assertVertexLoopSequence(
+                Arrays.asList(Vector3D.of(0, 2, 2), Vector3D.of(0, 2, 6), Vector3D.of(0, 4, 6), Vector3D.of(0, 4, 2)),
+                result.getVertices(), TEST_PRECISION);
+    }
+
+    @Test
+    public void testReverse() {
+        // arrange
+        EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+                Parallelogram.builder(TEST_PRECISION)
+                    .setPosition(Vector2D.of(2, 3))
+                    .setScale(2, 2)
+                    .build());
+
+        // act
+        EmbeddedAreaPlaneConvexSubset result = ps.reverse();
+
+        // assert
+        Assert.assertFalse(result.isFull());
+        Assert.assertFalse(result.isEmpty());
+        Assert.assertTrue(result.isFinite());
+        Assert.assertFalse(result.isInfinite());
+
+        Assert.assertEquals(4, result.getSize(), TEST_EPS);
+
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.MINUS_Z, result.getPlane().getNormal(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_Y, result.getPlane().getU(), TEST_EPS);
+
+        EuclideanTestUtils.assertVertexLoopSequence(
+                Arrays.asList(Vector3D.of(1, 4, 1), Vector3D.of(3, 4, 1), Vector3D.of(3, 2, 1), Vector3D.of(1, 2, 1)),
+                result.getVertices(), TEST_PRECISION);
+    }
+
+    @Test
+    public void testSplit_plus() {
+        // arrange
+        EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+                ConvexArea.convexPolygonFromVertices(Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(0, 1)),
+                        TEST_PRECISION));
+
+        Plane splitter = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.PLUS_X, TEST_PRECISION);
+
+        // act
+        Split<PlaneConvexSubset> split = ps.split(splitter);
+
+        // assert
+        Assert.assertEquals(SplitLocation.PLUS, split.getLocation());
+
+        Assert.assertNull(split.getMinus());
+        Assert.assertSame(ps, split.getPlus());
+    }
+
+    @Test
+    public void testSplit_minus() {
+        // arrange
+        EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+                ConvexArea.convexPolygonFromVertices(Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(0, 1)),
+                        TEST_PRECISION));
+
+        Plane splitter = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.MINUS_Z, TEST_PRECISION);
+
+        // act
+        Split<PlaneConvexSubset> split = ps.split(splitter);
+
+        // assert
+        Assert.assertEquals(SplitLocation.MINUS, split.getLocation());
+
+        Assert.assertSame(ps, split.getMinus());
+        Assert.assertNull(split.getPlus());
+    }
+
+    @Test
+    public void testSplit_both() {
+        // arrange
+        EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+                ConvexArea.convexPolygonFromVertices(Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(0, 1)),
+                        TEST_PRECISION));
+
+        Plane splitter = Planes.fromPointAndNormal(Vector3D.ZERO, Vector3D.of(-1, 1, 0), TEST_PRECISION);
+
+        // act
+        Split<PlaneConvexSubset> split = ps.split(splitter);
+
+        // assert
+        Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+        PlaneConvexSubset minus = split.getMinus();
+        EuclideanTestUtils.assertVertexLoopSequence(
+                Arrays.asList(Vector3D.of(0, 0, 1), Vector3D.of(1, 0, 1), Vector3D.of(0.5, 0.5, 1)),
+                minus.getVertices(), TEST_PRECISION);
+
+        PlaneConvexSubset plus = split.getPlus();
+        EuclideanTestUtils.assertVertexLoopSequence(
+                Arrays.asList(Vector3D.of(0, 0, 1), Vector3D.of(0.5, 0.5, 1), Vector3D.of(0, 1, 1)),
+                plus.getVertices(), TEST_PRECISION);
+    }
+
+    @Test
+    public void testSplit_neither() {
+        // arrange
+        EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+                ConvexArea.convexPolygonFromVertices(Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(0, 1)),
+                        TEST_PRECISION));
+
+        Plane splitter = Planes.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0, 1e-15, -1), TEST_PRECISION);
+
+        // act
+        Split<PlaneConvexSubset> split = ps.split(splitter);
+
+        // assert
+        Assert.assertEquals(SplitLocation.NEITHER, split.getLocation());
+
+        Assert.assertNull(split.getMinus());
+        Assert.assertNull(split.getPlus());
+    }
+
+    @Test
+    public void testSplit_usesVertexBasedSubsetsWhenPossible() {
+        // arrange
+        // create an infinite subset
+        EmbeddingPlane plane = Planes.fromPointAndPlaneVectors(Vector3D.ZERO,
+                Vector3D.Unit.PLUS_X, Vector3D.Unit.PLUS_Y, TEST_PRECISION);
+        EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(plane, ConvexArea.fromBounds(
+                    Lines.fromPointAndAngle(Vector2D.ZERO, 0, TEST_PRECISION),
+                    Lines.fromPointAndAngle(Vector2D.of(1, 0), PlaneAngleRadians.PI_OVER_TWO, TEST_PRECISION),
+                    Lines.fromPointAndAngle(Vector2D.of(0, 1), -PlaneAngleRadians.PI_OVER_TWO, TEST_PRECISION)
+                ));
+
+        Plane splitter = Planes.fromPointAndNormal(Vector3D.of(0.5, 0.5, 0), Vector3D.of(-1, 1, 0), TEST_PRECISION);
+
+        // act
+        Split<PlaneConvexSubset> split = ps.split(splitter);
+
+        // assert
+        Assert.assertTrue(ps.isInfinite());
+
+        Assert.assertEquals(SplitLocation.BOTH, split.getLocation());
+
+        PlaneConvexSubset plus = split.getPlus();
+        Assert.assertNotNull(plus);
+        Assert.assertTrue(plus.isInfinite());
+        Assert.assertTrue(plus instanceof EmbeddedAreaPlaneConvexSubset);
+
+        PlaneConvexSubset minus = split.getMinus();
+        Assert.assertNotNull(minus);
+        Assert.assertFalse(minus.isInfinite());
+        Assert.assertTrue(minus instanceof SimpleTriangle3D);
+    }
+
+    @Test
+    public void testToString() {
+        // arrange
+        EmbeddedAreaPlaneConvexSubset ps = new EmbeddedAreaPlaneConvexSubset(XY_PLANE_Z1,
+                ConvexArea.convexPolygonFromVertices(Arrays.asList(Vector2D.ZERO, Vector2D.of(1, 0), Vector2D.of(0, 1)),
+                        TEST_PRECISION));
+
+        // act
+        String str = ps.toString();
+
+        // assert
+        GeometryTestUtils.assertContains("EmbeddedAreaPlaneConvexSubset[plane= EmbeddingPlane[", str);
+        GeometryTestUtils.assertContains("subspaceRegion= ConvexArea[", str);
+    }
+
+    private static void checkPoints(EmbeddedAreaPlaneConvexSubset ps, RegionLocation loc, Vector3D... pts) {
+        for (Vector3D pt : pts) {
+            Assert.assertEquals("Unexpected location for point " + pt, loc, ps.classify(pt));
+        }
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubsetTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubsetTest.java
index 5860ac3..41dca76 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubsetTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubsetTest.java
@@ -18,6 +18,7 @@ package org.apache.commons.geometry.euclidean.threed;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.regex.Pattern;
... 6899 lines suppressed ...


[commons-geometry] 02/03: GEOMETRY-97: using the term 'centroid' to refer to geometric center rather than 'barycenter'

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

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

commit a70cea937b5621289a0be2e97b1a5f373b35b732
Author: Matt Juntunen <ma...@apache.org>
AuthorDate: Mon May 25 08:44:50 2020 -0400

    GEOMETRY-97: using the term 'centroid' to refer to geometric center rather than 'barycenter'
---
 .../org/apache/commons/geometry/core/Region.java   | 15 +++-
 .../core/partitioning/HyperplaneSubset.java        | 17 +++--
 .../partitioning/bsp/AbstractRegionBSPTree.java    | 22 +++---
 .../core/internal/HyperplaneSubsetsTest.java       |  2 +-
 .../AbstractConvexHyperplaneBoundedRegionTest.java |  2 +-
 .../bsp/AbstractRegionBSPTreeTest.java             |  6 +-
 .../core/partitioning/test/TestLineSegment.java    |  2 +-
 .../test/TestLineSegmentCollection.java            |  2 +-
 .../commons/geometry/euclidean/AbstractBounds.java |  6 +-
 .../geometry/euclidean/AbstractNSphere.java        |  2 +-
 .../commons/geometry/euclidean/oned/Interval.java  |  2 +-
 .../geometry/euclidean/oned/OrientedPoint.java     |  2 +-
 .../geometry/euclidean/oned/RegionBSPTree1D.java   | 24 +++---
 .../euclidean/threed/AbstractConvexPolygon3D.java  | 24 +++---
 .../threed/AbstractEmbeddedRegionPlaneSubset.java  |  8 +-
 .../geometry/euclidean/threed/ConvexVolume.java    | 26 +++----
 .../geometry/euclidean/threed/RegionBSPTree3D.java | 40 +++++-----
 .../euclidean/threed/SimpleTriangle3D.java         |  2 +-
 .../threed/line/EmbeddedTreeLineSubset3D.java      |  4 +-
 .../threed/line/LineSpanningSubset3D.java          |  2 +-
 .../euclidean/threed/line/LineSubset3D.java        |  8 +-
 .../geometry/euclidean/threed/line/Ray3D.java      |  2 +-
 .../euclidean/threed/line/ReverseRay3D.java        |  2 +-
 .../geometry/euclidean/threed/line/Segment3D.java  |  2 +-
 .../geometry/euclidean/twod/ConvexArea.java        |  4 +-
 .../euclidean/twod/EmbeddedTreeLineSubset.java     |  8 +-
 .../euclidean/twod/LineSpanningSubset.java         |  2 +-
 .../commons/geometry/euclidean/twod/Ray.java       |  2 +-
 .../geometry/euclidean/twod/RegionBSPTree2D.java   |  8 +-
 .../geometry/euclidean/twod/ReverseRay.java        |  2 +-
 .../commons/geometry/euclidean/twod/Segment.java   |  2 +-
 .../euclidean/DocumentationExamplesTest.java       | 22 +++---
 .../geometry/euclidean/oned/IntervalTest.java      | 16 ++--
 .../geometry/euclidean/oned/OrientedPointTest.java |  2 +-
 .../euclidean/oned/RegionBSPTree1DTest.java        | 36 ++++-----
 .../euclidean/threed/AbstractPlaneSubsetTest.java  |  2 +-
 .../geometry/euclidean/threed/Bounds3DTest.java    | 16 ++--
 .../euclidean/threed/ConvexVolumeTest.java         | 14 ++--
 .../threed/EmbeddedTreePlaneSubsetTest.java        |  6 +-
 .../geometry/euclidean/threed/PlanesTest.java      |  2 +-
 .../euclidean/threed/RegionBSPTree3DTest.java      | 58 +++++++--------
 .../euclidean/threed/SimpleTriangle3DTest.java     |  2 +-
 .../threed/VertexListConvexPolygon3DTest.java      |  8 +-
 .../threed/line/EmbeddedTreeLineSubset3DTest.java  |  8 +-
 .../geometry/euclidean/threed/line/Line3DTest.java |  2 +-
 .../geometry/euclidean/threed/line/Ray3DTest.java  |  6 +-
 .../euclidean/threed/line/ReverseRay3DTest.java    |  6 +-
 .../euclidean/threed/line/Segment3DTest.java       | 12 +--
 .../euclidean/threed/shape/ParallelepipedTest.java | 14 ++--
 .../euclidean/threed/shape/SphereTest.java         |  9 +--
 .../geometry/euclidean/twod/Bounds2DTest.java      | 16 ++--
 .../geometry/euclidean/twod/ConvexAreaTest.java    | 60 +++++++--------
 .../euclidean/twod/EmbeddedTreeLineSubsetTest.java | 10 +--
 .../euclidean/twod/LineSpanningSubsetTest.java     |  2 +-
 .../geometry/euclidean/twod/LineSubsetTest.java    |  2 +-
 .../commons/geometry/euclidean/twod/RayTest.java   |  6 +-
 .../euclidean/twod/RegionBSPTree2DTest.java        | 32 ++++----
 .../geometry/euclidean/twod/ReverseRayTest.java    |  6 +-
 .../geometry/euclidean/twod/SegmentTest.java       | 12 +--
 .../geometry/euclidean/twod/shape/CircleTest.java  |  8 +-
 .../euclidean/twod/shape/ParallelogramTest.java    | 16 ++--
 .../geometry/spherical/oned/AngularInterval.java   |  4 +-
 .../commons/geometry/spherical/oned/CutAngle.java  |  2 +-
 .../geometry/spherical/oned/RegionBSPTree1S.java   | 10 +--
 .../geometry/spherical/twod/ConvexArea2S.java      | 26 +++----
 .../geometry/spherical/twod/GreatCircleSubset.java |  8 +-
 .../geometry/spherical/twod/RegionBSPTree2S.java   | 10 +--
 .../spherical/DocumentationExamplesTest.java       |  4 +-
 .../spherical/oned/AngularIntervalTest.java        |  4 +-
 .../geometry/spherical/oned/CutAngleTest.java      |  2 +-
 .../spherical/oned/RegionBSPTree1STest.java        | 24 +++---
 .../geometry/spherical/twod/ConvexArea2STest.java  | 85 +++++++++++-----------
 .../twod/EmbeddedTreeSubGreatCircleTest.java       |  8 +-
 .../geometry/spherical/twod/GreatArcPathTest.java  |  4 +-
 .../geometry/spherical/twod/GreatArcTest.java      |  4 +-
 .../spherical/twod/GreatCircleSubsetTest.java      |  2 +-
 .../spherical/twod/RegionBSPTree2STest.java        | 82 ++++++++++-----------
 src/site/xdoc/index.xml                            |  4 +-
 src/site/xdoc/userguide/index.xml                  | 14 ++--
 79 files changed, 487 insertions(+), 473 deletions(-)

diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Region.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Region.java
index 308f83f..a649f7b 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Region.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Region.java
@@ -42,12 +42,19 @@ public interface Region<P extends Point<P>> extends Sized {
      */
     double getBoundarySize();
 
-    /** Get the barycenter of the region or null if no barycenter exists or
-     * one exists but is not unique. A barycenter will not exist for empty or
+    /** Get the centroid, or geometric center, of the region or null if no centroid
+     * exists or one exists but is not unique. A centroid will not exist for empty or
      * infinite regions.
-     * @return the barycenter of the region or null if no unique barycenter exists
+     *
+     * <p>The centroid of a geometric object is defined as the mean position of
+     * all points in the object, including interior points, vertices, and other points
+     * lying on the boundary. If a physical object has a uniform density, then its center
+     * of mass is the same as its geometric centroid.
+     * </p>
+     * @return the centroid of the region or null if no unique centroid exists
+     * @see <a href="https://en.wikipedia.org/wiki/Centroid">Centroid</a>
      */
-    P getBarycenter();
+    P getCentroid();
 
     /** Classify the given point with respect to the region.
      * @param pt the point to classify
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneSubset.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneSubset.java
index 580350c..43ac97f 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneSubset.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneSubset.java
@@ -65,12 +65,19 @@ public interface HyperplaneSubset<P extends Point<P>> extends Splittable<P, Hype
      */
     boolean isEmpty();
 
-    /** Get the barycenter of the hyperplane subset or null if no barycenter exists or
-     * one exists but is not unique. A barycenter will not exist for empty or
-     * infinite hyperplane subsets.
-     * @return the barycenter of the hyperplane subset or null if no unique barycenter exists
+    /** Get the centroid, or geometric center, of the hyperplane subset or null
+     * if no centroid exists or one exists but is not unique. A centroid will not
+     * exist for empty or infinite subsets.
+     *
+     * <p>The centroid of a geometric object is defined as the mean position of
+     * all points in the object, including interior points, vertices, and other points
+     * lying on the boundary. If a physical object has a uniform density, then its center
+     * of mass is the same as its geometric centroid.
+     * </p>
+     * @return the centroid of the hyperplane subset or null if no unique centroid exists
+     * @see <a href="https://en.wikipedia.org/wiki/Centroid">Centroid</a>
      */
-    P getBarycenter();
+    P getCentroid();
 
     /** Classify a point with respect to the subset region. The point is classified as follows:
      * <ul>
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTree.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTree.java
index 1df224b..84c1552 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTree.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTree.java
@@ -307,8 +307,8 @@ public abstract class AbstractRegionBSPTree<
 
     /** {@inheritDoc} */
     @Override
-    public P getBarycenter() {
-        return getRegionSizeProperties().getBarycenter();
+    public P getCentroid() {
+        return getRegionSizeProperties().getCentroid();
     }
 
     /** Helper method implementing the algorithm for splitting a tree by a hyperplane. Subclasses
@@ -890,16 +890,16 @@ public abstract class AbstractRegionBSPTree<
         /** The size of the region. */
         private final double size;
 
-        /** The barycenter of the region. */
-        private final P barycenter;
+        /** The centroid of the region. */
+        private final P centroid;
 
         /** Simple constructor.
          * @param size the region size
-         * @param barycenter the region barycenter
+         * @param centroid the region centroid
          */
-        public RegionSizeProperties(final double size, final P barycenter) {
+        public RegionSizeProperties(final double size, final P centroid) {
             this.size = size;
-            this.barycenter = barycenter;
+            this.centroid = centroid;
         }
 
         /** Get the size of the region.
@@ -909,11 +909,11 @@ public abstract class AbstractRegionBSPTree<
             return size;
         }
 
-        /** Get the barycenter of the region.
-         * @return the barycenter of the region
+        /** Get the centroid of the region.
+         * @return the centroid of the region
          */
-        public P getBarycenter() {
-            return barycenter;
+        public P getCentroid() {
+            return centroid;
         }
     }
 
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/HyperplaneSubsetsTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/HyperplaneSubsetsTest.java
index 5eeeca5..e17ebb9 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/HyperplaneSubsetsTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/HyperplaneSubsetsTest.java
@@ -112,7 +112,7 @@ public class HyperplaneSubsetsTest {
         }
 
         @Override
-        public TestPoint1D getBarycenter() {
+        public TestPoint1D getCentroid() {
             throw new UnsupportedOperationException();
         }
 
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegionTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegionTest.java
index 8c5a6bb..3b67d04 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegionTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegionTest.java
@@ -589,7 +589,7 @@ public class AbstractConvexHyperplaneBoundedRegionTest {
         }
 
         @Override
-        public TestPoint2D getBarycenter() {
+        public TestPoint2D getCentroid() {
             throw new UnsupportedOperationException();
         }
 
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java
index 59dd3b0..50e946f 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java
@@ -640,7 +640,7 @@ public class AbstractRegionBSPTreeTest {
         Assert.assertNotSame(second, third);
 
         Assert.assertEquals(1234, first.getSize(), PartitionTestUtils.EPS);
-        PartitionTestUtils.assertPointsEqual(new TestPoint2D(12, 34), first.getBarycenter());
+        PartitionTestUtils.assertPointsEqual(new TestPoint2D(12, 34), first.getCentroid());
     }
 
     @Test
@@ -651,10 +651,10 @@ public class AbstractRegionBSPTreeTest {
     }
 
     @Test
-    public void testGetBarycenter() {
+    public void testGetCentroid() {
         // act/assert
         // make sure our stub value is pulled
-        PartitionTestUtils.assertPointsEqual(new TestPoint2D(12, 34), tree.getBarycenter());
+        PartitionTestUtils.assertPointsEqual(new TestPoint2D(12, 34), tree.getCentroid());
     }
 
     @Test
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegment.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegment.java
index ab923b5..7ec2a4e 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegment.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegment.java
@@ -141,7 +141,7 @@ public class TestLineSegment implements HyperplaneConvexSubset<TestPoint2D> {
 
     /** {@inheritDoc} */
     @Override
-    public TestPoint2D getBarycenter() {
+    public TestPoint2D getCentroid() {
         return line.toSpace(0.5 * (end - start));
     }
 
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegmentCollection.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegmentCollection.java
index 4918fc3..5b05916 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegmentCollection.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/test/TestLineSegmentCollection.java
@@ -110,7 +110,7 @@ public class TestLineSegmentCollection implements HyperplaneSubset<TestPoint2D>
 
     /** {@inheritDoc} */
     @Override
-    public TestPoint2D getBarycenter() {
+    public TestPoint2D getCentroid() {
         throw new UnsupportedOperationException();
     }
 
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractBounds.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractBounds.java
index 307d98b..e1c7d7f 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractBounds.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractBounds.java
@@ -66,10 +66,10 @@ public abstract class AbstractBounds<
         return min.vectorTo(max);
     }
 
-    /** Return the center of the bounding box.
-     * @return the center of the bounding box
+    /** Get the centroid, or geometric center, of the bounding box.
+     * @return the centroid of the bounding box
      */
-    public P getBarycenter() {
+    public P getCentroid() {
         return min.lerp(max, 0.5);
     }
 
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractNSphere.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractNSphere.java
index d7a8abf..d58b948 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractNSphere.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractNSphere.java
@@ -110,7 +110,7 @@ public abstract class AbstractNSphere<V extends EuclideanVector<V>> implements R
      * <p>This method is an alias for {@link #getCenter()}.</p>
      */
     @Override
-    public V getBarycenter() {
+    public V getCentroid() {
         return getCenter();
     }
 
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Interval.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Interval.java
index 57a2dd6..020b1bc 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Interval.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Interval.java
@@ -281,7 +281,7 @@ public final class Interval implements HyperplaneBoundedRegion<Vector1D> {
 
     /** {@inheritDoc} */
     @Override
-    public Vector1D getBarycenter() {
+    public Vector1D getCentroid() {
         if (isInfinite()) {
             return null;
         }
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 1e641b8..8a70fd6 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
@@ -320,7 +320,7 @@ public final class OrientedPoint extends AbstractHyperplane<Vector1D>
         *  <p>This method returns the point for the defining hyperplane.</p>
         */
         @Override
-        public Vector1D getBarycenter() {
+        public Vector1D getCentroid() {
             return hyperplane.getPoint();
         }
 
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1D.java
index 28ba778..92224be 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1D.java
@@ -508,26 +508,26 @@ public final class RegionBSPTree1D extends AbstractRegionBSPTree<Vector1D, Regio
         /** Total computed size of all inside regions. */
         private double size;
 
-        /** Raw sum of the barycenters of each inside interval. */
-        private double rawBarycenterSum;
+        /** Raw sum of the centroids of each inside interval. */
+        private double rawCentroidSum;
 
-        /** The sum of the barycenter of each inside interval, scaled by the size of the interval. */
-        private double scaledBarycenterSum;
+        /** The sum of the centroids of each inside interval, scaled by the size of the interval. */
+        private double scaledCentroidSum;
 
         /** {@inheritDoc} */
         @Override
-        public void accept(OrientedPoint min, OrientedPoint max) {
+        public void accept(final OrientedPoint min, final OrientedPoint max) {
             ++count;
 
             final double minLoc = (min != null) ? min.getLocation() : Double.NEGATIVE_INFINITY;
             final double maxLoc = (max != null) ? max.getLocation() : Double.POSITIVE_INFINITY;
 
             final double intervalSize = maxLoc - minLoc;
-            final double intervalBarycenter = 0.5 * (maxLoc + minLoc);
+            final double intervalCentroid = 0.5 * (maxLoc + minLoc);
 
             size += intervalSize;
-            rawBarycenterSum += intervalBarycenter;
-            scaledBarycenterSum += intervalSize * intervalBarycenter;
+            rawCentroidSum += intervalCentroid;
+            scaledCentroidSum += intervalSize * intervalCentroid;
         }
 
         /** Get the computed properties for the region. This must only be called after
@@ -535,20 +535,20 @@ public final class RegionBSPTree1D extends AbstractRegionBSPTree<Vector1D, Regio
          * @return properties for the region
          */
         public RegionSizeProperties<Vector1D> getRegionSizeProperties() {
-            Vector1D barycenter = null;
+            Vector1D centroid = null;
 
             if (count > 0 && Double.isFinite(size)) {
                 if (size > 0.0) {
                     // use the scaled sum if we have a non-zero size
-                    barycenter = Vector1D.of(scaledBarycenterSum / size);
+                    centroid = Vector1D.of(scaledCentroidSum / size);
                 } else {
                     // use the raw sum if we don't have a size; this will be
                     // the case if the region only contains points with zero size
-                    barycenter = Vector1D.of(rawBarycenterSum / count);
+                    centroid = Vector1D.of(rawCentroidSum / count);
                 }
             }
 
-            return new RegionSizeProperties<>(size, barycenter);
+            return new RegionSizeProperties<>(size, centroid);
         }
     }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractConvexPolygon3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractConvexPolygon3D.java
index 8d69812..c8d0f32 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractConvexPolygon3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractConvexPolygon3D.java
@@ -95,13 +95,13 @@ abstract class AbstractConvexPolygon3D extends AbstractPlaneSubset implements Co
 
     /** {@inheritDoc} */
     @Override
-    public Vector3D getBarycenter() {
+    public Vector3D getCentroid() {
         final List<Vector3D> vertices = getVertices();
 
         double areaSum = 0.0;
-        double scaledBarycenterSumX = 0.0;
-        double scaledBarycenterSumY = 0.0;
-        double scaledBarycenterSumZ = 0.0;
+        double scaledCentroidSumX = 0.0;
+        double scaledCentroidSumY = 0.0;
+        double scaledCentroidSumZ = 0.0;
 
         Iterator<Vector3D> it = vertices.iterator();
 
@@ -114,19 +114,19 @@ abstract class AbstractConvexPolygon3D extends AbstractPlaneSubset implements Co
         Vector3D curVec = null;
 
         double triArea;
-        Vector3D triBarycenter;
+        Vector3D triCentroid;
         while (it.hasNext()) {
             curPt = it.next();
             curVec = startPt.vectorTo(curPt);
 
             triArea = 0.5 * prevVec.cross(curVec).norm();
-            triBarycenter = Vector3D.centroid(startPt, prevPt, curPt);
+            triCentroid = Vector3D.centroid(startPt, prevPt, curPt);
 
             areaSum += triArea;
 
-            scaledBarycenterSumX += triArea * triBarycenter.getX();
-            scaledBarycenterSumY += triArea * triBarycenter.getY();
-            scaledBarycenterSumZ += triArea * triBarycenter.getZ();
+            scaledCentroidSumX += triArea * triCentroid.getX();
+            scaledCentroidSumY += triArea * triCentroid.getY();
+            scaledCentroidSumZ += triArea * triCentroid.getZ();
 
             prevPt = curPt;
             prevVec = curVec;
@@ -135,9 +135,9 @@ abstract class AbstractConvexPolygon3D extends AbstractPlaneSubset implements Co
         if (areaSum > 0.0) {
             final double scale = 1 / areaSum;
             return Vector3D.of(
-                        scale * scaledBarycenterSumX,
-                        scale * scaledBarycenterSumY,
-                        scale * scaledBarycenterSumZ
+                        scale * scaledCentroidSumX,
+                        scale * scaledCentroidSumY,
+                        scale * scaledCentroidSumZ
                     );
         }
 
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractEmbeddedRegionPlaneSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractEmbeddedRegionPlaneSubset.java
index b19f3c4..d535533 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractEmbeddedRegionPlaneSubset.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractEmbeddedRegionPlaneSubset.java
@@ -69,10 +69,10 @@ abstract class AbstractEmbeddedRegionPlaneSubset extends AbstractPlaneSubset imp
 
     /** {@inheritDoc} */
     @Override
-    public Vector3D getBarycenter() {
-        final Vector2D subspaceBarycenter = getSubspaceRegion().getBarycenter();
-        if (subspaceBarycenter != null) {
-            return getPlane().toSpace(subspaceBarycenter);
+    public Vector3D getCentroid() {
+        final Vector2D subspaceCentroid = getSubspaceRegion().getCentroid();
+        if (subspaceCentroid != null) {
+            return getPlane().toSpace(subspaceCentroid);
         }
         return null;
     }
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 fb9e940..19465ee 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
@@ -66,9 +66,9 @@ public class ConvexVolume extends AbstractConvexHyperplaneBoundedRegion<Vector3D
 
             final Plane boundaryPlane = boundary.getPlane();
             final double boundaryArea = boundary.getSize();
-            final Vector3D boundaryBarycenter = boundary.getBarycenter();
+            final Vector3D boundaryCentroid = boundary.getCentroid();
 
-            volumeSum += boundaryArea * boundaryBarycenter.dot(boundaryPlane.getNormal());
+            volumeSum += boundaryArea * boundaryCentroid.dot(boundaryPlane.getNormal());
         }
 
         return volumeSum / 3.0;
@@ -76,7 +76,7 @@ public class ConvexVolume extends AbstractConvexHyperplaneBoundedRegion<Vector3D
 
     /** {@inheritDoc} */
     @Override
-    public Vector3D getBarycenter() {
+    public Vector3D getCentroid() {
         double volumeSum = 0.0;
 
         double sumX = 0.0;
@@ -90,15 +90,15 @@ public class ConvexVolume extends AbstractConvexHyperplaneBoundedRegion<Vector3D
 
             final Plane boundaryPlane = boundary.getPlane();
             final double boundaryArea = boundary.getSize();
-            final Vector3D boundaryBarycenter = boundary.getBarycenter();
+            final Vector3D boundaryCentroid = boundary.getCentroid();
 
-            final double scaledVolume = boundaryArea * boundaryBarycenter.dot(boundaryPlane.getNormal());
+            final double scaledVolume = boundaryArea * boundaryCentroid.dot(boundaryPlane.getNormal());
 
             volumeSum += scaledVolume;
 
-            sumX += scaledVolume * boundaryBarycenter.getX();
-            sumY += scaledVolume * boundaryBarycenter.getY();
-            sumZ += scaledVolume * boundaryBarycenter.getZ();
+            sumX += scaledVolume * boundaryCentroid.getX();
+            sumY += scaledVolume * boundaryCentroid.getY();
+            sumZ += scaledVolume * boundaryCentroid.getZ();
         }
 
         if (volumeSum > 0) {
@@ -106,12 +106,12 @@ public class ConvexVolume extends AbstractConvexHyperplaneBoundedRegion<Vector3D
 
             // Since the volume we used when adding together the boundary contributions
             // was 3x the actual pyramid size, we'll multiply by 1/4 here instead
-            // of 3/4 to adjust for the actual barycenter position in each pyramid.
-            final double barycenterScale = 1.0 / (4 * size);
+            // of 3/4 to adjust for the actual centroid position in each pyramid.
+            final double centroidScale = 1.0 / (4 * size);
             return Vector3D.of(
-                    sumX * barycenterScale,
-                    sumY * barycenterScale,
-                    sumZ * barycenterScale);
+                    sumX * centroidScale,
+                    sumY * centroidScale,
+                    sumZ * centroidScale);
         }
 
         return null;
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 66126fe..b4c1781 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
@@ -283,13 +283,13 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
      *  The volume of the region is computed using the equation
      *  <code>V = (1/3)*&Sigma;<sub>F</sub>[(C<sub>F</sub>&sdot;N<sub>F</sub>)*area(F)]</code>,
      *  where <code>F</code> represents each face in the region, <code>C<sub>F</sub></code>
-     *  represents the barycenter of the face, and <code>N<sub>F</sub></code> represents the
+     *  represents the centroid of the face, and <code>N<sub>F</sub></code> represents the
      *  normal of the face. (More details can be found in the article
      *  <a href="https://en.wikipedia.org/wiki/Polyhedron#Volume">here</a>.)
      *  This essentially splits up the region into pyramids with a 2D face forming
-     *  the base of each pyramid. The barycenter is computed in a similar way. The barycenter
+     *  the base of each pyramid. The centroid is computed in a similar way. The centroid
      *  of each pyramid is calculated using the fact that it is located 3/4 of the way along the
-     *  line from the apex to the base. The region barycenter then becomes the volume-weighted
+     *  line from the apex to the base. The region centroid then becomes the volume-weighted
      *  average of these pyramid centers.
      *  @see https://en.wikipedia.org/wiki/Polyhedron#Volume
      */
@@ -298,13 +298,13 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
         /** Accumulator for boundary volume contributions. */
         private double volumeSum;
 
-        /** Barycenter contribution x coordinate accumulator. */
+        /** Centroid contribution x coordinate accumulator. */
         private double sumX;
 
-        /** Barycenter contribution y coordinate accumulator. */
+        /** Centroid contribution y coordinate accumulator. */
         private double sumY;
 
-        /** Barycenter contribution z coordinate accumulator. */
+        /** Centroid contribution z coordinate accumulator. */
         private double sumZ;
 
         /** {@inheritDoc} */
@@ -324,7 +324,7 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
          */
         public RegionSizeProperties<Vector3D> getRegionSizeProperties() {
             double size = Double.POSITIVE_INFINITY;
-            Vector3D barycenter = null;
+            Vector3D centroid = null;
 
             // we only have a finite size if the volume sum is finite and positive
             // (negative indicates a finite outside surrounded by an infinite inside)
@@ -334,15 +334,15 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
 
                 // Since the volume we used when adding together the boundary contributions
                 // was 3x the actual pyramid size, we'll multiply by 1/4 here instead
-                // of 3/4 to adjust for the actual barycenter position in each pyramid.
-                final double barycenterScale = 1.0 / (4 * size);
-                barycenter =  Vector3D.of(
-                        sumX * barycenterScale,
-                        sumY * barycenterScale,
-                        sumZ * barycenterScale);
+                // of 3/4 to adjust for the actual centroid position in each pyramid.
+                final double centroidScale = 1.0 / (4 * size);
+                centroid =  Vector3D.of(
+                        sumX * centroidScale,
+                        sumY * centroidScale,
+                        sumZ * centroidScale);
             }
 
-            return new RegionSizeProperties<>(size, barycenter);
+            return new RegionSizeProperties<>(size, centroid);
         }
 
         /** Add the contribution of the given node cut boundary. If {@code reverse} is true,
@@ -355,23 +355,23 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
 
             final Plane boundaryPlane = boundarySubset.getPlane();
             final double boundaryArea = boundarySubset.getSize();
-            final Vector3D boundaryBarycenter = boundarySubset.getBarycenter();
+            final Vector3D boundaryCentroid = boundarySubset.getCentroid();
 
             if (Double.isInfinite(boundaryArea)) {
                 volumeSum = Double.POSITIVE_INFINITY;
-            } else if (boundaryBarycenter != null) {
+            } else if (boundaryCentroid != null) {
                 // the volume here is actually 3x the actual pyramid volume; we'll apply
                 // the final scaling all at once at the end
-                double scaledVolume = boundaryArea * boundaryBarycenter.dot(boundaryPlane.getNormal());
+                double scaledVolume = boundaryArea * boundaryCentroid.dot(boundaryPlane.getNormal());
                 if (reverse) {
                     scaledVolume = -scaledVolume;
                 }
 
                 volumeSum += scaledVolume;
 
-                sumX += scaledVolume * boundaryBarycenter.getX();
-                sumY += scaledVolume * boundaryBarycenter.getY();
-                sumZ += scaledVolume * boundaryBarycenter.getZ();
+                sumX += scaledVolume * boundaryCentroid.getX();
+                sumY += scaledVolume * boundaryCentroid.getY();
+                sumZ += scaledVolume * boundaryCentroid.getZ();
             }
         }
     }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SimpleTriangle3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SimpleTriangle3D.java
index e5e8b39..f016fbd 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SimpleTriangle3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SimpleTriangle3D.java
@@ -85,7 +85,7 @@ final class SimpleTriangle3D extends AbstractConvexPolygon3D implements Triangle
 
     /** {@inheritDoc} */
     @Override
-    public Vector3D getBarycenter() {
+    public Vector3D getCentroid() {
         return Vector3D.centroid(p1, p2, p3);
     }
 
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3D.java
index 71e2fa1..95b5d78 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3D.java
@@ -79,8 +79,8 @@ public final class EmbeddedTreeLineSubset3D extends LineSubset3D {
 
     /** {@inheritDoc} */
     @Override
-    public Vector3D getBarycenter() {
-        final Vector1D subcenter = region.getBarycenter();
+    public Vector3D getCentroid() {
+        final Vector1D subcenter = region.getCentroid();
         return subcenter != null ?
                 getLine().toSpace(subcenter) :
                 null;
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSpanningSubset3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSpanningSubset3D.java
index 0c75a9a..48ddfec 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSpanningSubset3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSpanningSubset3D.java
@@ -104,7 +104,7 @@ final class LineSpanningSubset3D extends LineConvexSubset3D {
      * <p>This method always returns {@code null}.</p>
      */
     @Override
-    public Vector3D getBarycenter() {
+    public Vector3D getCentroid() {
         return null; // infinite; no center
     }
 
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSubset3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSubset3D.java
index 0c3c110..2bea44e 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSubset3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/LineSubset3D.java
@@ -56,12 +56,12 @@ public abstract class LineSubset3D implements RegionEmbedding<Vector3D, Vector1D
         return line.toSubspace(pt);
     }
 
-    /** Get the center of the line subset, or null if the subset is empty of
-     * infinite.
-     * @return the center of the line subset, or null if the subset is empty of
+    /** Get the centroid, or geometric center, of the line subset or null if
+     * the subset is empty or infinite.
+     * @return the centroid of the line subset, or null if the subset is empty or
      *      infinite
      */
-    public abstract Vector3D getBarycenter();
+    public abstract Vector3D getCentroid();
 
     /** Get the 3D bounding box of the line subset or null if the subset is
      * empty or infinite.
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Ray3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Ray3D.java
index 1448efd..42df584 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Ray3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Ray3D.java
@@ -113,7 +113,7 @@ public final class Ray3D extends LineConvexSubset3D {
      * <p>This method always returns {@code null}.</p>
      */
     @Override
-    public Vector3D getBarycenter() {
+    public Vector3D getCentroid() {
         return null; // infinite; no center
     }
 
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/ReverseRay3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/ReverseRay3D.java
index 3fb8aa2..6e1e0ef 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/ReverseRay3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/ReverseRay3D.java
@@ -114,7 +114,7 @@ public final class ReverseRay3D extends LineConvexSubset3D {
      * <p>This method always returns {@code null}.</p>
      */
     @Override
-    public Vector3D getBarycenter() {
+    public Vector3D getCentroid() {
         return null; // infinite; no center
     }
 
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Segment3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Segment3D.java
index 62f0a4b..255a73b 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Segment3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/line/Segment3D.java
@@ -109,7 +109,7 @@ public final class Segment3D extends LineConvexSubset3D {
 
     /** {@inheritDoc} */
     @Override
-    public Vector3D getBarycenter() {
+    public Vector3D getCentroid() {
         return getLine().toSpace((0.5 * (end - start)) + start);
     }
 
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 0aecbeb..0a1a4cc 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
@@ -143,7 +143,7 @@ public class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vector2D,
 
     /** {@inheritDoc} */
     @Override
-    public Vector2D getBarycenter() {
+    public Vector2D getCentroid() {
         final List<LineConvexSubset> boundaries = getBoundaries();
 
         double quadrilateralAreaSum = 0.0;
@@ -156,7 +156,7 @@ public class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vector2D,
 
         for (final LineConvexSubset seg : boundaries) {
             if (seg.isInfinite()) {
-                // infinite => no barycenter
+                // infinite => no centroid
                 return null;
             }
 
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/EmbeddedTreeLineSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/EmbeddedTreeLineSubset.java
index 8a56114..4786c2a 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/EmbeddedTreeLineSubset.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/EmbeddedTreeLineSubset.java
@@ -91,10 +91,10 @@ public final class EmbeddedTreeLineSubset extends LineSubset {
 
     /** {@inheritDoc} */
     @Override
-    public Vector2D getBarycenter() {
-        final Vector1D subspaceBarycenter = region.getBarycenter();
-        if (subspaceBarycenter != null) {
-            return getLine().toSpace(subspaceBarycenter);
+    public Vector2D getCentroid() {
+        final Vector1D subspaceCentroid = region.getCentroid();
+        if (subspaceCentroid != null) {
+            return getLine().toSpace(subspaceCentroid);
         }
         return null;
     }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSpanningSubset.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSpanningSubset.java
index 8c504f5..a1c5d7a 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSpanningSubset.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/LineSpanningSubset.java
@@ -77,7 +77,7 @@ final class LineSpanningSubset extends LineConvexSubset {
      * <p>This method always returns {@code null}.</p>
      */
     @Override
-    public Vector2D getBarycenter() {
+    public Vector2D getCentroid() {
         return null;
     }
 
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Ray.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Ray.java
index 304b8a7..e127845 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Ray.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Ray.java
@@ -85,7 +85,7 @@ public final class Ray extends LineConvexSubset {
      * <p>This method always returns {@code null}.</p>
      */
     @Override
-    public Vector2D getBarycenter() {
+    public Vector2D getCentroid() {
         return null;
     }
 
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 004b369..fc44228 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
@@ -232,13 +232,13 @@ public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, Regio
 
             quadrilateralAreaSum += signedArea;
 
-            // compute scaled coordinate values for the barycenter
+            // compute scaled coordinate values for the centroid
             scaledSumX += signedArea * (startPoint.getX() + endPoint.getX());
             scaledSumY += signedArea * (startPoint.getY() + endPoint.getY());
         }
 
         double size = Double.POSITIVE_INFINITY;
-        Vector2D barycenter = null;
+        Vector2D centroid = null;
 
         // The area is finite only if the computed quadrilateral area is finite and non-negative.
         // Negative areas indicate that the region is inside-out, with a finite outside surrounded
@@ -247,11 +247,11 @@ public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, Regio
             size = 0.5 * quadrilateralAreaSum;
 
             if (quadrilateralAreaSum > 0.0) {
-                barycenter = Vector2D.of(scaledSumX, scaledSumY).multiply(1.0 / (3.0 * quadrilateralAreaSum));
+                centroid = Vector2D.of(scaledSumX, scaledSumY).multiply(1.0 / (3.0 * quadrilateralAreaSum));
             }
         }
 
-        return new RegionSizeProperties<>(size, barycenter);
+        return new RegionSizeProperties<>(size, centroid);
     }
 
     /** {@inheritDoc} */
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ReverseRay.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ReverseRay.java
index 6abec6a..b35e969 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ReverseRay.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ReverseRay.java
@@ -86,7 +86,7 @@ public final class ReverseRay extends LineConvexSubset {
      * <p>This method always returns {@code null}.</p>
      */
     @Override
-    public Vector2D getBarycenter() {
+    public Vector2D getCentroid() {
         return null;
     }
 
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java
index 8fa77b0..e36f878 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java
@@ -85,7 +85,7 @@ public final class Segment extends LineConvexSubset {
 
     /** {@inheritDoc} */
     @Override
-    public Vector2D getBarycenter() {
+    public Vector2D getCentroid() {
         return startPoint.lerp(endPoint, 0.5);
     }
 
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 0051704..2ba5d8a 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
@@ -67,7 +67,7 @@ public class DocumentationExamplesTest {
         // 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
+        // create a binary space partitioning (BSP) tree representing the unit cube
         // centered on the origin
         RegionBSPTree3D region = Parallelepiped.builder(precision)
                 .setPosition(Vector3D.ZERO)
@@ -84,11 +84,11 @@ public class DocumentationExamplesTest {
 
         // compute some properties of the intersection region
         double size = region.getSize(); // 0.8284271247461903
-        Vector3D center = region.getBarycenter(); // (0, 0, 0)
+        Vector3D centroid = region.getCentroid(); // (0, 0, 0)
 
         // -----------
         Assert.assertEquals(0.8284271247461903, size, TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, center, TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, centroid, TEST_EPS);
     }
 
     @Test
@@ -167,13 +167,13 @@ public class DocumentationExamplesTest {
         int count = tree.count(); // number of nodes in the tree = 11
         int height = tree.height(); // height of the tree = 3
         double size = tree.getSize(); // size of the region = 1
-        Vector2D barycenter = tree.getBarycenter(); // region barycenter = (0.5, 0.5)
+        Vector2D centroid = tree.getCentroid(); // region centroid = (0.5, 0.5)
 
         // ---------
         Assert.assertEquals(1, size, TEST_EPS);
         Assert.assertEquals(11, count);
         Assert.assertEquals(3, height);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), barycenter, TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), centroid, TEST_EPS);
     }
 
     @Test
@@ -195,13 +195,13 @@ public class DocumentationExamplesTest {
         int count = tree.count(); // number of nodes in the tree = 9
         int height = tree.height(); // height of the tree = 4
         double size = tree.getSize(); // size of the region = 1
-        Vector2D barycenter = tree.getBarycenter(); // region barycenter = (0.5, 0.5)
+        Vector2D centroid = tree.getCentroid(); // region centroid = (0.5, 0.5)
 
         // ---------
         Assert.assertEquals(1, size, TEST_EPS);
         Assert.assertEquals(9, count);
         Assert.assertEquals(4, height);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), barycenter, TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), centroid, TEST_EPS);
     }
 
     @Test
@@ -321,7 +321,7 @@ public class DocumentationExamplesTest {
 
         // compute some properties
         double size = tree.getSize(); // 1.75
-        Vector2D center = tree.getBarycenter(); // (0.75, 0.75)
+        Vector2D centroid = tree.getCentroid(); // (0.75, 0.75)
 
         // get a line path representing the boundary; a list is returned since trees
         // can represent disjoint regions
@@ -329,7 +329,7 @@ public class DocumentationExamplesTest {
 
         // ----------------
         Assert.assertEquals(1.75, size, TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.75, 0.75), center, TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.75, 0.75), centroid, TEST_EPS);
         Assert.assertEquals(1, boundaries.size());
     }
 
@@ -430,8 +430,8 @@ public class DocumentationExamplesTest {
         List<ConvexPolygon3D> faces = Planes.indexedConvexPolygons(vertices, faceIndices, precision);
         RegionBSPTree3D tree = RegionBSPTree3D.from(faces);
 
-        // split the region through its barycenter along a diagonal of the base
-        Plane cutter = Planes.fromPointAndNormal(tree.getBarycenter(), Vector3D.Unit.from(1, 1, 0), precision);
+        // split the region through its centroid along a diagonal of the base
+        Plane cutter = Planes.fromPointAndNormal(tree.getCentroid(), Vector3D.Unit.from(1, 1, 0), precision);
         Split<RegionBSPTree3D> split = tree.split(cutter);
 
         // compute some properties for the minus side of the split and convert back to hyperplane subsets
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/IntervalTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/IntervalTest.java
index 151e0fa..2a49706 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/IntervalTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/IntervalTest.java
@@ -405,21 +405,21 @@ public class IntervalTest {
     }
 
     @Test
-    public void testGetBarycenter() {
+    public void testGetCentroid() {
         // act/assert
         EuclideanTestUtils.assertCoordinatesEqual(Vector1D.ZERO,
-                Interval.of(-1, 1, TEST_PRECISION).getBarycenter(), TEST_EPS);
+                Interval.of(-1, 1, TEST_PRECISION).getCentroid(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(10),
-                Interval.of(10, 10, TEST_PRECISION).getBarycenter(), TEST_EPS);
+                Interval.of(10, 10, TEST_PRECISION).getCentroid(), TEST_EPS);
 
         EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(2),
-                Interval.of(1, 3, TEST_PRECISION).getBarycenter(), TEST_EPS);
+                Interval.of(1, 3, TEST_PRECISION).getCentroid(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(-1),
-                Interval.of(-2, 0, TEST_PRECISION).getBarycenter(), TEST_EPS);
+                Interval.of(-2, 0, TEST_PRECISION).getCentroid(), TEST_EPS);
 
-        Assert.assertNull(Interval.of(1, Double.POSITIVE_INFINITY, TEST_PRECISION).getBarycenter());
-        Assert.assertNull(Interval.of(Double.NEGATIVE_INFINITY, 1, TEST_PRECISION).getBarycenter());
-        Assert.assertNull(Interval.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TEST_PRECISION).getBarycenter());
+        Assert.assertNull(Interval.of(1, Double.POSITIVE_INFINITY, TEST_PRECISION).getCentroid());
+        Assert.assertNull(Interval.of(Double.NEGATIVE_INFINITY, 1, TEST_PRECISION).getCentroid());
+        Assert.assertNull(Interval.of(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TEST_PRECISION).getCentroid());
     }
 
     @Test
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 249d89b..1e97c50 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
@@ -434,7 +434,7 @@ public class OrientedPointTest {
         Assert.assertFalse(sub.isInfinite());
         Assert.assertTrue(sub.isFinite());
         Assert.assertEquals(0.0, sub.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(2), sub.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(2), sub.getCentroid(), TEST_EPS);
 
         List<? extends HyperplaneConvexSubset<Vector1D>> list = sub.toConvex();
         Assert.assertEquals(1, list.size());
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1DTest.java
index f6f1525..b355e57 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1DTest.java
@@ -962,7 +962,7 @@ public class RegionBSPTree1DTest {
     }
 
     @Test
-    public void testGetBarycenter_infinite() {
+    public void testGetCentroid_infinite() {
         // arrange
         RegionBSPTree1D full = RegionBSPTree1D.full();
 
@@ -973,22 +973,22 @@ public class RegionBSPTree1DTest {
         negHalfSpace.getRoot().cut(OrientedPoints.createPositiveFacing(3.0, TEST_PRECISION));
 
         // act/assert
-        Assert.assertNull(full.getBarycenter());
-        Assert.assertNull(posHalfSpace.getBarycenter());
-        Assert.assertNull(negHalfSpace.getBarycenter());
+        Assert.assertNull(full.getCentroid());
+        Assert.assertNull(posHalfSpace.getCentroid());
+        Assert.assertNull(negHalfSpace.getCentroid());
     }
 
     @Test
-    public void testGetBarycenter_empty() {
+    public void testGetCentroid_empty() {
         // arrange
         RegionBSPTree1D tree = RegionBSPTree1D.empty();
 
         // act/assert
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
     }
 
     @Test
-    public void testGetBarycenter_exactPoints() {
+    public void testGetCentroid_exactPoints() {
         // arrange
         RegionBSPTree1D singlePoint = RegionBSPTree1D.empty();
         singlePoint.add(Interval.of(1, 1, TEST_PRECISION));
@@ -999,12 +999,12 @@ public class RegionBSPTree1DTest {
         multiplePoints.add(Interval.of(6, 6, TEST_PRECISION));
 
         // act/assert
-        EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(1), singlePoint.getBarycenter(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(2), multiplePoints.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(1), singlePoint.getCentroid(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(2), multiplePoints.getCentroid(), TEST_EPS);
     }
 
     @Test
-    public void testGetBarycenter_pointsWithinPrecision() {
+    public void testGetCentroid_pointsWithinPrecision() {
      // arrange
         DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-1);
 
@@ -1017,23 +1017,23 @@ public class RegionBSPTree1DTest {
         multiplePoints.add(Interval.of(6, 6.02, precision));
 
         // act/assert
-        EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(1.01), singlePoint.getBarycenter(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(6.01 / 3), multiplePoints.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(1.01), singlePoint.getCentroid(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(6.01 / 3), multiplePoints.getCentroid(), TEST_EPS);
     }
 
     @Test
-    public void testGetBarycenter_nonEmptyIntervals() {
+    public void testGetCentroid_nonEmptyIntervals() {
         // arrange
         RegionBSPTree1D tree = RegionBSPTree1D.empty();
         tree.add(Interval.of(1, 2, TEST_PRECISION));
         tree.add(Interval.of(3, 5, TEST_PRECISION));
 
         // act/assert
-        EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(9.5 / 3), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(9.5 / 3), tree.getCentroid(), TEST_EPS);
     }
 
     @Test
-    public void testGetBarycenter_complementedRegion() {
+    public void testGetCentroid_complementedRegion() {
         // arrange
         RegionBSPTree1D tree = RegionBSPTree1D.empty();
         tree.add(Interval.of(Double.NEGATIVE_INFINITY, 2, TEST_PRECISION));
@@ -1042,11 +1042,11 @@ public class RegionBSPTree1DTest {
         tree.complement();
 
         // act/assert
-        EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(3), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(3), tree.getCentroid(), TEST_EPS);
     }
 
     @Test
-    public void testGetBarycenter_intervalWithPoints() {
+    public void testGetCentroid_intervalWithPoints() {
         // arrange
         RegionBSPTree1D tree = RegionBSPTree1D.empty();
         tree.add(Interval.of(1, 2, TEST_PRECISION));
@@ -1054,7 +1054,7 @@ public class RegionBSPTree1DTest {
         tree.add(Interval.of(5, 5, TEST_PRECISION));
 
         // act/assert
-        EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(1.5), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector1D.of(1.5), tree.getCentroid(), TEST_EPS);
     }
 
     @Test
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AbstractPlaneSubsetTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AbstractPlaneSubsetTest.java
index 846a95e..4527771 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AbstractPlaneSubsetTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AbstractPlaneSubsetTest.java
@@ -314,7 +314,7 @@ public class AbstractPlaneSubsetTest {
         }
 
         @Override
-        public Vector3D getBarycenter() {
+        public Vector3D getCentroid() {
             throw new UnsupportedOperationException();
         }
 
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Bounds3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Bounds3DTest.java
index 7693870..0fb2b0a 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Bounds3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Bounds3DTest.java
@@ -54,7 +54,7 @@ public class Bounds3DTest {
         EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMin(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMax(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, b.getDiagonal(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(p1, b.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(p1, b.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -71,7 +71,7 @@ public class Bounds3DTest {
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 5, 7), b.getMin(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 6, 11), b.getMax(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 1, 4), b.getDiagonal(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.5, 5.5, 9), b.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.5, 5.5, 9), b.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -86,7 +86,7 @@ public class Bounds3DTest {
         EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMin(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMax(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, b.getDiagonal(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(p1, b.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(p1, b.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -103,7 +103,7 @@ public class Bounds3DTest {
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 4, 7), b.getMin(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 6, 9), b.getMax(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 2, 2), b.getDiagonal(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 5, 8), b.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 5, 8), b.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -189,7 +189,7 @@ public class Bounds3DTest {
 
         // act/assert
         assertContainsStrict(b, true,
-                b.getBarycenter(),
+                b.getCentroid(),
                 Vector3D.of(0, 4, 8), Vector3D.of(2, 6, 10),
                 Vector3D.of(1, 5, 9),
                 Vector3D.of(0, 5, 9), Vector3D.of(2, 5, 9),
@@ -213,7 +213,7 @@ public class Bounds3DTest {
 
         // act/assert
         assertContainsWithPrecision(b, true,
-                b.getBarycenter(),
+                b.getCentroid(),
                 Vector3D.of(0, 4, 8), Vector3D.of(2, 6, 10),
                 Vector3D.of(1, 5, 9),
                 Vector3D.of(0, 5, 9), Vector3D.of(2, 5, 9),
@@ -374,7 +374,7 @@ public class Bounds3DTest {
 
         // assert
         Assert.assertEquals(8, p.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 5, 9), p.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 5, 9), p.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -496,7 +496,7 @@ public class Bounds3DTest {
         // assert
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 6, 11), b.getMin(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(5, 10, 15), b.getMax(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 8, 13), b.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(3, 8, 13), b.getCentroid(), TEST_EPS);
     }
 
     @Test
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 a0a83c1..7d94421 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
@@ -51,7 +51,7 @@ public class ConvexVolumeTest {
         Assert.assertFalse(vol.isEmpty());
 
         GeometryTestUtils.assertPositiveInfinity(vol.getSize());
-        Assert.assertNull(vol.getBarycenter());
+        Assert.assertNull(vol.getCentroid());
 
         Assert.assertEquals(0, vol.getBoundaries().size());
         Assert.assertEquals(0, vol.getBoundarySize(), TEST_EPS);
@@ -210,7 +210,7 @@ public class ConvexVolumeTest {
         // assert
         Assert.assertEquals(1, tree.getSize(), TEST_EPS);
         Assert.assertEquals(6, tree.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), tree.getCentroid(), TEST_EPS);
 
         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.OUTSIDE,
                 Vector3D.of(-1, 0.5, 0.5), Vector3D.of(2, 0.5, 0.5),
@@ -239,7 +239,7 @@ public class ConvexVolumeTest {
         Assert.assertFalse(vol.isEmpty());
 
         GeometryTestUtils.assertPositiveInfinity(vol.getSize());
-        Assert.assertNull(vol.getBarycenter());
+        Assert.assertNull(vol.getCentroid());
 
         Assert.assertEquals(1, vol.getBoundaries().size());
         GeometryTestUtils.assertPositiveInfinity(vol.getBoundarySize());
@@ -259,7 +259,7 @@ public class ConvexVolumeTest {
         Assert.assertFalse(vol.isEmpty());
 
         Assert.assertEquals(8, vol.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, 1), vol.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, 1), vol.getCentroid(), TEST_EPS);
 
         Assert.assertEquals(6, vol.getBoundaries().size());
         Assert.assertEquals(28, vol.getBoundarySize(), TEST_EPS);
@@ -313,11 +313,11 @@ public class ConvexVolumeTest {
 
         ConvexVolume minus = split.getMinus();
         Assert.assertEquals(0.5, minus.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-0.25, 0, 0), minus.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-0.25, 0, 0), minus.getCentroid(), TEST_EPS);
 
         ConvexVolume plus = split.getPlus();
         Assert.assertEquals(0.5, plus.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.25, 0, 0), plus.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.25, 0, 0), plus.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -377,7 +377,7 @@ public class ConvexVolumeTest {
         Assert.assertEquals(2, transformed.getSize(), TEST_EPS);
         Assert.assertEquals(10, transformed.getBoundarySize(), TEST_EPS);
 
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 2, 3), transformed.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2, 2, 3), transformed.getCentroid(), TEST_EPS);
     }
 
     private static ConvexVolume rect(Vector3D center, double xDelta, double yDelta, double zDelta) {
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubsetTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubsetTest.java
index 41dca76..245fd10 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubsetTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/EmbeddedTreePlaneSubsetTest.java
@@ -593,7 +593,7 @@ public class EmbeddedTreePlaneSubsetTest {
 
         // assert
         Assert.assertEquals(4, ps.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, 1), ps.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0, 0, 1), ps.getCentroid(), TEST_EPS);
 
         Bounds3D bounds = ps.getBounds();
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, -1, 1), bounds.getMin(), TEST_EPS);
@@ -620,7 +620,7 @@ public class EmbeddedTreePlaneSubsetTest {
 
         // assert
         Assert.assertEquals(2, ps.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0.5, 1), ps.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0.5, 1), ps.getCentroid(), TEST_EPS);
 
         Bounds3D bounds = ps.getBounds();
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-2, 0, 1), bounds.getMin(), TEST_EPS);
@@ -647,7 +647,7 @@ public class EmbeddedTreePlaneSubsetTest {
 
         // assert
         Assert.assertEquals(2, ps.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-0.5, -1, 1), ps.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-0.5, -1, 1), ps.getCentroid(), TEST_EPS);
 
         Bounds3D bounds = ps.getBounds();
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, -2, 1), bounds.getMin(), TEST_EPS);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlanesTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlanesTest.java
index ed8092d..6be9012 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlanesTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlanesTest.java
@@ -231,7 +231,7 @@ public class PlanesTest {
         // assert
         Assert.assertEquals(0.5, tri.getSize(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(5.0 / 3.0, 1, 4.0 / 3.0),
-                tri.getBarycenter(), TEST_EPS);
+                tri.getCentroid(), TEST_EPS);
     }
 
     @Test
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 f99430f..8ea9999 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
@@ -81,7 +81,7 @@ public class RegionBSPTree3DTest {
         Assert.assertFalse(tree.isFull());
         Assert.assertTrue(tree.isEmpty());
 
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
         Assert.assertEquals(0.0, tree.getSize(), TEST_EPS);
         Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
 
@@ -102,7 +102,7 @@ public class RegionBSPTree3DTest {
         Assert.assertTrue(tree.isFull());
         Assert.assertFalse(tree.isEmpty());
 
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
         GeometryTestUtils.assertPositiveInfinity(tree.getSize());
         Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
 
@@ -216,7 +216,7 @@ public class RegionBSPTree3DTest {
 
         // assert
         Assert.assertEquals(15.0 / 8.0, result.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.75, 0.75, 0.75), result.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.75, 0.75, 0.75), result.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -264,7 +264,7 @@ public class RegionBSPTree3DTest {
 
         EuclideanTestUtils.assertPositiveInfinity(tree.getSize());
         EuclideanTestUtils.assertPositiveInfinity(tree.getBoundarySize());
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
 
         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.INSIDE,
                 Vector3D.of(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE),
@@ -316,7 +316,7 @@ public class RegionBSPTree3DTest {
 
         Assert.assertEquals(1, tree.getSize(), TEST_EPS);
         Assert.assertEquals(6, tree.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), tree.getCentroid(), TEST_EPS);
 
         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.INSIDE, Vector3D.of(0.5, 0.5, 0.5));
         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.BOUNDARY, min, max);
@@ -388,7 +388,7 @@ public class RegionBSPTree3DTest {
 
         // act
         RegionBSPTree3D tree = volume.toTree();
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
 
         // assert
         Assert.assertTrue(tree.isFull());
@@ -405,7 +405,7 @@ public class RegionBSPTree3DTest {
         // assert
         GeometryTestUtils.assertPositiveInfinity(tree.getSize());
         GeometryTestUtils.assertPositiveInfinity(tree.getBoundarySize());
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
 
         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.OUTSIDE, Vector3D.of(0, 0, 1));
         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.BOUNDARY, Vector3D.ZERO);
@@ -431,7 +431,7 @@ public class RegionBSPTree3DTest {
         // assert
         Assert.assertEquals(1, tree.getSize(), TEST_EPS);
         Assert.assertEquals(6, tree.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), tree.getCentroid(), TEST_EPS);
 
         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.OUTSIDE,
                 Vector3D.of(-1, 0.5, 0.5), Vector3D.of(2, 0.5, 0.5),
@@ -784,7 +784,7 @@ public class RegionBSPTree3DTest {
 
         EuclideanTestUtils.assertPositiveInfinity(tree.getSize());
         Assert.assertEquals(6, tree.getBoundarySize(), TEST_EPS);
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
 
         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.INSIDE,
                 Vector3D.of(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE),
@@ -806,7 +806,7 @@ public class RegionBSPTree3DTest {
 
         Assert.assertEquals(1.0, tree.getSize(), TEST_EPS);
         Assert.assertEquals(6.0, tree.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, tree.getCentroid(), TEST_EPS);
 
         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.OUTSIDE,
                 Vector3D.of(-1, 0, 0),
@@ -868,7 +868,7 @@ public class RegionBSPTree3DTest {
 
         Assert.assertEquals(2.0, tree.getSize(), TEST_EPS);
         Assert.assertEquals(12.0, tree.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 0, 0), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 0, 0), tree.getCentroid(), TEST_EPS);
 
         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.OUTSIDE,
                 Vector3D.of(-1, 0, 0),
@@ -893,7 +893,7 @@ public class RegionBSPTree3DTest {
 
         Assert.assertEquals(2.0, tree.getSize(), TEST_EPS);
         Assert.assertEquals(10.0, tree.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0, 0), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0, 0), tree.getCentroid(), TEST_EPS);
 
         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.OUTSIDE,
                 Vector3D.of(-1, 0, 0),
@@ -921,7 +921,7 @@ public class RegionBSPTree3DTest {
 
         Assert.assertEquals(2.0, tree.getSize(), eps);
         Assert.assertEquals(10.0, tree.getBoundarySize(), eps);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5 + 5.4166e-8, 0, 0), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5 + 5.4166e-8, 0, 0), tree.getCentroid(), TEST_EPS);
 
         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.OUTSIDE,
                 Vector3D.of(-1, 0, 0),
@@ -945,7 +945,7 @@ public class RegionBSPTree3DTest {
 
         Assert.assertEquals(2.0, tree.getSize(), TEST_EPS);
         Assert.assertEquals(12.0, tree.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0), tree.getCentroid(), TEST_EPS);
 
 
         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.OUTSIDE,
@@ -972,7 +972,7 @@ public class RegionBSPTree3DTest {
 
         Assert.assertEquals(2.0, tree.getSize(), TEST_EPS);
         Assert.assertEquals(12.0, tree.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), tree.getCentroid(), TEST_EPS);
 
         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.OUTSIDE,
                 Vector3D.of(-1, 0, 0),
@@ -1007,7 +1007,7 @@ public class RegionBSPTree3DTest {
         // assert
         Assert.assertEquals(1.0 / 3.0, tree.getSize(), TEST_EPS);
         Assert.assertEquals(2.0 * Math.sqrt(3.0), tree.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.5, 2.5, 3.5), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.5, 2.5, 3.5), tree.getCentroid(), TEST_EPS);
 
         double third = 1.0 / 3.0;
         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.BOUNDARY,
@@ -1041,7 +1041,7 @@ public class RegionBSPTree3DTest {
 
         Assert.assertEquals(sphereVolume(radius), tree.getSize(), approximationTolerance);
         Assert.assertEquals(sphereSurface(radius), tree.getBoundarySize(), approximationTolerance);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 3), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 3), tree.getCentroid(), TEST_EPS);
 
         EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.OUTSIDE,
                 Vector3D.of(-0.1, 2, 3),
@@ -1154,7 +1154,7 @@ public class RegionBSPTree3DTest {
 
         Assert.assertEquals(sphereVolume(radius), result.getSize(), tolerance);
         Assert.assertEquals(sphereSurface(radius), result.getBoundarySize(), tolerance);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, result.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, result.getCentroid(), TEST_EPS);
 
         EuclideanTestUtils.assertRegionLocation(result, RegionLocation.OUTSIDE,
                 Vector3D.of(-1.1, 0, 0),
@@ -1232,7 +1232,7 @@ public class RegionBSPTree3DTest {
 
         Assert.assertEquals(sphereVolume(radius), result.getSize(), tolerance);
         Assert.assertEquals(sphereSurface(radius), result.getBoundarySize(), tolerance);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, result.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, result.getCentroid(), TEST_EPS);
 
         EuclideanTestUtils.assertRegionLocation(result, RegionLocation.OUTSIDE,
                 Vector3D.of(-1.1, 0, 0),
@@ -1346,7 +1346,7 @@ public class RegionBSPTree3DTest {
 
         Assert.assertEquals(0.0, result.getSize(), TEST_EPS);
         Assert.assertEquals(0.0, result.getBoundarySize(), TEST_EPS);
-        Assert.assertNull(result.getBarycenter());
+        Assert.assertNull(result.getCentroid());
 
         EuclideanTestUtils.assertRegionLocation(result, RegionLocation.OUTSIDE,
                 Vector3D.of(-1.1, 0, 0),
@@ -1420,7 +1420,7 @@ public class RegionBSPTree3DTest {
 
         Assert.assertEquals(0.0, result.getSize(), TEST_EPS);
         Assert.assertEquals(0.0, result.getBoundarySize(), TEST_EPS);
-        Assert.assertNull(result.getBarycenter());
+        Assert.assertNull(result.getCentroid());
 
         EuclideanTestUtils.assertRegionLocation(result, RegionLocation.OUTSIDE,
                 Vector3D.of(-1.1, 0, 0),
@@ -1503,7 +1503,7 @@ public class RegionBSPTree3DTest {
 
         ConvexVolume vol = result.get(0);
         Assert.assertEquals(1, vol.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.5, 2.5, 3.5), vol.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.5, 2.5, 3.5), vol.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -1524,10 +1524,10 @@ public class RegionBSPTree3DTest {
         ConvexVolume large = smallFirst ? result.get(1) : result.get(0);
 
         Assert.assertEquals(1, small.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(4.5, 5.5, 6.5), small.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(4.5, 5.5, 6.5), small.getCentroid(), TEST_EPS);
 
         Assert.assertEquals(2, large.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 0.5, 0.5), large.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 0.5, 0.5), large.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -1545,11 +1545,11 @@ public class RegionBSPTree3DTest {
 
         RegionBSPTree3D minus = split.getMinus();
         Assert.assertEquals(0.5, minus.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-0.25, 0, 0), minus.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-0.25, 0, 0), minus.getCentroid(), TEST_EPS);
 
         RegionBSPTree3D plus = split.getPlus();
         Assert.assertEquals(0.5, plus.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.25, 0, 0), plus.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.25, 0, 0), plus.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -1560,15 +1560,15 @@ public class RegionBSPTree3DTest {
         // act/assert
         ConvexVolume rootVol = tree.getRoot().getNodeRegion();
         GeometryTestUtils.assertPositiveInfinity(rootVol.getSize());
-        Assert.assertNull(rootVol.getBarycenter());
+        Assert.assertNull(rootVol.getCentroid());
 
         ConvexVolume plusVol = tree.getRoot().getPlus().getNodeRegion();
         GeometryTestUtils.assertPositiveInfinity(plusVol.getSize());
-        Assert.assertNull(plusVol.getBarycenter());
+        Assert.assertNull(plusVol.getCentroid());
 
         ConvexVolume centerVol = tree.findNode(Vector3D.of(0.5, 0.5, 0.5)).getNodeRegion();
         Assert.assertEquals(1, centerVol.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), centerVol.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), centerVol.getCentroid(), TEST_EPS);
     }
 
     // GEOMETRY-59
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SimpleTriangle3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SimpleTriangle3DTest.java
index 2c358ea..4ab80a8 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SimpleTriangle3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SimpleTriangle3DTest.java
@@ -72,7 +72,7 @@ public class SimpleTriangle3DTest {
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 3), subspaceVertices.get(2), TEST_EPS);
 
         Assert.assertEquals(0.5, tri.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(5.0 / 3.0, 7.0 / 3.0, 1), tri.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(5.0 / 3.0, 7.0 / 3.0, 1), tri.getCentroid(), TEST_EPS);
 
         Bounds3D bounds = tri.getBounds();
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 1), bounds.getMin(), TEST_EPS);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/VertexListConvexPolygon3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/VertexListConvexPolygon3DTest.java
index a438c51..0d72e77 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/VertexListConvexPolygon3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/VertexListConvexPolygon3DTest.java
@@ -59,7 +59,7 @@ public class VertexListConvexPolygon3DTest {
         Assert.assertFalse(p.isInfinite());
 
         Assert.assertEquals(0.5, p.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.0 / 3.0, 1.0 / 3.0, 1), p.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1.0 / 3.0, 1.0 / 3.0, 1), p.getCentroid(), TEST_EPS);
 
         Assert.assertSame(XY_PLANE_Z1, p.getPlane());
 
@@ -94,9 +94,9 @@ public class VertexListConvexPolygon3DTest {
     }
 
     @Test
-    public void testGetBarycenter_linearVertices() {
+    public void testGetCentroid_linearVertices() {
         // this should not happen with all of the checks in place for constructing these
-        // instances; this test is to ensure that the barycenter computation can still handle
+        // instances; this test is to ensure that the centroid computation can still handle
         // the situation
 
         // arrange
@@ -104,7 +104,7 @@ public class VertexListConvexPolygon3DTest {
         VertexListConvexPolygon3D p = new VertexListConvexPolygon3D(XY_PLANE_Z1, vertices);
 
         // act
-        Vector3D center = p.getBarycenter();
+        Vector3D center = p.getCentroid();
 
         // assert
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 0, 0), center, TEST_EPS);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3DTest.java
index 2b86fc5..03ad3f3 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/EmbeddedTreeLineSubset3DTest.java
@@ -99,7 +99,7 @@ public class EmbeddedTreeLineSubset3DTest {
         Assert.assertFalse(full.isFinite());
 
         GeometryTestUtils.assertPositiveInfinity(full.getSize());
-        Assert.assertNull(full.getBarycenter());
+        Assert.assertNull(full.getCentroid());
         Assert.assertNull(full.getBounds());
     }
 
@@ -113,7 +113,7 @@ public class EmbeddedTreeLineSubset3DTest {
         Assert.assertTrue(empty.isFinite());
 
         Assert.assertEquals(0, empty.getSize(), TEST_EPS);
-        Assert.assertNull(empty.getBarycenter());
+        Assert.assertNull(empty.getCentroid());
         Assert.assertNull(empty.getBounds());
     }
 
@@ -128,7 +128,7 @@ public class EmbeddedTreeLineSubset3DTest {
         Assert.assertFalse(half.isFinite());
 
         GeometryTestUtils.assertPositiveInfinity(half.getSize());
-        Assert.assertNull(half.getBarycenter());
+        Assert.assertNull(half.getCentroid());
         Assert.assertNull(half.getBounds());
     }
 
@@ -147,7 +147,7 @@ public class EmbeddedTreeLineSubset3DTest {
         Assert.assertTrue(sub.isFinite());
 
         Assert.assertEquals(2 * sqrt2, sub.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-0.5, -0.5, 1), sub.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-0.5, -0.5, 1), sub.getCentroid(), TEST_EPS);
 
         Bounds3D bounds = sub.getBounds();
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-2, -2, 1), bounds.getMin(), TEST_EPS);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/Line3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/Line3DTest.java
index 0eda275..1c9287b 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/Line3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/Line3DTest.java
@@ -360,7 +360,7 @@ public class Line3DTest {
         Assert.assertNull(span.getStartPoint());
         Assert.assertNull(span.getEndPoint());
 
-        Assert.assertNull(span.getBarycenter());
+        Assert.assertNull(span.getCentroid());
         Assert.assertNull(span.getBounds());
 
         GeometryTestUtils.assertNegativeInfinity(span.getSubspaceStart());
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/Ray3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/Ray3DTest.java
index 33c104e..8d43f6f 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/Ray3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/Ray3DTest.java
@@ -56,7 +56,7 @@ public class Ray3DTest {
 
         GeometryTestUtils.assertPositiveInfinity(ray.getSize());
 
-        Assert.assertNull(ray.getBarycenter());
+        Assert.assertNull(ray.getCentroid());
         Assert.assertNull(ray.getBounds());
     }
 
@@ -94,7 +94,7 @@ public class Ray3DTest {
 
         GeometryTestUtils.assertPositiveInfinity(ray.getSize());
 
-        Assert.assertNull(ray.getBarycenter());
+        Assert.assertNull(ray.getCentroid());
         Assert.assertNull(ray.getBounds());
     }
 
@@ -137,7 +137,7 @@ public class Ray3DTest {
 
         GeometryTestUtils.assertPositiveInfinity(ray.getSize());
 
-        Assert.assertNull(ray.getBarycenter());
+        Assert.assertNull(ray.getCentroid());
         Assert.assertNull(ray.getBounds());
     }
 
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/ReverseRay3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/ReverseRay3DTest.java
index 1165a0e..633f90e 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/ReverseRay3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/ReverseRay3DTest.java
@@ -56,7 +56,7 @@ public class ReverseRay3DTest {
 
         GeometryTestUtils.assertPositiveInfinity(revRay.getSize());
 
-        Assert.assertNull(revRay.getBarycenter());
+        Assert.assertNull(revRay.getCentroid());
         Assert.assertNull(revRay.getBounds());
     }
 
@@ -94,7 +94,7 @@ public class ReverseRay3DTest {
 
         GeometryTestUtils.assertPositiveInfinity(revRay.getSize());
 
-        Assert.assertNull(revRay.getBarycenter());
+        Assert.assertNull(revRay.getCentroid());
         Assert.assertNull(revRay.getBounds());
     }
 
@@ -137,7 +137,7 @@ public class ReverseRay3DTest {
 
         GeometryTestUtils.assertPositiveInfinity(revRay.getSize());
 
-        Assert.assertNull(revRay.getBarycenter());
+        Assert.assertNull(revRay.getCentroid());
         Assert.assertNull(revRay.getBounds());
     }
 
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/Segment3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/Segment3DTest.java
index 07a5600..42c15e8 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/Segment3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/line/Segment3DTest.java
@@ -56,7 +56,7 @@ public class Segment3DTest {
 
         Assert.assertEquals(2, seg.getSize(), TEST_EPS);
 
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 2), seg.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 2), seg.getCentroid(), TEST_EPS);
         Bounds3D bounds = seg.getBounds();
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, 2), bounds.getMin(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 3, 2), bounds.getMax(), TEST_EPS);
@@ -101,7 +101,7 @@ public class Segment3DTest {
 
         Assert.assertEquals(4, seg.getSize(), TEST_EPS);
 
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, 2), seg.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 1, 2), seg.getCentroid(), TEST_EPS);
         Bounds3D bounds = seg.getBounds();
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, -1, 2), bounds.getMin(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 3, 2), bounds.getMax(), TEST_EPS);
@@ -129,7 +129,7 @@ public class Segment3DTest {
 
         Assert.assertEquals(0, seg.getSize(), TEST_EPS);
 
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 0), seg.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 0), seg.getCentroid(), TEST_EPS);
         Bounds3D bounds = seg.getBounds();
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 0), bounds.getMin(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 0), bounds.getMax(), TEST_EPS);
@@ -181,7 +181,7 @@ public class Segment3DTest {
 
         Assert.assertEquals(3, seg.getSize(), TEST_EPS);
 
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 0.5), seg.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 0.5), seg.getCentroid(), TEST_EPS);
         Bounds3D bounds = seg.getBounds();
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, -1), bounds.getMin(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 2), bounds.getMax(), TEST_EPS);
@@ -207,7 +207,7 @@ public class Segment3DTest {
 
         Assert.assertEquals(3, seg.getSize(), TEST_EPS);
 
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 0.5), seg.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 0.5), seg.getCentroid(), TEST_EPS);
         Bounds3D bounds = seg.getBounds();
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, -1), bounds.getMin(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 2), bounds.getMax(), TEST_EPS);
@@ -233,7 +233,7 @@ public class Segment3DTest {
 
         Assert.assertEquals(0, seg.getSize(), TEST_EPS);
 
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 1), seg.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 1), seg.getCentroid(), TEST_EPS);
         Bounds3D bounds = seg.getBounds();
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 1), bounds.getMin(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, 0, 1), bounds.getMax(), TEST_EPS);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/shape/ParallelepipedTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/shape/ParallelepipedTest.java
index c460019..779fd9c 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/shape/ParallelepipedTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/shape/ParallelepipedTest.java
@@ -61,7 +61,7 @@ public class ParallelepipedTest {
         // assert
         Assert.assertEquals(1, p.getSize(), TEST_EPS);
         Assert.assertEquals(6, p.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, p.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, p.getCentroid(), TEST_EPS);
 
         List<PlaneConvexSubset> boundaries = p.getBoundaries();
         Assert.assertEquals(6, boundaries.size());
@@ -96,7 +96,7 @@ public class ParallelepipedTest {
         Assert.assertEquals(2, p.getSize(), TEST_EPS);
         Assert.assertEquals(4 + (4 * Math.sqrt(2.5)), p.getBoundarySize(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2 * invSqrt2, invSqrt2, 2),
-                p.getBarycenter(), TEST_EPS);
+                p.getCentroid(), TEST_EPS);
 
         assertVertices(p,
             Vector3D.of(0, invSqrt2, 1.5),
@@ -128,7 +128,7 @@ public class ParallelepipedTest {
         Assert.assertEquals(2, p.getSize(), TEST_EPS);
         Assert.assertEquals(4 + (4 * Math.sqrt(2.5)), p.getBoundarySize(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2 * invSqrt2, invSqrt2, -2),
-                p.getBarycenter(), TEST_EPS);
+                p.getCentroid(), TEST_EPS);
 
         assertVertices(p,
             Vector3D.of(0, invSqrt2, -1.5),
@@ -233,7 +233,7 @@ public class ParallelepipedTest {
         // assert
         Assert.assertEquals(1, p.getSize(), TEST_EPS);
         Assert.assertEquals(6, p.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, p.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, p.getCentroid(), TEST_EPS);
 
         List<PlaneConvexSubset> boundaries = p.getBoundaries();
         Assert.assertEquals(6, boundaries.size());
@@ -266,7 +266,7 @@ public class ParallelepipedTest {
         // assert
         Assert.assertEquals(6, p.getSize(), TEST_EPS);
         Assert.assertEquals(22, p.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, -1), p.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, -1), p.getCentroid(), TEST_EPS);
 
         assertVertices(p,
             Vector3D.of(0, 1.5, 0.5),
@@ -294,7 +294,7 @@ public class ParallelepipedTest {
         // assert
         Assert.assertEquals(0.125, p.getSize(), TEST_EPS);
         Assert.assertEquals(1.5, p.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, p.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.ZERO, p.getCentroid(), TEST_EPS);
 
         assertVertices(p,
             Vector3D.of(-0.25, -0.25, -0.25),
@@ -319,7 +319,7 @@ public class ParallelepipedTest {
 
         // assert
         Assert.assertEquals(27, tree.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2.5, 3.5, 4.5), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2.5, 3.5, 4.5), tree.getCentroid(), TEST_EPS);
     }
 
     private static void assertVertices(Parallelepiped p, Vector3D... vertices) {
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/shape/SphereTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/shape/SphereTest.java
index 9751927..6d810e1 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/shape/SphereTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/shape/SphereTest.java
@@ -56,7 +56,7 @@ public class SphereTest {
         Assert.assertFalse(s.isEmpty());
 
         Assert.assertSame(center, s.getCenter());
-        Assert.assertSame(center, s.getBarycenter());
+        Assert.assertSame(center, s.getCentroid());
 
         Assert.assertEquals(3, s.getRadius(), 0.0);
 
@@ -345,7 +345,7 @@ public class SphereTest {
         double eps = 0.1;
         Assert.assertEquals(s.getSize(), tree.getSize(), eps);
         Assert.assertEquals(s.getBoundarySize(), tree.getBoundarySize(), eps);
-        EuclideanTestUtils.assertCoordinatesEqual(s.getBarycenter(), tree.getBarycenter(), eps);
+        EuclideanTestUtils.assertCoordinatesEqual(s.getCentroid(), tree.getCentroid(), eps);
     }
 
     @Test
@@ -522,8 +522,7 @@ public class SphereTest {
             }
         }
 
-
-        // sphere must contain barycenter
-        EuclideanTestUtils.assertRegionLocation(s, RegionLocation.INSIDE, tree.getBarycenter());
+        // sphere must contain centroid
+        EuclideanTestUtils.assertRegionLocation(s, RegionLocation.INSIDE, tree.getCentroid());
     }
 }
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Bounds2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Bounds2DTest.java
index ee30794..f03d454 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Bounds2DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Bounds2DTest.java
@@ -54,7 +54,7 @@ public class Bounds2DTest {
         EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMin(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMax(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, b.getDiagonal(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(p1, b.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(p1, b.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -71,7 +71,7 @@ public class Bounds2DTest {
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0, 5), b.getMin(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 6), b.getMax(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 1), b.getDiagonal(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.5, 5.5), b.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.5, 5.5), b.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -86,7 +86,7 @@ public class Bounds2DTest {
         EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMin(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(p1, b.getMax(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, b.getDiagonal(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(p1, b.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(p1, b.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -103,7 +103,7 @@ public class Bounds2DTest {
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 4), b.getMin(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 6), b.getMax(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 2), b.getDiagonal(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 5), b.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 5), b.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -185,7 +185,7 @@ public class Bounds2DTest {
 
         // act/assert
         assertContainsStrict(b, true,
-                b.getBarycenter(),
+                b.getCentroid(),
                 Vector2D.of(0, 4), Vector2D.of(2, 6),
                 Vector2D.of(1, 5),
                 Vector2D.of(0, 5), Vector2D.of(2, 5),
@@ -207,7 +207,7 @@ public class Bounds2DTest {
 
         // act/assert
         assertContainsWithPrecision(b, true,
-                b.getBarycenter(),
+                b.getCentroid(),
                 Vector2D.of(1, 5), Vector2D.of(0, 4), Vector2D.of(2, 6),
                 Vector2D.of(0, 5), Vector2D.of(2, 5),
                 Vector2D.of(1, 4), Vector2D.of(1, 6),
@@ -346,7 +346,7 @@ public class Bounds2DTest {
 
         // assert
         Assert.assertEquals(4, p.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 5), p.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 5), p.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -468,7 +468,7 @@ public class Bounds2DTest {
         // assert
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 6), b.getMin(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(5, 10), b.getMax(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 8), b.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(3, 8), b.getCentroid(), TEST_EPS);
     }
 
     @Test
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 e5ca116..dc68d0d 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
@@ -53,7 +53,7 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(0.0, area.getBoundarySize(), TEST_EPS);
         GeometryTestUtils.assertPositiveInfinity(area.getSize());
-        Assert.assertNull(area.getBarycenter());
+        Assert.assertNull(area.getCentroid());
         Assert.assertNull(area.getBounds());
     }
 
@@ -104,7 +104,7 @@ public class ConvexAreaTest {
         Assert.assertFalse(tree.isEmpty());
 
         Assert.assertEquals(1, tree.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), tree.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -188,11 +188,11 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(2, transformed.getSize(), TEST_EPS);
         Assert.assertEquals(6, transformed.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.5, 3), transformed.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.5, 3), transformed.getCentroid(), TEST_EPS);
 
         EuclideanTestUtils.assertRegionLocation(transformed, RegionLocation.BOUNDARY,
                 Vector2D.of(1, 2), Vector2D.of(2, 2), Vector2D.of(2, 4), Vector2D.of(1, 4));
-        EuclideanTestUtils.assertRegionLocation(transformed, RegionLocation.INSIDE, transformed.getBarycenter());
+        EuclideanTestUtils.assertRegionLocation(transformed, RegionLocation.INSIDE, transformed.getCentroid());
     }
 
     @Test
@@ -216,11 +216,11 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(2, transformed.getSize(), TEST_EPS);
         Assert.assertEquals(6, transformed.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1.5, 3), transformed.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1.5, 3), transformed.getCentroid(), TEST_EPS);
 
         EuclideanTestUtils.assertRegionLocation(transformed, RegionLocation.BOUNDARY,
                 Vector2D.of(-1, 2), Vector2D.of(-2, 2), Vector2D.of(-2, 4), Vector2D.of(-1, 4));
-        EuclideanTestUtils.assertRegionLocation(transformed, RegionLocation.INSIDE, transformed.getBarycenter());
+        EuclideanTestUtils.assertRegionLocation(transformed, RegionLocation.INSIDE, transformed.getCentroid());
     }
 
     @Test
@@ -244,11 +244,11 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(2, transformed.getSize(), TEST_EPS);
         Assert.assertEquals(6, transformed.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1.5, -3), transformed.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1.5, -3), transformed.getCentroid(), TEST_EPS);
 
         EuclideanTestUtils.assertRegionLocation(transformed, RegionLocation.BOUNDARY,
                 Vector2D.of(-1, -2), Vector2D.of(-2, -2), Vector2D.of(-2, -4), Vector2D.of(-1, -4));
-        EuclideanTestUtils.assertRegionLocation(transformed, RegionLocation.INSIDE, transformed.getBarycenter());
+        EuclideanTestUtils.assertRegionLocation(transformed, RegionLocation.INSIDE, transformed.getCentroid());
     }
 
     @Test
@@ -459,7 +459,7 @@ public class ConvexAreaTest {
 
         GeometryTestUtils.assertPositiveInfinity(minus.getBoundarySize());
         GeometryTestUtils.assertPositiveInfinity(minus.getSize());
-        Assert.assertNull(minus.getBarycenter());
+        Assert.assertNull(minus.getCentroid());
 
         List<LineConvexSubset> minusSegments = minus.getBoundaries();
         Assert.assertEquals(1, minusSegments.size());
@@ -471,7 +471,7 @@ public class ConvexAreaTest {
 
         GeometryTestUtils.assertPositiveInfinity(plus.getBoundarySize());
         GeometryTestUtils.assertPositiveInfinity(plus.getSize());
-        Assert.assertNull(plus.getBarycenter());
+        Assert.assertNull(plus.getCentroid());
 
         List<LineConvexSubset> plusSegments = plus.getBoundaries();
         Assert.assertEquals(1, plusSegments.size());
@@ -496,7 +496,7 @@ public class ConvexAreaTest {
 
         GeometryTestUtils.assertPositiveInfinity(minus.getBoundarySize());
         GeometryTestUtils.assertPositiveInfinity(minus.getSize());
-        Assert.assertNull(minus.getBarycenter());
+        Assert.assertNull(minus.getCentroid());
 
         Assert.assertEquals(2, minus.getBoundaries().size());
 
@@ -506,7 +506,7 @@ public class ConvexAreaTest {
 
         GeometryTestUtils.assertPositiveInfinity(plus.getBoundarySize());
         GeometryTestUtils.assertPositiveInfinity(plus.getSize());
-        Assert.assertNull(plus.getBarycenter());
+        Assert.assertNull(plus.getCentroid());
 
         Assert.assertEquals(2, plus.getBoundaries().size());
     }
@@ -561,7 +561,7 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(4, minus.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(1, minus.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.5, 1.5), minus.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.5, 1.5), minus.getCentroid(), TEST_EPS);
 
         Assert.assertEquals(4, minus.getBoundaries().size());
 
@@ -571,7 +571,7 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(4, plus.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(1, plus.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2.5, 1.5), plus.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2.5, 1.5), plus.getCentroid(), TEST_EPS);
 
         Assert.assertEquals(4, plus.getBoundaries().size());
     }
@@ -594,7 +594,7 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(2 + Math.sqrt(2), minus.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(0.5, minus.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(4.0 / 3.0, 5.0 / 3.0), minus.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(4.0 / 3.0, 5.0 / 3.0), minus.getCentroid(), TEST_EPS);
 
         Assert.assertEquals(3, minus.getBoundaries().size());
 
@@ -604,7 +604,7 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(2 + Math.sqrt(2), plus.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(0.5, plus.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(5.0 / 3.0, 4.0 / 3.0), plus.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(5.0 / 3.0, 4.0 / 3.0), plus.getCentroid(), TEST_EPS);
 
         Assert.assertEquals(3, plus.getBoundaries().size());
     }
@@ -627,7 +627,7 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(2 + Math.sqrt(2), minus.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(0.5, minus.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(5.0 / 3.0, 4.0 / 3.0), minus.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(5.0 / 3.0, 4.0 / 3.0), minus.getCentroid(), TEST_EPS);
 
         Assert.assertEquals(3, minus.getBoundaries().size());
 
@@ -637,7 +637,7 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(2 + Math.sqrt(2), plus.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(0.5, plus.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(4.0 / 3.0, 5.0 / 3.0), plus.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(4.0 / 3.0, 5.0 / 3.0), plus.getCentroid(), TEST_EPS);
 
         Assert.assertEquals(3, plus.getBoundaries().size());
     }
@@ -838,7 +838,7 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(0.5, area.getSize(), TEST_EPS);
         Assert.assertEquals(2 + Math.sqrt(2), area.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.centroid(p0, p1, p2), area.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.centroid(p0, p1, p2), area.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -857,7 +857,7 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(1, area.getSize(), TEST_EPS);
         Assert.assertEquals(4, area.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -877,7 +877,7 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(1, area.getSize(), TEST_EPS);
         Assert.assertEquals(4, area.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -904,7 +904,7 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(1, area.getSize(), eps);
         Assert.assertEquals(4, area.getBoundarySize(), eps);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), eps);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getCentroid(), eps);
     }
 
     @Test
@@ -924,7 +924,7 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(1, area.getSize(), TEST_EPS);
         Assert.assertEquals(4, area.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -1021,7 +1021,7 @@ public class ConvexAreaTest {
 
         GeometryTestUtils.assertPositiveInfinity(area.getBoundarySize());
         GeometryTestUtils.assertPositiveInfinity(area.getSize());
-        Assert.assertNull(area.getBarycenter());
+        Assert.assertNull(area.getCentroid());
 
         List<LineConvexSubset> segments = area.getBoundaries();
         Assert.assertEquals(1, segments.size());
@@ -1047,7 +1047,7 @@ public class ConvexAreaTest {
 
         GeometryTestUtils.assertPositiveInfinity(area.getBoundarySize());
         GeometryTestUtils.assertPositiveInfinity(area.getSize());
-        Assert.assertNull(area.getBarycenter());
+        Assert.assertNull(area.getCentroid());
 
         List<LineConvexSubset> segments = area.getBoundaries();
         Assert.assertEquals(2, segments.size());
@@ -1075,7 +1075,7 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(4 + (2 * Math.sqrt(2)), area.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(2, area.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-2.0 / 3.0, -2.0 / 3.0), area.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-2.0 / 3.0, -2.0 / 3.0), area.getCentroid(), TEST_EPS);
 
         List<LineConvexSubset> segments = area.getBoundaries();
         Assert.assertEquals(3, segments.size());
@@ -1101,7 +1101,7 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(4, area.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(1, area.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getCentroid(), TEST_EPS);
 
         List<LineConvexSubset> segments = area.getBoundaries();
         Assert.assertEquals(4, segments.size());
@@ -1133,7 +1133,7 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(4, area.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(1, area.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getCentroid(), TEST_EPS);
 
         List<LineConvexSubset> segments = area.getBoundaries();
         Assert.assertEquals(4, segments.size());
@@ -1163,7 +1163,7 @@ public class ConvexAreaTest {
 
         Assert.assertEquals(4, area.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(1, area.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getCentroid(), TEST_EPS);
 
         List<LineConvexSubset> segments = area.getBoundaries();
         Assert.assertEquals(4, segments.size());
@@ -1193,7 +1193,7 @@ public class ConvexAreaTest {
 
         GeometryTestUtils.assertPositiveInfinity(area.getBoundarySize());
         GeometryTestUtils.assertPositiveInfinity(area.getSize());
-        Assert.assertNull(area.getBarycenter());
+        Assert.assertNull(area.getCentroid());
 
         List<LineConvexSubset> segments = area.getBoundaries();
         Assert.assertEquals(1, segments.size());
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/EmbeddedTreeLineSubsetTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/EmbeddedTreeLineSubsetTest.java
index fa7350a..7d25693 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/EmbeddedTreeLineSubsetTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/EmbeddedTreeLineSubsetTest.java
@@ -55,7 +55,7 @@ public class EmbeddedTreeLineSubsetTest {
         Assert.assertTrue(sub.isFinite());
 
         Assert.assertEquals(0, sub.getSize(), TEST_EPS);
-        Assert.assertNull(sub.getBarycenter());
+        Assert.assertNull(sub.getCentroid());
     }
 
     @Test
@@ -73,7 +73,7 @@ public class EmbeddedTreeLineSubsetTest {
         Assert.assertFalse(sub.isFinite());
 
         GeometryTestUtils.assertPositiveInfinity(sub.getSize());
-        Assert.assertNull(sub.getBarycenter());
+        Assert.assertNull(sub.getCentroid());
     }
 
     @Test
@@ -95,7 +95,7 @@ public class EmbeddedTreeLineSubsetTest {
         Assert.assertFalse(sub.isFinite());
 
         GeometryTestUtils.assertPositiveInfinity(sub.getSize());
-        Assert.assertNull(sub.getBarycenter());
+        Assert.assertNull(sub.getCentroid());
     }
 
     @Test
@@ -172,7 +172,7 @@ public class EmbeddedTreeLineSubsetTest {
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(4, 1), segments.get(1).getEndPoint(), TEST_EPS);
 
         Assert.assertEquals(5, subset.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.7, 1), subset.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.7, 1), subset.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -217,7 +217,7 @@ public class EmbeddedTreeLineSubsetTest {
         Assert.assertEquals(bTreeCount, bTree.count());
 
         GeometryTestUtils.assertPositiveInfinity(subset.getSize());
-        Assert.assertNull(subset.getBarycenter());
+        Assert.assertNull(subset.getCentroid());
     }
 
     @Test
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineSpanningSubsetTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineSpanningSubsetTest.java
index b0bbe36..d99e73b 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineSpanningSubsetTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineSpanningSubsetTest.java
@@ -51,7 +51,7 @@ public class LineSpanningSubsetTest {
         Assert.assertFalse(result.isFinite());
 
         GeometryTestUtils.assertPositiveInfinity(result.getSize());
-        Assert.assertNull(result.getBarycenter());
+        Assert.assertNull(result.getCentroid());
         Assert.assertNull(result.getBounds());
 
         Assert.assertNull(result.getStartPoint());
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineSubsetTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineSubsetTest.java
index bc35628..94157ab 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineSubsetTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/LineSubsetTest.java
@@ -308,7 +308,7 @@ public class LineSubsetTest {
             }
 
             @Override
-            public Vector2D getBarycenter() {
+            public Vector2D getCentroid() {
                 return null;
             }
 
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RayTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RayTest.java
index e335b4d..e7d9525 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RayTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RayTest.java
@@ -56,7 +56,7 @@ public class RayTest {
         GeometryTestUtils.assertPositiveInfinity(ray.getSubspaceEnd());
 
         GeometryTestUtils.assertPositiveInfinity(ray.getSize());
-        Assert.assertNull(ray.getBarycenter());
+        Assert.assertNull(ray.getCentroid());
         Assert.assertNull(ray.getBounds());
 
         EuclideanTestUtils.assertCoordinatesEqual(p0.vectorTo(p1), ray.getDirection(), TEST_EPS);
@@ -99,7 +99,7 @@ public class RayTest {
         GeometryTestUtils.assertPositiveInfinity(ray.getSubspaceEnd());
 
         GeometryTestUtils.assertPositiveInfinity(ray.getSize());
-        Assert.assertNull(ray.getBarycenter());
+        Assert.assertNull(ray.getCentroid());
         Assert.assertNull(ray.getBounds());
 
         EuclideanTestUtils.assertCoordinatesEqual(p0.vectorTo(p1), ray.getDirection(), TEST_EPS);
@@ -150,7 +150,7 @@ public class RayTest {
         GeometryTestUtils.assertPositiveInfinity(ray.getSubspaceEnd());
 
         GeometryTestUtils.assertPositiveInfinity(ray.getSize());
-        Assert.assertNull(ray.getBarycenter());
+        Assert.assertNull(ray.getCentroid());
         Assert.assertNull(ray.getBounds());
 
         EuclideanTestUtils.assertCoordinatesEqual(p0.vectorTo(p1), ray.getDirection(), TEST_EPS);
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 7c7a524..4db2834 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
@@ -261,7 +261,7 @@ public class RegionBSPTree2DTest {
 
         Assert.assertEquals(7, tree.getSize(), TEST_EPS);
         Assert.assertEquals(12, tree.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.5, 1.5), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.5, 1.5), tree.getCentroid(), TEST_EPS);
 
         checkClassify(tree, RegionLocation.INSIDE,
                 Vector2D.of(1, 1), Vector2D.of(1.5, 1.5), Vector2D.of(2, 2));
@@ -353,7 +353,7 @@ public class RegionBSPTree2DTest {
         Assert.assertFalse(area.isEmpty());
 
         Assert.assertEquals(1, area.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), area.getCentroid(), TEST_EPS);
 
         checkClassify(area, RegionLocation.INSIDE, Vector2D.of(0.5, 0.5));
         checkClassify(area, RegionLocation.BOUNDARY, Vector2D.ZERO, Vector2D.of(1, 1));
@@ -381,7 +381,7 @@ public class RegionBSPTree2DTest {
 
         // assert
         Collections.sort(result, (a, b) ->
-            Vector2D.COORDINATE_ASCENDING_ORDER.compare(a.getBarycenter(), b.getBarycenter()));
+            Vector2D.COORDINATE_ASCENDING_ORDER.compare(a.getCentroid(), b.getCentroid()));
 
         Assert.assertEquals(2, result.size());
 
@@ -390,7 +390,7 @@ public class RegionBSPTree2DTest {
         Assert.assertFalse(firstArea.isEmpty());
 
         Assert.assertEquals(0.5, firstArea.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.0 / 3.0, 2.0 / 3.0), firstArea.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.0 / 3.0, 2.0 / 3.0), firstArea.getCentroid(), TEST_EPS);
 
         checkClassify(firstArea, RegionLocation.INSIDE, Vector2D.of(1.0 / 3.0, 2.0 / 3.0));
         checkClassify(firstArea, RegionLocation.BOUNDARY, Vector2D.ZERO, Vector2D.of(1, 1), Vector2D.of(0.5, 0.5));
@@ -403,7 +403,7 @@ public class RegionBSPTree2DTest {
         Assert.assertFalse(secondArea.isEmpty());
 
         Assert.assertEquals(0.5, secondArea.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2.0 / 3.0, 1.0 / 3.0), secondArea.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2.0 / 3.0, 1.0 / 3.0), secondArea.getCentroid(), TEST_EPS);
 
         checkClassify(secondArea, RegionLocation.INSIDE, Vector2D.of(2.0 / 3.0, 1.0 / 3.0));
         checkClassify(secondArea, RegionLocation.BOUNDARY, Vector2D.ZERO, Vector2D.of(1, 1), Vector2D.of(0.5, 0.5));
@@ -574,7 +574,7 @@ public class RegionBSPTree2DTest {
 
         // act/assert
         GeometryTestUtils.assertPositiveInfinity(tree.getSize());
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
 
         Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
 
@@ -589,7 +589,7 @@ public class RegionBSPTree2DTest {
 
         // act/assert
         Assert.assertEquals(0, tree.getSize(), TEST_EPS);
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
 
         Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
 
@@ -605,7 +605,7 @@ public class RegionBSPTree2DTest {
 
         // act/assert
         GeometryTestUtils.assertPositiveInfinity(tree.getSize());
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
 
         GeometryTestUtils.assertPositiveInfinity(tree.getBoundarySize());
 
@@ -635,7 +635,7 @@ public class RegionBSPTree2DTest {
 
         // act/assert
         GeometryTestUtils.assertPositiveInfinity(tree.getSize());
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
 
         GeometryTestUtils.assertPositiveInfinity(tree.getBoundarySize());
 
@@ -664,7 +664,7 @@ public class RegionBSPTree2DTest {
 
         // act/assert
         GeometryTestUtils.assertPositiveInfinity(tree.getSize());
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
 
         GeometryTestUtils.assertPositiveInfinity(tree.getBoundarySize());
 
@@ -711,7 +711,7 @@ public class RegionBSPTree2DTest {
 
         // act/assert
         Assert.assertEquals(1, tree.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), tree.getCentroid(), TEST_EPS);
 
         Assert.assertEquals(4, tree.getBoundarySize(), TEST_EPS);
 
@@ -741,7 +741,7 @@ public class RegionBSPTree2DTest {
 
         // act/assert
         GeometryTestUtils.assertPositiveInfinity(tree.getSize());
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
 
         GeometryTestUtils.assertPositiveInfinity(tree.getBoundarySize());
 
@@ -779,7 +779,7 @@ public class RegionBSPTree2DTest {
 
         // act/assert
         Assert.assertEquals(0.5, tree.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1.0 / 3.0), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1.0 / 3.0), tree.getCentroid(), TEST_EPS);
 
         Assert.assertEquals(1.0 + Math.sqrt(2) + Math.sqrt(5), tree.getBoundarySize(), TEST_EPS);
 
@@ -810,7 +810,7 @@ public class RegionBSPTree2DTest {
 
         // act/assert
         GeometryTestUtils.assertPositiveInfinity(tree.getSize());
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
 
         Assert.assertEquals(1.0 + Math.sqrt(2) + Math.sqrt(5), tree.getBoundarySize(), TEST_EPS);
 
@@ -841,7 +841,7 @@ public class RegionBSPTree2DTest {
 
         // act/assert
         Assert.assertEquals(8, tree.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.5, 1.5), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1.5, 1.5), tree.getCentroid(), TEST_EPS);
 
         Assert.assertEquals(16, tree.getBoundarySize(), TEST_EPS);
 
@@ -882,7 +882,7 @@ public class RegionBSPTree2DTest {
 
         // act/assert
         GeometryTestUtils.assertPositiveInfinity(tree.getSize());
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
 
         Assert.assertEquals(16, tree.getBoundarySize(), TEST_EPS);
 
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/ReverseRayTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/ReverseRayTest.java
index 431a393..4c19a00 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/ReverseRayTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/ReverseRayTest.java
@@ -56,7 +56,7 @@ public class ReverseRayTest {
         Assert.assertEquals(1, revRay.getSubspaceEnd(), TEST_EPS);
 
         GeometryTestUtils.assertPositiveInfinity(revRay.getSize());
-        Assert.assertNull(revRay.getBarycenter());
+        Assert.assertNull(revRay.getCentroid());
         Assert.assertNull(revRay.getBounds());
     }
 
@@ -97,7 +97,7 @@ public class ReverseRayTest {
         Assert.assertEquals(3, revRay.getSubspaceEnd(), TEST_EPS);
 
         GeometryTestUtils.assertPositiveInfinity(revRay.getSize());
-        Assert.assertNull(revRay.getBarycenter());
+        Assert.assertNull(revRay.getCentroid());
         Assert.assertNull(revRay.getBounds());
     }
 
@@ -146,7 +146,7 @@ public class ReverseRayTest {
         Assert.assertEquals(-2, revRay.getSubspaceEnd(), TEST_EPS);
 
         GeometryTestUtils.assertPositiveInfinity(revRay.getSize());
-        Assert.assertNull(revRay.getBarycenter());
+        Assert.assertNull(revRay.getCentroid());
         Assert.assertNull(revRay.getBounds());
     }
 
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SegmentTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SegmentTest.java
index 7c5af10..08a2440 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SegmentTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/SegmentTest.java
@@ -51,7 +51,7 @@ public class SegmentTest {
 
         EuclideanTestUtils.assertCoordinatesEqual(p1, seg.getStartPoint(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(p2, seg.getEndPoint(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 2), seg.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2, 2), seg.getCentroid(), TEST_EPS);
 
         Assert.assertEquals(1, seg.getSubspaceStart(), TEST_EPS);
         Assert.assertEquals(3, seg.getSubspaceEnd(), TEST_EPS);
@@ -94,7 +94,7 @@ public class SegmentTest {
 
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2), seg.getStartPoint(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 3), seg.getEndPoint(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2.5), seg.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2.5), seg.getCentroid(), TEST_EPS);
 
         Assert.assertEquals(2, seg.getSubspaceStart(), TEST_EPS);
         Assert.assertEquals(3, seg.getSubspaceEnd(), TEST_EPS);
@@ -120,7 +120,7 @@ public class SegmentTest {
 
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2), seg.getStartPoint(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2), seg.getEndPoint(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2), seg.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2), seg.getCentroid(), TEST_EPS);
 
         Assert.assertEquals(2, seg.getSubspaceStart(), TEST_EPS);
         Assert.assertEquals(2, seg.getSubspaceEnd(), TEST_EPS);
@@ -170,7 +170,7 @@ public class SegmentTest {
 
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, -1), seg.getStartPoint(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 2), seg.getEndPoint(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 0.5), seg.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 0.5), seg.getCentroid(), TEST_EPS);
 
         Assert.assertEquals(-1, seg.getSubspaceStart(), TEST_EPS);
         Assert.assertEquals(2, seg.getSubspaceEnd(), TEST_EPS);
@@ -194,7 +194,7 @@ public class SegmentTest {
 
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, -1), seg.getStartPoint(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 2), seg.getEndPoint(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 0.5), seg.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 0.5), seg.getCentroid(), TEST_EPS);
 
         Assert.assertEquals(-1, seg.getSubspaceStart(), TEST_EPS);
         Assert.assertEquals(2, seg.getSubspaceEnd(), TEST_EPS);
@@ -218,7 +218,7 @@ public class SegmentTest {
 
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 1), seg.getStartPoint(), TEST_EPS);
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 1), seg.getEndPoint(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 1), seg.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-1, 1), seg.getCentroid(), TEST_EPS);
 
         Assert.assertEquals(1, seg.getSubspaceStart(), TEST_EPS);
         Assert.assertEquals(1, seg.getSubspaceEnd(), TEST_EPS);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/shape/CircleTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/shape/CircleTest.java
index 47d2fcd..0193002 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/shape/CircleTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/shape/CircleTest.java
@@ -63,7 +63,7 @@ public class CircleTest {
         Assert.assertFalse(c.isEmpty());
 
         Assert.assertSame(center, c.getCenter());
-        Assert.assertSame(center, c.getBarycenter());
+        Assert.assertSame(center, c.getCentroid());
 
         Assert.assertEquals(3, c.getRadius(), 0.0);
 
@@ -373,7 +373,7 @@ public class CircleTest {
         double eps = 5e-3;
         Assert.assertEquals(c.getSize(), tree.getSize(), eps);
         Assert.assertEquals(c.getBoundarySize(), tree.getBoundarySize(), eps);
-        EuclideanTestUtils.assertCoordinatesEqual(c.getBarycenter(), tree.getBarycenter(), eps);
+        EuclideanTestUtils.assertCoordinatesEqual(c.getCentroid(), tree.getCentroid(), eps);
     }
 
     @Test
@@ -538,8 +538,8 @@ public class CircleTest {
             Assert.assertTrue("Expected vertex to be contained in circle: " + vertex, c.contains(vertex));
         }
 
-        // circle must contain barycenter
-        EuclideanTestUtils.assertRegionLocation(c, RegionLocation.INSIDE, tree.getBarycenter());
+        // circle must contain centroid
+        EuclideanTestUtils.assertRegionLocation(c, RegionLocation.INSIDE, tree.getCentroid());
 
         // area must be less than the circle
         Assert.assertTrue("Expected approximation area to be less than circle", tree.getSize() < c.getSize());
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/shape/ParallelogramTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/shape/ParallelogramTest.java
index 0a3f508..3837edf 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/shape/ParallelogramTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/shape/ParallelogramTest.java
@@ -46,7 +46,7 @@ public class ParallelogramTest {
         // assert
         Assert.assertEquals(1, box.getSize(), TEST_EPS);
         Assert.assertEquals(4, box.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, box.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, box.getCentroid(), TEST_EPS);
 
         List<Vector2D> vertices = box.getVertices();
         Assert.assertEquals(4, vertices.size());
@@ -72,7 +72,7 @@ public class ParallelogramTest {
 
         Assert.assertEquals(2, p.getSize(), TEST_EPS);
         Assert.assertEquals(4 * Math.sqrt(2.5), p.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2 * invSqrt2, invSqrt2), p.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(2 * invSqrt2, invSqrt2), p.getCentroid(), TEST_EPS);
 
         List<Vector2D> vertices = p.getVertices();
         Assert.assertEquals(4, vertices.size());
@@ -98,7 +98,7 @@ public class ParallelogramTest {
 
         Assert.assertEquals(2, p.getSize(), TEST_EPS);
         Assert.assertEquals(4 * Math.sqrt(2.5), p.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-2 * invSqrt2, invSqrt2), p.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(-2 * invSqrt2, invSqrt2), p.getCentroid(), TEST_EPS);
 
         List<Vector2D> vertices = p.getVertices();
         Assert.assertEquals(4, vertices.size());
@@ -185,7 +185,7 @@ public class ParallelogramTest {
         // assert
         Assert.assertEquals(1, p.getSize(), TEST_EPS);
         Assert.assertEquals(4, p.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, p.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.ZERO, p.getCentroid(), TEST_EPS);
 
         List<Vector2D> vertices = p.getVertices();
         Assert.assertEquals(4, vertices.size());
@@ -210,7 +210,7 @@ public class ParallelogramTest {
         // assert
         Assert.assertEquals(2, p.getSize(), TEST_EPS);
         Assert.assertEquals(6, p.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2), p.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2), p.getCentroid(), TEST_EPS);
 
         List<Vector2D> vertices = p.getVertices();
         Assert.assertEquals(4, vertices.size());
@@ -235,7 +235,7 @@ public class ParallelogramTest {
         // assert
         Assert.assertEquals(2, p.getSize(), TEST_EPS);
         Assert.assertEquals(6, p.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2), p.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2), p.getCentroid(), TEST_EPS);
 
         List<Vector2D> vertices = p.getVertices();
         Assert.assertEquals(4, vertices.size());
@@ -260,7 +260,7 @@ public class ParallelogramTest {
         // assert
         Assert.assertEquals(4, p.getSize(), TEST_EPS);
         Assert.assertEquals(8, p.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2), p.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 2), p.getCentroid(), TEST_EPS);
 
         List<Vector2D> vertices = p.getVertices();
         Assert.assertEquals(4, vertices.size());
@@ -283,7 +283,7 @@ public class ParallelogramTest {
         Assert.assertFalse(tree.isEmpty());
 
         Assert.assertEquals(4, tree.getSize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 2), tree.getBarycenter(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 2), tree.getCentroid(), TEST_EPS);
     }
 
     private static void assertSegment(LineConvexSubset segment, Vector2D start, Vector2D end) {
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/AngularInterval.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/AngularInterval.java
index 43c110f..e00fb56 100644
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/AngularInterval.java
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/AngularInterval.java
@@ -108,7 +108,7 @@ public class AngularInterval implements HyperplaneBoundedRegion<Point1S> {
      *  the full space.
      * @return the midpoint of the interval or null if the interval represents
      *      the full space
-     * @see #getBarycenter()
+     * @see #getCentroid()
      */
     public Point1S getMidPoint() {
         return midpoint;
@@ -152,7 +152,7 @@ public class AngularInterval implements HyperplaneBoundedRegion<Point1S> {
      * @see #getMidPoint()
      */
     @Override
-    public Point1S getBarycenter() {
+    public Point1S getCentroid() {
         return getMidPoint();
     }
 
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 534109a..f771e01 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
@@ -306,7 +306,7 @@ public final class CutAngle extends AbstractHyperplane<Point1S> {
          * <p>This method returns the point for the underlying hyperplane.</p>
          */
         @Override
-        public Point1S getBarycenter() {
+        public Point1S getCentroid() {
             return hyperplane.getPoint();
         }
 
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1S.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1S.java
index 2c68838..bfca36d 100644
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1S.java
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1S.java
@@ -343,7 +343,7 @@ public class RegionBSPTree1S extends AbstractRegionBSPTree<Point1S, RegionBSPTre
         }
 
         double size = 0;
-        Vector2D scaledBarycenterSum = Vector2D.ZERO;
+        Vector2D scaledCentroidSum = Vector2D.ZERO;
 
         double intervalSize;
 
@@ -351,16 +351,16 @@ public class RegionBSPTree1S extends AbstractRegionBSPTree<Point1S, RegionBSPTre
             intervalSize = interval.getSize();
 
             size += intervalSize;
-            scaledBarycenterSum = scaledBarycenterSum.add(interval.getBarycenter().getVector().withNorm(intervalSize));
+            scaledCentroidSum = scaledCentroidSum.add(interval.getCentroid().getVector().withNorm(intervalSize));
         }
 
         final DoublePrecisionContext precision = ((CutAngle) getRoot().getCutHyperplane()).getPrecision();
 
-        final Point1S barycenter = scaledBarycenterSum.eq(Vector2D.ZERO, precision) ?
+        final Point1S centroid = scaledCentroidSum.eq(Vector2D.ZERO, precision) ?
                  null :
-                 Point1S.from(scaledBarycenterSum);
+                 Point1S.from(scaledCentroidSum);
 
-        return new RegionSizeProperties<>(size, barycenter);
+        return new RegionSizeProperties<>(size, centroid);
     }
 
     /** {@inheritDoc} */
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/ConvexArea2S.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/ConvexArea2S.java
index 19d88bb..a144a66 100644
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/ConvexArea2S.java
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/ConvexArea2S.java
@@ -127,39 +127,39 @@ public final class ConvexArea2S extends AbstractConvexHyperplaneBoundedRegion<Po
 
     /** {@inheritDoc} */
     @Override
-    public Point2S getBarycenter() {
-        final Vector3D weighted = getWeightedBarycenterVector();
+    public Point2S getCentroid() {
+        final Vector3D weighted = getWeightedCentroidVector();
         return weighted == null ? null : Point2S.from(weighted);
     }
 
-    /** Returns the weighted vector for the barycenter. This vector is computed by scaling the
+    /** Returns the weighted vector for the centroid. This vector is computed by scaling the
      * pole vector of the great circle of each boundary arc by the size of the arc and summing
-     * the results. By combining the weighted barycenter vectors of multiple areas, a single
-     * barycenter can be computed for the whole group.
-     * @return weighted barycenter vector.
+     * the results. By combining the weighted centroid vectors of multiple areas, a single
+     * centroid can be computed for the whole group.
+     * @return weighted centroid vector.
      * @see <a href="https://archive.org/details/centroidinertiat00broc">
      *  <em>The Centroid and Inertia Tensor for a Spherical Triangle</em> - John E. Brock</a>
      */
-    Vector3D getWeightedBarycenterVector() {
+    Vector3D getWeightedCentroidVector() {
         final List<GreatArc> arcs = getBoundaries();
         switch (arcs.size()) {
         case 0:
-            // full space; no barycenter
+            // full space; no centroid
             return null;
         case 1:
-            // hemisphere; barycenter is the pole of the hemisphere
+            // hemisphere; centroid is the pole of the hemisphere
             final GreatArc singleArc = arcs.get(0);
             return singleArc.getCircle().getPole().withNorm(singleArc.getSize());
         default:
             // 2 or more sides; use an extension of the approach outlined here:
             // https://archive.org/details/centroidinertiat00broc
-            // In short, the barycenter is the sum of the pole vectors of each side
+            // In short, the centroid is the sum of the pole vectors of each side
             // multiplied by their arc lengths.
-            Vector3D barycenter = Vector3D.ZERO;
+            Vector3D centroid = Vector3D.ZERO;
             for (final GreatArc arc : getBoundaries()) {
-                barycenter = barycenter.add(arc.getCircle().getPole().withNorm(arc.getSize()));
+                centroid = centroid.add(arc.getCircle().getPole().withNorm(arc.getSize()));
             }
-            return barycenter;
+            return centroid;
         }
     }
 
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircleSubset.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircleSubset.java
index 87ba6c0..1c5800f 100644
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircleSubset.java
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatCircleSubset.java
@@ -88,10 +88,10 @@ public abstract class GreatCircleSubset implements HyperplaneSubset<Point2S>, Re
 
     /** {@inheritDoc} */
     @Override
-    public Point2S getBarycenter() {
-        final Point1S subspaceBarycenter = getSubspaceRegion().getBarycenter();
-        if (subspaceBarycenter != null) {
-            return getCircle().toSpace(subspaceBarycenter);
+    public Point2S getCentroid() {
+        final Point1S subspaceCentroid = getSubspaceRegion().getCentroid();
+        if (subspaceCentroid != null) {
+            return getCircle().toSpace(subspaceCentroid);
         }
         return null;
     }
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 1c5e38e..ba9210c 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
@@ -171,18 +171,18 @@ public class RegionBSPTree2S extends AbstractRegionBSPTree<Point2S, RegionBSPTre
         final DoublePrecisionContext precision = ((GreatArc) getRoot().getCut()).getPrecision();
 
         double sizeSum = 0;
-        Vector3D barycenterVector = Vector3D.ZERO;
+        Vector3D centroidVector = Vector3D.ZERO;
 
         for (final ConvexArea2S area : areas) {
             sizeSum += area.getSize();
-            barycenterVector = barycenterVector.add(area.getWeightedBarycenterVector());
+            centroidVector = centroidVector.add(area.getWeightedCentroidVector());
         }
 
-        final Point2S barycenter = barycenterVector.eq(Vector3D.ZERO, precision) ?
+        final Point2S centroid = centroidVector.eq(Vector3D.ZERO, precision) ?
                 null :
-                Point2S.from(barycenterVector);
+                Point2S.from(centroidVector);
 
-        return new RegionSizeProperties<>(sizeSum, barycenter);
+        return new RegionSizeProperties<>(sizeSum, centroid);
     }
 
     /** {@inheritDoc} */
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/DocumentationExamplesTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/DocumentationExamplesTest.java
index be7bb39..7a576c9 100644
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/DocumentationExamplesTest.java
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/DocumentationExamplesTest.java
@@ -117,8 +117,8 @@ public class DocumentationExamplesTest {
         // convert to a region
         RegionBSPTree2S tree = path.toTree();
 
-        // split in two through the barycenter
-        GreatCircle splitter = GreatCircles.fromPoints(tree.getBarycenter(), Point2S.PLUS_K, precision);
+        // split in two through the centroid
+        GreatCircle splitter = GreatCircles.fromPoints(tree.getCentroid(), Point2S.PLUS_K, precision);
         Split<RegionBSPTree2S> split = tree.split(splitter);
 
         // compute some properties for the minus side
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/AngularIntervalTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/AngularIntervalTest.java
index 3ab0f11..8f8feb5 100644
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/AngularIntervalTest.java
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/AngularIntervalTest.java
@@ -848,7 +848,7 @@ public class AngularIntervalTest {
         Assert.assertNull(interval.getMaxBoundary());
         Assert.assertEquals(PlaneAngleRadians.TWO_PI, interval.getMax(), TEST_EPS);
 
-        Assert.assertNull(interval.getBarycenter());
+        Assert.assertNull(interval.getCentroid());
         Assert.assertNull(interval.getMidPoint());
 
         Assert.assertEquals(PlaneAngleRadians.TWO_PI, interval.getSize(), TEST_EPS);
@@ -874,7 +874,7 @@ public class AngularIntervalTest {
         Assert.assertEquals(max, interval.getMax(), TEST_EPS);
 
         Assert.assertEquals(0.5 * (max + min), interval.getMidPoint().getAzimuth(), TEST_EPS);
-        Assert.assertSame(interval.getMidPoint(), interval.getBarycenter());
+        Assert.assertSame(interval.getMidPoint(), interval.getCentroid());
 
         Assert.assertEquals(0, interval.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(max - min, interval.getSize(), TEST_EPS);
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 66708f9..901fc87 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
@@ -402,7 +402,7 @@ public class CutAngleTest {
         Assert.assertFalse(sub.isInfinite());
         Assert.assertTrue(sub.isFinite());
         Assert.assertEquals(0.0, sub.getSize(), TEST_EPS);
-        SphericalTestUtils.assertPointsEqual(Point1S.of(1), sub.getBarycenter(), TEST_EPS);
+        SphericalTestUtils.assertPointsEqual(Point1S.of(1), sub.getCentroid(), TEST_EPS);
 
         List<? extends HyperplaneConvexSubset<Point1S>> list = sub.toConvex();
         Assert.assertEquals(1, list.size());
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1STest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1STest.java
index 8a482ba..273c96c 100644
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1STest.java
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1STest.java
@@ -51,7 +51,7 @@ public class RegionBSPTree1STest {
 
         Assert.assertEquals(0, tree.getSize(), TEST_EPS);
         Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
     }
 
     @Test
@@ -65,7 +65,7 @@ public class RegionBSPTree1STest {
 
         Assert.assertEquals(PlaneAngleRadians.TWO_PI, tree.getSize(), TEST_EPS);
         Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
     }
 
     @Test
@@ -79,7 +79,7 @@ public class RegionBSPTree1STest {
 
         Assert.assertEquals(0, tree.getSize(), TEST_EPS);
         Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
     }
 
     @Test
@@ -93,7 +93,7 @@ public class RegionBSPTree1STest {
 
         Assert.assertEquals(PlaneAngleRadians.TWO_PI, tree.getSize(), TEST_EPS);
         Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
     }
 
     @Test
@@ -107,7 +107,7 @@ public class RegionBSPTree1STest {
 
         Assert.assertEquals(0, tree.getSize(), TEST_EPS);
         Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
     }
 
     @Test
@@ -150,7 +150,7 @@ public class RegionBSPTree1STest {
             Assert.assertEquals(PlaneAngleRadians.PI_OVER_TWO, tree.getSize(), TEST_EPS);
             Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
             Assert.assertEquals(PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(theta + (0.25 * PlaneAngleRadians.PI)),
-                    tree.getBarycenter().getNormalizedAzimuth(), TEST_EPS);
+                    tree.getCentroid().getNormalizedAzimuth(), TEST_EPS);
         }
     }
 
@@ -676,7 +676,7 @@ public class RegionBSPTree1STest {
         // act/assert
         Assert.assertEquals(1.5 * PlaneAngleRadians.PI, tree.getSize(), TEST_EPS);
         Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
-        Assert.assertEquals(0.25 * PlaneAngleRadians.PI, tree.getBarycenter().getAzimuth(), TEST_EPS);
+        Assert.assertEquals(0.25 * PlaneAngleRadians.PI, tree.getCentroid().getAzimuth(), TEST_EPS);
     }
 
     @Test
@@ -688,7 +688,7 @@ public class RegionBSPTree1STest {
         // act/assert
         Assert.assertEquals(1.5 * PlaneAngleRadians.PI, tree.getSize(), TEST_EPS);
         Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
-        Assert.assertEquals(1.25 * PlaneAngleRadians.PI, tree.getBarycenter().getAzimuth(), TEST_EPS);
+        Assert.assertEquals(1.25 * PlaneAngleRadians.PI, tree.getCentroid().getAzimuth(), TEST_EPS);
     }
 
     @Test
@@ -701,7 +701,7 @@ public class RegionBSPTree1STest {
         // act/assert
         Assert.assertEquals(0.2, tree.getSize(), TEST_EPS);
         Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
-        Assert.assertEquals(0.15, tree.getBarycenter().getAzimuth(), TEST_EPS);
+        Assert.assertEquals(0.15, tree.getCentroid().getAzimuth(), TEST_EPS);
     }
 
     @Test
@@ -715,9 +715,9 @@ public class RegionBSPTree1STest {
         Assert.assertEquals(0.6, tree.getSize(), TEST_EPS);
         Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
 
-        Vector2D barycenterVector = Point1S.of(0.1).getVector().withNorm(0.2)
+        Vector2D centroidVector = Point1S.of(0.1).getVector().withNorm(0.2)
                 .add(Point1S.of(0.5).getVector().withNorm(0.4));
-        Assert.assertEquals(Point1S.from(barycenterVector).getAzimuth(), tree.getBarycenter().getAzimuth(), TEST_EPS);
+        Assert.assertEquals(Point1S.from(centroidVector).getAzimuth(), tree.getCentroid().getAzimuth(), TEST_EPS);
     }
 
     @Test
@@ -730,7 +730,7 @@ public class RegionBSPTree1STest {
         // act/assert
         Assert.assertEquals(4, tree.getSize(), TEST_EPS);
         Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
-        Assert.assertNull(tree.getBarycenter()); // no unique barycenter exists
+        Assert.assertNull(tree.getCentroid()); // no unique centroid exists
     }
 
     @Test
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/ConvexArea2STest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/ConvexArea2STest.java
index 444ff67..ed71e15 100644
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/ConvexArea2STest.java
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/ConvexArea2STest.java
@@ -52,7 +52,7 @@ public class ConvexArea2STest {
         Assert.assertFalse(area.isEmpty());
         Assert.assertEquals(0, area.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(4 * PlaneAngleRadians.PI, area.getSize(), TEST_EPS);
-        Assert.assertNull(area.getBarycenter());
+        Assert.assertNull(area.getCentroid());
 
         Assert.assertEquals(0, area.getBoundaries().size());
 
@@ -72,7 +72,7 @@ public class ConvexArea2STest {
         Assert.assertFalse(area.isEmpty());
         Assert.assertEquals(0, area.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(4 * PlaneAngleRadians.PI, area.getSize(), TEST_EPS);
-        Assert.assertNull(area.getBarycenter());
+        Assert.assertNull(area.getCentroid());
 
         Assert.assertEquals(0, area.getBoundaries().size());
 
@@ -95,8 +95,8 @@ public class ConvexArea2STest {
         Assert.assertFalse(area.isEmpty());
         Assert.assertEquals(2 * PlaneAngleRadians.PI, area.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(2 * PlaneAngleRadians.PI, area.getSize(), TEST_EPS);
-        SphericalTestUtils.assertPointsEq(Point2S.PLUS_J, area.getBarycenter(), TEST_EPS);
-        checkBarycenterConsistency(area);
+        SphericalTestUtils.assertPointsEq(Point2S.PLUS_J, area.getCentroid(), TEST_EPS);
+        checkCentroidConsistency(area);
 
         Assert.assertEquals(1, area.getBoundaries().size());
         GreatArc arc = area.getBoundaries().get(0);
@@ -127,8 +127,9 @@ public class ConvexArea2STest {
         Assert.assertFalse(area.isEmpty());
         Assert.assertEquals(2 * PlaneAngleRadians.PI, area.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(PlaneAngleRadians.PI_OVER_TWO, area.getSize(), TEST_EPS);
-        SphericalTestUtils.assertPointsEq(Point2S.of(0.125 * PlaneAngleRadians.PI, PlaneAngleRadians.PI_OVER_TWO), area.getBarycenter(), TEST_EPS);
-        checkBarycenterConsistency(area);
+        SphericalTestUtils.assertPointsEq(Point2S.of(0.125 * PlaneAngleRadians.PI, PlaneAngleRadians.PI_OVER_TWO),
+                area.getCentroid(), TEST_EPS);
+        checkCentroidConsistency(area);
 
         List<GreatArc> arcs = sortArcs(area.getBoundaries());
         Assert.assertEquals(2, arcs.size());
@@ -162,8 +163,8 @@ public class ConvexArea2STest {
         Assert.assertFalse(area.isEmpty());
         Assert.assertEquals(2 * PlaneAngleRadians.PI, area.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(PlaneAngleRadians.PI, area.getSize(), TEST_EPS);
-        SphericalTestUtils.assertPointsEq(Point2S.of(0, 0.25 * PlaneAngleRadians.PI), area.getBarycenter(), TEST_EPS);
-        checkBarycenterConsistency(area);
+        SphericalTestUtils.assertPointsEq(Point2S.of(0, 0.25 * PlaneAngleRadians.PI), area.getCentroid(), TEST_EPS);
+        checkCentroidConsistency(area);
 
         List<GreatArc> arcs = sortArcs(area.getBoundaries());
         Assert.assertEquals(2, arcs.size());
@@ -201,10 +202,10 @@ public class ConvexArea2STest {
         Assert.assertEquals(1.5 * PlaneAngleRadians.PI, area.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(PlaneAngleRadians.PI_OVER_TWO, area.getSize(), TEST_EPS);
 
-        Point2S expectedBarycenter = triangleBarycenter(Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K);
-        SphericalTestUtils.assertPointsEq(expectedBarycenter, area.getBarycenter(), TEST_EPS);
+        Point2S expectedCentroid = triangleCentroid(Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K);
+        SphericalTestUtils.assertPointsEq(expectedCentroid, area.getCentroid(), TEST_EPS);
 
-        checkBarycenterConsistency(area);
+        checkCentroidConsistency(area);
 
         List<GreatArc> arcs = sortArcs(area.getBoundaries());
         Assert.assertEquals(3, arcs.size());
@@ -252,10 +253,10 @@ public class ConvexArea2STest {
         double size = PlaneAngleRadians.TWO_PI - a.angle(b) - b.angle(c) - c.angle(a);
         Assert.assertEquals(size, area.getSize(), TEST_EPS);
 
-        Point2S expectedBarycenter = triangleBarycenter(p1, p2, p3);
-        SphericalTestUtils.assertPointsEq(expectedBarycenter, area.getBarycenter(), TEST_EPS);
+        Point2S expectedCentroid = triangleCentroid(p1, p2, p3);
+        SphericalTestUtils.assertPointsEq(expectedCentroid, area.getCentroid(), TEST_EPS);
 
-        checkBarycenterConsistency(area);
+        checkCentroidConsistency(area);
 
         List<GreatArc> arcs = sortArcs(area.getBoundaries());
         Assert.assertEquals(3, arcs.size());
@@ -299,7 +300,7 @@ public class ConvexArea2STest {
         double size = 2 * PlaneAngleRadians.PI - c1.angle(c2) - c2.angle(c3) - c3.angle(c4) - c4.angle(c1);
         Assert.assertEquals(size, area.getSize(), TEST_EPS);
 
-        checkBarycenterConsistency(area);
+        checkCentroidConsistency(area);
 
         List<GreatArc> arcs = sortArcs(area.getBoundaries());
         Assert.assertEquals(4, arcs.size());
@@ -346,10 +347,10 @@ public class ConvexArea2STest {
         Assert.assertEquals(1.5 * PlaneAngleRadians.PI, area.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(PlaneAngleRadians.PI_OVER_TWO, area.getSize(), TEST_EPS);
 
-        Point2S expectedBarycenter = triangleBarycenter(Point2S.MINUS_I, Point2S.MINUS_K, Point2S.MINUS_J);
-        SphericalTestUtils.assertPointsEq(expectedBarycenter, area.getBarycenter(), TEST_EPS);
+        Point2S expectedCentroid = triangleCentroid(Point2S.MINUS_I, Point2S.MINUS_K, Point2S.MINUS_J);
+        SphericalTestUtils.assertPointsEq(expectedCentroid, area.getCentroid(), TEST_EPS);
 
-        checkBarycenterConsistency(area);
+        checkCentroidConsistency(area);
 
         List<GreatArc> arcs = sortArcs(area.getBoundaries());
         Assert.assertEquals(3, arcs.size());
@@ -391,8 +392,8 @@ public class ConvexArea2STest {
         Assert.assertFalse(area.isEmpty());
         Assert.assertEquals(2 * PlaneAngleRadians.PI, area.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(PlaneAngleRadians.PI, area.getSize(), TEST_EPS);
-        SphericalTestUtils.assertPointsEq(Point2S.of(0, 0.25 * PlaneAngleRadians.PI), area.getBarycenter(), TEST_EPS);
-        checkBarycenterConsistency(area);
+        SphericalTestUtils.assertPointsEq(Point2S.of(0, 0.25 * PlaneAngleRadians.PI), area.getCentroid(), TEST_EPS);
+        checkCentroidConsistency(area);
 
         List<GreatArc> arcs = sortArcs(area.getBoundaries());
         Assert.assertEquals(2, arcs.size());
@@ -428,10 +429,10 @@ public class ConvexArea2STest {
         Assert.assertEquals(1.5 * PlaneAngleRadians.PI, area.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(PlaneAngleRadians.PI_OVER_TWO, area.getSize(), TEST_EPS);
 
-        Point2S expectedBarycenter = triangleBarycenter(Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K);
-        SphericalTestUtils.assertPointsEq(expectedBarycenter, area.getBarycenter(), TEST_EPS);
+        Point2S expectedCentroid = triangleCentroid(Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K);
+        SphericalTestUtils.assertPointsEq(expectedCentroid, area.getCentroid(), TEST_EPS);
 
-        checkBarycenterConsistency(area);
+        checkCentroidConsistency(area);
 
         List<GreatArc> arcs = sortArcs(area.getBoundaries());
         Assert.assertEquals(3, arcs.size());
@@ -465,8 +466,8 @@ public class ConvexArea2STest {
         // assert
         Assert.assertEquals(PlaneAngleRadians.PI_OVER_TWO, area.getSize(), TEST_EPS);
 
-        Point2S expectedBarycenter = triangleBarycenter(Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K);
-        SphericalTestUtils.assertPointsEq(expectedBarycenter, area.getBarycenter(), TEST_EPS);
+        Point2S expectedCentroid = triangleCentroid(Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K);
+        SphericalTestUtils.assertPointsEq(expectedCentroid, area.getCentroid(), TEST_EPS);
 
         List<Point2S> vertices = area.getBoundaryPath().getVertices();
         Assert.assertEquals(4, vertices.size());
@@ -504,10 +505,10 @@ public class ConvexArea2STest {
         Assert.assertEquals(1.5 * PlaneAngleRadians.PI, area.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(PlaneAngleRadians.PI_OVER_TWO, area.getSize(), TEST_EPS);
 
-        Point2S expectedBarycenter = triangleBarycenter(Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K);
-        SphericalTestUtils.assertPointsEq(expectedBarycenter, area.getBarycenter(), TEST_EPS);
+        Point2S expectedCentroid = triangleCentroid(Point2S.PLUS_I, Point2S.PLUS_J, Point2S.PLUS_K);
+        SphericalTestUtils.assertPointsEq(expectedCentroid, area.getCentroid(), TEST_EPS);
 
-        checkBarycenterConsistency(area);
+        checkCentroidConsistency(area);
 
         List<GreatArc> arcs = sortArcs(area.getBoundaries());
         Assert.assertEquals(3, arcs.size());
@@ -611,10 +612,10 @@ public class ConvexArea2STest {
         Assert.assertEquals(1.5 * PlaneAngleRadians.PI, area.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(PlaneAngleRadians.PI_OVER_TWO, area.getSize(), TEST_EPS);
 
-        Point2S expectedBarycenter = triangleBarycenter(Point2S.MINUS_J, Point2S.PLUS_I, Point2S.PLUS_K);
-        SphericalTestUtils.assertPointsEq(expectedBarycenter, area.getBarycenter(), TEST_EPS);
+        Point2S expectedCentroid = triangleCentroid(Point2S.MINUS_J, Point2S.PLUS_I, Point2S.PLUS_K);
+        SphericalTestUtils.assertPointsEq(expectedCentroid, area.getCentroid(), TEST_EPS);
 
-        checkBarycenterConsistency(area);
+        checkCentroidConsistency(area);
 
         List<GreatArc> arcs = sortArcs(area.getBoundaries());
         Assert.assertEquals(3, arcs.size());
@@ -755,7 +756,7 @@ public class ConvexArea2STest {
         Assert.assertFalse(tree.isEmpty());
 
         Assert.assertEquals(area.getSize(), tree.getSize(), TEST_EPS);
-        SphericalTestUtils.assertPointsEq(area.getBarycenter(), tree.getBarycenter(), TEST_EPS);
+        SphericalTestUtils.assertPointsEq(area.getCentroid(), tree.getCentroid(), TEST_EPS);
     }
 
     private static List<GreatArc> sortArcs(List<GreatArc> arcs) {
@@ -767,8 +768,8 @@ public class ConvexArea2STest {
         return result;
     }
 
-    private static Point2S triangleBarycenter(Point2S p1, Point2S p2, Point2S p3) {
-        // compute the barycenter as the sum of the cross product of each point pair weighted by
+    private static Point2S triangleCentroid(Point2S p1, Point2S p2, Point2S p3) {
+        // compute the centroid as the sum of the cross product of each point pair weighted by
         // the angle between the points
         Vector3D v1 = p1.getVector();
         Vector3D v2 = p2.getVector();
@@ -801,16 +802,16 @@ public class ConvexArea2STest {
         }
     }
 
-    private static void checkBarycenterConsistency(ConvexArea2S area) {
-        Point2S barycenter = area.getBarycenter();
+    private static void checkCentroidConsistency(ConvexArea2S area) {
+        Point2S centroid = area.getCentroid();
         double size = area.getSize();
 
-        SphericalTestUtils.checkClassify(area, RegionLocation.INSIDE, barycenter);
+        SphericalTestUtils.checkClassify(area, RegionLocation.INSIDE, centroid);
 
-        GreatCircle circle = GreatCircles.fromPole(barycenter.getVector(), TEST_PRECISION);
+        GreatCircle circle = GreatCircles.fromPole(centroid.getVector(), TEST_PRECISION);
         for (double az = 0; az <= PlaneAngleRadians.TWO_PI; az += 0.2) {
             Point2S pt = circle.toSpace(Point1S.of(az));
-            GreatCircle splitter = GreatCircles.fromPoints(barycenter, pt, TEST_PRECISION);
+            GreatCircle splitter = GreatCircles.fromPoints(centroid, pt, TEST_PRECISION);
 
             Split<ConvexArea2S> split = area.split(splitter);
 
@@ -822,11 +823,11 @@ public class ConvexArea2STest {
             ConvexArea2S plus = split.getPlus();
             double plusSize = plus.getSize();
 
-            Point2S computedBarycenter = Point2S.from(minus.getWeightedBarycenterVector()
-                    .add(plus.getWeightedBarycenterVector()));
+            Point2S computedCentroid = Point2S.from(minus.getWeightedCentroidVector()
+                    .add(plus.getWeightedCentroidVector()));
 
             Assert.assertEquals(size, minusSize + plusSize, TEST_EPS);
-            SphericalTestUtils.assertPointsEq(barycenter, computedBarycenter, TEST_EPS);
+            SphericalTestUtils.assertPointsEq(centroid, computedCentroid, TEST_EPS);
         }
     }
 }
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/EmbeddedTreeSubGreatCircleTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/EmbeddedTreeSubGreatCircleTest.java
index abbf849..cb213b8 100644
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/EmbeddedTreeSubGreatCircleTest.java
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/EmbeddedTreeSubGreatCircleTest.java
@@ -55,7 +55,7 @@ public class EmbeddedTreeSubGreatCircleTest {
         Assert.assertFalse(sub.isInfinite());
 
         Assert.assertEquals(0, sub.getSize(), TEST_EPS);
-        Assert.assertNull(sub.getBarycenter());
+        Assert.assertNull(sub.getCentroid());
 
         for (double az = 0; az <= PlaneAngleRadians.TWO_PI; az += 0.5) {
             for (double p = 0; p <= PlaneAngleRadians.PI; p += 0.5) {
@@ -76,7 +76,7 @@ public class EmbeddedTreeSubGreatCircleTest {
         Assert.assertFalse(sub.isInfinite());
 
         Assert.assertEquals(PlaneAngleRadians.TWO_PI, sub.getSize(), TEST_EPS);
-        Assert.assertNull(sub.getBarycenter());
+        Assert.assertNull(sub.getCentroid());
 
         for (double az = 0; az < PlaneAngleRadians.TWO_PI; az += 0.1) {
             checkClassify(sub, RegionLocation.INSIDE, Point2S.of(az, PlaneAngleRadians.PI_OVER_TWO));
@@ -99,7 +99,7 @@ public class EmbeddedTreeSubGreatCircleTest {
         Assert.assertFalse(sub.isInfinite());
 
         Assert.assertEquals(0, sub.getSize(), TEST_EPS);
-        Assert.assertNull(sub.getBarycenter());
+        Assert.assertNull(sub.getCentroid());
 
         for (double az = 0; az <= PlaneAngleRadians.TWO_PI; az += 0.5) {
             for (double p = 0; p <= PlaneAngleRadians.PI; p += 0.5) {
@@ -124,7 +124,7 @@ public class EmbeddedTreeSubGreatCircleTest {
 
         Assert.assertEquals(1, sub.getSize(), TEST_EPS);
         SphericalTestUtils.assertPointsEq(Point2S.of(1.5, PlaneAngleRadians.PI_OVER_TWO),
-                sub.getBarycenter(), TEST_EPS);
+                sub.getCentroid(), TEST_EPS);
 
         checkClassify(sub, RegionLocation.INSIDE, Point2S.of(1.5, PlaneAngleRadians.PI_OVER_TWO));
 
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcPathTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcPathTest.java
index b59209c..e21186d 100644
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcPathTest.java
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcPathTest.java
@@ -335,7 +335,7 @@ public class GreatArcPathTest {
         Assert.assertFalse(tree.isEmpty());
 
         Assert.assertEquals(PlaneAngleRadians.TWO_PI, tree.getSize(), TEST_EPS);
-        SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, tree.getBarycenter(), TEST_EPS);
+        SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, tree.getCentroid(), TEST_EPS);
 
         SphericalTestUtils.checkClassify(tree, RegionLocation.INSIDE, Point2S.PLUS_K);
         SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE, Point2S.MINUS_K);
@@ -363,7 +363,7 @@ public class GreatArcPathTest {
                 .add(Point2S.PLUS_J.getVector())
                 .add(Point2S.PLUS_K.getVector()));
 
-        SphericalTestUtils.assertPointsEq(bc, tree.getBarycenter(), TEST_EPS);
+        SphericalTestUtils.assertPointsEq(bc, tree.getCentroid(), TEST_EPS);
 
         SphericalTestUtils.checkClassify(tree, RegionLocation.INSIDE, Point2S.of(0.5, 0.5));
         SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE,
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcTest.java
index 3a405d2..70bfa52 100644
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcTest.java
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcTest.java
@@ -55,7 +55,7 @@ public class GreatArcTest {
         Assert.assertNull(arc.getEndPoint());
 
         Assert.assertEquals(PlaneAngleRadians.TWO_PI, arc.getSize(), TEST_EPS);
-        Assert.assertNull(arc.getBarycenter());
+        Assert.assertNull(arc.getCentroid());
 
         for (double az = 0; az < PlaneAngleRadians.TWO_PI; az += 0.1) {
             checkClassify(arc, RegionLocation.INSIDE, Point2S.of(az, PlaneAngleRadians.PI_OVER_TWO));
@@ -373,7 +373,7 @@ public class GreatArcTest {
         checkClassify(arc, RegionLocation.OUTSIDE, mid.antipodal());
 
         Assert.assertEquals(start.distance(end), arc.getSize(), TEST_EPS);
-        SphericalTestUtils.assertPointsEq(mid, arc.getBarycenter(), TEST_EPS);
+        SphericalTestUtils.assertPointsEq(mid, arc.getCentroid(), TEST_EPS);
     }
 
     private static void checkGreatCircle(GreatCircle circle, Vector3D pole, Vector3D x) {
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatCircleSubsetTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatCircleSubsetTest.java
index 9bad029..43de49f 100644
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatCircleSubsetTest.java
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatCircleSubsetTest.java
@@ -257,7 +257,7 @@ public class GreatCircleSubsetTest {
         }
 
         @Override
-        public Point2S getBarycenter() {
+        public Point2S getCentroid() {
             return null;
         }
 
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 3b41a70..98b6872 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
@@ -39,9 +39,9 @@ public class RegionBSPTree2STest {
 
     private static final double TEST_EPS = 1e-10;
 
-    // alternative epsilon value for checking the barycenters of complex
+    // alternative epsilon value for checking the centroids of complex
     // or very small regions
-    private static final double BARYCENTER_EPS = 1e-5;
+    private static final double CENTROID_EPS = 1e-5;
 
     private static final DoublePrecisionContext TEST_PRECISION =
             new EpsilonDoublePrecisionContext(TEST_EPS);
@@ -360,7 +360,7 @@ public class RegionBSPTree2STest {
         SphericalTestUtils.assertPointsEq(Point2S.MINUS_I,
                 tree.project(Point2S.of(PlaneAngleRadians.PI + 0.5, PlaneAngleRadians.PI_OVER_TWO)), TEST_EPS);
 
-        SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, tree.project(tree.getBarycenter()), TEST_EPS);
+        SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, tree.project(tree.getCentroid()), TEST_EPS);
     }
 
     @Test
@@ -377,7 +377,7 @@ public class RegionBSPTree2STest {
 
         // act/assert
         Assert.assertEquals(4 * PlaneAngleRadians.PI, tree.getSize(), TEST_EPS);
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
 
         Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
 
@@ -392,7 +392,7 @@ public class RegionBSPTree2STest {
 
         // act/assert
         Assert.assertEquals(0, tree.getSize(), TEST_EPS);
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
 
         Assert.assertEquals(0, tree.getBoundarySize(), TEST_EPS);
 
@@ -409,9 +409,9 @@ public class RegionBSPTree2STest {
         // act/assert
         Assert.assertEquals(PlaneAngleRadians.TWO_PI, tree.getSize(), TEST_EPS);
         Assert.assertEquals(PlaneAngleRadians.TWO_PI, tree.getBoundarySize(), TEST_EPS);
-        SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, tree.getBarycenter(), TEST_EPS);
+        SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, tree.getCentroid(), TEST_EPS);
 
-        checkBarycenterConsistency(tree);
+        checkCentroidConsistency(tree);
 
         List<GreatArc> arcs = tree.getBoundaries();
         Assert.assertEquals(1, arcs.size());
@@ -443,7 +443,7 @@ public class RegionBSPTree2STest {
         // assert
         Assert.assertEquals(2 * PlaneAngleRadians.PI, tree.getSize(), TEST_EPS);
         Assert.assertEquals(4 * PlaneAngleRadians.PI, tree.getBoundarySize(), TEST_EPS);
-        Assert.assertNull(tree.getBarycenter());
+        Assert.assertNull(tree.getCentroid());
 
         List<GreatArcPath> paths = tree.getBoundaryPaths();
         Assert.assertEquals(2, paths.size());
@@ -475,9 +475,9 @@ public class RegionBSPTree2STest {
         Point2S center = Point2S.from(Point2S.MINUS_K.getVector()
                 .add(Point2S.PLUS_I.getVector())
                 .add(Point2S.MINUS_J.getVector()));
-        SphericalTestUtils.assertPointsEq(center, tree.getBarycenter(), TEST_EPS);
+        SphericalTestUtils.assertPointsEq(center, tree.getCentroid(), TEST_EPS);
 
-        checkBarycenterConsistency(tree);
+        checkCentroidConsistency(tree);
 
         List<GreatArcPath> paths = tree.getBoundaryPaths();
         Assert.assertEquals(1, paths.size());
@@ -509,9 +509,9 @@ public class RegionBSPTree2STest {
         Point2S center = Point2S.from(Point2S.MINUS_K.getVector()
                 .add(Point2S.PLUS_I.getVector())
                 .add(Point2S.MINUS_J.getVector()));
-        SphericalTestUtils.assertPointsEq(center.antipodal(), tree.getBarycenter(), TEST_EPS);
+        SphericalTestUtils.assertPointsEq(center.antipodal(), tree.getCentroid(), TEST_EPS);
 
-        checkBarycenterConsistency(tree);
+        checkCentroidConsistency(tree);
 
         List<GreatArcPath> paths = tree.getBoundaryPaths();
         Assert.assertEquals(1, paths.size());
@@ -552,8 +552,8 @@ public class RegionBSPTree2STest {
         double boundarySize = 4 * (outerSideLength + innerSideLength);
         Assert.assertEquals(boundarySize, tree.getBoundarySize(), TEST_EPS);
 
-        SphericalTestUtils.assertPointsEq(center, tree.getBarycenter(), TEST_EPS);
-        checkBarycenterConsistency(tree);
+        SphericalTestUtils.assertPointsEq(center, tree.getCentroid(), TEST_EPS);
+        checkCentroidConsistency(tree);
 
         SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE, center);
     }
@@ -594,8 +594,8 @@ public class RegionBSPTree2STest {
         double boundarySize = 4 * (outerSideLength + midSideLength + innerSideLength);
         Assert.assertEquals(boundarySize, tree.getBoundarySize(), TEST_EPS);
 
-        SphericalTestUtils.assertPointsEq(center.antipodal(), tree.getBarycenter(), TEST_EPS);
-        checkBarycenterConsistency(tree);
+        SphericalTestUtils.assertPointsEq(center.antipodal(), tree.getCentroid(), TEST_EPS);
+        checkCentroidConsistency(tree);
 
         SphericalTestUtils.checkClassify(tree, RegionLocation.OUTSIDE, center);
     }
@@ -620,8 +620,8 @@ public class RegionBSPTree2STest {
         double boundarySize = 8 * sphericalHypot(radius, radius);
         Assert.assertEquals(boundarySize, tree.getBoundarySize(), TEST_EPS);
 
-        // should be null since no unique barycenter exists
-        Assert.assertNull(tree.getBarycenter());
+        // should be null since no unique centroid exists
+        Assert.assertNull(tree.getCentroid());
     }
 
     @Test
@@ -716,10 +716,10 @@ public class RegionBSPTree2STest {
         Assert.assertEquals(1.5 * PlaneAngleRadians.PI, tree.getBoundarySize(), TEST_EPS);
         Assert.assertEquals(PlaneAngleRadians.PI_OVER_TWO, tree.getSize(), TEST_EPS);
 
-        Point2S expectedBarycenter = triangleBarycenter(Point2S.MINUS_J, Point2S.PLUS_I, Point2S.PLUS_K);
-        SphericalTestUtils.assertPointsEq(expectedBarycenter, tree.getBarycenter(), TEST_EPS);
+        Point2S expectedCentroid = triangleCentroid(Point2S.MINUS_J, Point2S.PLUS_I, Point2S.PLUS_K);
+        SphericalTestUtils.assertPointsEq(expectedCentroid, tree.getCentroid(), TEST_EPS);
 
-        checkBarycenterConsistency(tree);
+        checkCentroidConsistency(tree);
 
         SphericalTestUtils.checkClassify(tree, RegionLocation.INSIDE,
                 Point2S.of(-0.25 * PlaneAngleRadians.PI, 0.25 * PlaneAngleRadians.PI));
@@ -745,16 +745,16 @@ public class RegionBSPTree2STest {
         // act/assert
         ConvexArea2S rootRegion = root.getNodeRegion();
         Assert.assertEquals(4 * PlaneAngleRadians.PI, rootRegion.getSize(), TEST_EPS);
-        Assert.assertNull(rootRegion.getBarycenter());
+        Assert.assertNull(rootRegion.getCentroid());
 
         ConvexArea2S minusRegion = minus.getNodeRegion();
         Assert.assertEquals(2 * PlaneAngleRadians.PI, minusRegion.getSize(), TEST_EPS);
-        SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, minusRegion.getBarycenter(), TEST_EPS);
+        SphericalTestUtils.assertPointsEq(Point2S.PLUS_K, minusRegion.getCentroid(), TEST_EPS);
 
         ConvexArea2S minusPlusRegion = minusPlus.getNodeRegion();
         Assert.assertEquals(PlaneAngleRadians.PI, minusPlusRegion.getSize(), TEST_EPS);
         SphericalTestUtils.assertPointsEq(Point2S.of(1.5 * PlaneAngleRadians.PI, 0.25 * PlaneAngleRadians.PI),
-                minusPlusRegion.getBarycenter(), TEST_EPS);
+                minusPlusRegion.getCentroid(), TEST_EPS);
     }
 
     @Test
@@ -784,24 +784,24 @@ public class RegionBSPTree2STest {
         Assert.assertEquals(0.013964220234478741, france.getSize(), TEST_EPS);
 
         SphericalTestUtils.assertPointsEq(Point2S.of(0.04368552749392928, 0.7590839905197961),
-                france.getBarycenter(), BARYCENTER_EPS);
+                france.getCentroid(), CENTROID_EPS);
 
-        checkBarycenterConsistency(france);
+        checkCentroidConsistency(france);
     }
 
     @Test
-    public void testCircleToPolygonBarycenter() {
+    public void testCircleToPolygonCentroid() {
         double radius = 0.0001;
         Point2S center = Point2S.of(1.0, 1.0);
         int numPts = 200;
 
         // counterclockwise
         RegionBSPTree2S ccw = circleToPolygon(center, radius, numPts, false);
-        SphericalTestUtils.assertPointsEq(center, ccw.getBarycenter(), BARYCENTER_EPS);
+        SphericalTestUtils.assertPointsEq(center, ccw.getCentroid(), CENTROID_EPS);
 
-        // clockwise; barycenter should just be antipodal for the circle center
+        // clockwise; centroid should just be antipodal for the circle center
         RegionBSPTree2S cw = circleToPolygon(center, radius, numPts, true);
-        SphericalTestUtils.assertPointsEq(center.antipodal(), cw.getBarycenter(), BARYCENTER_EPS);
+        SphericalTestUtils.assertPointsEq(center.antipodal(), cw.getCentroid(), CENTROID_EPS);
     }
 
     @Test
@@ -849,8 +849,8 @@ public class RegionBSPTree2STest {
             ));
     }
 
-    private static Point2S triangleBarycenter(Point2S p1, Point2S p2, Point2S p3) {
-        // compute the barycenter using intersection mid point arcs
+    private static Point2S triangleCentroid(Point2S p1, Point2S p2, Point2S p3) {
+        // compute the centroid using intersection mid point arcs
         GreatCircle c1 = GreatCircles.fromPoints(p1, p2.slerp(p3, 0.5), TEST_PRECISION);
         GreatCircle c2 = GreatCircles.fromPoints(p2, p1.slerp(p3, 0.5), TEST_PRECISION);
 
@@ -888,14 +888,14 @@ public class RegionBSPTree2STest {
         return Point2S.of(Math.toRadians(longitude), Math.toRadians(90.0 - latitude));
     }
 
-    private static void checkBarycenterConsistency(RegionBSPTree2S region) {
-        Point2S barycenter = region.getBarycenter();
+    private static void checkCentroidConsistency(RegionBSPTree2S region) {
+        Point2S centroid = region.getCentroid();
         double size = region.getSize();
 
-        GreatCircle circle = GreatCircles.fromPole(barycenter.getVector(), TEST_PRECISION);
+        GreatCircle circle = GreatCircles.fromPole(centroid.getVector(), TEST_PRECISION);
         for (double az = 0; az <= PlaneAngleRadians.TWO_PI; az += 0.2) {
             Point2S pt = circle.toSpace(Point1S.of(az));
-            GreatCircle splitter = GreatCircles.fromPoints(barycenter, pt, TEST_PRECISION);
+            GreatCircle splitter = GreatCircles.fromPoints(centroid, pt, TEST_PRECISION);
 
             Split<RegionBSPTree2S> split = region.split(splitter);
 
@@ -907,18 +907,18 @@ public class RegionBSPTree2STest {
             RegionBSPTree2S plus = split.getPlus();
             double plusSize = plus.getSize();
 
-            Point2S computedBarycenter = Point2S.from(weightedBarycenterVector(minus)
-                    .add(weightedBarycenterVector(plus)));
+            Point2S computedCentroid = Point2S.from(weightedCentroidVector(minus)
+                    .add(weightedCentroidVector(plus)));
 
             Assert.assertEquals(size, minusSize + plusSize, TEST_EPS);
-            SphericalTestUtils.assertPointsEq(barycenter, computedBarycenter, TEST_EPS);
+            SphericalTestUtils.assertPointsEq(centroid, computedCentroid, TEST_EPS);
         }
     }
 
-    private static Vector3D weightedBarycenterVector(RegionBSPTree2S tree) {
+    private static Vector3D weightedCentroidVector(RegionBSPTree2S tree) {
         Vector3D sum = Vector3D.ZERO;
         for (ConvexArea2S convex : tree.toConvex()) {
-            sum = sum.add(convex.getWeightedBarycenterVector());
+            sum = sum.add(convex.getWeightedCentroidVector());
         }
 
         return sum;
diff --git a/src/site/xdoc/index.xml b/src/site/xdoc/index.xml
index 8e1c7a1..2da9596 100644
--- a/src/site/xdoc/index.xml
+++ b/src/site/xdoc/index.xml
@@ -45,7 +45,7 @@
 // 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
+// create a binary space partitioning (BSP) tree representing the unit cube
 // centered on the origin
 RegionBSPTree3D region = Parallelepiped.builder(precision)
         .setPosition(Vector3D.ZERO)
@@ -62,7 +62,7 @@ region.intersection(copy);
 
 // compute some properties of the intersection region
 double size = region.getSize(); // 0.8284271247461903
-Vector3D center = region.getBarycenter(); // (0, 0, 0)
+Vector3D centroid = region.getCentroid(); // (0, 0, 0)
 </source>
     </section>
 
diff --git a/src/site/xdoc/userguide/index.xml b/src/site/xdoc/userguide/index.xml
index c33edc5..35f195a 100644
--- a/src/site/xdoc/userguide/index.xml
+++ b/src/site/xdoc/userguide/index.xml
@@ -350,7 +350,7 @@ currentNode.insertCut(Lines.fromPointAndDirection(Vector2D.of(0, 1), Vector2D.Un
 int count = tree.count(); // number of nodes in the tree = 11
 int height = tree.height(); // height of the tree = 3
 double size = tree.getSize(); // size of the region = 1
-Vector2D barycenter = tree.getBarycenter(); // region barycenter = (0.5, 0.5)
+Vector2D centroid = tree.getCentroid(); // region centroid = (0.5, 0.5)
         </source>
 
         <h5>Standard BSP Tree Region Creation</h5>
@@ -376,7 +376,7 @@ tree.insert(Arrays.asList(
 int count = tree.count(); // number of nodes in the tree = 9
 int height = tree.height(); // height of the tree = 4
 double size = tree.getSize(); // size of the region = 1
-Vector2D barycenter = tree.getBarycenter(); // region barycenter = (0.5, 0.5)
+Vector2D centroid = tree.getCentroid(); // region centroid = (0.5, 0.5)
         </source>
 
       </subsection>
@@ -666,7 +666,7 @@ tree.union(copy);
 
 // compute some properties
 double size = tree.getSize(); // 1.75
-Vector2D center = tree.getBarycenter(); // (0.75, 0.75)
+Vector2D centroid = tree.getCentroid(); // (0.75, 0.75)
 
 // get a line path representing the boundary; a list is returned since trees
 // can represent disjoint regions
@@ -831,8 +831,8 @@ int[][] faceIndices = {
 List&lt;ConvexPolygon3D&gt; faces = Planes.indexedConvexPolygons(vertices, faceIndices, precision);
 RegionBSPTree3D tree = RegionBSPTree3D.from(faces);
 
-// split the region through its barycenter along a diagonal of the base
-Plane cutter = Planes.fromPointAndNormal(tree.getBarycenter(), Vector3D.Unit.from(1, 1, 0), precision);
+// split the region through its centroid along a diagonal of the base
+Plane cutter = Planes.fromPointAndNormal(tree.getCentroid(), Vector3D.Unit.from(1, 1, 0), precision);
 Split&lt;RegionBSPTree3D&gt; split = tree.split(cutter);
 
 // compute some properties for the minus side of the split and convert back to hyperplane subsets
@@ -1059,8 +1059,8 @@ GreatArcPath path = GreatArcPath.builder(precision)
 // convert to a region
 RegionBSPTree2S tree = path.toTree();
 
-// split in two through the barycenter
-GreatCircle splitter = GreatCircles.fromPoints(tree.getBarycenter(), Point2S.PLUS_K, precision);
+// split in two through the centroid
+GreatCircle splitter = GreatCircles.fromPoints(tree.getCentroid(), Point2S.PLUS_K, precision);
 Split&lt;RegionBSPTree2S&gt; split = tree.split(splitter);
 
 // compute some properties for the minus side