You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by er...@apache.org on 2019/11/24 23:09:52 UTC

[commons-geometry] 01/04: GEOMETRY-32: refactoring BSP and related classes; also addresses issues GEOMETRY-32 (simplify Transform interface), GEOMETRY-33 (Region API), and GEOMETRY-34 (SubHyperplane optimized implementations)

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

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

commit 77a2792463b091a37eea3859032793648a9b525d
Author: Matt Juntunen <ma...@hotmail.com>
AuthorDate: Sun Nov 24 01:04:02 2019 -0500

    GEOMETRY-32: refactoring BSP and related classes; also addresses issues GEOMETRY-32 (simplify Transform interface), GEOMETRY-33 (Region API), and GEOMETRY-34 (SubHyperplane optimized implementations)
---
 .../apache/commons/geometry/core/Embedding.java    |   71 +
 .../org/apache/commons/geometry/core/Geometry.java |    8 +-
 .../org/apache/commons/geometry/core/Region.java   |   82 +
 ...etryValueException.java => RegionLocation.java} |   28 +-
 .../org/apache/commons/geometry/core/Spatial.java  |    6 +
 .../apache/commons/geometry/core/Transform.java    |   56 +
 .../org/apache/commons/geometry/core/Vector.java   |    8 +-
 .../geometry/core/exception/GeometryException.java |    2 +-
 .../core/exception/GeometryValueException.java     |    2 +-
 .../core/exception/IllegalNormException.java       |    2 +-
 .../Equivalency.java}                              |   31 +-
 .../core/internal/GeometryInternalError.java       |    2 +-
 .../geometry/core/internal/IteratorTransform.java  |   92 +
 .../geometry/core/internal/SimpleTupleFormat.java  |   17 +-
 .../AbstractConvexHyperplaneBoundedRegion.java     |  379 ++++
 .../AbstractEmbeddingSubHyperplane.java            |  110 +
 .../core/partitioning/AbstractHyperplane.java      |   70 +
 .../geometry/core/partitioning/AbstractRegion.java |  516 -----
 .../core/partitioning/AbstractSubHyperplane.java   |  189 --
 .../geometry/core/partitioning/BSPTree.java        |  781 -------
 .../geometry/core/partitioning/BSPTreeVisitor.java |  112 -
 .../core/partitioning/BoundaryAttribute.java       |  100 -
 .../core/partitioning/BoundaryBuilder.java         |   99 -
 .../core/partitioning/BoundaryProjection.java      |   84 -
 .../core/partitioning/BoundaryProjector.java       |  200 --
 .../core/partitioning/BoundarySizeVisitor.java     |   67 -
 .../core/partitioning/Characterization.java        |  196 --
 .../core/partitioning/ConvexSubHyperplane.java     |   50 +
 .../geometry/core/partitioning/Embedding.java      |   62 -
 .../{Side.java => EmbeddingHyperplane.java}        |   26 +-
 .../geometry/core/partitioning/Hyperplane.java     |  105 +-
 .../{Side.java => HyperplaneBoundedRegion.java}    |   26 +-
 .../{Side.java => HyperplaneLocation.java}         |   26 +-
 .../geometry/core/partitioning/InsideFinder.java   |  149 --
 .../geometry/core/partitioning/NodesSet.java       |   72 -
 .../commons/geometry/core/partitioning/Region.java |  204 --
 .../geometry/core/partitioning/RegionFactory.java  |  383 ----
 .../commons/geometry/core/partitioning/Split.java  |   97 +
 .../partitioning/{Side.java => SplitLocation.java} |   27 +-
 .../partitioning/{Side.java => Splittable.java}    |   26 +-
 .../geometry/core/partitioning/SubHyperplane.java  |  194 +-
 .../geometry/core/partitioning/Transform.java      |   77 -
 .../core/partitioning/bsp/AbstractBSPTree.java     | 1108 ++++++++++
 .../bsp/AbstractBSPTreeMergeOperator.java          |  147 ++
 .../partitioning/bsp/AbstractRegionBSPTree.java    |  966 +++++++++
 .../core/partitioning/bsp/AttributeBSPTree.java    |  144 ++
 .../geometry/core/partitioning/bsp/BSPSubtree.java |   53 +
 .../geometry/core/partitioning/bsp/BSPTree.java    |  225 ++
 .../core/partitioning/bsp/BSPTreePrinter.java      |  118 +
 .../core/partitioning/bsp/BSPTreeVisitor.java      |  173 ++
 .../core/partitioning/bsp/RegionCutBoundary.java   |  109 +
 .../{Side.java => bsp/package-info.java}           |   26 +-
 .../geometry/core/partitioning/package-info.java   |   93 +-
 .../core/precision/DoublePrecisionContext.java     |   20 +-
 .../precision/EpsilonDoublePrecisionContext.java   |    2 +-
 .../commons/geometry/core/EmbeddingTest.java       |   98 +
 .../commons/geometry/core/GeometryTestUtils.java   |   39 +-
 .../core/internal/IteratorTransformTest.java       |   90 +
 .../core/partition/test/PartitionTestUtils.java    |  115 +
 .../geometry/core/partition/test/TestBSPTree.java  |   64 +
 .../geometry/core/partition/test/TestLine.java     |  280 +++
 .../core/partition/test/TestLineSegment.java       |  340 +++
 .../partition/test/TestLineSegmentCollection.java  |  197 ++
 .../test/TestLineSegmentCollectionBuilder.java     |  101 +
 .../geometry/core/partition/test/TestPoint1D.java  |   83 +
 .../geometry/core/partition/test/TestPoint2D.java  |  107 +
 .../core/partition/test/TestTransform2D.java       |   60 +
 .../AbstractConvexHyperplaneBoundedRegionTest.java |  546 +++++
 .../AbstractEmbeddingSubHyperplaneTest.java        |  187 ++
 .../core/partitioning/AbstractHyperplaneTest.java  |  113 +
 .../geometry/core/partitioning/SplitTest.java      |   64 +
 .../geometry/core/partitioning/TreeBuilder.java    |  168 --
 .../geometry/core/partitioning/TreeDumper.java     |  101 -
 .../geometry/core/partitioning/TreePrinter.java    |  135 --
 .../bsp/AbstractBSPTreeMergeOperatorTest.java      |  561 +++++
 .../core/partitioning/bsp/AbstractBSPTreeTest.java | 1806 ++++++++++++++++
 .../bsp/AbstractRegionBSPTreeTest.java             | 2257 ++++++++++++++++++++
 .../partitioning/bsp/AttributeBSPTreeTest.java     |  220 ++
 .../core/partitioning/bsp/BSPTreeVisitorTest.java  |  142 ++
 .../partitioning/bsp/RegionCutBoundaryTest.java    |  146 ++
 .../core/precision/DoublePrecisionContextTest.java |   13 +
 .../EpsilonDoublePrecisionContextTest.java         |   22 +
 .../commons/geometry/enclosing/EnclosingBall.java  |    2 +-
 .../threed/enclosing/SphereGenerator.java          |   11 +-
 .../euclidean/AbstractAffineTransformMatrix.java   |   51 +
 .../geometry/euclidean/AffineTransformMatrix.java  |   75 -
 .../geometry/euclidean/EuclideanTransform.java     |   40 +
 .../geometry/euclidean/EuclideanVector.java        |   21 +-
 .../euclidean/MultiDimensionalEuclideanVector.java |   18 +-
 .../exception/NonInvertibleTransformException.java |    2 +-
 .../euclidean/internal/AbstractPathConnector.java  |  460 ++++
 .../geometry/euclidean/internal/Matrices.java      |    2 +-
 .../euclidean/oned/AffineTransformMatrix1D.java    |   50 +-
 .../euclidean/oned/FunctionTransform1D.java        |   95 +
 .../commons/geometry/euclidean/oned/Interval.java  |  513 ++++-
 .../geometry/euclidean/oned/IntervalsSet.java      |  619 ------
 .../geometry/euclidean/oned/OrientedPoint.java     |  435 +++-
 .../geometry/euclidean/oned/RegionBSPTree1D.java   |  578 +++++
 .../geometry/euclidean/oned/SubOrientedPoint.java  |   79 -
 .../geometry/euclidean/oned/Transform1D.java       |   22 +-
 .../commons/geometry/euclidean/oned/Vector1D.java  |  105 +-
 .../euclidean/threed/AbstractSubLine3D.java        |   62 +
 .../euclidean/threed/AbstractSubPlane.java         |  147 ++
 .../euclidean/threed/AffineTransformMatrix3D.java  |  153 +-
 .../geometry/euclidean/threed/ConvexSubPlane.java  |  172 ++
 .../geometry/euclidean/threed/ConvexVolume.java    |  188 ++
 .../euclidean/threed/FunctionTransform3D.java      |  107 +
 .../commons/geometry/euclidean/threed/Line.java    |  269 ---
 .../commons/geometry/euclidean/threed/Line3D.java  |  432 ++++
 .../euclidean/threed/OutlineExtractor.java         |  263 ---
 .../commons/geometry/euclidean/threed/Plane.java   |  804 +++----
 .../geometry/euclidean/threed/PolyhedronsSet.java  |  704 ------
 .../geometry/euclidean/threed/RegionBSPTree3D.java |  698 ++++++
 .../commons/geometry/euclidean/threed/Segment.java |   65 -
 .../geometry/euclidean/threed/Segment3D.java       |  271 +++
 .../euclidean/threed/SphericalCoordinates.java     |    9 +-
 .../commons/geometry/euclidean/threed/SubLine.java |  147 --
 .../geometry/euclidean/threed/SubLine3D.java       |  124 ++
 .../geometry/euclidean/threed/SubPlane.java        |  229 +-
 .../geometry/euclidean/threed/Transform3D.java     |   22 +-
 .../geometry/euclidean/threed/Vector3D.java        |  151 +-
 .../threed/rotation/AxisAngleSequence.java         |    8 +-
 .../euclidean/threed/rotation/AxisSequence.java    |    4 +-
 .../threed/rotation/QuaternionRotation.java        |  147 +-
 .../euclidean/threed/rotation/Rotation3D.java      |   24 +-
 .../euclidean/twod/AbstractSegmentConnector.java   |  306 +++
 .../geometry/euclidean/twod/AbstractSubLine.java   |  131 ++
 .../euclidean/twod/AffineTransformMatrix2D.java    |   74 +-
 .../geometry/euclidean/twod/ConvexArea.java        |  294 +++
 .../euclidean/twod/FunctionTransform2D.java        |  103 +
 .../twod/InteriorAngleSegmentConnector.java        |  125 ++
 .../commons/geometry/euclidean/twod/Line.java      |  337 ++-
 .../geometry/euclidean/twod/NestedLoops.java       |  195 --
 .../geometry/euclidean/twod/PolarCoordinates.java  |   14 +-
 .../geometry/euclidean/twod/PolygonsSet.java       | 1101 ----------
 .../commons/geometry/euclidean/twod/Polyline.java  |  861 ++++++++
 .../geometry/euclidean/twod/RegionBSPTree2D.java   |  506 +++++
 .../commons/geometry/euclidean/twod/Segment.java   |  332 ++-
 .../commons/geometry/euclidean/twod/SubLine.java   |  300 +--
 .../geometry/euclidean/twod/Transform2D.java       |   22 +-
 .../commons/geometry/euclidean/twod/Vector2D.java  |  137 +-
 .../core/partitioning/CharacterizationTest.java    |  427 ----
 .../geometry/euclidean/EuclideanTestUtils.java     |  314 +--
 .../oned/AffineTransformMatrix1DTest.java          |   45 +
 .../euclidean/oned/FunctionTransform1DTest.java    |  177 ++
 .../geometry/euclidean/oned/IntervalTest.java      |  940 +++++++-
 .../geometry/euclidean/oned/IntervalsSetTest.java  |  592 -----
 .../geometry/euclidean/oned/OrientedPointTest.java |  466 +++-
 .../euclidean/oned/RegionBSPTree1DTest.java        | 1231 +++++++++++
 .../euclidean/oned/SubOrientedPointTest.java       |  169 --
 .../geometry/euclidean/oned/Vector1DTest.java      |   48 +-
 .../threed/AffineTransformMatrix3DTest.java        |  121 ++
 .../euclidean/threed/ConvexSubPlaneTest.java       |  641 ++++++
 .../euclidean/threed/ConvexVolumeTest.java         |  228 ++
 .../euclidean/threed/FunctionTransform3DTest.java  |  197 ++
 .../geometry/euclidean/threed/Line3DTest.java      |  459 ++++
 .../geometry/euclidean/threed/LineTest.java        |  151 --
 .../geometry/euclidean/threed/OBJWriter.java       |  318 ---
 .../geometry/euclidean/threed/PLYParser.java       |  289 ---
 .../geometry/euclidean/threed/PlaneTest.java       |  890 ++++++--
 .../euclidean/threed/PolyhedronsSetTest.java       | 1655 --------------
 .../euclidean/threed/RegionBSPTree3DTest.java      | 1633 ++++++++++++++
 .../geometry/euclidean/threed/Segment3DTest.java   |  387 ++++
 .../euclidean/threed/SphericalCoordinatesTest.java |   19 +
 .../geometry/euclidean/threed/SubLine3DTest.java   |  199 ++
 .../geometry/euclidean/threed/SubLineTest.java     |  170 --
 .../geometry/euclidean/threed/SubPlaneTest.java    |  496 +++++
 .../geometry/euclidean/threed/Vector3DTest.java    |   82 +-
 .../threed/rotation/QuaternionRotationTest.java    |   77 +-
 .../twod/AbstractSegmentConnectorTest.java         |  525 +++++
 .../twod/AffineTransformMatrix2DTest.java          |   54 +
 .../geometry/euclidean/twod/ConvexAreaTest.java    | 1210 +++++++++++
 .../euclidean/twod/FunctionTransform2DTest.java    |  190 ++
 .../twod/InteriorAngleSegmentConnectorTest.java    |  342 +++
 .../commons/geometry/euclidean/twod/LineTest.java  |  420 +++-
 .../geometry/euclidean/twod/NestedLoopsTest.java   |   73 -
 .../euclidean/twod/PolarCoordinatesTest.java       |   20 +
 .../geometry/euclidean/twod/PolygonsSetTest.java   | 1849 ----------------
 .../geometry/euclidean/twod/PolylineTest.java      | 1306 +++++++++++
 .../euclidean/twod/RegionBSPTree2DTest.java        | 1237 +++++++++++
 .../geometry/euclidean/twod/SegmentTest.java       |  875 +++++++-
 .../geometry/euclidean/twod/SubLineTest.java       |  682 +++++-
 .../geometry/euclidean/twod/Vector2DTest.java      |   60 +-
 .../geometry/euclidean/threed/issue-1211.bsp       |   14 -
 .../threed/pentomino-N-bad-orientation.ply         |   40 -
 .../geometry/euclidean/threed/pentomino-N-hole.ply |   39 -
 .../euclidean/threed/pentomino-N-out-of-plane.ply  |   40 -
 .../euclidean/threed/pentomino-N-too-close.ply     |   86 -
 .../geometry/euclidean/threed/pentomino-N.ply      |   39 -
 .../twod/hull/AbstractConvexHullGenerator2D.java   |    5 +-
 .../geometry/euclidean/twod/hull/ConvexHull2D.java |   31 +-
 .../euclidean/twod/hull/MonotoneChain.java         |    4 +-
 .../apache/commons/geometry/hull/ConvexHull.java   |    2 +-
 .../hull/ConvexHullGenerator2DAbstractTest.java    |   12 +-
 .../geometry/spherical/oned/AngularInterval.java   |  630 ++++++
 .../commons/geometry/spherical/oned/Arc.java       |  133 --
 .../commons/geometry/spherical/oned/ArcsSet.java   |  925 --------
 .../commons/geometry/spherical/oned/CutAngle.java  |  518 +++++
 .../geometry/spherical/oned/LimitAngle.java        |  133 --
 .../commons/geometry/spherical/oned/Point1S.java   |  393 ++++
 .../geometry/spherical/oned/RegionBSPTree1S.java   |  511 +++++
 .../commons/geometry/spherical/oned/S1Point.java   |  177 --
 .../geometry/spherical/oned/SubLimitAngle.java     |   65 -
 .../geometry/spherical/oned/Transform1S.java       |  236 ++
 .../spherical/twod/AbstractGreatArcConnector.java  |  303 +++
 .../spherical/twod/AbstractSubGreatCircle.java     |   69 +
 .../commons/geometry/spherical/twod/Circle.java    |  336 ---
 .../geometry/spherical/twod/ConvexArea2S.java      |  305 +++
 .../commons/geometry/spherical/twod/Edge.java      |  222 --
 .../geometry/spherical/twod/EdgesBuilder.java      |  170 --
 .../commons/geometry/spherical/twod/GreatArc.java  |  227 ++
 .../geometry/spherical/twod/GreatArcPath.java      |  688 ++++++
 .../geometry/spherical/twod/GreatCircle.java       |  446 ++++
 .../twod/InteriorAngleGreatArcConnector.java       |  127 ++
 .../commons/geometry/spherical/twod/Point2S.java   |  317 +++
 .../spherical/twod/PropertiesComputer.java         |  175 --
 .../geometry/spherical/twod/RegionBSPTree2S.java   |  303 +++
 .../commons/geometry/spherical/twod/S2Point.java   |  228 --
 .../spherical/twod/SphericalPolygonsSet.java       |  558 -----
 .../commons/geometry/spherical/twod/SubCircle.java |   71 -
 .../geometry/spherical/twod/SubGreatCircle.java    |  233 ++
 .../geometry/spherical/twod/Transform2S.java       |  264 +++
 .../commons/geometry/spherical/twod/Vertex.java    |  123 --
 .../geometry/spherical/SphericalTestUtils.java     |  132 +-
 .../spherical/oned/AngularIntervalTest.java        |  894 ++++++++
 .../commons/geometry/spherical/oned/ArcTest.java   |   93 -
 .../geometry/spherical/oned/ArcsSetTest.java       |  599 ------
 .../geometry/spherical/oned/CutAngleTest.java      |  596 ++++++
 .../geometry/spherical/oned/LimitAngleTest.java    |   45 -
 .../geometry/spherical/oned/Point1STest.java       |  481 +++++
 .../spherical/oned/RegionBSPTree1STest.java        |  923 ++++++++
 .../geometry/spherical/oned/S1PointTest.java       |   86 -
 .../geometry/spherical/oned/Transform1STest.java   |  254 +++
 .../twod/AbstractGreatArcPathConnectorTest.java    |  305 +++
 .../geometry/spherical/twod/CircleTest.java        |  191 --
 .../geometry/spherical/twod/ConvexArea2STest.java  |  795 +++++++
 .../geometry/spherical/twod/GreatArcPathTest.java  |  641 ++++++
 .../geometry/spherical/twod/GreatArcTest.java      |  383 ++++
 .../geometry/spherical/twod/GreatCircleTest.java   |  753 +++++++
 .../twod/InteriorAngleGreatArcConnectorTest.java   |  221 ++
 .../geometry/spherical/twod/Point2STest.java       |  346 +++
 .../spherical/twod/RegionBSPTree2STest.java        |  714 +++++++
 .../geometry/spherical/twod/S2PointTest.java       |   93 -
 .../spherical/twod/SphericalPolygonsSetTest.java   |  570 -----
 .../geometry/spherical/twod/SubCircleTest.java     |  146 --
 .../spherical/twod/SubGreatCircleTest.java         |  529 +++++
 .../geometry/spherical/twod/Transform2STest.java   |  287 +++
 .../checkstyle/checkstyle-suppressions.xml         |    7 +
 248 files changed, 48857 insertions(+), 20978 deletions(-)

diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Embedding.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Embedding.java
new file mode 100644
index 0000000..cf258c8
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Embedding.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.core;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** This interface defines mappings between a space and one of its subspaces.
+
+ * <p>Subspaces are the lower-dimension subsets of a space. For example,
+ * in an n-dimension space, the subspaces are the (n-1) dimension space,
+ * the (n-2) dimension space, and so on. This interface can be used regardless
+ * of the difference in number of dimensions between the space and the target
+ * subspace. For example, a line in 3D Euclidean space can use this interface
+ * to map directly from 3D Euclidean space to 1D Euclidean space (ie, the location
+ * along the line).</p>
+ *
+ * @param <P> Point type defining the embedding space.
+ * @param <S> Point type defining the embedded subspace.
+ */
+public interface Embedding<P extends Point<P>, S extends Point<S>> {
+
+    /** Transform a space point into a subspace point.
+     * @param point n-dimension point of the space
+     * @return lower-dimension point of the subspace corresponding to
+     *      the specified space point
+     * @see #toSpace
+     */
+    S toSubspace(P point);
+
+    /** Transform a collection of space points into subspace points.
+     * @param points collection of n-dimension points to transform
+     * @return collection of transformed lower-dimension points.
+     * @see #toSubspace(Point)
+     */
+    default List<S> toSubspace(final Collection<P> points) {
+        return points.stream().map(this::toSubspace).collect(Collectors.toList());
+    }
+
+    /** Transform a subspace point into a space point.
+     * @param point lower-dimension point of the subspace
+     * @return n-dimension point of the space corresponding to the
+     *      specified subspace point
+     * @see #toSubspace(Point)
+     */
+    P toSpace(S point);
+
+    /** Transform a collection of subspace points into space points.
+     * @param points collection of lower-dimension points to transform
+     * @return collection of transformed n-dimension points.
+     * @see #toSpace(Point)
+     */
+    default List<P> toSpace(final Collection<S> points) {
+        return points.stream().map(this::toSpace).collect(Collectors.toList());
+    }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Geometry.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Geometry.java
index 819a9e5..37c40aa 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Geometry.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Geometry.java
@@ -23,8 +23,8 @@ public final class Geometry {
     /** Alias for {@link Math#PI}, placed here for completeness. */
     public static final double PI = Math.PI;
 
-    /** Constant value for {@code -pi} */
-    public static final double MINUS_PI = - Math.PI;
+    /** Constant value for {@code -pi}. */
+    public static final double MINUS_PI = -Math.PI;
 
     /** Constant value for {@code 2*pi}. */
     public static final double TWO_PI = 2.0 * Math.PI;
@@ -36,7 +36,7 @@ public final class Geometry {
     public static final double HALF_PI = 0.5 * Math.PI;
 
     /** Constant value for {@code - pi/2}. */
-    public static final double MINUS_HALF_PI = - 0.5 * Math.PI;
+    public static final double MINUS_HALF_PI = -0.5 * Math.PI;
 
     /** Constant value for {@code  3*pi/2}. */
     public static final double THREE_HALVES_PI = 1.5 * Math.PI;
@@ -46,6 +46,6 @@ public final class Geometry {
      */
     public static final double ZERO_PI = 0.0;
 
-    /** Private constructor */
+    /** Private constructor. */
     private Geometry() {}
 }
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
new file mode 100644
index 0000000..233cd4b
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Region.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.core;
+
+/** Interface representing a region in a space. A region partitions a space
+ * into sets of points lying on the inside, outside, and boundary.
+ * @param <P> Point implementation type
+ */
+public interface Region<P extends Point<P>> {
+
+    /** Return true if the region spans the entire space. In other words,
+     * a region is full if no points in the space are classified as
+     * {@link RegionLocation#OUTSIDE outside}.
+     * @return true if the region spans the entire space
+     */
+    boolean isFull();
+
+    /** Return true if the region is completely empty, ie all points in
+     * the space are classified as {@link RegionLocation#OUTSIDE outside}.
+     * @return true if the region is empty
+     */
+    boolean isEmpty();
+
+    /** Get the size of the region. The meaning of this will vary depending on
+     * the space and dimension of the region. For example, in Euclidean space,
+     * this will be a length in 1D, an area in 2D, and a volume in 3D.
+     * @return the size of the region
+     */
+    double getSize();
+
+    /** Get the size of the boundary of the region. The size is a value in
+     * the {@code d-1} dimension space. For example, in Euclidean space,
+     * this will be a length in 2D and an area in 3D.
+     * @return the size of the boundary of the region
+     */
+    double getBoundarySize();
+
+    /** Get the barycenter of the region or null if none exists. A barycenter
+     * will not exist for empty or infinite regions.
+     * @return the barycenter of the region or null if none exists
+     */
+    P getBarycenter();
+
+    /** Classify the given point with respect to the region.
+     * @param pt the point to classify
+     * @return the location of the point with respect to the region
+     */
+    RegionLocation classify(P pt);
+
+    /** Return true if the given point is on the inside or boundary
+     * of the region.
+     * @param pt the point to test
+     * @return true if the point is on the inside or boundary of the region
+     */
+    default boolean contains(P pt) {
+        final RegionLocation location = classify(pt);
+        return location != null && location != RegionLocation.OUTSIDE;
+    }
+
+    /** Project a point onto the boundary of the region. Null is returned if
+     * the region contains no boundaries (ie, is either {@link #isFull() full}
+     * or {@link #isEmpty() empty}).
+     * @param pt pt to project
+     * @return projection of the point on the boundary of the region or null
+     *      if the region does not contain any boundaries
+     */
+    P project(P pt);
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryValueException.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionLocation.java
similarity index 63%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryValueException.java
copy to commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionLocation.java
index a5f7d3f..19f5068 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryValueException.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/RegionLocation.java
@@ -14,20 +14,26 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.commons.geometry.core.exception;
+package org.apache.commons.geometry.core;
 
-/** Exception thrown to indicate that a value used in a geometric operation
- * is not valid.
+/** Enumeration containing the possible locations of a point with
+ * respect to a region.
+ * @see Region
  */
-public class GeometryValueException extends GeometryException {
+public enum RegionLocation {
 
-    /** Serializable version identifier */
-    private static final long serialVersionUID = 20190210L;
+    /** Value indicating that a point lies on the inside of
+     * a region.
+     */
+    INSIDE,
+
+    /** Value indicating that a point lies on the outside of
+     * a region.
+     */
+    OUTSIDE,
 
-    /** Simple constructor with error message.
-     * @param msg exception message string
+    /** Value indicating that a point lies on the boundary of
+     * a region.
      */
-    public GeometryValueException(String msg) {
-        super(msg);
-    }
+    BOUNDARY
 }
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Spatial.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Spatial.java
index c6f76c1..ae5e277 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Spatial.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Spatial.java
@@ -38,4 +38,10 @@ public interface Spatial {
      *      are NaN
      */
     boolean isInfinite();
+
+    /** Returns true if all values in this element are finite, meaning
+     * they are not NaN or infinite.
+     * @return true if all values in this element are finite
+     */
+    boolean isFinite();
 }
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Transform.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Transform.java
new file mode 100644
index 0000000..7eedd67
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Transform.java
@@ -0,0 +1,56 @@
+/*
+ * 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 java.util.function.Function;
+
+/** This interface represents an <em>inversible affine transform</em> in a space.
+ * Common examples of this type of transform in Euclidean space include
+ * scalings, translations, and rotations.
+ *
+ * <h2>Implementation Note</h2>
+ * <p>Implementations are responsible for ensuring that they meet the geometric
+ * requirements outlined above. These are:
+ * <ol>
+ *      <li>The transform must be <a href="https://en.wikipedia.org/wiki/Affine_transformation">affine</a>.
+ *      This means that points and parallel lines must be preserved by the transformation. For example,
+ *      a translation or rotation in Euclidean 3D space meets this requirement because a mapping exists for
+ *      all points and lines that are parallel before the transform remain parallel afterwards.
+ *      However, a projective transform that causes parallel lines to meet at a point in infinity does not.
+ *      </li>
+ *      <li>The transform must be <em>inversible</em>. An inverse transform must exist that will return
+ *      the original point if given the transformed point. In other words, for a transform {@code t}, there
+ *      must exist an inverse {@code inv} such that {@code inv.apply(t.apply(pt))} returns a point equal to
+ *      the input point {@code pt}.
+ *      </li>
+ * </ol>
+ * Implementations that do not meet these requirements cannot be expected to produce correct results in
+ * algorithms that use this interface.
+ *
+ * @param <P> Point implementation type
+ * @see <a href="https://en.wikipedia.org/wiki/Affine_transformation">Affine Space</a>
+ */
+public interface Transform<P extends Point<P>> extends Function<P, P> {
+
+    /** Return true if the transform preserves the orientation of the space.
+     * For example, in Euclidean 2D space, this will be true for translations,
+     * rotations, and scalings but will be false for reflections.
+     * @return true if the transform preserves the orientation of the space
+     * @see <a href="https://en.wikipedia.org/wiki/Orientation_(vector_space)">Orientation</a>
+     */
+    boolean preservesOrientation();
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java
index 873e24e..c62abe7 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java
@@ -16,8 +16,6 @@
  */
 package org.apache.commons.geometry.core;
 
-import org.apache.commons.geometry.core.exception.IllegalNormException;
-
 /** Interface representing a vector in a vector space or displacement vectors
  * in an affine space.
  *
@@ -96,7 +94,8 @@ public interface Vector<V extends Vector<V>> extends Spatial {
     /** Get a normalized vector aligned with the instance. The returned
      * vector has a magnitude of 1.
      * @return a new normalized vector
-     * @exception IllegalNormException if the norm is zero, NaN, or infinite
+     * @throws org.apache.commons.geometry.core.exception.IllegalNormException if the norm is
+     *      zero, NaN, or infinite
      */
     V normalize();
 
@@ -131,7 +130,8 @@ public interface Vector<V extends Vector<V>> extends Spatial {
     /** Compute the angular separation between two vectors in radians.
      * @param v other vector
      * @return angular separation between this instance and v in radians
-     * @exception IllegalNormException if either vector has a zero, NaN, or infinite norm
+     * @throws org.apache.commons.geometry.core.exception.IllegalNormException if either
+     *      vector has a zero, NaN, or infinite norm
      */
     double angle(V v);
 }
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryException.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryException.java
index 57462be..d37bef8 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryException.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryException.java
@@ -22,7 +22,7 @@ package org.apache.commons.geometry.core.exception;
  */
 public class GeometryException extends RuntimeException {
 
-    /** Serializable version identifier */
+    /** Serializable version identifier. */
     private static final long serialVersionUID = 20180909L;
 
     /** Simple constructor with error message.
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryValueException.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryValueException.java
index a5f7d3f..592045b 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryValueException.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/GeometryValueException.java
@@ -21,7 +21,7 @@ package org.apache.commons.geometry.core.exception;
  */
 public class GeometryValueException extends GeometryException {
 
-    /** Serializable version identifier */
+    /** Serializable version identifier. */
     private static final long serialVersionUID = 20190210L;
 
     /** Simple constructor with error message.
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/IllegalNormException.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/IllegalNormException.java
index 6993704..ad65322 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/IllegalNormException.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/IllegalNormException.java
@@ -21,7 +21,7 @@ package org.apache.commons.geometry.core.exception;
  */
 public class IllegalNormException extends GeometryValueException {
 
-    /** Serializable version identifier */
+    /** Serializable version identifier. */
     private static final long serialVersionUID = 20180909L;
 
     /** Simple constructor accepting the illegal norm value.
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/IllegalNormException.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/Equivalency.java
similarity index 52%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/IllegalNormException.java
copy to commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/Equivalency.java
index 6993704..cd52a73 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/exception/IllegalNormException.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/Equivalency.java
@@ -14,27 +14,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.commons.geometry.core.exception;
+package org.apache.commons.geometry.core.internal;
 
-/** Exception thrown when an illegal vector norm value is encountered
- * in an operation.
+/** Interface for determining equivalency, not exact equality, between
+ * two objects. This is performs a function similar to {@link Object#equals(Object)}
+ * but allows fuzzy comparisons to occur instead of strict equality. This is
+ * especially useful when comparing objects with floating point values that
+ * may not be exact but are operationally equivalent.
+ * @param <T> The type being compared
  */
-public class IllegalNormException extends GeometryValueException {
+public interface Equivalency<T> {
 
-    /** Serializable version identifier */
-    private static final long serialVersionUID = 20180909L;
-
-    /** Simple constructor accepting the illegal norm value.
-     * @param norm the illegal norm value
-     */
-    public IllegalNormException(double norm) {
-        super("Illegal norm: " + norm);
-    }
-
-    /** Constructor accepting an error message.
-     * @param msg the error message
+    /** Determine if this object is equivalent (effectively equal) to the argument.
+     * @param other the object to compare for equivalency
+     * @return true if this object is equivalent to the argument; false otherwise
      */
-    public IllegalNormException(String msg) {
-        super(msg);
-    }
+    boolean eq(T other);
 }
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalError.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalError.java
index 219ccdb..bc52f62 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalError.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/GeometryInternalError.java
@@ -27,7 +27,7 @@ public class GeometryInternalError extends IllegalStateException {
             "error in the algorithm implementation than in the calling code or data. Please file a bug report " +
             "with the developers.";
 
-    /** Serializable version identifier */
+    /** Serializable version identifier. */
     private static final long serialVersionUID = 20180913L;
 
     /** Simple constructor with a default error message.
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/IteratorTransform.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/IteratorTransform.java
new file mode 100644
index 0000000..b60d40d
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/IteratorTransform.java
@@ -0,0 +1,92 @@
+/*
+ * 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 java.util.Collection;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.NoSuchElementException;
+
+/** Class that wraps another iterator, converting each input iterator value into
+ * one or more output iterator values.
+ * @param <I> Input iterator type
+ * @param <T> Output iterator type
+ */
+public abstract class IteratorTransform<I, T> implements Iterator<T> {
+
+    /** Input iterator instance that supplies the input values for this instance. */
+    private final Iterator<I> inputIterator;
+
+    /** Output value queue. */
+    private final Deque<T> outputQueue = new LinkedList<>();
+
+    /** Create a new instance that uses the given iterator as the input source.
+     * @param inputIterator iterator supplying input values
+     */
+    public IteratorTransform(final Iterator<I> inputIterator) {
+        this.inputIterator = inputIterator;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean hasNext() {
+        return loadNextOutput();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public T next() {
+        if (outputQueue.isEmpty()) {
+            throw new NoSuchElementException();
+        }
+
+        return outputQueue.removeFirst();
+    }
+
+    /** Load the next output values into the output queue. Returns true if the output queue
+     * contains more entries.
+     * @return true if more output values are available
+     */
+    private boolean loadNextOutput() {
+        while (outputQueue.isEmpty() && inputIterator.hasNext()) {
+            acceptInput(inputIterator.next());
+        }
+
+        return !outputQueue.isEmpty();
+    }
+
+    /** Add a value to the output queue.
+     * @param value value to add to the output queue
+     */
+    protected void addOutput(final T value) {
+        outputQueue.add(value);
+    }
+
+    /** Add multiple values to the output queue.
+     * @param values values to add to the output queue
+     */
+    protected void addAllOutput(final Collection<T> values) {
+        outputQueue.addAll(values);
+    }
+
+    /** Accept a value from the input iterator. This method should take
+     * the input value and add one or more values to the output queue.
+     * @param input value from the input iterator
+     */
+    protected abstract void acceptInput(I input);
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/SimpleTupleFormat.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/SimpleTupleFormat.java
index 15637e0..ce0c9ac 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/SimpleTupleFormat.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/internal/SimpleTupleFormat.java
@@ -25,7 +25,7 @@ public class SimpleTupleFormat {
     /** Default value separator string. */
     private static final String DEFAULT_SEPARATOR = ",";
 
-    /** Space character */
+    /** Space character. */
     private static final String SPACE = " ";
 
     /** Static instance configured with default values. Tuples in this format
@@ -34,13 +34,13 @@ public class SimpleTupleFormat {
     private static final SimpleTupleFormat DEFAULT_INSTANCE =
             new SimpleTupleFormat(",", "(", ")");
 
-    /** String separating tuple values */
+    /** String separating tuple values. */
     private final String separator;
 
-    /** String used to signal the start of a tuple; may be null */
+    /** String used to signal the start of a tuple; may be null. */
     private final String prefix;
 
-    /** String used to signal the end of a tuple; may be null */
+    /** String used to signal the end of a tuple; may be null. */
     private final String suffix;
 
     /** Constructs a new instance with the default string separator (a comma)
@@ -298,8 +298,7 @@ public class SimpleTupleFormat {
             matchSequence(str, separator, pos);
 
             return value;
-        }
-        catch (NumberFormatException exc) {
+        } catch (NumberFormatException exc) {
             fail(String.format("unable to parse number from string \"%s\"", substr), str, pos, exc);
             return 0.0; // for the compiler
         }
@@ -342,7 +341,7 @@ public class SimpleTupleFormat {
         int idx = pos.getIndex();
         final int len = str.length();
 
-        for (; idx<len; ++idx) {
+        for (; idx < len; ++idx) {
             if (!Character.isWhitespace(str.codePointAt(idx))) {
                 break;
             }
@@ -369,7 +368,7 @@ public class SimpleTupleFormat {
 
         int i = idx;
         int s = 0;
-        for (; i<inputLength && s<seqLength; ++i, ++s) {
+        for (; i < inputLength && s < seqLength; ++i, ++s) {
             if (str.codePointAt(i) != seq.codePointAt(s)) {
                 break;
             }
@@ -444,7 +443,7 @@ public class SimpleTupleFormat {
      */
     private static class TupleParseException extends IllegalArgumentException {
 
-        /** Serializable version identifier */
+        /** Serializable version identifier. */
         private static final long serialVersionUID = 20180629;
 
         /** Simple constructor.
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegion.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegion.java
new file mode 100644
index 0000000..d920335
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractConvexHyperplaneBoundedRegion.java
@@ -0,0 +1,379 @@
+/*
+ * 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.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.exception.GeometryException;
+
+/** Base class for convex hyperplane-bounded regions. This class provides generic implementations of many
+ * algorithms related to convex regions.
+ * @param <P> Point implementation type
+ * @param <S> Convex subhyperplane implementation type
+ */
+public abstract class AbstractConvexHyperplaneBoundedRegion<P extends Point<P>, S extends ConvexSubHyperplane<P>>
+    implements HyperplaneBoundedRegion<P>, Serializable {
+
+    /** Serializable UID. */
+    private static final long serialVersionUID = 20190812L;
+
+    /** List of boundaries for the region. */
+    private final List<S> boundaries;
+
+    /** Simple constructor. Callers are responsible for ensuring that the given list of subhyperplanes
+     * represent a valid convex region boundary. No validation is performed.
+     * @param boundaries the boundaries of the convex region
+     */
+    protected AbstractConvexHyperplaneBoundedRegion(final List<S> boundaries) {
+        this.boundaries = Collections.unmodifiableList(boundaries);
+    }
+
+    /** Get the boundaries of the convex region. The exact ordering of the boundaries
+     * is not guaranteed.
+     * @return the boundaries of the convex region
+     */
+    public List<S> getBoundaries() {
+        return boundaries;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isFull() {
+        // no boundaries => no outside
+        return boundaries.isEmpty();
+    }
+
+    /** {@inheritDoc}
+     *
+     * <p>This method always returns false.</p>
+     */
+    @Override
+    public boolean isEmpty() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getBoundarySize() {
+        double sum = 0.0;
+        for (final S boundary : boundaries) {
+            sum += boundary.getSize();
+        }
+
+        return sum;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public RegionLocation classify(P pt) {
+        boolean isOn = false;
+
+        HyperplaneLocation loc;
+        for (final S boundary : boundaries) {
+            loc = boundary.getHyperplane().classify(pt);
+
+            if (loc == HyperplaneLocation.PLUS) {
+                return RegionLocation.OUTSIDE;
+            } else if (loc == HyperplaneLocation.ON) {
+                isOn = true;
+            }
+        }
+
+        return isOn ? RegionLocation.BOUNDARY : RegionLocation.INSIDE;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public P project(P pt) {
+
+        P projected;
+        double dist;
+
+        P closestPt = null;
+        double closestDist = Double.POSITIVE_INFINITY;
+
+        for (final S boundary : boundaries) {
+            projected = boundary.closest(pt);
+            dist = pt.distance(projected);
+
+            if (projected != null && (closestPt == null || dist < closestDist)) {
+                closestPt = projected;
+                closestDist = dist;
+            }
+        }
+
+        return closestPt;
+    }
+
+    /** Trim the given convex subhyperplane to the portion contained inside this instance.
+     * @param convexSubHyperplane convex subhyperplane to trim. Null is returned if the subhyperplane
+     * does not intersect the instance.
+     * @return portion of the argument that lies entirely inside the region represented by
+     *      this instance, or null if it does not intersect.
+     */
+    public ConvexSubHyperplane<P> trim(final ConvexSubHyperplane<P> convexSubHyperplane) {
+        ConvexSubHyperplane<P> remaining = convexSubHyperplane;
+        for (final S boundary : boundaries) {
+            remaining = remaining.split(boundary.getHyperplane()).getMinus();
+            if (remaining == null) {
+                break;
+            }
+        }
+
+        return remaining;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(this.getClass().getSimpleName())
+            .append("[boundaries= ")
+            .append(boundaries);
+
+        return sb.toString();
+    }
+
+    /** Generic, internal transform method. Subclasses should use this to implement their own transform methods.
+     * @param transform the transform to apply to the instance
+     * @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 subhpType the type used for the boundary subhyperplanes
+     * @param factory function used to create new convex region instances
+     * @param <R> Region implementation type
+     * @return the result of the transform operation
+     */
+    protected <R extends AbstractConvexHyperplaneBoundedRegion<P, S>> R transformInternal(
+            final Transform<P> transform, final R thisInstance, final Class<S> subhpType,
+            final Function<List<S>, R> factory) {
+
+        if (isFull()) {
+            return thisInstance;
+        }
+
+        final List<S> origBoundaries = getBoundaries();
+
+        final int size = origBoundaries.size();
+        final List<S> tBoundaries = new ArrayList<>(size);
+
+        // determine if the hyperplanes should be reversed
+        final S boundary = origBoundaries.get(0);
+        ConvexSubHyperplane<P> tBoundary = boundary.transform(transform);
+
+        final boolean reverseDirection = swapsInsideOutside(transform);
+
+        // transform all of the segments
+        if (reverseDirection) {
+            tBoundary = tBoundary.reverse();
+        }
+        tBoundaries.add(subhpType.cast(tBoundary));
+
+        for (int i = 1; i < origBoundaries.size(); ++i) {
+            tBoundary = origBoundaries.get(i).transform(transform);
+
+            if (reverseDirection) {
+                tBoundary = tBoundary.reverse();
+            }
+
+            tBoundaries.add(subhpType.cast(tBoundary));
+        }
+
+        return factory.apply(tBoundaries);
+    }
+
+    /** Return true if the given transform swaps the inside and outside of
+     * the region.
+     *
+     * <p>The default behavior of this method is to return true if the transform
+     * does not preserve spatial orientation (ie, {@link Transform#preservesOrientation()}
+     * is false). Subclasses may need to override this method to implement the correct
+     * behavior for their space and dimension.</p>
+     * @param transform transform to check
+     * @return true if the given transform swaps the interior and exterior of
+     *      the region
+     */
+    protected boolean swapsInsideOutside(final Transform<P> transform) {
+        return !transform.preservesOrientation();
+    }
+
+    /** 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 subhpType the type used for the boundary subhyperplanes
+     * @param factory function used to create new convex region instances
+     * @param <R> Region implementation type
+     * @return the result of the split operation
+     */
+    protected <R extends AbstractConvexHyperplaneBoundedRegion<P, S>> Split<R> splitInternal(
+            final Hyperplane<P> splitter, final R thisInstance, final Class<S> subhpType,
+            final Function<List<S>, R> factory) {
+
+        if (isFull()) {
+            final R minus = factory.apply(Arrays.asList(subhpType.cast(splitter.span())));
+            final R plus = factory.apply(Arrays.asList(subhpType.cast(splitter.reverse().span())));
+
+            return new Split<>(minus, plus);
+        } else {
+            final ConvexSubHyperplane<P> trimmedSplitter = trim(splitter.span());
+
+            if (trimmedSplitter == null) {
+                // The splitter lies entirely outside of the region; we need
+                // to determine whether we lie on the plus or minus side of the splitter.
+                // We can use the first boundary to determine this. If the boundary is entirely
+                // on the minus side of the splitter or lies directly on the splitter and has
+                // the same orientation, then the area lies on the minus side of the splitter.
+                // Otherwise, it lies on the plus side.
+                final ConvexSubHyperplane<P> testSegment = boundaries.get(0);
+                final SplitLocation testLocation = testSegment.split(splitter).getLocation();
+
+                if (SplitLocation.MINUS == testLocation ||
+                        (SplitLocation.NEITHER == testLocation &&
+                            splitter.similarOrientation(testSegment.getHyperplane()))) {
+                    return new Split<>(thisInstance, null);
+                }
+
+                return new Split<>(null, thisInstance);
+            }
+
+            final List<S> minusBoundaries = new ArrayList<>();
+            final List<S> plusBoundaries = new ArrayList<>();
+
+            Split<? extends ConvexSubHyperplane<P>> split;
+            ConvexSubHyperplane<P> minusBoundary;
+            ConvexSubHyperplane<P> plusBoundary;
+
+            for (final S boundary : boundaries) {
+                split = boundary.split(splitter);
+
+                minusBoundary = split.getMinus();
+                plusBoundary = split.getPlus();
+
+                if (minusBoundary != null) {
+                    minusBoundaries.add(subhpType.cast(minusBoundary));
+                }
+
+                if (plusBoundary != null) {
+                    plusBoundaries.add(subhpType.cast(plusBoundary));
+                }
+            }
+
+            minusBoundaries.add(subhpType.cast(trimmedSplitter));
+            plusBoundaries.add(subhpType.cast(trimmedSplitter.reverse()));
+
+            return new Split<>(factory.apply(minusBoundaries), factory.apply(plusBoundaries));
+        }
+    }
+
+    /** Internal class encapsulating the logic for building convex region boundaries from collections of
+     * hyperplanes.
+     * @param <P> Point implementation type
+     * @param <S> ConvexSubHyperplane implementation type
+     */
+    protected static class ConvexRegionBoundaryBuilder<P extends Point<P>, S extends ConvexSubHyperplane<P>> {
+
+        /** Convex subhyperplane implementation type. */
+        private final Class<S> subhyperplaneType;
+
+        /** Construct a new instance for building convex region boundaries with the given convex subhyperplane
+         * implementation type.
+         * @param subhyperplaneType Convex subhyperplane implementation type
+         */
+        public ConvexRegionBoundaryBuilder(final Class<S> subhyperplaneType) {
+            this.subhyperplaneType = subhyperplaneType;
+        }
+
+        /** Compute a list of convex subhyperplanes representing the boundaries of the convex region
+         * bounded by the given collection of hyperplanes.
+         * @param bounds hyperplanes defining the convex region
+         * @return a list of convex subhyperplanes representing the boundaries of the convex region
+         * @throws GeometryException if the given hyperplanes do not form a convex region
+         */
+        public List<S> build(final Iterable<? extends Hyperplane<P>> bounds) {
+
+            final List<S> boundaries = new ArrayList<>();
+
+            // cut each hyperplane by every other hyperplane in order to get the subplane boundaries
+            boolean notConvex = false;
+            int outerIdx = 0;
+            ConvexSubHyperplane<P> subhp;
+
+            for (final Hyperplane<P> hp : bounds) {
+                ++outerIdx;
+                subhp = hp.span();
+
+                int innerIdx = 0;
+                for (final Hyperplane<P> splitter : bounds) {
+                    ++innerIdx;
+
+                    if (hp != splitter) {
+                        final Split<? extends ConvexSubHyperplane<P>> split = subhp.split(splitter);
+
+                        if (split.getLocation() == SplitLocation.NEITHER) {
+                            if (hp.similarOrientation(splitter)) {
+                                // two or more splitters are the equivalent; only
+                                // use the segment from the first one
+                                if (outerIdx > innerIdx) {
+                                    subhp = null;
+                                }
+                            } else {
+                                // two or more splitters are coincident and have opposite
+                                // orientations, meaning that no area is on the minus side
+                                // of both
+                                notConvex = true;
+                                break;
+                            }
+                        } else {
+                            subhp = subhp.split(splitter).getMinus();
+                        }
+
+                        if (subhp == null) {
+                            break;
+                        }
+                    } else if (outerIdx > innerIdx) {
+                        // this hyperplane is duplicated in the list; skip all but the
+                        // first insertion of its subhyperplane
+                        subhp = null;
+                        break;
+                    }
+                }
+
+                if (notConvex) {
+                    break;
+                }
+
+                if (subhp != null) {
+                    boundaries.add(subhyperplaneType.cast(subhp));
+                }
+            }
+
+            if (notConvex || (outerIdx > 0 && boundaries.isEmpty())) {
+                throw new GeometryException("Bounding hyperplanes do not produce a convex region: " + bounds);
+            }
+
+            return boundaries;
+        }
+    }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractEmbeddingSubHyperplane.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractEmbeddingSubHyperplane.java
new file mode 100644
index 0000000..6bfb2b8
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractEmbeddingSubHyperplane.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.core.partitioning;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Region;
+import org.apache.commons.geometry.core.RegionLocation;
+
+/** Abstract base class for subhyperplane 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 AbstractEmbeddingSubHyperplane<
+    P extends Point<P>,
+    S extends Point<S>,
+    H extends EmbeddingHyperplane<P, S>> implements SubHyperplane<P>, Serializable {
+
+    /** Serializable UID. */
+    private static final long serialVersionUID = 20190729L;
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isFull() {
+        return getSubspaceRegion().isFull();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isEmpty() {
+        return getSubspaceRegion().isEmpty();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isInfinite() {
+        return Double.isInfinite(getSize());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isFinite() {
+        return !isInfinite();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getSize() {
+        return getSubspaceRegion().getSize();
+    }
+
+    /** {@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();
+
+    /** Return the embedded subspace region for this instance.
+     * @return the embedded subspace region for this instance
+     */
+    public abstract HyperplaneBoundedRegion<S> getSubspaceRegion();
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractHyperplane.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractHyperplane.java
new file mode 100644
index 0000000..03d1723
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractHyperplane.java
@@ -0,0 +1,70 @@
+/*
+ * 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.io.Serializable;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+
+/** Base class for hyperplane implementations.
+ * @param <P> Point implementation type
+ */
+public abstract class AbstractHyperplane<P extends Point<P>> implements Hyperplane<P>, Serializable {
+
+    /** Serializable UID. */
+    private static final long serialVersionUID = 1L;
+
+    /** Precision object used to perform floating point comparisons. */
+    private final DoublePrecisionContext precision;
+
+    /** Construct an instance using the given precision context.
+     * @param precision object used to perform floating point comparisons
+     */
+    protected AbstractHyperplane(final DoublePrecisionContext precision) {
+        this.precision = precision;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public HyperplaneLocation classify(final P point) {
+        final double offsetValue = offset(point);
+
+        final int cmp = precision.sign(offsetValue);
+        if (cmp > 0) {
+            return HyperplaneLocation.PLUS;
+        } else if (cmp < 0) {
+            return HyperplaneLocation.MINUS;
+        }
+        return HyperplaneLocation.ON;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean contains(final P point) {
+        final HyperplaneLocation loc = classify(point);
+        return loc == HyperplaneLocation.ON;
+    }
+
+    /** Get the precision object used to perform floating point
+     * comparisons for this instance.
+     * @return the precision object for this instance
+     */
+    public DoublePrecisionContext getPrecision() {
+        return precision;
+    }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractRegion.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractRegion.java
deleted file mode 100644
index d3047c8..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractRegion.java
+++ /dev/null
@@ -1,516 +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.ArrayList;
-import java.util.Collection;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.TreeSet;
-
-import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-
-/** Abstract class for all regions, independent of geometry type or dimension.
-
- * @param <P> Point type defining the space
- * @param <S> Point type defining the sub-space
- */
-public abstract class AbstractRegion<P extends Point<P>, S extends Point<S>> implements Region<P> {
-
-    /** Inside/Outside BSP tree. */
-    private BSPTree<P> tree;
-
-    /** Precision context used to determine floating point equality. */
-    private final DoublePrecisionContext precision;
-
-    /** Size of the instance. */
-    private double size;
-
-    /** Barycenter. */
-    private P barycenter;
-
-    /** Build a region representing the whole space.
-     * @param precision precision context used to compare floating point numbers
-     */
-    protected AbstractRegion(final DoublePrecisionContext precision) {
-        this.tree      = new BSPTree<>(Boolean.TRUE);
-        this.precision = precision;
-    }
-
-    /** Build a region from an inside/outside BSP tree.
-     * <p>The leaf nodes of the BSP tree <em>must</em> have a
-     * {@code Boolean} attribute representing the inside status of
-     * the corresponding cell (true for inside cells, false for outside
-     * cells). In order to avoid building too many small objects, it is
-     * recommended to use the predefined constants
-     * {@code Boolean.TRUE} and {@code Boolean.FALSE}. The
-     * tree also <em>must</em> have either null internal nodes or
-     * internal nodes representing the boundary as specified in the
-     * {@link #getTree getTree} method).</p>
-     * @param tree inside/outside BSP tree representing the region
-     * @param precision precision context used to compare floating point values
-     */
-    protected AbstractRegion(final BSPTree<P> tree, final DoublePrecisionContext precision) {
-        this.tree      = tree;
-        this.precision = precision;
-    }
-
-    /** Build a Region from a Boundary REPresentation (B-rep).
-     * <p>The boundary is provided as a collection of {@link
-     * SubHyperplane sub-hyperplanes}. Each sub-hyperplane has the
-     * interior part of the region on its minus side and the exterior on
-     * its plus side.</p>
-     * <p>The boundary elements can be in any order, and can form
-     * several non-connected sets (like for example polygons with holes
-     * or a set of disjoints polyhedrons considered as a whole). In
-     * fact, the elements do not even need to be connected together
-     * (their topological connections are not used here). However, if the
-     * boundary does not really separate an inside open from an outside
-     * open (open having here its topological meaning), then subsequent
-     * calls to the {@link #checkPoint(Point) checkPoint} method will not be
-     * meaningful anymore.</p>
-     * <p>If the boundary is empty, the region will represent the whole
-     * space.</p>
-     * @param boundary collection of boundary elements, as a
-     * collection of {@link SubHyperplane SubHyperplane} objects
-     * @param precision precision context used to compare floating point values
-     */
-    protected AbstractRegion(final Collection<SubHyperplane<P>> boundary, final DoublePrecisionContext precision) {
-
-        this.precision = precision;
-
-        if (boundary.size() == 0) {
-
-            // the tree represents the whole space
-            tree = new BSPTree<>(Boolean.TRUE);
-
-        } else {
-
-            // sort the boundary elements in decreasing size order
-            // (we don't want equal size elements to be removed, so
-            // we use a trick to fool the TreeSet)
-            final TreeSet<SubHyperplane<P>> ordered = new TreeSet<>(new Comparator<SubHyperplane<P>>() {
-                /** {@inheritDoc} */
-                @Override
-                public int compare(final SubHyperplane<P> o1, final SubHyperplane<P> o2) {
-                    final double size1 = o1.getSize();
-                    final double size2 = o2.getSize();
-                    return (size2 < size1) ? -1 : ((o1 == o2) ? 0 : +1);
-                }
-            });
-            ordered.addAll(boundary);
-
-            // build the tree top-down
-            tree = new BSPTree<>();
-            insertCuts(tree, ordered);
-
-            // set up the inside/outside flags
-            tree.visit(new BSPTreeVisitor<P>() {
-
-                /** {@inheritDoc} */
-                @Override
-                public Order visitOrder(final BSPTree<P> node) {
-                    return Order.PLUS_SUB_MINUS;
-                }
-
-                /** {@inheritDoc} */
-                @Override
-                public void visitInternalNode(final BSPTree<P> node) {
-                }
-
-                /** {@inheritDoc} */
-                @Override
-                public void visitLeafNode(final BSPTree<P> node) {
-                    if (node.getParent() == null || node == node.getParent().getMinus()) {
-                        node.setAttribute(Boolean.TRUE);
-                    } else {
-                        node.setAttribute(Boolean.FALSE);
-                    }
-                }
-            });
-
-        }
-
-    }
-
-    /** Build a convex region from an array of bounding hyperplanes.
-     * @param hyperplanes array of bounding hyperplanes (if null, an
-     * empty region will be built)
-     * @param precision precision context used to compare floating point values
-     */
-    public AbstractRegion(final Hyperplane<P>[] hyperplanes, final DoublePrecisionContext precision) {
-        this.precision = precision;
-        if ((hyperplanes == null) || (hyperplanes.length == 0)) {
-            tree = new BSPTree<>(Boolean.FALSE);
-        } else {
-
-            // use the first hyperplane to build the right class
-            tree = hyperplanes[0].wholeSpace().getTree(false);
-
-            // chop off parts of the space
-            BSPTree<P> node = tree;
-            node.setAttribute(Boolean.TRUE);
-            for (final Hyperplane<P> hyperplane : hyperplanes) {
-                if (node.insertCut(hyperplane)) {
-                    node.setAttribute(null);
-                    node.getPlus().setAttribute(Boolean.FALSE);
-                    node = node.getMinus();
-                    node.setAttribute(Boolean.TRUE);
-                }
-            }
-
-        }
-
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public abstract AbstractRegion<P, S> buildNew(BSPTree<P> newTree);
-
-    /** Get the object used to determine floating point equality for this region.
-     * @return the floating point precision context for the instance
-     */
-    public DoublePrecisionContext getPrecision() {
-        return precision;
-    }
-
-    /** Recursively build a tree by inserting cut sub-hyperplanes.
-     * @param node current tree node (it is a leaf node at the beginning
-     * of the call)
-     * @param boundary collection of edges belonging to the cell defined
-     * by the node
-     */
-    private void insertCuts(final BSPTree<P> node, final Collection<SubHyperplane<P>> boundary) {
-
-        final Iterator<SubHyperplane<P>> iterator = boundary.iterator();
-
-        // build the current level
-        Hyperplane<P> inserted = null;
-        while ((inserted == null) && iterator.hasNext()) {
-            inserted = iterator.next().getHyperplane();
-            if (!node.insertCut(inserted.copySelf())) {
-                inserted = null;
-            }
-        }
-
-        if (!iterator.hasNext()) {
-            return;
-        }
-
-        // distribute the remaining edges in the two sub-trees
-        final ArrayList<SubHyperplane<P>> plusList  = new ArrayList<>();
-        final ArrayList<SubHyperplane<P>> minusList = new ArrayList<>();
-        while (iterator.hasNext()) {
-            final SubHyperplane<P> other = iterator.next();
-            final SubHyperplane.SplitSubHyperplane<P> split = other.split(inserted);
-            switch (split.getSide()) {
-            case PLUS:
-                plusList.add(other);
-                break;
-            case MINUS:
-                minusList.add(other);
-                break;
-            case BOTH:
-                plusList.add(split.getPlus());
-                minusList.add(split.getMinus());
-                break;
-            default:
-                // ignore the sub-hyperplanes belonging to the cut hyperplane
-            }
-        }
-
-        // recurse through lower levels
-        insertCuts(node.getPlus(),  plusList);
-        insertCuts(node.getMinus(), minusList);
-
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public AbstractRegion<P, S> copySelf() {
-        return buildNew(tree.copySelf());
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public boolean isEmpty() {
-        return isEmpty(tree);
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public boolean isEmpty(final BSPTree<P> node) {
-
-        // we use a recursive function rather than the BSPTreeVisitor
-        // interface because we can stop visiting the tree as soon as we
-        // have found an inside cell
-
-        if (node.getCut() == null) {
-            // if we find an inside node, the region is not empty
-            return !((Boolean) node.getAttribute());
-        }
-
-        // check both sides of the sub-tree
-        return isEmpty(node.getMinus()) && isEmpty(node.getPlus());
-
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public boolean isFull() {
-        return isFull(tree);
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public boolean isFull(final BSPTree<P> node) {
-
-        // we use a recursive function rather than the BSPTreeVisitor
-        // interface because we can stop visiting the tree as soon as we
-        // have found an outside cell
-
-        if (node.getCut() == null) {
-            // if we find an outside node, the region does not cover full space
-            return (Boolean) node.getAttribute();
-        }
-
-        // check both sides of the sub-tree
-        return isFull(node.getMinus()) && isFull(node.getPlus());
-
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public boolean contains(final Region<P> region) {
-        return new RegionFactory<P>().difference(region, this).isEmpty();
-    }
-
-    /** {@inheritDoc}
-     */
-    @Override
-    public BoundaryProjection<P> projectToBoundary(final P point) {
-        final BoundaryProjector<P, S> projector = new BoundaryProjector<>(point);
-        getTree(true).visit(projector);
-        return projector.getProjection();
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public Location checkPoint(final P point) {
-        return checkPoint(tree, point);
-    }
-
-    /** Check a point with respect to the region starting at a given node.
-     * @param node root node of the region
-     * @param point point to check
-     * @return a code representing the point status: either {@link
-     * Region.Location#INSIDE INSIDE}, {@link Region.Location#OUTSIDE
-     * OUTSIDE} or {@link Region.Location#BOUNDARY BOUNDARY}
-     */
-    protected Location checkPoint(final BSPTree<P> node, final P point) {
-        final BSPTree<P> cell = node.getCell(point, precision);
-        if (cell.getCut() == null) {
-            // the point is in the interior of a cell, just check the attribute
-            return ((Boolean) cell.getAttribute()) ? Location.INSIDE : Location.OUTSIDE;
-        }
-
-        // the point is on a cut-sub-hyperplane, is it on a boundary ?
-        final Location minusCode = checkPoint(cell.getMinus(), point);
-        final Location plusCode  = checkPoint(cell.getPlus(),  point);
-        return (minusCode == plusCode) ? minusCode : Location.BOUNDARY;
-
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public BSPTree<P> getTree(final boolean includeBoundaryAttributes) {
-        if (includeBoundaryAttributes && (tree.getCut() != null) && (tree.getAttribute() == null)) {
-            // compute the boundary attributes
-            tree.visit(new BoundaryBuilder<P>());
-        }
-        return tree;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public double getBoundarySize() {
-        final BoundarySizeVisitor<P> visitor = new BoundarySizeVisitor<>();
-        getTree(true).visit(visitor);
-        return visitor.getSize();
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public double getSize() {
-        if (barycenter == null) {
-            computeGeometricalProperties();
-        }
-        return size;
-    }
-
-    /** Set the size of the instance.
-     * @param size size of the instance
-     */
-    protected void setSize(final double size) {
-        this.size = size;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public P getBarycenter() {
-        if (barycenter == null) {
-            computeGeometricalProperties();
-        }
-        return barycenter;
-    }
-
-    /** Set the barycenter of the instance.
-     * @param barycenter barycenter of the instance
-     */
-    protected void setBarycenter(final P barycenter) {
-        this.barycenter = barycenter;
-    }
-
-    /** Compute some geometrical properties.
-     * <p>The properties to compute are the barycenter and the size.</p>
-     */
-    protected abstract void computeGeometricalProperties();
-
-    /** {@inheritDoc} */
-    @Override
-    public SubHyperplane<P> intersection(final SubHyperplane<P> sub) {
-        return recurseIntersection(tree, sub);
-    }
-
-    /** Recursively compute the parts of a sub-hyperplane that are
-     * contained in the region.
-     * @param node current BSP tree node
-     * @param sub sub-hyperplane traversing the region
-     * @return filtered sub-hyperplane
-     */
-    private SubHyperplane<P> recurseIntersection(final BSPTree<P> node, final SubHyperplane<P> sub) {
-
-        if (node.getCut() == null) {
-            return (Boolean) node.getAttribute() ? sub.copySelf() : null;
-        }
-
-        final Hyperplane<P> hyperplane = node.getCut().getHyperplane();
-        final SubHyperplane.SplitSubHyperplane<P> split = sub.split(hyperplane);
-        if (split.getPlus() != null) {
-            if (split.getMinus() != null) {
-                // both sides
-                final SubHyperplane<P> plus  = recurseIntersection(node.getPlus(),  split.getPlus());
-                final SubHyperplane<P> minus = recurseIntersection(node.getMinus(), split.getMinus());
-                if (plus == null) {
-                    return minus;
-                } else if (minus == null) {
-                    return plus;
-                } else {
-                    return plus.reunite(minus);
-                }
-            } else {
-                // only on plus side
-                return recurseIntersection(node.getPlus(), sub);
-            }
-        } else if (split.getMinus() != null) {
-            // only on minus side
-            return recurseIntersection(node.getMinus(), sub);
-        } else {
-            // on hyperplane
-            return recurseIntersection(node.getPlus(),
-                                       recurseIntersection(node.getMinus(), sub));
-        }
-
-    }
-
-    /** Transform a region.
-     * <p>Applying a transform to a region consist in applying the
-     * transform to all the hyperplanes of the underlying BSP tree and
-     * of the boundary (and also to the sub-hyperplanes embedded in
-     * these hyperplanes) and to the barycenter. The instance is not
-     * modified, a new instance is built.</p>
-     * @param transform transform to apply
-     * @return a new region, resulting from the application of the
-     * transform to the instance
-     */
-    public AbstractRegion<P, S> applyTransform(final Transform<P, S> transform) {
-
-        // transform the tree, except for boundary attribute splitters
-        final Map<BSPTree<P>, BSPTree<P>> map = new HashMap<>();
-        final BSPTree<P> transformedTree = recurseTransform(getTree(false), transform, map);
-
-        // set up the boundary attributes splitters
-        for (final Map.Entry<BSPTree<P>, BSPTree<P>> entry : map.entrySet()) {
-            if (entry.getKey().getCut() != null) {
-                @SuppressWarnings("unchecked")
-                BoundaryAttribute<P> original = (BoundaryAttribute<P>) entry.getKey().getAttribute();
-                if (original != null) {
-                    @SuppressWarnings("unchecked")
-                    BoundaryAttribute<P> transformed = (BoundaryAttribute<P>) entry.getValue().getAttribute();
-                    for (final BSPTree<P> splitter : original.getSplitters()) {
-                        transformed.getSplitters().add(map.get(splitter));
-                    }
-                }
-            }
-        }
-
-        return buildNew(transformedTree);
-
-    }
-
-    /** Recursively transform an inside/outside BSP-tree.
-     * @param node current BSP tree node
-     * @param transform transform to apply
-     * @param map transformed nodes map
-     * @return a new tree
-     */
-    @SuppressWarnings("unchecked")
-    private BSPTree<P> recurseTransform(final BSPTree<P> node, final Transform<P, S> transform,
-                                        final Map<BSPTree<P>, BSPTree<P>> map) {
-
-        final BSPTree<P> transformedNode;
-        if (node.getCut() == null) {
-            transformedNode = new BSPTree<>(node.getAttribute());
-        } else {
-
-            final SubHyperplane<P>  sub = node.getCut();
-            final SubHyperplane<P> tSub = ((AbstractSubHyperplane<P, S>) sub).applyTransform(transform);
-            BoundaryAttribute<P> attribute = (BoundaryAttribute<P>) node.getAttribute();
-            if (attribute != null) {
-                final SubHyperplane<P> tPO = (attribute.getPlusOutside() == null) ?
-                    null : ((AbstractSubHyperplane<P, S>) attribute.getPlusOutside()).applyTransform(transform);
-                final SubHyperplane<P> tPI = (attribute.getPlusInside()  == null) ?
-                    null  : ((AbstractSubHyperplane<P, S>) attribute.getPlusInside()).applyTransform(transform);
-                // we start with an empty list of splitters, it will be filled in out of recursion
-                attribute = new BoundaryAttribute<>(tPO, tPI, new NodesSet<P>());
-            }
-
-            transformedNode = new BSPTree<>(tSub,
-                                             recurseTransform(node.getPlus(),  transform, map),
-                                             recurseTransform(node.getMinus(), transform, map),
-                                             attribute);
-        }
-
-        map.put(node, transformedNode);
-        return transformedNode;
-
-    }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractSubHyperplane.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractSubHyperplane.java
deleted file mode 100644
index c8dfad1..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractSubHyperplane.java
+++ /dev/null
@@ -1,189 +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.HashMap;
-import java.util.Map;
-
-import org.apache.commons.geometry.core.Point;
-
-/** This class implements the dimension-independent parts of {@link SubHyperplane}.
-
- * <p>sub-hyperplanes are obtained when parts of an {@link
- * Hyperplane hyperplane} are chopped off by other hyperplanes that
- * intersect it. The remaining part is a convex region. Such objects
- * appear in {@link BSPTree BSP trees} as the intersection of a cut
- * hyperplane with the convex region which it splits, the chopping
- * hyperplanes are the cut hyperplanes closer to the tree root.</p>
-
- * @param <P> Point type defining the space
- * @param <S> Point type defining the sub-space
- */
-public abstract class AbstractSubHyperplane<P extends Point<P>, S extends Point<S>>
-    implements SubHyperplane<P> {
-
-    /** Underlying hyperplane. */
-    private final Hyperplane<P> hyperplane;
-
-    /** Remaining region of the hyperplane. */
-    private final Region<S> remainingRegion;
-
-    /** Build a sub-hyperplane from an hyperplane and a region.
-     * @param hyperplane underlying hyperplane
-     * @param remainingRegion remaining region of the hyperplane
-     */
-    protected AbstractSubHyperplane(final Hyperplane<P> hyperplane,
-                                    final Region<S> remainingRegion) {
-        this.hyperplane      = hyperplane;
-        this.remainingRegion = remainingRegion;
-    }
-
-    /** Build a sub-hyperplane from an hyperplane and a region.
-     * @param hyper underlying hyperplane
-     * @param remaining remaining region of the hyperplane
-     * @return a new sub-hyperplane
-     */
-    protected abstract AbstractSubHyperplane<P, S> buildNew(final Hyperplane<P> hyper,
-                                                            final Region<S> remaining);
-
-    /** {@inheritDoc} */
-    @Override
-    public AbstractSubHyperplane<P, S> copySelf() {
-        return buildNew(hyperplane.copySelf(), remainingRegion);
-    }
-
-    /** Get the underlying hyperplane.
-     * @return underlying hyperplane
-     */
-    @Override
-    public Hyperplane<P> getHyperplane() {
-        return hyperplane;
-    }
-
-    /** Get the remaining region of the hyperplane.
-     * <p>The returned region is expressed in the canonical hyperplane
-     * frame and has the hyperplane dimension. For example a chopped
-     * hyperplane in the 3D Euclidean is a 2D plane and the
-     * corresponding region is a convex 2D polygon.</p>
-     * @return remaining region of the hyperplane
-     */
-    public Region<S> getRemainingRegion() {
-        return remainingRegion;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public double getSize() {
-        return remainingRegion.getSize();
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public AbstractSubHyperplane<P, S> reunite(final SubHyperplane<P> other) {
-        @SuppressWarnings("unchecked")
-        AbstractSubHyperplane<P, S> o = (AbstractSubHyperplane<P, S>) other;
-        return buildNew(hyperplane,
-                        new RegionFactory<S>().union(remainingRegion, o.remainingRegion));
-    }
-
-    /** Apply a transform to the instance.
-     * <p>The instance must be a (D-1)-dimension sub-hyperplane with
-     * respect to the transform <em>not</em> a (D-2)-dimension
-     * sub-hyperplane the transform knows how to transform by
-     * itself. The transform will consist in transforming first the
-     * hyperplane and then the all region using the various methods
-     * provided by the transform.</p>
-     * @param transform D-dimension transform to apply
-     * @return the transformed instance
-     */
-    public AbstractSubHyperplane<P, S> applyTransform(final Transform<P, S> transform) {
-        final Hyperplane<P> tHyperplane = transform.apply(hyperplane);
-
-        // transform the tree, except for boundary attribute splitters
-        final Map<BSPTree<S>, BSPTree<S>> map = new HashMap<>();
-        final BSPTree<S> tTree =
-            recurseTransform(remainingRegion.getTree(false), tHyperplane, transform, map);
-
-        // set up the boundary attributes splitters
-        for (final Map.Entry<BSPTree<S>, BSPTree<S>> entry : map.entrySet()) {
-            if (entry.getKey().getCut() != null) {
-                @SuppressWarnings("unchecked")
-                BoundaryAttribute<S> original = (BoundaryAttribute<S>) entry.getKey().getAttribute();
-                if (original != null) {
-                    @SuppressWarnings("unchecked")
-                    BoundaryAttribute<S> transformed = (BoundaryAttribute<S>) entry.getValue().getAttribute();
-                    for (final BSPTree<S> splitter : original.getSplitters()) {
-                        transformed.getSplitters().add(map.get(splitter));
-                    }
-                }
-            }
-        }
-
-        return buildNew(tHyperplane, remainingRegion.buildNew(tTree));
-
-    }
-
-    /** Recursively transform a BSP-tree from a sub-hyperplane.
-     * @param node current BSP tree node
-     * @param transformed image of the instance hyperplane by the transform
-     * @param transform transform to apply
-     * @param map transformed nodes map
-     * @return a new tree
-     */
-    private BSPTree<S> recurseTransform(final BSPTree<S> node,
-                                        final Hyperplane<P> transformed,
-                                        final Transform<P, S> transform,
-                                        final Map<BSPTree<S>, BSPTree<S>> map) {
-
-        final BSPTree<S> transformedNode;
-        if (node.getCut() == null) {
-            transformedNode = new BSPTree<>(node.getAttribute());
-        } else {
-
-            @SuppressWarnings("unchecked")
-            BoundaryAttribute<S> attribute = (BoundaryAttribute<S>) node.getAttribute();
-            if (attribute != null) {
-                final SubHyperplane<S> tPO = (attribute.getPlusOutside() == null) ?
-                    null : transform.apply(attribute.getPlusOutside(), hyperplane, transformed);
-                final SubHyperplane<S> tPI = (attribute.getPlusInside() == null) ?
-                    null : transform.apply(attribute.getPlusInside(), hyperplane, transformed);
-                // we start with an empty list of splitters, it will be filled in out of recursion
-                attribute = new BoundaryAttribute<>(tPO, tPI, new NodesSet<S>());
-            }
-
-            transformedNode = new BSPTree<>(transform.apply(node.getCut(), hyperplane, transformed),
-                    recurseTransform(node.getPlus(),  transformed, transform, map),
-                    recurseTransform(node.getMinus(), transformed, transform, map),
-                    attribute);
-        }
-
-        map.put(node, transformedNode);
-        return transformedNode;
-
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public abstract SplitSubHyperplane<P> split(Hyperplane<P> hyper);
-
-    /** {@inheritDoc} */
-    @Override
-    public boolean isEmpty() {
-        return remainingRegion.isEmpty();
-    }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTree.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTree.java
deleted file mode 100644
index 9a00a89..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTree.java
+++ /dev/null
@@ -1,781 +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.ArrayList;
-import java.util.List;
-
-import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor.Order;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-
-/** This class represent a Binary Space Partition tree.
-
- * <p>BSP trees are an efficient way to represent space partitions and
- * to associate attributes with each cell. Each node in a BSP tree
- * represents a convex region which is partitioned in two convex
- * sub-regions at each side of a cut hyperplane. The root tree
- * contains the complete space.</p>
-
- * <p>The main use of such partitions is to use a boolean attribute to
- * define an inside/outside property, hence representing arbitrary
- * polytopes (line segments in 1D, polygons in 2D and polyhedrons in
- * 3D) and to operate on them.</p>
-
- * <p>Another example would be to represent Voronoi tesselations, the
- * attribute of each cell holding the defining point of the cell.</p>
-
- * <p>The application-defined attributes are shared among copied
- * instances and propagated to split parts. These attributes are not
- * used by the BSP-tree algorithms themselves, so the application can
- * use them for any purpose. Since the tree visiting method holds
- * internal and leaf nodes differently, it is possible to use
- * different classes for internal nodes attributes and leaf nodes
- * attributes. This should be used with care, though, because if the
- * tree is modified in any way after attributes have been set, some
- * internal nodes may become leaf nodes and some leaf nodes may become
- * internal nodes.</p>
-
- * <p>One of the main sources for the development of this package was
- * Bruce Naylor, John Amanatides and William Thibault paper <a
- * href="http://www.cs.yorku.ca/~amana/research/bsptSetOp.pdf">Merging
- * BSP Trees Yields Polyhedral Set Operations</a> Proc. Siggraph '90,
- * Computer Graphics 24(4), August 1990, pp 115-124, published by the
- * Association for Computing Machinery (ACM).</p>
-
- * @param <P> Point type defining the space
- */
-public class BSPTree<P extends Point<P>> {
-
-    /** Cut sub-hyperplane. */
-    private SubHyperplane<P> cut;
-
-    /** Tree at the plus side of the cut hyperplane. */
-    private BSPTree<P> plus;
-
-    /** Tree at the minus side of the cut hyperplane. */
-    private BSPTree<P> minus;
-
-    /** Parent tree. */
-    private BSPTree<P> parent;
-
-    /** Application-defined attribute. */
-    private Object attribute;
-
-    /** Build a tree having only one root cell representing the whole space.
-     */
-    public BSPTree() {
-        cut       = null;
-        plus      = null;
-        minus     = null;
-        parent    = null;
-        attribute = null;
-    }
-
-    /** Build a tree having only one root cell representing the whole space.
-     * @param attribute attribute of the tree (may be null)
-     */
-    public BSPTree(final Object attribute) {
-        cut    = null;
-        plus   = null;
-        minus  = null;
-        parent = null;
-        this.attribute = attribute;
-    }
-
-    /** Build a BSPTree from its underlying elements.
-     * <p>This method does <em>not</em> perform any verification on
-     * consistency of its arguments, it should therefore be used only
-     * when then caller knows what it is doing.</p>
-     * <p>This method is mainly useful to build trees
-     * bottom-up. Building trees top-down is realized with the help of
-     * method {@link #insertCut insertCut}.</p>
-     * @param cut cut sub-hyperplane for the tree
-     * @param plus plus side sub-tree
-     * @param minus minus side sub-tree
-     * @param attribute attribute associated with the node (may be null)
-     * @see #insertCut
-     */
-    public BSPTree(final SubHyperplane<P> cut, final BSPTree<P> plus, final BSPTree<P> minus,
-                   final Object attribute) {
-        this.cut       = cut;
-        this.plus      = plus;
-        this.minus     = minus;
-        this.parent    = null;
-        this.attribute = attribute;
-        plus.parent    = this;
-        minus.parent   = this;
-    }
-
-    /** Insert a cut sub-hyperplane in a node.
-     * <p>The sub-tree starting at this node will be completely
-     * overwritten. The new cut sub-hyperplane will be built from the
-     * intersection of the provided hyperplane with the cell. If the
-     * hyperplane does intersect the cell, the cell will have two
-     * children cells with {@code null} attributes on each side of
-     * the inserted cut sub-hyperplane. If the hyperplane does not
-     * intersect the cell then <em>no</em> cut hyperplane will be
-     * inserted and the cell will be changed to a leaf cell. The
-     * attribute of the node is never changed.</p>
-     * <p>This method is mainly useful when called on leaf nodes
-     * (i.e. nodes for which {@link #getCut getCut} returns
-     * {@code null}), in this case it provides a way to build a
-     * tree top-down (whereas the {@link #BSPTree(SubHyperplane,
-     * BSPTree, BSPTree, Object) 4 arguments constructor} is devoted to
-     * build trees bottom-up).</p>
-     * @param hyperplane hyperplane to insert, it will be chopped in
-     * order to fit in the cell defined by the parent nodes of the
-     * instance
-     * @return true if a cut sub-hyperplane has been inserted (i.e. if
-     * the cell now has two leaf child nodes)
-     * @see #BSPTree(SubHyperplane, BSPTree, BSPTree, Object)
-     */
-    public boolean insertCut(final Hyperplane<P> hyperplane) {
-
-        if (cut != null) {
-            plus.parent  = null;
-            minus.parent = null;
-        }
-
-        final SubHyperplane<P> chopped = fitToCell(hyperplane.wholeHyperplane());
-        if (chopped == null || chopped.isEmpty()) {
-            cut          = null;
-            plus         = null;
-            minus        = null;
-            return false;
-        }
-
-        cut          = chopped;
-        plus         = new BSPTree<>();
-        plus.parent  = this;
-        minus        = new BSPTree<>();
-        minus.parent = this;
-        return true;
-
-    }
-
-    /** Copy the instance.
-     * <p>The instance created is completely independent of the original
-     * one. A deep copy is used, none of the underlying objects are
-     * shared (except for the nodes attributes and immutable
-     * objects).</p>
-     * @return a new tree, copy of the instance
-     */
-    public BSPTree<P> copySelf() {
-
-        if (cut == null) {
-            return new BSPTree<>(attribute);
-        }
-
-        return new BSPTree<>(cut.copySelf(), plus.copySelf(), minus.copySelf(),
-                           attribute);
-
-    }
-
-    /** Get the cut sub-hyperplane.
-     * @return cut sub-hyperplane, null if this is a leaf tree
-     */
-    public SubHyperplane<P> getCut() {
-        return cut;
-    }
-
-    /** Get the tree on the plus side of the cut hyperplane.
-     * @return tree on the plus side of the cut hyperplane, null if this
-     * is a leaf tree
-     */
-    public BSPTree<P> getPlus() {
-        return plus;
-    }
-
-    /** Get the tree on the minus side of the cut hyperplane.
-     * @return tree on the minus side of the cut hyperplane, null if this
-     * is a leaf tree
-     */
-    public BSPTree<P> getMinus() {
-        return minus;
-    }
-
-    /** Get the parent node.
-     * @return parent node, null if the node has no parents
-     */
-    public BSPTree<P> getParent() {
-        return parent;
-    }
-
-    /** Associate an attribute with the instance.
-     * @param attribute attribute to associate with the node
-     * @see #getAttribute
-     */
-    public void setAttribute(final Object attribute) {
-        this.attribute = attribute;
-    }
-
-    /** Get the attribute associated with the instance.
-     * @return attribute associated with the node or null if no
-     * attribute has been explicitly set using the {@link #setAttribute
-     * setAttribute} method
-     * @see #setAttribute
-     */
-    public Object getAttribute() {
-        return attribute;
-    }
-
-    /** Visit the BSP tree nodes.
-     * @param visitor object visiting the tree nodes
-     */
-    public void visit(final BSPTreeVisitor<P> visitor) {
-        if (cut == null) {
-            visitor.visitLeafNode(this);
-        } else {
-            Order order = visitor.visitOrder(this);
-            switch (order) {
-            case PLUS_MINUS_SUB:
-                plus.visit(visitor);
-                minus.visit(visitor);
-                visitor.visitInternalNode(this);
-                break;
-            case PLUS_SUB_MINUS:
-                plus.visit(visitor);
-                visitor.visitInternalNode(this);
-                minus.visit(visitor);
-                break;
-            case MINUS_PLUS_SUB:
-                minus.visit(visitor);
-                plus.visit(visitor);
-                visitor.visitInternalNode(this);
-                break;
-            case MINUS_SUB_PLUS:
-                minus.visit(visitor);
-                visitor.visitInternalNode(this);
-                plus.visit(visitor);
-                break;
-            case SUB_PLUS_MINUS:
-                visitor.visitInternalNode(this);
-                plus.visit(visitor);
-                minus.visit(visitor);
-                break;
-            case SUB_MINUS_PLUS:
-                visitor.visitInternalNode(this);
-                minus.visit(visitor);
-                plus.visit(visitor);
-                break;
-            default:
-                // we shouldn't end up here since all possibilities are
-                // covered above
-                throw new IllegalStateException("Invalid node visit order: " + order);
-            }
-        }
-    }
-
-    /** Fit a sub-hyperplane inside the cell defined by the instance.
-     * <p>Fitting is done by chopping off the parts of the
-     * sub-hyperplane that lie outside of the cell using the
-     * cut-hyperplanes of the parent nodes of the instance.</p>
-     * @param sub sub-hyperplane to fit
-     * @return a new sub-hyperplane, guaranteed to have no part outside
-     * of the instance cell
-     */
-    private SubHyperplane<P> fitToCell(final SubHyperplane<P> sub) {
-        SubHyperplane<P> s = sub;
-        for (BSPTree<P> tree = this; tree.parent != null && s != null; tree = tree.parent) {
-            if (tree == tree.parent.plus) {
-                s = s.split(tree.parent.cut.getHyperplane()).getPlus();
-            } else {
-                s = s.split(tree.parent.cut.getHyperplane()).getMinus();
-            }
-        }
-        return s;
-    }
-
-    /** Get the cell to which a point belongs.
-     * <p>If the returned cell is a leaf node the points belongs to the
-     * interior of the node, if the cell is an internal node the points
-     * belongs to the node cut sub-hyperplane.</p>
-     * @param point point to check
-     * @param precision precision context used to determine which points
-     * close to a cut hyperplane are considered to belong to the hyperplane itself
-     * @return the tree cell to which the point belongs
-     */
-    public BSPTree<P> getCell(final P point, final DoublePrecisionContext precision) {
-
-        if (cut == null) {
-            return this;
-        }
-
-        // position of the point with respect to the cut hyperplane
-        final double offset = cut.getHyperplane().getOffset(point);
-
-        final int comparison = precision.compare(offset, 0.0);
-        if (comparison == 0) {
-            return this;
-        } else if (comparison < 0) {
-            // point is on the minus side of the cut hyperplane
-            return minus.getCell(point, precision);
-        } else {
-            // point is on the plus side of the cut hyperplane
-            return plus.getCell(point, precision);
-        }
-
-    }
-
-    /** Get the cells whose cut sub-hyperplanes are close to the point.
-     * @param point point to check
-     * @param maxOffset offset below which a cut sub-hyperplane is considered
-     * close to the point (in absolute value)
-     * @return close cells (may be empty if all cut sub-hyperplanes are farther
-     * than maxOffset from the point)
-     */
-    public List<BSPTree<P>> getCloseCuts(final P point, final double maxOffset) {
-        final List<BSPTree<P>> close = new ArrayList<>();
-        recurseCloseCuts(point, maxOffset, close);
-        return close;
-    }
-
-    /** Get the cells whose cut sub-hyperplanes are close to the point.
-     * @param point point to check
-     * @param maxOffset offset below which a cut sub-hyperplane is considered
-     * close to the point (in absolute value)
-     * @param close list to fill
-     */
-    private void recurseCloseCuts(final P point, final double maxOffset,
-                                  final List<BSPTree<P>> close) {
-        if (cut != null) {
-
-            // position of the point with respect to the cut hyperplane
-            final double offset = cut.getHyperplane().getOffset(point);
-
-            if (offset < -maxOffset) {
-                // point is on the minus side of the cut hyperplane
-                minus.recurseCloseCuts(point, maxOffset, close);
-            } else if (offset > maxOffset) {
-                // point is on the plus side of the cut hyperplane
-                plus.recurseCloseCuts(point, maxOffset, close);
-            } else {
-                // point is close to the cut hyperplane
-                close.add(this);
-                minus.recurseCloseCuts(point, maxOffset, close);
-                plus.recurseCloseCuts(point, maxOffset, close);
-            }
-
-        }
-    }
-
-    /** Perform condensation on a tree.
-     * <p>The condensation operation is not recursive, it must be called
-     * explicitly from leaves to root.</p>
-     */
-    private void condense() {
-        if ((cut != null) && (plus.cut == null) && (minus.cut == null) &&
-            (((plus.attribute == null) && (minus.attribute == null)) ||
-             ((plus.attribute != null) && plus.attribute.equals(minus.attribute)))) {
-            attribute = (plus.attribute == null) ? minus.attribute : plus.attribute;
-            cut       = null;
-            plus      = null;
-            minus     = null;
-        }
-    }
-
-    /** Merge a BSP tree with the instance.
-     * <p>All trees are modified (parts of them are reused in the new
-     * tree), it is the responsibility of the caller to ensure a copy
-     * has been done before if any of the former tree should be
-     * preserved, <em>no</em> such copy is done here!</p>
-     * <p>The algorithm used here is directly derived from the one
-     * described in the Naylor, Amanatides and Thibault paper (section
-     * III, Binary Partitioning of a BSP Tree).</p>
-     * @param tree other tree to merge with the instance (will be
-     * <em>unusable</em> after the operation, as well as the
-     * instance itself)
-     * @param leafMerger object implementing the final merging phase
-     * (this is where the semantic of the operation occurs, generally
-     * depending on the attribute of the leaf node)
-     * @return a new tree, result of <code>instance &lt;op&gt;
-     * tree</code>, this value can be ignored if parentTree is not null
-     * since all connections have already been established
-     */
-    public BSPTree<P> merge(final BSPTree<P> tree, final LeafMerger<P> leafMerger) {
-        return merge(tree, leafMerger, null, false);
-    }
-
-    /** Merge a BSP tree with the instance.
-     * @param tree other tree to merge with the instance (will be
-     * <em>unusable</em> after the operation, as well as the
-     * instance itself)
-     * @param leafMerger object implementing the final merging phase
-     * (this is where the semantic of the operation occurs, generally
-     * depending on the attribute of the leaf node)
-     * @param parentTree parent tree to connect to (may be null)
-     * @param isPlusChild if true and if parentTree is not null, the
-     * resulting tree should be the plus child of its parent, ignored if
-     * parentTree is null
-     * @return a new tree, result of <code>instance &lt;op&gt;
-     * tree</code>, this value can be ignored if parentTree is not null
-     * since all connections have already been established
-     */
-    private BSPTree<P> merge(final BSPTree<P> tree, final LeafMerger<P> leafMerger,
-                             final BSPTree<P> parentTree, final boolean isPlusChild) {
-        if (cut == null) {
-            // cell/tree operation
-            return leafMerger.merge(this, tree, parentTree, isPlusChild, true);
-        } else if (tree.cut == null) {
-            // tree/cell operation
-            return leafMerger.merge(tree, this, parentTree, isPlusChild, false);
-        } else {
-            // tree/tree operation
-            final BSPTree<P> merged = tree.split(cut);
-            if (parentTree != null) {
-                merged.parent = parentTree;
-                if (isPlusChild) {
-                    parentTree.plus = merged;
-                } else {
-                    parentTree.minus = merged;
-                }
-            }
-
-            // merging phase
-            plus.merge(merged.plus, leafMerger, merged, true);
-            minus.merge(merged.minus, leafMerger, merged, false);
-            merged.condense();
-            if (merged.cut != null) {
-                merged.cut = merged.fitToCell(merged.cut.getHyperplane().wholeHyperplane());
-            }
-
-            return merged;
-
-        }
-    }
-
-    /** This interface gather the merging operations between a BSP tree
-     * leaf and another BSP tree.
-     * <p>As explained in Bruce Naylor, John Amanatides and William
-     * Thibault paper <a
-     * href="http://www.cs.yorku.ca/~amana/research/bsptSetOp.pdf">Merging
-     * BSP Trees Yields Polyhedral Set Operations</a>,
-     * the operations on {@link BSPTree BSP trees} can be expressed as a
-     * generic recursive merging operation where only the final part,
-     * when one of the operand is a leaf, is specific to the real
-     * operation semantics. For example, a tree representing a region
-     * using a boolean attribute to identify inside cells and outside
-     * cells would use four different objects to implement the final
-     * merging phase of the four set operations union, intersection,
-     * difference and symmetric difference (exclusive or).</p>
-     * @param <S> Type of the space.
-     */
-    public interface LeafMerger<S extends Point<S>> {
-
-        /** Merge a leaf node and a tree node.
-         * <p>This method is called at the end of a recursive merging
-         * resulting from a {@code tree1.merge(tree2, leafMerger)}
-         * call, when one of the sub-trees involved is a leaf (i.e. when
-         * its cut-hyperplane is null). This is the only place where the
-         * precise semantics of the operation are required. For all upper
-         * level nodes in the tree, the merging operation is only a
-         * generic partitioning algorithm.</p>
-         * <p>Since the final operation may be non-commutative, it is
-         * important to know if the leaf node comes from the instance tree
-         * ({@code tree1}) or the argument tree
-         * ({@code tree2}). The third argument of the method is
-         * devoted to this. It can be ignored for commutative
-         * operations.</p>
-         * <p>The {@link BSPTree#insertInTree BSPTree.insertInTree} method
-         * may be useful to implement this method.</p>
-         * @param leaf leaf node (its cut hyperplane is guaranteed to be
-         * null)
-         * @param tree tree node (its cut hyperplane may be null or not)
-         * @param parentTree parent tree to connect to (may be null)
-         * @param isPlusChild if true and if parentTree is not null, the
-         * resulting tree should be the plus child of its parent, ignored if
-         * parentTree is null
-         * @param leafFromInstance if true, the leaf node comes from the
-         * instance tree ({@code tree1}) and the tree node comes from
-         * the argument tree ({@code tree2})
-         * @return the BSP tree resulting from the merging (may be one of
-         * the arguments)
-         */
-        BSPTree<S> merge(BSPTree<S> leaf, BSPTree<S> tree, BSPTree<S> parentTree,
-                         boolean isPlusChild, boolean leafFromInstance);
-
-    }
-
-    /** This interface handles the corner cases when an internal node cut sub-hyperplane vanishes.
-     * <p>
-     * Such cases happens for example when a cut sub-hyperplane is inserted into
-     * another tree (during a merge operation), and is split in several parts,
-     * some of which becomes smaller than the tolerance. The corresponding node
-     * as then no cut sub-hyperplane anymore, but does have children. This interface
-     * specifies how to handle this situation.
-     * setting
-     * </p>
-     * @param <S> Type of the space.
-     */
-    public interface VanishingCutHandler<S extends Point<S>> {
-
-        /** Fix a node with both vanished cut and children.
-         * @param node node to fix
-         * @return fixed node
-         */
-        BSPTree<S> fixNode(BSPTree<S> node);
-
-    }
-
-    /** Split a BSP tree by an external sub-hyperplane.
-     * <p>Split a tree in two halves, on each side of the
-     * sub-hyperplane. The instance is not modified.</p>
-     * <p>The tree returned is not upward-consistent: despite all of its
-     * sub-trees cut sub-hyperplanes (including its own cut
-     * sub-hyperplane) are bounded to the current cell, it is <em>not</em>
-     * attached to any parent tree yet. This tree is intended to be
-     * later inserted into an higher level tree.</p>
-     * <p>The algorithm used here is the one given in Naylor, Amanatides
-     * and Thibault paper (section III, Binary Partitioning of a BSP
-     * Tree).</p>
-     * @param sub partitioning sub-hyperplane, must be already clipped
-     * to the convex region represented by the instance, will be used as
-     * the cut sub-hyperplane of the returned tree
-     * @return a tree having the specified sub-hyperplane as its cut
-     * sub-hyperplane, the two parts of the split instance as its two
-     * sub-trees and a null parent
-     */
-    public BSPTree<P> split(final SubHyperplane<P> sub) {
-
-        if (cut == null) {
-            return new BSPTree<>(sub, copySelf(), new BSPTree<P>(attribute), null);
-        }
-
-        final Hyperplane<P> cHyperplane = cut.getHyperplane();
-        final Hyperplane<P> sHyperplane = sub.getHyperplane();
-        final SubHyperplane.SplitSubHyperplane<P> subParts = sub.split(cHyperplane);
-        switch (subParts.getSide()) {
-        case PLUS :
-        { // the partitioning sub-hyperplane is entirely in the plus sub-tree
-            final BSPTree<P> split = plus.split(sub);
-            if (cut.split(sHyperplane).getSide() == Side.PLUS) {
-                split.plus =
-                    new BSPTree<>(cut.copySelf(), split.plus, minus.copySelf(), attribute);
-                split.plus.condense();
-                split.plus.parent = split;
-            } else {
-                split.minus =
-                    new BSPTree<>(cut.copySelf(), split.minus, minus.copySelf(), attribute);
-                split.minus.condense();
-                split.minus.parent = split;
-            }
-            return split;
-        }
-        case MINUS :
-        { // the partitioning sub-hyperplane is entirely in the minus sub-tree
-            final BSPTree<P> split = minus.split(sub);
-            if (cut.split(sHyperplane).getSide() == Side.PLUS) {
-                split.plus =
-                    new BSPTree<>(cut.copySelf(), plus.copySelf(), split.plus, attribute);
-                split.plus.condense();
-                split.plus.parent = split;
-            } else {
-                split.minus =
-                    new BSPTree<>(cut.copySelf(), plus.copySelf(), split.minus, attribute);
-                split.minus.condense();
-                split.minus.parent = split;
-            }
-            return split;
-        }
-        case BOTH :
-        {
-            final SubHyperplane.SplitSubHyperplane<P> cutParts = cut.split(sHyperplane);
-            final BSPTree<P> split =
-                new BSPTree<>(sub, plus.split(subParts.getPlus()), minus.split(subParts.getMinus()),
-                               null);
-            split.plus.cut          = cutParts.getPlus();
-            split.minus.cut         = cutParts.getMinus();
-            final BSPTree<P> tmp    = split.plus.minus;
-            split.plus.minus        = split.minus.plus;
-            split.plus.minus.parent = split.plus;
-            split.minus.plus        = tmp;
-            split.minus.plus.parent = split.minus;
-            split.plus.condense();
-            split.minus.condense();
-            return split;
-        }
-        default :
-            return cHyperplane.sameOrientationAs(sHyperplane) ?
-                   new BSPTree<>(sub, plus.copySelf(),  minus.copySelf(), attribute) :
-                   new BSPTree<>(sub, minus.copySelf(), plus.copySelf(),  attribute);
-        }
-
-    }
-
-    /** Insert the instance into another tree.
-     * <p>The instance itself is modified so its former parent should
-     * not be used anymore.</p>
-     * @param parentTree parent tree to connect to (may be null)
-     * @param isPlusChild if true and if parentTree is not null, the
-     * resulting tree should be the plus child of its parent, ignored if
-     * parentTree is null
-     * @param vanishingHandler handler to use for handling very rare corner
-     * cases of vanishing cut sub-hyperplanes in internal nodes during merging
-     * @see LeafMerger
-     */
-    public void insertInTree(final BSPTree<P> parentTree, final boolean isPlusChild,
-                             final VanishingCutHandler<P> vanishingHandler) {
-
-        // set up parent/child links
-        parent = parentTree;
-        if (parentTree != null) {
-            if (isPlusChild) {
-                parentTree.plus = this;
-            } else {
-                parentTree.minus = this;
-            }
-        }
-
-        // make sure the inserted tree lies in the cell defined by its parent nodes
-        if (cut != null) {
-
-            // explore the parent nodes from here towards tree root
-            for (BSPTree<P> tree = this; tree.parent != null; tree = tree.parent) {
-
-                // this is an hyperplane of some parent node
-                final Hyperplane<P> hyperplane = tree.parent.cut.getHyperplane();
-
-                // chop off the parts of the inserted tree that extend
-                // on the wrong side of this parent hyperplane
-                if (tree == tree.parent.plus) {
-                    cut = cut.split(hyperplane).getPlus();
-                    plus.chopOffMinus(hyperplane, vanishingHandler);
-                    minus.chopOffMinus(hyperplane, vanishingHandler);
-                } else {
-                    cut = cut.split(hyperplane).getMinus();
-                    plus.chopOffPlus(hyperplane, vanishingHandler);
-                    minus.chopOffPlus(hyperplane, vanishingHandler);
-                }
-
-                if (cut == null) {
-                    // the cut sub-hyperplane has vanished
-                    final BSPTree<P> fixed = vanishingHandler.fixNode(this);
-                    cut       = fixed.cut;
-                    plus      = fixed.plus;
-                    minus     = fixed.minus;
-                    attribute = fixed.attribute;
-                    if (cut == null) {
-                        break;
-                    }
-                }
-
-            }
-
-            // since we may have drop some parts of the inserted tree,
-            // perform a condensation pass to keep the tree structure simple
-            condense();
-
-        }
-
-    }
-
-    /** Prune a tree around a cell.
-     * <p>
-     * This method can be used to extract a convex cell from a tree.
-     * The original cell may either be a leaf node or an internal node.
-     * If it is an internal node, it's subtree will be ignored (i.e. the
-     * extracted cell will be a leaf node in all cases). The original
-     * tree to which the original cell belongs is not touched at all,
-     * a new independent tree will be built.
-     * </p>
-     * @param cellAttribute attribute to set for the leaf node
-     * corresponding to the initial instance cell
-     * @param otherLeafsAttributes attribute to set for the other leaf
-     * nodes
-     * @param internalAttributes attribute to set for the internal nodes
-     * @return a new tree (the original tree is left untouched) containing
-     * a single branch with the cell as a leaf node, and other leaf nodes
-     * as the remnants of the pruned branches
-     */
-    public BSPTree<P> pruneAroundConvexCell(final Object cellAttribute,
-                                            final Object otherLeafsAttributes,
-                                            final Object internalAttributes) {
-
-        // build the current cell leaf
-        BSPTree<P> tree = new BSPTree<>(cellAttribute);
-
-        // build the pruned tree bottom-up
-        for (BSPTree<P> current = this; current.parent != null; current = current.parent) {
-            final SubHyperplane<P> parentCut = current.parent.cut.copySelf();
-            final BSPTree<P>       sibling   = new BSPTree<>(otherLeafsAttributes);
-            if (current == current.parent.plus) {
-                tree = new BSPTree<>(parentCut, tree, sibling, internalAttributes);
-            } else {
-                tree = new BSPTree<>(parentCut, sibling, tree, internalAttributes);
-            }
-        }
-
-        return tree;
-
-    }
-
-    /** Chop off parts of the tree.
-     * <p>The instance is modified in place, all the parts that are on
-     * the minus side of the chopping hyperplane are discarded, only the
-     * parts on the plus side remain.</p>
-     * @param hyperplane chopping hyperplane
-     * @param vanishingHandler handler to use for handling very rare corner
-     * cases of vanishing cut sub-hyperplanes in internal nodes during merging
-     */
-    private void chopOffMinus(final Hyperplane<P> hyperplane, final VanishingCutHandler<P> vanishingHandler) {
-        if (cut != null) {
-
-            cut = cut.split(hyperplane).getPlus();
-            plus.chopOffMinus(hyperplane, vanishingHandler);
-            minus.chopOffMinus(hyperplane, vanishingHandler);
-
-            if (cut == null) {
-                // the cut sub-hyperplane has vanished
-                final BSPTree<P> fixed = vanishingHandler.fixNode(this);
-                cut       = fixed.cut;
-                plus      = fixed.plus;
-                minus     = fixed.minus;
-                attribute = fixed.attribute;
-            }
-
-        }
-    }
-
-    /** Chop off parts of the tree.
-     * <p>The instance is modified in place, all the parts that are on
-     * the plus side of the chopping hyperplane are discarded, only the
-     * parts on the minus side remain.</p>
-     * @param hyperplane chopping hyperplane
-     * @param vanishingHandler handler to use for handling very rare corner
-     * cases of vanishing cut sub-hyperplanes in internal nodes during merging
-     */
-    private void chopOffPlus(final Hyperplane<P> hyperplane, final VanishingCutHandler<P> vanishingHandler) {
-        if (cut != null) {
-
-            cut = cut.split(hyperplane).getMinus();
-            plus.chopOffPlus(hyperplane, vanishingHandler);
-            minus.chopOffPlus(hyperplane, vanishingHandler);
-
-            if (cut == null) {
-                // the cut sub-hyperplane has vanished
-                final BSPTree<P> fixed = vanishingHandler.fixNode(this);
-                cut       = fixed.cut;
-                plus      = fixed.plus;
-                minus     = fixed.minus;
-                attribute = fixed.attribute;
-            }
-
-        }
-    }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTreeVisitor.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTreeVisitor.java
deleted file mode 100644
index 52d0eee..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTreeVisitor.java
+++ /dev/null
@@ -1,112 +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;
-
-/** This interface is used to visit {@link BSPTree BSP tree} nodes.
-
- * <p>Navigation through {@link BSPTree BSP trees} can be done using
- * two different point of views:</p>
- * <ul>
- *   <li>
- *     the first one is in a node-oriented way using the {@link
- *     BSPTree#getPlus}, {@link BSPTree#getMinus} and {@link
- *     BSPTree#getParent} methods. Terminal nodes without associated
- *     {@link SubHyperplane sub-hyperplanes} can be visited this way,
- *     there is no constraint in the visit order, and it is possible
- *     to visit either all nodes or only a subset of the nodes
- *   </li>
- *   <li>
- *     the second one is in a sub-hyperplane-oriented way using
- *     classes implementing this interface which obeys the visitor
- *     design pattern. The visit order is provided by the visitor as
- *     each node is first encountered. Each node is visited exactly
- *     once.
- *   </li>
- * </ul>
-
- * @param <P> Point type defining the space
-
- * @see BSPTree
- * @see SubHyperplane
- */
-public interface BSPTreeVisitor<P extends Point<P>> {
-
-    /** Enumerate for visit order with respect to plus sub-tree, minus sub-tree and cut sub-hyperplane. */
-    enum Order {
-        /** Indicator for visit order plus sub-tree, then minus sub-tree,
-         * and last cut sub-hyperplane.
-         */
-        PLUS_MINUS_SUB,
-
-        /** Indicator for visit order plus sub-tree, then cut sub-hyperplane,
-         * and last minus sub-tree.
-         */
-        PLUS_SUB_MINUS,
-
-        /** Indicator for visit order minus sub-tree, then plus sub-tree,
-         * and last cut sub-hyperplane.
-         */
-        MINUS_PLUS_SUB,
-
-        /** Indicator for visit order minus sub-tree, then cut sub-hyperplane,
-         * and last plus sub-tree.
-         */
-        MINUS_SUB_PLUS,
-
-        /** Indicator for visit order cut sub-hyperplane, then plus sub-tree,
-         * and last minus sub-tree.
-         */
-        SUB_PLUS_MINUS,
-
-        /** Indicator for visit order cut sub-hyperplane, then minus sub-tree,
-         * and last plus sub-tree.
-         */
-        SUB_MINUS_PLUS;
-    }
-
-    /** Determine the visit order for this node.
-     * <p>Before attempting to visit an internal node, this method is
-     * called to determine the desired ordering of the visit. It is
-     * guaranteed that this method will be called before {@link
-     * #visitInternalNode visitInternalNode} for a given node, it will be
-     * called exactly once for each internal node.</p>
-     * @param node BSP node guaranteed to have a non null cut sub-hyperplane
-     * @return desired visit order, must be one of
-     * {@link Order#PLUS_MINUS_SUB}, {@link Order#PLUS_SUB_MINUS},
-     * {@link Order#MINUS_PLUS_SUB}, {@link Order#MINUS_SUB_PLUS},
-     * {@link Order#SUB_PLUS_MINUS}, {@link Order#SUB_MINUS_PLUS}
-     */
-    Order visitOrder(BSPTree<P> node);
-
-    /** Visit a BSP tree node node having a non-null sub-hyperplane.
-     * <p>It is guaranteed that this method will be called after {@link
-     * #visitOrder visitOrder} has been called for a given node,
-     * it wil be called exactly once for each internal node.</p>
-     * @param node BSP node guaranteed to have a non null cut sub-hyperplane
-     * @see #visitLeafNode
-     */
-    void visitInternalNode(BSPTree<P> node);
-
-    /** Visit a leaf BSP tree node node having a null sub-hyperplane.
-     * @param node leaf BSP node having a null sub-hyperplane
-     * @see #visitInternalNode
-     */
-    void visitLeafNode(BSPTree<P> node);
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryAttribute.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryAttribute.java
deleted file mode 100644
index 0476c34..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryAttribute.java
+++ /dev/null
@@ -1,100 +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;
-
-/** Class holding boundary attributes.
- *
- * <p>This class is used for the attributes associated with the
- * nodes of region boundary shell trees returned by the {@link
- * Region#getTree(boolean) Region.getTree(includeBoundaryAttributes)}
- * when the boolean {@code includeBoundaryAttributes} parameter is
- * set to {@code true}. It contains the parts of the node cut
- * sub-hyperplane that belong to the boundary.</p>
- *
- * <p>This class is a simple placeholder, it does not provide any
- * processing methods.</p>
- *
- * @param <P> Point type defining the space
- * @see Region#getTree
- */
-public class BoundaryAttribute<P extends Point<P>> {
-
-    /** Part of the node cut sub-hyperplane that belongs to the
-     * boundary and has the outside of the region on the plus side of
-     * its underlying hyperplane (may be null).
-     */
-    private final SubHyperplane<P> plusOutside;
-
-    /** Part of the node cut sub-hyperplane that belongs to the
-     * boundary and has the inside of the region on the plus side of
-     * its underlying hyperplane (may be null).
-     */
-    private final SubHyperplane<P> plusInside;
-
-    /** Sub-hyperplanes that were used to split the boundary part. */
-    private final NodesSet<P> splitters;
-
-    /** Simple constructor.
-     * @param plusOutside part of the node cut sub-hyperplane that
-     * belongs to the boundary and has the outside of the region on
-     * the plus side of its underlying hyperplane (may be null)
-     * @param plusInside part of the node cut sub-hyperplane that
-     * belongs to the boundary and has the inside of the region on the
-     * plus side of its underlying hyperplane (may be null)
-     * @param splitters sub-hyperplanes that were used to
-     * split the boundary part (may be null)
-     */
-    BoundaryAttribute(final SubHyperplane<P> plusOutside,
-                      final SubHyperplane<P> plusInside,
-                      final NodesSet<P> splitters) {
-        this.plusOutside = plusOutside;
-        this.plusInside  = plusInside;
-        this.splitters   = splitters;
-    }
-
-    /** Get the part of the node cut sub-hyperplane that belongs to the
-     * boundary and has the outside of the region on the plus side of
-     * its underlying hyperplane.
-     * @return part of the node cut sub-hyperplane that belongs to the
-     * boundary and has the outside of the region on the plus side of
-     * its underlying hyperplane
-     */
-    public SubHyperplane<P> getPlusOutside() {
-        return plusOutside;
-    }
-
-    /** Get the part of the node cut sub-hyperplane that belongs to the
-     * boundary and has the inside of the region on the plus side of
-     * its underlying hyperplane.
-     * @return part of the node cut sub-hyperplane that belongs to the
-     * boundary and has the inside of the region on the plus side of
-     * its underlying hyperplane
-     */
-    public SubHyperplane<P> getPlusInside() {
-        return plusInside;
-    }
-
-    /** Get the sub-hyperplanes that were used to split the boundary part.
-     * @return sub-hyperplanes that were used to split the boundary part
-     */
-    public NodesSet<P> getSplitters() {
-        return splitters;
-    }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryBuilder.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryBuilder.java
deleted file mode 100644
index 63d19b8..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryBuilder.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;
-
-/** Visitor building boundary shell tree.
- *
- * <p>
- * The boundary shell is represented as {@link BoundaryAttribute boundary attributes}
- * at each internal node.
- * </p>
- *
- * @param <P> Point type defining the space.
- */
-class BoundaryBuilder<P extends Point<P>> implements BSPTreeVisitor<P> {
-
-    /** {@inheritDoc} */
-    @Override
-    public Order visitOrder(BSPTree<P> node) {
-        return Order.PLUS_MINUS_SUB;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public void visitInternalNode(BSPTree<P> node) {
-
-        SubHyperplane<P> plusOutside = null;
-        SubHyperplane<P> plusInside  = null;
-        NodesSet<P>      splitters   = null;
-
-        // characterize the cut sub-hyperplane,
-        // first with respect to the plus sub-tree
-        final Characterization<P> plusChar = new Characterization<>(node.getPlus(), node.getCut().copySelf());
-
-        if (plusChar.touchOutside()) {
-            // plusChar.outsideTouching() corresponds to a subset of the cut sub-hyperplane
-            // known to have outside cells on its plus side, we want to check if parts
-            // of this subset do have inside cells on their minus side
-            final Characterization<P> minusChar = new Characterization<>(node.getMinus(), plusChar.outsideTouching());
-            if (minusChar.touchInside()) {
-                // this part belongs to the boundary,
-                // it has the outside on its plus side and the inside on its minus side
-                plusOutside = minusChar.insideTouching();
-                splitters = new NodesSet<>();
-                splitters.addAll(minusChar.getInsideSplitters());
-                splitters.addAll(plusChar.getOutsideSplitters());
-            }
-        }
-
-        if (plusChar.touchInside()) {
-            // plusChar.insideTouching() corresponds to a subset of the cut sub-hyperplane
-            // known to have inside cells on its plus side, we want to check if parts
-            // of this subset do have outside cells on their minus side
-            final Characterization<P> minusChar = new Characterization<>(node.getMinus(), plusChar.insideTouching());
-            if (minusChar.touchOutside()) {
-                // this part belongs to the boundary,
-                // it has the inside on its plus side and the outside on its minus side
-                plusInside = minusChar.outsideTouching();
-                if (splitters == null) {
-                    splitters = new NodesSet<>();
-                }
-                splitters.addAll(minusChar.getOutsideSplitters());
-                splitters.addAll(plusChar.getInsideSplitters());
-            }
-        }
-
-        if (splitters != null) {
-            // the parent nodes are natural splitters for boundary sub-hyperplanes
-            for (BSPTree<P> up = node.getParent(); up != null; up = up.getParent()) {
-                splitters.add(up);
-            }
-        }
-
-        // set the boundary attribute at non-leaf nodes
-        node.setAttribute(new BoundaryAttribute<>(plusOutside, plusInside, splitters));
-
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public void visitLeafNode(BSPTree<P> node) {
-    }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjection.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjection.java
deleted file mode 100644
index 27709c2..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjection.java
+++ /dev/null
@@ -1,84 +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;
-
-/** Class holding the result of point projection on region boundary.
- *
- * <p>This class is a simple placeholder, it does not provide any
- * processing methods.</p>
- *
- * <p>Instances of this class are guaranteed to be immutable</p>
- *
- * @param <P> Point type defining the space
- * @see AbstractRegion#projectToBoundary(Point)
- */
-public class BoundaryProjection<P extends Point<P>> {
-
-    /** Original point. */
-    private final P original;
-
-    /** Projected point. */
-    private final P projected;
-
-    /** Offset of the point with respect to the boundary it is projected on. */
-    private final double offset;
-
-    /** Constructor from raw elements.
-     * @param original original point
-     * @param projected projected point
-     * @param offset offset of the point with respect to the boundary it is projected on
-     */
-    public BoundaryProjection(final P original, final P projected, final double offset) {
-        this.original  = original;
-        this.projected = projected;
-        this.offset    = offset;
-    }
-
-    /** Get the original point.
-     * @return original point
-     */
-    public P getOriginal() {
-        return original;
-    }
-
-    /** Projected point.
-     * @return projected point, or null if there are no boundary
-     */
-    public P getProjected() {
-        return projected;
-    }
-
-    /** Offset of the point with respect to the boundary it is projected on.
-     * <p>
-     * The offset with respect to the boundary is negative if the {@link
-     * #getOriginal() original point} is inside the region, and positive otherwise.
-     * </p>
-     * <p>
-     * If there are no boundary, the value is set to either {@code
-     * Double.POSITIVE_INFINITY} if the region is empty (i.e. all points are
-     * outside of the region) or {@code Double.NEGATIVE_INFINITY} if the region
-     * covers the whole space (i.e. all points are inside of the region).
-     * </p>
-     * @return offset of the point with respect to the boundary it is projected on
-     */
-    public double getOffset() {
-        return offset;
-    }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjector.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjector.java
deleted file mode 100644
index f694a89..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjector.java
+++ /dev/null
@@ -1,200 +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.ArrayList;
-import java.util.List;
-
-import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.partitioning.Region.Location;
-
-/** Local tree visitor to compute projection on boundary.
- * @param <P> Point type defining the space
- * @param <S> Point type defining the sub-space
- */
-class BoundaryProjector<P extends Point<P>, S extends Point<S>> implements BSPTreeVisitor<P> {
-
-    /** Original point. */
-    private final P original;
-
-    /** Current best projected point. */
-    private P projected;
-
-    /** Leaf node closest to the test point. */
-    private BSPTree<P> leaf;
-
-    /** Current offset. */
-    private double offset;
-
-    /** Simple constructor.
-     * @param original original point
-     */
-    BoundaryProjector(final P original) {
-        this.original  = original;
-        this.projected = null;
-        this.leaf      = null;
-        this.offset    = Double.POSITIVE_INFINITY;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public Order visitOrder(final BSPTree<P> node) {
-        // we want to visit the tree so that the first encountered
-        // leaf is the one closest to the test point
-        if (node.getCut().getHyperplane().getOffset(original) <= 0) {
-            return Order.MINUS_SUB_PLUS;
-        } else {
-            return Order.PLUS_SUB_MINUS;
-        }
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public void visitInternalNode(final BSPTree<P> node) {
-
-        // project the point on the cut sub-hyperplane
-        final Hyperplane<P> hyperplane = node.getCut().getHyperplane();
-        final double signedOffset = hyperplane.getOffset(original);
-        if (Math.abs(signedOffset) < offset) {
-
-            // project point
-            final P regular = hyperplane.project(original);
-
-            // get boundary parts
-            final List<Region<S>> boundaryParts = boundaryRegions(node);
-
-            // check if regular projection really belongs to the boundary
-            boolean regularFound = false;
-            for (final Region<S> part : boundaryParts) {
-                if (!regularFound && belongsToPart(regular, hyperplane, part)) {
-                    // the projected point lies in the boundary
-                    projected    = regular;
-                    offset       = Math.abs(signedOffset);
-                    regularFound = true;
-                }
-            }
-
-            if (!regularFound) {
-                // the regular projected point is not on boundary,
-                // so we have to check further if a singular point
-                // (i.e. a vertex in 2D case) is a possible projection
-                for (final Region<S> part : boundaryParts) {
-                    final P spI = singularProjection(regular, hyperplane, part);
-                    if (spI != null) {
-                        final double distance = original.distance(spI);
-                        if (distance < offset) {
-                            projected = spI;
-                            offset    = distance;
-                        }
-                    }
-                }
-
-            }
-
-        }
-
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public void visitLeafNode(final BSPTree<P> node) {
-        if (leaf == null) {
-            // this is the first leaf we visit,
-            // it is the closest one to the original point
-            leaf = node;
-        }
-    }
-
-    /** Get the projection.
-     * @return projection
-     */
-    public BoundaryProjection<P> getProjection() {
-
-        // fix offset sign
-        offset = Math.copySign(offset, (Boolean) leaf.getAttribute() ? -1 : +1);
-
-        return new BoundaryProjection<>(original, projected, offset);
-
-    }
-
-    /** Extract the regions of the boundary on an internal node.
-     * @param node internal node
-     * @return regions in the node sub-hyperplane
-     */
-    private List<Region<S>> boundaryRegions(final BSPTree<P> node) {
-
-        final List<Region<S>> regions = new ArrayList<>(2);
-
-        @SuppressWarnings("unchecked")
-        final BoundaryAttribute<P> ba = (BoundaryAttribute<P>) node.getAttribute();
-        addRegion(ba.getPlusInside(),  regions);
-        addRegion(ba.getPlusOutside(), regions);
-
-        return regions;
-
-    }
-
-    /** Add a boundary region to a list.
-     * @param sub sub-hyperplane defining the region
-     * @param list to fill up
-     */
-    private void addRegion(final SubHyperplane<P> sub, final List<Region<S>> list) {
-        if (sub != null) {
-            @SuppressWarnings("unchecked")
-            final Region<S> region = ((AbstractSubHyperplane<P, S>) sub).getRemainingRegion();
-            if (region != null) {
-                list.add(region);
-            }
-        }
-    }
-
-    /** Check if a projected point lies on a boundary part.
-     * @param point projected point to check
-     * @param hyperplane hyperplane into which the point was projected
-     * @param part boundary part
-     * @return true if point lies on the boundary part
-     */
-    private boolean belongsToPart(final P point, final Hyperplane<P> hyperplane,
-                                  final Region<S> part) {
-
-        // there is a non-null sub-space, we can dive into smaller dimensions
-        @SuppressWarnings("unchecked")
-        final Embedding<P, S> embedding = (Embedding<P, S>) hyperplane;
-        return part.checkPoint(embedding.toSubSpace(point)) != Location.OUTSIDE;
-
-    }
-
-    /** Get the projection to the closest boundary singular point.
-     * @param point projected point to check
-     * @param hyperplane hyperplane into which the point was projected
-     * @param part boundary part
-     * @return projection to a singular point of boundary part (may be null)
-     */
-    private P singularProjection(final P point, final Hyperplane<P> hyperplane,
-                                        final Region<S> part) {
-
-        // there is a non-null sub-space, we can dive into smaller dimensions
-        @SuppressWarnings("unchecked")
-        final Embedding<P, S> embedding = (Embedding<P, S>) hyperplane;
-        final BoundaryProjection<S> bp = part.projectToBoundary(embedding.toSubSpace(point));
-
-        // back to initial dimension
-        return (bp.getProjected() == null) ? null : embedding.toSpace(bp.getProjected());
-
-    }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundarySizeVisitor.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundarySizeVisitor.java
deleted file mode 100644
index e84c70a..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundarySizeVisitor.java
+++ /dev/null
@@ -1,67 +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;
-
-/** Visitor computing the boundary size.
- * @param <P> Point type defining the space
- */
-class BoundarySizeVisitor<P extends Point<P>> implements BSPTreeVisitor<P> {
-
-    /** Size of the boundary. */
-    private double boundarySize;
-
-    /** Simple constructor.
-     */
-    BoundarySizeVisitor() {
-        boundarySize = 0;
-    }
-
-    /** {@inheritDoc}*/
-    @Override
-    public Order visitOrder(final BSPTree<P> node) {
-        return Order.MINUS_SUB_PLUS;
-    }
-
-    /** {@inheritDoc}*/
-    @Override
-    public void visitInternalNode(final BSPTree<P> node) {
-        @SuppressWarnings("unchecked")
-        final BoundaryAttribute<P> attribute =
-            (BoundaryAttribute<P>) node.getAttribute();
-        if (attribute.getPlusOutside() != null) {
-            boundarySize += attribute.getPlusOutside().getSize();
-        }
-        if (attribute.getPlusInside() != null) {
-            boundarySize += attribute.getPlusInside().getSize();
-        }
-    }
-
-    /** {@inheritDoc}*/
-    @Override
-    public void visitLeafNode(final BSPTree<P> node) {
-    }
-
-    /** Get the size of the boundary.
-     * @return size of the boundary
-     */
-    public double getSize() {
-        return boundarySize;
-    }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Characterization.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Characterization.java
deleted file mode 100644
index d9ec6e7..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Characterization.java
+++ /dev/null
@@ -1,196 +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.ArrayList;
-import java.util.List;
-
-import org.apache.commons.geometry.core.Point;
-
-/** Cut sub-hyperplanes characterization with respect to inside/outside cells.
- * @see BoundaryBuilder
- * @param <P> Point type defining the space
- */
-class Characterization<P extends Point<P>> {
-
-    /** Part of the cut sub-hyperplane that touch outside cells. */
-    private SubHyperplane<P> outsideTouching;
-
-    /** Part of the cut sub-hyperplane that touch inside cells. */
-    private SubHyperplane<P> insideTouching;
-
-    /** Nodes that were used to split the outside touching part. */
-    private final NodesSet<P> outsideSplitters;
-
-    /** Nodes that were used to split the outside touching part. */
-    private final NodesSet<P> insideSplitters;
-
-    /** Simple constructor.
-     * <p>Characterization consists in splitting the specified
-     * sub-hyperplane into several parts lying in inside and outside
-     * cells of the tree. The principle is to compute characterization
-     * twice for each cut sub-hyperplane in the tree, once on the plus
-     * node and once on the minus node. The parts that have the same flag
-     * (inside/inside or outside/outside) do not belong to the boundary
-     * while parts that have different flags (inside/outside or
-     * outside/inside) do belong to the boundary.</p>
-     * @param node current BSP tree node
-     * @param sub sub-hyperplane to characterize
-     */
-    Characterization(final BSPTree<P> node, final SubHyperplane<P> sub) {
-        outsideTouching  = null;
-        insideTouching   = null;
-        outsideSplitters = new NodesSet<>();
-        insideSplitters  = new NodesSet<>();
-        characterize(node, sub, new ArrayList<BSPTree<P>>());
-    }
-
-    /** Filter the parts of an hyperplane belonging to the boundary.
-     * <p>The filtering consist in splitting the specified
-     * sub-hyperplane into several parts lying in inside and outside
-     * cells of the tree. The principle is to call this method twice for
-     * each cut sub-hyperplane in the tree, once on the plus node and
-     * once on the minus node. The parts that have the same flag
-     * (inside/inside or outside/outside) do not belong to the boundary
-     * while parts that have different flags (inside/outside or
-     * outside/inside) do belong to the boundary.</p>
-     * @param node current BSP tree node
-     * @param sub sub-hyperplane to characterize
-     * @param splitters nodes that did split the current one
-     */
-    private void characterize(final BSPTree<P> node, final SubHyperplane<P> sub,
-                              final List<BSPTree<P>> splitters) {
-        if (node.getCut() == null) {
-            // we have reached a leaf node
-            final boolean inside = (Boolean) node.getAttribute();
-            if (inside) {
-                addInsideTouching(sub, splitters);
-            } else {
-                addOutsideTouching(sub, splitters);
-            }
-        } else {
-            final Hyperplane<P> hyperplane = node.getCut().getHyperplane();
-            final SubHyperplane.SplitSubHyperplane<P> split = sub.split(hyperplane);
-            switch (split.getSide()) {
-            case PLUS:
-                characterize(node.getPlus(),  sub, splitters);
-                break;
-            case MINUS:
-                characterize(node.getMinus(), sub, splitters);
-                break;
-            case BOTH:
-                splitters.add(node);
-                characterize(node.getPlus(),  split.getPlus(),  splitters);
-                characterize(node.getMinus(), split.getMinus(), splitters);
-                splitters.remove(splitters.size() - 1);
-                break;
-            default:
-                // If we reach this point, then the sub-hyperplane we're
-                // testing lies directly on this node's hyperplane. In theory,
-                // this shouldn't ever happen with correctly-formed trees. However,
-                // this does actually occur in practice, especially with manually
-                // built trees or very complex models. Rather than throwing an
-                // exception, we'll attempt to handle this situation gracefully
-                // by treating these sub-hyperplanes as if they lie on the minus
-                // side of the cut hyperplane.
-                characterize(node.getMinus(), sub, splitters);
-                break;
-            }
-        }
-    }
-
-    /** Add a part of the cut sub-hyperplane known to touch an outside cell.
-     * @param sub part of the cut sub-hyperplane known to touch an outside cell
-     * @param splitters sub-hyperplanes that did split the current one
-     */
-    private void addOutsideTouching(final SubHyperplane<P> sub,
-                                    final List<BSPTree<P>> splitters) {
-        if (outsideTouching == null) {
-            outsideTouching = sub;
-        } else {
-            outsideTouching = outsideTouching.reunite(sub);
-        }
-        outsideSplitters.addAll(splitters);
-    }
-
-    /** Add a part of the cut sub-hyperplane known to touch an inside cell.
-     * @param sub part of the cut sub-hyperplane known to touch an inside cell
-     * @param splitters sub-hyperplanes that did split the current one
-     */
-    private void addInsideTouching(final SubHyperplane<P> sub,
-                                   final List<BSPTree<P>> splitters) {
-        if (insideTouching == null) {
-            insideTouching = sub;
-        } else {
-            insideTouching = insideTouching.reunite(sub);
-        }
-        insideSplitters.addAll(splitters);
-    }
-
-    /** Check if the cut sub-hyperplane touches outside cells.
-     * @return true if the cut sub-hyperplane touches outside cells
-     */
-    public boolean touchOutside() {
-        return outsideTouching != null && !outsideTouching.isEmpty();
-    }
-
-    /** Get all the parts of the cut sub-hyperplane known to touch outside cells.
-     * @return parts of the cut sub-hyperplane known to touch outside cells
-     * (may be null or empty)
-     */
-    public SubHyperplane<P> outsideTouching() {
-        return outsideTouching;
-    }
-
-    /** Get the nodes that were used to split the outside touching part.
-     * <p>
-     * Splitting nodes are internal nodes (i.e. they have a non-null
-     * cut sub-hyperplane).
-     * </p>
-     * @return nodes that were used to split the outside touching part
-     */
-    public NodesSet<P> getOutsideSplitters() {
-        return outsideSplitters;
-    }
-
-    /** Check if the cut sub-hyperplane touches inside cells.
-     * @return true if the cut sub-hyperplane touches inside cells
-     */
-    public boolean touchInside() {
-        return insideTouching != null && !insideTouching.isEmpty();
-    }
-
-    /** Get all the parts of the cut sub-hyperplane known to touch inside cells.
-     * @return parts of the cut sub-hyperplane known to touch inside cells
-     * (may be null or empty)
-     */
-    public SubHyperplane<P> insideTouching() {
-        return insideTouching;
-    }
-
-    /** Get the nodes that were used to split the inside touching part.
-     * <p>
-     * Splitting nodes are internal nodes (i.e. they have a non-null
-     * cut sub-hyperplane).
-     * </p>
-     * @return nodes that were used to split the inside touching part
-     */
-    public NodesSet<P> getInsideSplitters() {
-        return insideSplitters;
-    }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/ConvexSubHyperplane.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/ConvexSubHyperplane.java
new file mode 100644
index 0000000..606e233
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/ConvexSubHyperplane.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.core.partitioning;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Transform;
+
+/** Extension of the {@link SubHyperplane} interface with the additional restriction
+ * that instances represent convex regions of space.
+ * @param <P> Point implementation type
+ */
+public interface ConvexSubHyperplane<P extends Point<P>> extends SubHyperplane<P> {
+
+    /** Reverse the orientation of the hyperplane for this instance. The subhyperplane
+     * occupies the same locations in space but with a reversed orientation.
+     * @return a convex subhyperplane representing the same region but with the
+     *      opposite orientation.
+     */
+    ConvexSubHyperplane<P> reverse();
+
+    /** {@inheritDoc}
+     *
+     * <p>The parts resulting from a split operation with a convex subhyperplane
+     * are guaranteed to also be convex.</p>
+     */
+    @Override
+    Split<? extends ConvexSubHyperplane<P>> split(Hyperplane<P> splitter);
+
+    /** {@inheritDoc}
+     *
+     * <p>Convex subhyperplanes subjected to affine transformations remain
+     * convex.</p>
+     */
+    @Override
+    ConvexSubHyperplane<P> transform(Transform<P> transform);
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Embedding.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Embedding.java
deleted file mode 100644
index dfc40bf..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Embedding.java
+++ /dev/null
@@ -1,62 +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;
-
-/** This interface defines mappers between a space and one of its sub-spaces.
-
- * <p>Sub-spaces are the lower dimensions subsets of a n-dimensions
- * space. The (n-1)-dimension sub-spaces are specific sub-spaces known
- * as {@link Hyperplane hyperplanes}. This interface can be used regardless
- * of the dimensions differences. For example, a line in 3D Euclidean space
- * can map directly from 3 dimensions to 1.</p>
-
- * <p>In the 3D Euclidean space, hyperplanes are 2D planes, and the 1D
- * sub-spaces are lines.</p>
-
- * <p>
- * Note that this interface is <em>not</em> intended to be implemented
- * by Apache Commons Geometry users, it is only intended to be implemented
- * within the library itself. New methods may be added even for minor
- * versions, which breaks compatibility for external implementations.
- * </p>
-
- * @param <P> Point type defining the embedding space.
- * @param <S> Point type defining the embedded sub-space.
-
- * @see Hyperplane
- */
-public interface Embedding<P extends Point<P>, S extends Point<S>> {
-
-    /** Transform a space point into a sub-space point.
-     * @param point n-dimension point of the space
-     * @return (n-1)-dimension point of the sub-space corresponding to
-     * the specified space point
-     * @see #toSpace
-     */
-    S toSubSpace(P point);
-
-    /** Transform a sub-space point into a space point.
-     * @param point (n-1)-dimension point of the sub-space
-     * @return n-dimension point of the space corresponding to the
-     * specified sub-space point
-     * @see #toSubSpace
-     */
-    P toSpace(S point);
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/EmbeddingHyperplane.java
similarity index 68%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
copy to commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/EmbeddingHyperplane.java
index 046defe..71b1297 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/EmbeddingHyperplane.java
@@ -16,21 +16,15 @@
  */
 package org.apache.commons.geometry.core.partitioning;
 
-/** Enumerate representing the location of an element with respect to an
- * {@link Hyperplane hyperplane} of a space.
- */
-public enum Side {
-
-    /** Code for the plus side of the hyperplane. */
-    PLUS,
-
-    /** Code for the minus side of the hyperplane. */
-    MINUS,
-
-    /** Code for elements crossing the hyperplane from plus to minus side. */
-    BOTH,
-
-    /** Code for the hyperplane itself. */
-    HYPER;
+import org.apache.commons.geometry.core.Embedding;
+import org.apache.commons.geometry.core.Point;
 
+/** Hyperplane that also embeds a subspace.
+ * @param <P> Point implementation type
+ * @param <S> Subspace point implementation type
+ * @see Hyperplane
+ * @see Embedding
+ */
+public interface EmbeddingHyperplane<P extends Point<P>, S extends Point<S>>
+    extends Hyperplane<P>, Embedding<P, S> {
 }
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Hyperplane.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Hyperplane.java
index 28d9d12..d7bd09f 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Hyperplane.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Hyperplane.java
@@ -17,80 +17,69 @@
 package org.apache.commons.geometry.core.partitioning;
 
 import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.Transform;
 
-/** This interface represents an hyperplane of a space.
-
- * <p>The most prominent place where hyperplane appears in space
- * partitioning is as cutters. Each partitioning node in a {@link
- * BSPTree BSP tree} has a cut {@link SubHyperplane sub-hyperplane}
- * which is either an hyperplane or a part of an hyperplane. In an
- * n-dimensions Euclidean space, an hyperplane is an (n-1)-dimensions
- * hyperplane (for example a traditional plane in the 3D Euclidean
- * space). They can be more exotic objects in specific fields, for
- * example a circle on the surface of the unit sphere.</p>
-
- * <p>
- * Note that this interface is <em>not</em> intended to be implemented
- * by Apache Commons Geometry users, it is only intended to be implemented
- * within the library itself. New methods may be added even for minor
- * versions, which breaks compatibility for external implementations.
- * </p>
-
- * @param <P> Point type defining the space
+/** Interface representing a hyperplane, which is a subspace of degree
+ * one less than the space it is embedded in.
+ * @param <P> Point implementation type
  */
 public interface Hyperplane<P extends Point<P>> {
 
-    /** Copy the instance.
-     * <p>The instance created is completely independant of the original
-     * one. A deep copy is used, none of the underlying objects are
-     * shared (except for immutable objects).</p>
-     * @return a new hyperplane, copy of the instance
+    /** Get the offset (oriented distance) of a point with respect
+     * to this instance. Points with an offset of zero lie on the
+     * hyperplane itself.
+     * @param point the point to compute the offset for
+     * @return the offset of the point
      */
-    Hyperplane<P> copySelf();
+    double offset(P point);
 
-    /** Get the offset (oriented distance) of a point.
-     * <p>The offset is 0 if the point is on the underlying hyperplane,
-     * it is positive if the point is on one particular side of the
-     * hyperplane, and it is negative if the point is on the other side,
-     * according to the hyperplane natural orientation.</p>
-     * @param point point to check
-     * @return offset of the point
+    /** Classify a point with respect to this hyperplane.
+     * @param point the point to classify
+     * @return the relative location of the point with
+     *      respect to this instance
      */
-    double getOffset(P point);
+    HyperplaneLocation classify(P point);
 
-    /** Project a point to the hyperplane.
-     * @param point point to project
-     * @return projected point
+    /** Return true if the given point lies on the hyperplane.
+     * @param point the point to test
+     * @return true if the point lies on the hyperplane
      */
-    P project(P point);
+    boolean contains(P point);
 
-    /** Get the object used to determine floating point equality for this hyperplane.
-     * This determines which points belong to the hyperplane and which do not, or pictured
-     * another way, the "thickness" of the hyperplane.
-     * @return the floating point precision context for the instance
+    /** Project a point onto this instance.
+     * @param point the point to project
+     * @return the projection of the point onto this instance. The returned
+     *      point lies on the hyperplane.
      */
-    DoublePrecisionContext getPrecision();
+    P project(P point);
 
-    /** Check if the instance has the same orientation as another hyperplane.
-     * <p>This method is expected to be called on parallel hyperplanes. The
-     * method should <em>not</em> re-check for parallelism, only for
-     * orientation, typically by testing something like the sign of the
-     * dot-products of normals.</p>
-     * @param other other hyperplane to check against the instance
-     * @return true if the instance and the other hyperplane have
-     * the same orientation
+    /** Return a hyperplane that has the opposite orientation as this instance.
+     * That is, the plus side of this instance is the minus side of the returned
+     * instance and vice versa.
+     * @return a hyperplane with the opposite orientation
      */
-    boolean sameOrientationAs(Hyperplane<P> other);
+    Hyperplane<P> reverse();
 
-    /** Build a sub-hyperplane covering the whole hyperplane.
-     * @return a sub-hyperplane covering the whole hyperplane
+    /** Transform this instance using the given {@link Transform}.
+     * @param transform object to transform this instance with
+     * @return a new, transformed hyperplane
      */
-    SubHyperplane<P> wholeHyperplane();
+    Hyperplane<P> transform(Transform<P> transform);
 
-    /** Build a region covering the whole space.
-     * @return a region containing the instance
+    /** Return true if this instance has a similar orientation to the given hyperplane,
+     * meaning that they point in generally the same direction. This method is not
+     * used to determine exact equality of hyperplanes, but rather to determine whether
+     * two hyperplanes that contain the same points are parallel (point in the same direction)
+     * or anti-parallel (point in opposite directions).
+     * @param other the hyperplane to compare with
+     * @return true if the hyperplanes point in generally the same direction and could
+     *      possibly be parallel
      */
-    Region<P> wholeSpace();
+    boolean similarOrientation(Hyperplane<P> other);
 
+    /** Return a {@link ConvexSubHyperplane} spanning this entire hyperplane. The returned
+     * subhyperplane contains all points lying in this hyperplane and no more.
+     * @return a {@link ConvexSubHyperplane} containing all points lying in this hyperplane
+     */
+    ConvexSubHyperplane<P> span();
 }
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneBoundedRegion.java
similarity index 60%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
copy to commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneBoundedRegion.java
index 046defe..0718d09 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneBoundedRegion.java
@@ -16,21 +16,15 @@
  */
 package org.apache.commons.geometry.core.partitioning;
 
-/** Enumerate representing the location of an element with respect to an
- * {@link Hyperplane hyperplane} of a space.
- */
-public enum Side {
-
-    /** Code for the plus side of the hyperplane. */
-    PLUS,
-
-    /** Code for the minus side of the hyperplane. */
-    MINUS,
-
-    /** Code for elements crossing the hyperplane from plus to minus side. */
-    BOTH,
-
-    /** Code for the hyperplane itself. */
-    HYPER;
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Region;
 
+/** Interface representing regions with boundaries defined by hyperplanes or
+ * portions of hyperplanes. This interface is intended to represent closed regions
+ * with finite sizes as well as infinite and empty spaces. Regions of this type
+ * can be recursively split by hyperplanes into similar regions.
+ * @param <P> Point implementation type
+ */
+public interface HyperplaneBoundedRegion<P extends Point<P>>
+    extends Region<P>, Splittable<P, HyperplaneBoundedRegion<P>> {
 }
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneLocation.java
similarity index 68%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
copy to commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneLocation.java
index 046defe..ce45f0f 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/HyperplaneLocation.java
@@ -16,21 +16,23 @@
  */
 package org.apache.commons.geometry.core.partitioning;
 
-/** Enumerate representing the location of an element with respect to an
- * {@link Hyperplane hyperplane} of a space.
+/** Enumeration containing possible locations of a point with respect to
+ * a hyperplane.
+ * @see Hyperplane
  */
-public enum Side {
+public enum HyperplaneLocation {
 
-    /** Code for the plus side of the hyperplane. */
-    PLUS,
-
-    /** Code for the minus side of the hyperplane. */
+    /** Value indicating that a point lies on the minus side of
+     * a hyperplane.
+     */
     MINUS,
 
-    /** Code for elements crossing the hyperplane from plus to minus side. */
-    BOTH,
-
-    /** Code for the hyperplane itself. */
-    HYPER;
+    /** Value indicating that a point lies on the plus side of
+     * a hyperplane.
+     */
+    PLUS,
 
+    /** Value indicating that a point lies directly on a hyperplane.
+     */
+    ON
 }
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/InsideFinder.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/InsideFinder.java
deleted file mode 100644
index cb384d9..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/InsideFinder.java
+++ /dev/null
@@ -1,149 +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;
-
-/** Utility class checking if inside nodes can be found
- * on the plus and minus sides of an hyperplane.
- * @param <P> Point type defining the space
- */
-class InsideFinder<P extends Point<P>> {
-
-    /** Region on which to operate. */
-    private final Region<P> region;
-
-    /** Indicator of inside leaf nodes found on the plus side. */
-    private boolean plusFound;
-
-    /** Indicator of inside leaf nodes found on the plus side. */
-    private boolean minusFound;
-
-    /** Simple constructor.
-     * @param region region on which to operate
-     */
-    InsideFinder(final Region<P> region) {
-        this.region = region;
-        plusFound  = false;
-        minusFound = false;
-    }
-
-    /** Search recursively for inside leaf nodes on each side of the given hyperplane.
-
-     * <p>The algorithm used here is directly derived from the one
-     * described in section III (<i>Binary Partitioning of a BSP
-     * Tree</i>) of the Bruce Naylor, John Amanatides and William
-     * Thibault paper <a
-     * href="http://www.cs.yorku.ca/~amana/research/bsptSetOp.pdf">Merging
-     * BSP Trees Yields Polyhedral Set Operations</a> Proc. Siggraph
-     * '90, Computer Graphics 24(4), August 1990, pp 115-124, published
-     * by the Association for Computing Machinery (ACM)..</p>
-
-     * @param node current BSP tree node
-     * @param sub sub-hyperplane
-     */
-    public void recurseSides(final BSPTree<P> node, final SubHyperplane<P> sub) {
-
-        if (node.getCut() == null) {
-            if ((Boolean) node.getAttribute()) {
-                // this is an inside cell expanding across the hyperplane
-                plusFound  = true;
-                minusFound = true;
-            }
-            return;
-        }
-
-        final Hyperplane<P> hyperplane = node.getCut().getHyperplane();
-        final SubHyperplane.SplitSubHyperplane<P> split = sub.split(hyperplane);
-        switch (split.getSide()) {
-        case PLUS :
-            // the sub-hyperplane is entirely in the plus sub-tree
-            if (node.getCut().split(sub.getHyperplane()).getSide() == Side.PLUS) {
-                if (!region.isEmpty(node.getMinus())) {
-                    plusFound  = true;
-                }
-            } else {
-                if (!region.isEmpty(node.getMinus())) {
-                    minusFound = true;
-                }
-            }
-            if (!(plusFound && minusFound)) {
-                recurseSides(node.getPlus(), sub);
-            }
-            break;
-        case MINUS :
-            // the sub-hyperplane is entirely in the minus sub-tree
-            if (node.getCut().split(sub.getHyperplane()).getSide() == Side.PLUS) {
-                if (!region.isEmpty(node.getPlus())) {
-                    plusFound  = true;
-                }
-            } else {
-                if (!region.isEmpty(node.getPlus())) {
-                    minusFound = true;
-                }
-            }
-            if (!(plusFound && minusFound)) {
-                recurseSides(node.getMinus(), sub);
-            }
-            break;
-        case BOTH :
-            // the sub-hyperplane extends in both sub-trees
-
-            // explore first the plus sub-tree
-            recurseSides(node.getPlus(), split.getPlus());
-
-            // if needed, explore the minus sub-tree
-            if (!(plusFound && minusFound)) {
-                recurseSides(node.getMinus(), split.getMinus());
-            }
-            break;
-        default :
-            // the sub-hyperplane and the cut sub-hyperplane share the same hyperplane
-            if (node.getCut().getHyperplane().sameOrientationAs(sub.getHyperplane())) {
-                if ((node.getPlus().getCut() != null) || ((Boolean) node.getPlus().getAttribute())) {
-                    plusFound  = true;
-                }
-                if ((node.getMinus().getCut() != null) || ((Boolean) node.getMinus().getAttribute())) {
-                    minusFound = true;
-                }
-            } else {
-                if ((node.getPlus().getCut() != null) || ((Boolean) node.getPlus().getAttribute())) {
-                    minusFound = true;
-                }
-                if ((node.getMinus().getCut() != null) || ((Boolean) node.getMinus().getAttribute())) {
-                    plusFound  = true;
-                }
-            }
-        }
-
-    }
-
-    /** Check if inside leaf nodes have been found on the plus side.
-     * @return true if inside leaf nodes have been found on the plus side
-     */
-    public boolean plusFound() {
-        return plusFound;
-    }
-
-    /** Check if inside leaf nodes have been found on the minus side.
-     * @return true if inside leaf nodes have been found on the minus side
-     */
-    public boolean minusFound() {
-        return minusFound;
-    }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/NodesSet.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/NodesSet.java
deleted file mode 100644
index 54e0c3d..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/NodesSet.java
+++ /dev/null
@@ -1,72 +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.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
-import org.apache.commons.geometry.core.Point;
-
-/** Set of {@link BSPTree BSP tree} nodes.
- * @see BoundaryAttribute
- * @param <P> Point type defining the space
- */
-public class NodesSet<P extends Point<P>> implements Iterable<BSPTree<P>> {
-
-    /** List of sub-hyperplanes. */
-    private final List<BSPTree<P>> list;
-
-    /** Simple constructor.
-     */
-    public NodesSet() {
-        list = new ArrayList<>();
-    }
-
-    /** Add a node if not already known.
-     * @param node node to add
-     */
-    public void add(final BSPTree<P> node) {
-
-        for (final BSPTree<P> existing : list) {
-            if (node == existing) {
-                // the node is already known, don't add it
-                return;
-            }
-        }
-
-        // the node was not known, add it
-        list.add(node);
-
-    }
-
-    /** Add nodes if they are not already known.
-     * @param iterator nodes iterator
-     */
-    public void addAll(final Iterable<BSPTree<P>> iterator) {
-        for (final BSPTree<P> node : iterator) {
-            add(node);
-        }
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public Iterator<BSPTree<P>> iterator() {
-        return list.iterator();
-    }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Region.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Region.java
deleted file mode 100644
index 4fef7e3..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Region.java
+++ /dev/null
@@ -1,204 +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;
-
-/** This interface represents a region of a space as a partition.
-
- * <p>Region are subsets of a space, they can be infinite (whole
- * space, half space, infinite stripe ...) or finite (polygons in 2D,
- * polyhedrons in 3D ...). Their main characteristic is to separate
- * points that are considered to be <em>inside</em> the region from
- * points considered to be <em>outside</em> of it. In between, there
- * may be points on the <em>boundary</em> of the region.</p>
-
- * <p>This implementation is limited to regions for which the boundary
- * is composed of several {@link SubHyperplane sub-hyperplanes},
- * including regions with no boundary at all: the whole space and the
- * empty region. They are not necessarily finite and not necessarily
- * path-connected. They can contain holes.</p>
-
- * <p>Regions can be combined using the traditional sets operations :
- * union, intersection, difference and symetric difference (exclusive
- * or) for the binary operations, complement for the unary
- * operation.</p>
-
- * <p>
- * Note that this interface is <em>not</em> intended to be implemented
- * by Apache Commons Math users, it is only intended to be implemented
- * within the library itself. New methods may be added even for minor
- * versions, which breaks compatibility for external implementations.
- * </p>
-
- * @param <P> Point type defining the space
- */
-public interface Region<P extends Point<P>> {
-
-    /** Enumerate for the location of a point with respect to the region. */
-    enum Location {
-        /** Code for points inside the partition. */
-        INSIDE,
-
-        /** Code for points outside of the partition. */
-        OUTSIDE,
-
-        /** Code for points on the partition boundary. */
-        BOUNDARY;
-    }
-
-    /** Build a region using the instance as a prototype.
-     * <p>This method allow to create new instances without knowing
-     * exactly the type of the region. It is an application of the
-     * prototype design pattern.</p>
-     * <p>The leaf nodes of the BSP tree <em>must</em> have a
-     * {@code Boolean} attribute representing the inside status of
-     * the corresponding cell (true for inside cells, false for outside
-     * cells). In order to avoid building too many small objects, it is
-     * recommended to use the predefined constants
-     * {@code Boolean.TRUE} and {@code Boolean.FALSE}. The
-     * tree also <em>must</em> have either null internal nodes or
-     * internal nodes representing the boundary as specified in the
-     * {@link #getTree getTree} method).</p>
-     * @param newTree inside/outside BSP tree representing the new region
-     * @return the built region
-     */
-    Region<P> buildNew(BSPTree<P> newTree);
-
-    /** Copy the instance.
-     * <p>The instance created is completely independant of the original
-     * one. A deep copy is used, none of the underlying objects are
-     * shared (except for the underlying tree {@code Boolean}
-     * attributes and immutable objects).</p>
-     * @return a new region, copy of the instance
-     */
-    Region<P> copySelf();
-
-    /** Check if the instance is empty.
-     * @return true if the instance is empty
-     */
-    boolean isEmpty();
-
-    /** Check if the sub-tree starting at a given node is empty.
-     * @param node root node of the sub-tree (<em>must</em> have {@link
-     * Region Region} tree semantics, i.e. the leaf nodes must have
-     * {@code Boolean} attributes representing an inside/outside
-     * property)
-     * @return true if the sub-tree starting at the given node is empty
-     */
-    boolean isEmpty(final BSPTree<P> node);
-
-    /** Check if the instance covers the full space.
-     * @return true if the instance covers the full space
-     */
-    boolean isFull();
-
-    /** Check if the sub-tree starting at a given node covers the full space.
-     * @param node root node of the sub-tree (<em>must</em> have {@link
-     * Region Region} tree semantics, i.e. the leaf nodes must have
-     * {@code Boolean} attributes representing an inside/outside
-     * property)
-     * @return true if the sub-tree starting at the given node covers the full space
-     */
-    boolean isFull(final BSPTree<P> node);
-
-    /** Check if the instance entirely contains another region.
-     * @param region region to check against the instance
-     * @return true if the instance contains the specified tree
-     */
-    boolean contains(final Region<P> region);
-
-    /** Check a point with respect to the region.
-     * @param point point to check
-     * @return a code representing the point status: either {@link
-     * Location#INSIDE}, {@link Location#OUTSIDE} or {@link Location#BOUNDARY}
-     */
-    Location checkPoint(final P point);
-
-    /** Project a point on the boundary of the region.
-     * @param point point to check
-     * @return projection of the point on the boundary
-     */
-    BoundaryProjection<P> projectToBoundary(final P point);
-
-    /** Get the underlying BSP tree.
-
-     * <p>Regions are represented by an underlying inside/outside BSP
-     * tree whose leaf attributes are {@code Boolean} instances
-     * representing inside leaf cells if the attribute value is
-     * {@code true} and outside leaf cells if the attribute is
-     * {@code false}. These leaf attributes are always present and
-     * guaranteed to be non null.</p>
-
-     * <p>In addition to the leaf attributes, the internal nodes which
-     * correspond to cells split by cut sub-hyperplanes may contain
-     * {@link BoundaryAttribute BoundaryAttribute} objects representing
-     * the parts of the corresponding cut sub-hyperplane that belong to
-     * the boundary. When the boundary attributes have been computed,
-     * all internal nodes are guaranteed to have non-null
-     * attributes, however some {@link BoundaryAttribute
-     * BoundaryAttribute} instances may have their {@link
-     * BoundaryAttribute#getPlusInside() getPlusInside} and {@link
-     * BoundaryAttribute#getPlusOutside() getPlusOutside} methods both
-     * returning null if the corresponding cut sub-hyperplane does not
-     * have any parts belonging to the boundary.</p>
-
-     * <p>Since computing the boundary is not always required and can be
-     * time-consuming for large trees, these internal nodes attributes
-     * are computed using lazy evaluation only when required by setting
-     * the {@code includeBoundaryAttributes} argument to
-     * {@code true}. Once computed, these attributes remain in the
-     * tree, which implies that in this case, further calls to the
-     * method for the same region will always include these attributes
-     * regardless of the value of the
-     * {@code includeBoundaryAttributes} argument.</p>
-
-     * @param includeBoundaryAttributes if true, the boundary attributes
-     * at internal nodes are guaranteed to be included (they may be
-     * included even if the argument is false, if they have already been
-     * computed due to a previous call)
-     * @return underlying BSP tree
-     * @see BoundaryAttribute
-     */
-    BSPTree<P> getTree(final boolean includeBoundaryAttributes);
-
-    /** Get the size of the boundary.
-     * @return the size of the boundary (this is 0 in 1D, a length in
-     * 2D, an area in 3D ...)
-     */
-    double getBoundarySize();
-
-    /** Get the size of the instance.
-     * @return the size of the instance (this is a length in 1D, an area
-     * in 2D, a volume in 3D ...)
-     */
-    double getSize();
-
-    /** Get the barycenter of the instance.
-     * @return an object representing the barycenter
-     */
-    P getBarycenter();
-
-    /** Get the parts of a sub-hyperplane that are contained in the region.
-     * <p>The parts of the sub-hyperplane that belong to the boundary are
-     * <em>not</em> included in the resulting parts.</p>
-     * @param sub sub-hyperplane traversing the region
-     * @return filtered sub-hyperplane
-     */
-    SubHyperplane<P> intersection(final SubHyperplane<P> sub);
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/RegionFactory.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/RegionFactory.java
deleted file mode 100644
index c15676c..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/RegionFactory.java
+++ /dev/null
@@ -1,383 +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.HashMap;
-import java.util.Map;
-
-import org.apache.commons.geometry.core.Point;
-import org.apache.commons.geometry.core.partitioning.BSPTree.VanishingCutHandler;
-import org.apache.commons.geometry.core.partitioning.Region.Location;
-import org.apache.commons.geometry.core.partitioning.SubHyperplane.SplitSubHyperplane;
-
-/** This class is a factory for {@link Region}.
-
- * @param <P> Point type defining the space
- */
-public class RegionFactory<P extends Point<P>> {
-
-    /** Visitor removing internal nodes attributes. */
-    private final NodesCleaner nodeCleaner;
-
-    /** Simple constructor.
-     */
-    public RegionFactory() {
-        nodeCleaner = new NodesCleaner();
-    }
-
-    /** Build a convex region from a collection of bounding hyperplanes.
-     * @param hyperplanes collection of bounding hyperplanes
-     * @return a new convex region, or null if the collection is empty
-     */
-    @SafeVarargs
-    public final Region<P> buildConvex(final Hyperplane<P> ... hyperplanes) {
-        if ((hyperplanes == null) || (hyperplanes.length == 0)) {
-            return null;
-        }
-
-        // use the first hyperplane to build the right class
-        final Region<P> region = hyperplanes[0].wholeSpace();
-
-        // chop off parts of the space
-        BSPTree<P> node = region.getTree(false);
-        node.setAttribute(Boolean.TRUE);
-        for (final Hyperplane<P> hyperplane : hyperplanes) {
-            if (node.insertCut(hyperplane)) {
-                node.setAttribute(null);
-                node.getPlus().setAttribute(Boolean.FALSE);
-                node = node.getMinus();
-                node.setAttribute(Boolean.TRUE);
-            } else {
-                // the hyperplane could not be inserted in the current leaf node
-                // either it is completely outside (which means the input hyperplanes
-                // are wrong), or it is parallel to a previous hyperplane
-                SubHyperplane<P> s = hyperplane.wholeHyperplane();
-                for (BSPTree<P> tree = node; tree.getParent() != null && s != null; tree = tree.getParent()) {
-                    final Hyperplane<P>         other = tree.getParent().getCut().getHyperplane();
-                    final SplitSubHyperplane<P> split = s.split(other);
-                    switch (split.getSide()) {
-                        case HYPER :
-                            // the hyperplane is parallel to a previous hyperplane
-                            if (!hyperplane.sameOrientationAs(other)) {
-                                // this hyperplane is opposite to the other one,
-                                // the region is thinner than the tolerance, we consider it empty
-                                return getComplement(hyperplanes[0].wholeSpace());
-                            }
-                            // the hyperplane is an extension of an already known hyperplane, we just ignore it
-                            break;
-                        case PLUS :
-                            // the hyperplane is outside of the current convex zone,
-                            // the input hyperplanes are inconsistent
-                            throw new IllegalArgumentException("Hyperplanes do not define a convex region");
-                        default :
-                            s = split.getMinus();
-                    }
-                }
-            }
-        }
-
-        return region;
-
-    }
-
-    /** Compute the union of two regions.
-     * @param region1 first region (will be unusable after the operation as
-     * parts of it will be reused in the new region)
-     * @param region2 second region (will be unusable after the operation as
-     * parts of it will be reused in the new region)
-     * @return a new region, result of {@code region1 union region2}
-     */
-    public Region<P> union(final Region<P> region1, final Region<P> region2) {
-        final BSPTree<P> tree =
-            region1.getTree(false).merge(region2.getTree(false), new UnionMerger());
-        tree.visit(nodeCleaner);
-        return region1.buildNew(tree);
-    }
-
-    /** Compute the intersection of two regions.
-     * @param region1 first region (will be unusable after the operation as
-     * parts of it will be reused in the new region)
-     * @param region2 second region (will be unusable after the operation as
-     * parts of it will be reused in the new region)
-     * @return a new region, result of {@code region1 intersection region2}
-     */
-    public Region<P> intersection(final Region<P> region1, final Region<P> region2) {
-        final BSPTree<P> tree =
-            region1.getTree(false).merge(region2.getTree(false), new IntersectionMerger());
-        tree.visit(nodeCleaner);
-        return region1.buildNew(tree);
-    }
-
-    /** Compute the symmetric difference (exclusive or) of two regions.
-     * @param region1 first region (will be unusable after the operation as
-     * parts of it will be reused in the new region)
-     * @param region2 second region (will be unusable after the operation as
-     * parts of it will be reused in the new region)
-     * @return a new region, result of {@code region1 xor region2}
-     */
-    public Region<P> xor(final Region<P> region1, final Region<P> region2) {
-        final BSPTree<P> tree =
-            region1.getTree(false).merge(region2.getTree(false), new XorMerger());
-        tree.visit(nodeCleaner);
-        return region1.buildNew(tree);
-    }
-
-    /** Compute the difference of two regions.
-     * @param region1 first region (will be unusable after the operation as
-     * parts of it will be reused in the new region)
-     * @param region2 second region (will be unusable after the operation as
-     * parts of it will be reused in the new region)
-     * @return a new region, result of {@code region1 minus region2}
-     */
-    public Region<P> difference(final Region<P> region1, final Region<P> region2) {
-        final BSPTree<P> tree =
-            region1.getTree(false).merge(region2.getTree(false), new DifferenceMerger(region1, region2));
-        tree.visit(nodeCleaner);
-        return region1.buildNew(tree);
-    }
-
-    /** Get the complement of the region (exchanged interior/exterior).
-     * @param region region to complement, it will not modified, a new
-     * region independent region will be built
-     * @return a new region, complement of the specified one
-     */
-    /** Get the complement of the region (exchanged interior/exterior).
-     * @param region region to complement, it will not modified, a new
-     * region independent region will be built
-     * @return a new region, complement of the specified one
-     */
-    public Region<P> getComplement(final Region<P> region) {
-        return region.buildNew(recurseComplement(region.getTree(false)));
-    }
-
-    /** Recursively build the complement of a BSP tree.
-     * @param node current node of the original tree
-     * @return new tree, complement of the node
-     */
-    private BSPTree<P> recurseComplement(final BSPTree<P> node) {
-
-        // transform the tree, except for boundary attribute splitters
-        final Map<BSPTree<P>, BSPTree<P>> map = new HashMap<>();
-        final BSPTree<P> transformedTree = recurseComplement(node, map);
-
-        // set up the boundary attributes splitters
-        for (final Map.Entry<BSPTree<P>, BSPTree<P>> entry : map.entrySet()) {
-            if (entry.getKey().getCut() != null) {
-                @SuppressWarnings("unchecked")
-                BoundaryAttribute<P> original = (BoundaryAttribute<P>) entry.getKey().getAttribute();
-                if (original != null) {
-                    @SuppressWarnings("unchecked")
-                    BoundaryAttribute<P> transformed = (BoundaryAttribute<P>) entry.getValue().getAttribute();
-                    for (final BSPTree<P> splitter : original.getSplitters()) {
-                        transformed.getSplitters().add(map.get(splitter));
-                    }
-                }
-            }
-        }
-
-        return transformedTree;
-
-    }
-
-    /** Recursively build the complement of a BSP tree.
-     * @param node current node of the original tree
-     * @param map transformed nodes map
-     * @return new tree, complement of the node
-     */
-    private BSPTree<P> recurseComplement(final BSPTree<P> node,
-                                         final Map<BSPTree<P>, BSPTree<P>> map) {
-
-        final BSPTree<P> transformedNode;
-        if (node.getCut() == null) {
-            transformedNode = new BSPTree<>(((Boolean) node.getAttribute()) ? Boolean.FALSE : Boolean.TRUE);
-        } else {
-
-            @SuppressWarnings("unchecked")
-            BoundaryAttribute<P> attribute = (BoundaryAttribute<P>) node.getAttribute();
-            if (attribute != null) {
-                final SubHyperplane<P> plusOutside =
-                        (attribute.getPlusInside() == null) ? null : attribute.getPlusInside().copySelf();
-                final SubHyperplane<P> plusInside  =
-                        (attribute.getPlusOutside() == null) ? null : attribute.getPlusOutside().copySelf();
-                // we start with an empty list of splitters, it will be filled in out of recursion
-                attribute = new BoundaryAttribute<>(plusOutside, plusInside, new NodesSet<P>());
-            }
-
-            transformedNode = new BSPTree<>(node.getCut().copySelf(),
-                                             recurseComplement(node.getPlus(),  map),
-                                             recurseComplement(node.getMinus(), map),
-                                             attribute);
-        }
-
-        map.put(node, transformedNode);
-        return transformedNode;
-
-    }
-
-    /** BSP tree leaf merger computing union of two regions. */
-    private class UnionMerger implements BSPTree.LeafMerger<P> {
-        /** {@inheritDoc} */
-        @Override
-        public BSPTree<P> merge(final BSPTree<P> leaf, final BSPTree<P> tree,
-                                final BSPTree<P> parentTree,
-                                final boolean isPlusChild, final boolean leafFromInstance) {
-            if ((Boolean) leaf.getAttribute()) {
-                // the leaf node represents an inside cell
-                leaf.insertInTree(parentTree, isPlusChild, new VanishingToLeaf(true));
-                return leaf;
-            }
-            // the leaf node represents an outside cell
-            tree.insertInTree(parentTree, isPlusChild, new VanishingToLeaf(false));
-            return tree;
-        }
-    }
-
-    /** BSP tree leaf merger computing intersection of two regions. */
-    private class IntersectionMerger implements BSPTree.LeafMerger<P> {
-        /** {@inheritDoc} */
-        @Override
-        public BSPTree<P> merge(final BSPTree<P> leaf, final BSPTree<P> tree,
-                                final BSPTree<P> parentTree,
-                                final boolean isPlusChild, final boolean leafFromInstance) {
-            if ((Boolean) leaf.getAttribute()) {
-                // the leaf node represents an inside cell
-                tree.insertInTree(parentTree, isPlusChild, new VanishingToLeaf(true));
-                return tree;
-            }
-            // the leaf node represents an outside cell
-            leaf.insertInTree(parentTree, isPlusChild, new VanishingToLeaf(false));
-            return leaf;
-        }
-    }
-
-    /** BSP tree leaf merger computing symmetric difference (exclusive or) of two regions. */
-    private class XorMerger implements BSPTree.LeafMerger<P> {
-        /** {@inheritDoc} */
-        @Override
-        public BSPTree<P> merge(final BSPTree<P> leaf, final BSPTree<P> tree,
-                                final BSPTree<P> parentTree, final boolean isPlusChild,
-                                final boolean leafFromInstance) {
-            BSPTree<P> t = tree;
-            if ((Boolean) leaf.getAttribute()) {
-                // the leaf node represents an inside cell
-                t = recurseComplement(t);
-            }
-            t.insertInTree(parentTree, isPlusChild, new VanishingToLeaf(true));
-            return t;
-        }
-    }
-
-    /** BSP tree leaf merger computing difference of two regions. */
-    private class DifferenceMerger implements BSPTree.LeafMerger<P>, VanishingCutHandler<P> {
-
-        /** Region to subtract from. */
-        private final Region<P> region1;
-
-        /** Region to subtract. */
-        private final Region<P> region2;
-
-        /** Simple constructor.
-         * @param region1 region to subtract from
-         * @param region2 region to subtract
-         */
-        DifferenceMerger(final Region<P> region1, final Region<P> region2) {
-            this.region1 = region1.copySelf();
-            this.region2 = region2.copySelf();
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public BSPTree<P> merge(final BSPTree<P> leaf, final BSPTree<P> tree,
-                                final BSPTree<P> parentTree, final boolean isPlusChild,
-                                final boolean leafFromInstance) {
-            if ((Boolean) leaf.getAttribute()) {
-                // the leaf node represents an inside cell
-                final BSPTree<P> argTree =
-                    recurseComplement(leafFromInstance ? tree : leaf);
-                argTree.insertInTree(parentTree, isPlusChild, this);
-                return argTree;
-            }
-            // the leaf node represents an outside cell
-            final BSPTree<P> instanceTree =
-                leafFromInstance ? leaf : tree;
-            instanceTree.insertInTree(parentTree, isPlusChild, this);
-            return instanceTree;
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public BSPTree<P> fixNode(final BSPTree<P> node) {
-            // get a representative point in the degenerate cell
-            final BSPTree<P> cell = node.pruneAroundConvexCell(Boolean.TRUE, Boolean.FALSE, null);
-            final Region<P> r = region1.buildNew(cell);
-            final P p = r.getBarycenter();
-            return new BSPTree<>(region1.checkPoint(p) == Location.INSIDE &&
-                                  region2.checkPoint(p) == Location.OUTSIDE);
-        }
-
-    }
-
-    /** Visitor removing internal nodes attributes. */
-    private class NodesCleaner implements  BSPTreeVisitor<P> {
-
-        /** {@inheritDoc} */
-        @Override
-        public Order visitOrder(final BSPTree<P> node) {
-            return Order.PLUS_SUB_MINUS;
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public void visitInternalNode(final BSPTree<P> node) {
-            node.setAttribute(null);
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public void visitLeafNode(final BSPTree<P> node) {
-        }
-
-    }
-
-    /** Handler replacing nodes with vanishing cuts with leaf nodes. */
-    private class VanishingToLeaf implements VanishingCutHandler<P> {
-
-        /** Inside/outside indocator to use for ambiguous nodes. */
-        private final boolean inside;
-
-        /** Simple constructor.
-         * @param inside inside/outside indicator to use for ambiguous nodes
-         */
-        VanishingToLeaf(final boolean inside) {
-            this.inside = inside;
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public BSPTree<P> fixNode(final BSPTree<P> node) {
-            if (node.getPlus().getAttribute().equals(node.getMinus().getAttribute())) {
-                // no ambiguity
-                return new BSPTree<>(node.getPlus().getAttribute());
-            } else {
-                // ambiguous node
-                return new BSPTree<>(inside);
-            }
-        }
-
-    }
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Split.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Split.java
new file mode 100644
index 0000000..20307e3
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Split.java
@@ -0,0 +1,97 @@
+/*
+ * 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;
+
+/** Class containing the result of splitting an object with a hyperplane.
+ * @param <T> Split type
+ */
+public final class Split<T> {
+
+    /** Part of the object lying on the minus side of the splitting hyperplane.
+     */
+    private final T minus;
+
+    /** Part of the object lying on the plus side of the splitting hyperplane.
+     */
+    private final T plus;
+
+    /** Build a new instance from its parts.
+     * @param minus part of the object lying on the minus side of the
+     *      splitting hyperplane or null if no such part exists
+     * @param plus part of the object lying on the plus side of the
+     *      splitting hyperplane or null if no such part exists.
+     */
+    public Split(final T minus, final T plus) {
+        this.minus = minus;
+        this.plus = plus;
+    }
+
+    /** Get the part of the object lying on the minus side of the splitting
+     * hyperplane or null if no such part exists.
+     * @return part of the object lying on the minus side of the splitting
+     *      hyperplane
+     */
+    public T getMinus() {
+        return minus;
+    }
+
+    /** Get the part of the object lying on the plus side of the splitting
+     * hyperplane or null if no such part exists.
+     * @return part of the object lying on the plus side of the splitting
+     *      hyperplane
+     */
+    public T getPlus() {
+        return plus;
+    }
+
+    /** Get the location of the object with respect to its splitting
+     * hyperplane.
+     * @return
+     *  <ul>
+     *      <li>{@link SplitLocation#PLUS} - if only {@link #getPlus()} is not null</li>
+     *      <li>{@link SplitLocation#MINUS} - if only {@link #getMinus()} is not null</li>
+     *      <li>{@link SplitLocation#BOTH} - if both {@link #getPlus()} and {@link #getMinus()}
+     *          are not null</li>
+     *      <li>{@link SplitLocation#NEITHER} - if both {@link #getPlus()} and {@link #getMinus()}
+     *          are null</li>
+     *  </ul>
+     */
+    public SplitLocation getLocation() {
+        if (minus != null) {
+            return plus != null ? SplitLocation.BOTH : SplitLocation.MINUS;
+        } else if (plus != null) {
+            return SplitLocation.PLUS;
+        }
+        return SplitLocation.NEITHER;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(this.getClass().getSimpleName())
+            .append("[location= ")
+            .append(getLocation())
+            .append(", minus= ")
+            .append(minus)
+            .append(", plus= ")
+            .append(plus)
+            .append(']');
+
+        return sb.toString();
+    }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SplitLocation.java
similarity index 53%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
copy to commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SplitLocation.java
index 046defe..cbf2445 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SplitLocation.java
@@ -16,21 +16,30 @@
  */
 package org.apache.commons.geometry.core.partitioning;
 
-/** Enumerate representing the location of an element with respect to an
- * {@link Hyperplane hyperplane} of a space.
+/** Enumeration representing the location of a split object with respect
+ * to its splitting {@link Hyperplane hyperplane}.
  */
-public enum Side {
+public enum SplitLocation {
 
-    /** Code for the plus side of the hyperplane. */
+    /** Value indicating that the split object lies entirely on the
+     * plus side of the splitting hyperplane.
+     */
     PLUS,
 
-    /** Code for the minus side of the hyperplane. */
+    /** Value indicating that the split object lies entirely on the
+     * minus side of the splitting hyperplane.
+     */
     MINUS,
 
-    /** Code for elements crossing the hyperplane from plus to minus side. */
+    /** Value indicating that the split object lies in both the plus
+     * and minus sides of the splitting hyperplane.
+     */
     BOTH,
 
-    /** Code for the hyperplane itself. */
-    HYPER;
-
+    /** Value indicating that the split object lies neither on the plus
+     * or minus sides of the splitting hyperplane. This is the case when
+     * the object lies entirely on the hyperplane or is empty (and
+     * therefore "lies" nowhere).
+     */
+    NEITHER;
 }
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Splittable.java
similarity index 64%
copy from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
copy to commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Splittable.java
index 046defe..60cef06 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Splittable.java
@@ -16,21 +16,17 @@
  */
 package org.apache.commons.geometry.core.partitioning;
 
-/** Enumerate representing the location of an element with respect to an
- * {@link Hyperplane hyperplane} of a space.
- */
-public enum Side {
-
-    /** Code for the plus side of the hyperplane. */
-    PLUS,
-
-    /** Code for the minus side of the hyperplane. */
-    MINUS,
+import org.apache.commons.geometry.core.Point;
 
-    /** Code for elements crossing the hyperplane from plus to minus side. */
-    BOTH,
-
-    /** Code for the hyperplane itself. */
-    HYPER;
+/** Interface representing objects that can be split by hyperplanes.
+ * @param <P> Point implementation type
+ * @param <S> Split type
+ */
+public interface Splittable<P extends Point<P>, S extends Splittable<P, S>> {
 
+    /** Split this instance with the given hyperplane.
+     * @param splitter the hyperplane to split this object with.
+     * @return result of the split operation
+     */
+    Split<? extends S> split(Hyperplane<P> splitter);
 }
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SubHyperplane.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SubHyperplane.java
index 7237bb7..887f2a8 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SubHyperplane.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SubHyperplane.java
@@ -16,127 +16,129 @@
  */
 package org.apache.commons.geometry.core.partitioning;
 
-import org.apache.commons.geometry.core.Point;
-
-/** This interface represents the remaining parts of an hyperplane after
- * other parts have been chopped off.
+import java.util.List;
 
- * <p>sub-hyperplanes are obtained when parts of an {@link
- * Hyperplane hyperplane} are chopped off by other hyperplanes that
- * intersect it. The remaining part is a convex region. Such objects
- * appear in {@link BSPTree BSP trees} as the intersection of a cut
- * hyperplane with the convex region which it splits, the chopping
- * hyperplanes are the cut hyperplanes closer to the tree root.</p>
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.Transform;
 
- * <p>
- * Note that this interface is <em>not</em> intended to be implemented
- * by Apache Commons Math users, it is only intended to be implemented
- * within the library itself. New methods may be added even for minor
- * versions, which breaks compatibility for external implementations.
- * </p>
+/** Interface representing subhyperplanes, which are regions
+ * embedded in a hyperplane.
 
- * @param <P> Point type defining the embedding space.
+ * @param <P> Point implementation type
  */
-public interface SubHyperplane<P extends Point<P>> {
-
-    /** Copy the instance.
-     * <p>The instance created is completely independent of the original
-     * one. A deep copy is used, none of the underlying objects are
-     * shared (except for the nodes attributes and immutable
-     * objects).</p>
-     * @return a new sub-hyperplane, copy of the instance
-     */
-    SubHyperplane<P> copySelf();
+public interface SubHyperplane<P extends Point<P>> extends Splittable<P, SubHyperplane<P>> {
 
-    /** Get the underlying hyperplane.
-     * @return underlying hyperplane
+    /** Get the hyperplane that this instance is embedded in.
+     * @return the hyperplane that this instance is embedded in.
      */
     Hyperplane<P> getHyperplane();
 
-    /** Check if the instance is empty.
-     * @return true if the instance is empty
+    /** Return true if this instance contains all points in the
+     * hyperplane.
+     * @return true if this instance contains all points in the
+     *      hyperplane
+     */
+    boolean isFull();
+
+    /** Return true if this instance does not contain any points.
+     * @return true if this instance does not contain any points
      */
     boolean isEmpty();
 
-    /** Get the size of the instance.
-     * @return the size of the instance (this is a length in 1D, an area
-     * in 2D, a volume in 3D ...)
+    /** Return true if this instance has infinite size.
+     * @return true if this instance has infinite size
+     */
+    boolean isInfinite();
+
+    /** Return true if this instance has finite size.
+     * @return true if this instance has finite size
+     */
+    boolean isFinite();
+
+    /** Return the size of this instance. This will have different
+     * meanings in different spaces and dimensions. For example, in
+     * Euclidean space, this will be length in 2D and area in 3D.
+     * @return the size of this instance
      */
     double getSize();
 
-    /** Split the instance in two parts by an hyperplane.
-     * @param hyperplane splitting hyperplane
-     * @return an object containing both the part of the instance
-     * on the plus side of the hyperplane and the part of the
-     * instance on the minus side of the hyperplane
+    /** Classify a point with respect to the subhyperplane's region. The point is
+     * classified as follows:
+     * <ul>
+     *  <li>{@link RegionLocation#INSIDE INSIDE} - The point lies on the hyperplane
+     *      and inside of the subhyperplane's region.</li>
+     *  <li>{@link RegionLocation#BOUNDARY BOUNDARY} - The point lies on the hyperplane
+     *      and is on the boundary of the subhyperplane's region.</li>
+     *  <li>{@link RegionLocation#OUTSIDE OUTSIDE} - The point does not lie on
+     *      the hyperplane or it does lie on the hyperplane but is outside of the
+     *      subhyperplane's region.</li>
+     * </ul>
+     * @param point the point to classify
+     * @return classification of the point with respect to the subhyperplane's hyperplane
+     *      and region
      */
-    SplitSubHyperplane<P> split(Hyperplane<P> hyperplane);
+    RegionLocation classify(P point);
 
-    /** Compute the union of the instance and another sub-hyperplane.
-     * @param other other sub-hyperplane to union (<em>must</em> be in the
-     * same hyperplane as the instance)
-     * @return a new sub-hyperplane, union of the instance and other
+    /** Return true if the subhyperplane contains the given point, meaning that the point
+     * lies on the hyperplane and is not on the outside of the subhyperplane's region.
+     * @param point the point to check
+     * @return true if the point is contained in the subhyperplane
      */
-    SubHyperplane<P> reunite(SubHyperplane<P> other);
+    default boolean contains(P point) {
+        final RegionLocation loc = classify(point);
+        return loc != null && loc != RegionLocation.OUTSIDE;
+    }
 
-    /** Class holding the results of the {@link #split split} method.
-     * @param <U> Type of the embedding space.
+    /** Return the closest point to the argument that is contained in the subhyperplane
+     * (ie, not classified as {@link RegionLocation#OUTSIDE outside}), or null if no
+     * such point exists.
+     * @param point the reference point
+     * @return the closest point to the reference point that is contained in the subhyperplane,
+     *      or null if no such point exists
      */
-    class SplitSubHyperplane<U extends Point<U>> {
+    P closest(P point);
 
-        /** Part of the sub-hyperplane on the plus side of the splitting hyperplane. */
-        private final SubHyperplane<U> plus;
+    /** Return a {@link Builder} instance for joining multiple
+     * subhyperplanes together.
+     * @return a new builder instance
+     */
+    Builder<P> builder();
 
-        /** Part of the sub-hyperplane on the minus side of the splitting hyperplane. */
-        private final SubHyperplane<U> minus;
+    /** Return a new subhyperplane instance resulting from the application
+     * of the given transform. The current instance is not modified.
+     * @param transform the transform instance to apply
+     * @return new transformed subhyperplane instance
+     */
+    SubHyperplane<P> transform(Transform<P> transform);
 
-        /** Build a SplitSubHyperplane from its parts.
-         * @param plus part of the sub-hyperplane on the plus side of the
-         * splitting hyperplane
-         * @param minus part of the sub-hyperplane on the minus side of the
-         * splitting hyperplane
-         */
-        public SplitSubHyperplane(final SubHyperplane<U> plus,
-                                  final SubHyperplane<U> minus) {
-            this.plus  = plus;
-            this.minus = minus;
-        }
-
-        /** Get the part of the sub-hyperplane on the plus side of the splitting hyperplane.
-         * @return part of the sub-hyperplane on the plus side of the splitting hyperplane
-         */
-        public SubHyperplane<U> getPlus() {
-            return plus;
-        }
+    /** Convert this instance into a list of convex child subhyperplanes.
+     * @return a list of convex subhyperplanes representing the same subspace
+     *      region as this instance
+     */
+    List<? extends ConvexSubHyperplane<P>> toConvex();
 
-        /** Get the part of the sub-hyperplane on the minus side of the splitting hyperplane.
-         * @return part of the sub-hyperplane on the minus side of the splitting hyperplane
+    /** Interface for joining multiple {@link SubHyperplane}s into a single
+     * instance.
+     * @param <P> Point implementation type
+     */
+    interface Builder<P extends Point<P>> {
+
+        /** Add a {@link SubHyperplane} instance to the builder.
+         * @param sub subhyperplane to add to this instance
          */
-        public SubHyperplane<U> getMinus() {
-            return minus;
-        }
-
-        /** Get the side of the split sub-hyperplane with respect to its splitter.
-         * @return {@link Side#PLUS} if only {@link #getPlus()} is neither null nor empty,
-         * {@link Side#MINUS} if only {@link #getMinus()} is neither null nor empty,
-         * {@link Side#BOTH} if both {@link #getPlus()} and {@link #getMinus()}
-         * are neither null nor empty or {@link Side#HYPER} if both {@link #getPlus()} and
-         * {@link #getMinus()} are either null or empty
+        void add(SubHyperplane<P> sub);
+
+        /** Add a {@link ConvexSubHyperplane} instance to the builder.
+         * @param sub convex subhyperplane to add to this instance
          */
-        public Side getSide() {
-            if (plus != null && !plus.isEmpty()) {
-                if (minus != null && !minus.isEmpty()) {
-                    return Side.BOTH;
-                } else {
-                    return Side.PLUS;
-                }
-            } else if (minus != null && !minus.isEmpty()) {
-                return Side.MINUS;
-            } else {
-                return Side.HYPER;
-            }
-        }
+        void add(ConvexSubHyperplane<P> sub);
 
+        /** Get a {@link SubHyperplane} representing the union
+         * of all input subhyperplanes.
+         * @return subhyperplane representing the union of all input
+         *      subhyperplanes
+         */
+        SubHyperplane<P> build();
     }
-
 }
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Transform.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Transform.java
deleted file mode 100644
index 53cb056..0000000
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Transform.java
+++ /dev/null
@@ -1,77 +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;
-
-
-/** This interface represents an inversible affine transform in a space.
- * <p>Inversible affine transform include for example scalings,
- * translations, rotations.</p>
-
- * <p>Transforms are dimension-specific. The consistency rules between
- * the three {@code apply} methods are the following ones for a
- * transformed defined for dimension D:</p>
- * <ul>
- *   <li>
- *     the transform can be applied to a point in the
- *     D-dimension space using its {@link #apply(Point)}
- *     method
- *   </li>
- *   <li>
- *     the transform can be applied to a (D-1)-dimension
- *     hyperplane in the D-dimension space using its
- *     {@link #apply(Hyperplane)} method
- *   </li>
- *   <li>
- *     the transform can be applied to a (D-2)-dimension
- *     sub-hyperplane in a (D-1)-dimension hyperplane using
- *     its {@link #apply(SubHyperplane, Hyperplane, Hyperplane)}
- *     method
- *   </li>
- * </ul>
-
- * @param <P> Point type defining the embedding space.
- * @param <S> Point type defining the embedded sub-space.
- */
-public interface Transform<P extends Point<P>, S extends Point<S>> {
-
-    /** Transform a point of a space.
-     * @param point point to transform
-     * @return a new object representing the transformed point
-     */
-    P apply(P point);
-
-    /** Transform an hyperplane of a space.
-     * @param hyperplane hyperplane to transform
-     * @return a new object representing the transformed hyperplane
-     */
-    Hyperplane<P> apply(Hyperplane<P> hyperplane);
-
-    /** Transform a sub-hyperplane embedded in an hyperplane.
-     * @param sub sub-hyperplane to transform
-     * @param original hyperplane in which the sub-hyperplane is
-     * defined (this is the original hyperplane, the transform has
-     * <em>not</em> been applied to it)
-     * @param transformed hyperplane in which the sub-hyperplane is
-     * defined (this is the transformed hyperplane, the transform
-     * <em>has</em> been applied to it)
-     * @return a new object representing the transformed sub-hyperplane
-     */
-    SubHyperplane<S> apply(SubHyperplane<S> sub, Hyperplane<P> original, Hyperplane<P> transformed);
-
-}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTree.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTree.java
new file mode 100644
index 0000000..157eb16
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTree.java
@@ -0,0 +1,1108 @@
+/*
+ * 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.bsp;
+
+import java.io.Serializable;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.NoSuchElementException;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+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.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.Order;
+
+/** Abstract class for Binary Space Partitioning (BSP) tree implementations.
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+public abstract class AbstractBSPTree<P extends Point<P>, N extends AbstractBSPTree.AbstractNode<P, N>>
+    implements BSPTree<P, N>, Serializable {
+
+    /** Serializable UID. */
+    private static final long serialVersionUID = 20190330L;
+
+    /** The default number of levels to print when creating a string representation of the tree. */
+    private static final int DEFAULT_TREE_STRING_MAX_DEPTH = 8;
+
+    /** Integer value set on various node fields when a value is unknown. */
+    private static final int UNKNOWN_VALUE = -1;
+
+    /** The root node for the tree. */
+    private N root;
+
+    /** The current modification version for the tree structure. This is incremented each time
+     * a structural change occurs in the tree and is used to determine when cached values
+     * must be recomputed.
+     */
+    private int version = 0;
+
+    /** {@inheritDoc} */
+    @Override
+    public N getRoot() {
+        if (root == null) {
+            setRoot(createNode());
+        }
+        return root;
+    }
+
+    /** Set the root node for the tree. Cached tree properties are invalidated
+     * with {@link #invalidate()}.
+     * @param root new root node for the tree
+     */
+    protected void setRoot(final N root) {
+        this.root = root;
+
+        this.root.makeRoot();
+
+        invalidate();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int count() {
+        return getRoot().count();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int height() {
+        return getRoot().height();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void accept(final BSPTreeVisitor<P, N> visitor) {
+        acceptVisitor(getRoot(), visitor);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public N findNode(final P pt, final NodeCutRule cutBehavior) {
+        return findNode(getRoot(), pt, cutBehavior);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void insert(final SubHyperplane<P> sub) {
+        insert(sub.toConvex());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void insert(final ConvexSubHyperplane<P> convexSub) {
+        insertRecursive(getRoot(), convexSub,
+                convexSub.getHyperplane().span());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void insert(final Iterable<? extends ConvexSubHyperplane<P>> convexSubs) {
+        for (final ConvexSubHyperplane<P> convexSub : convexSubs) {
+            insert(convexSub);
+        }
+    }
+
+    /** Return an iterator over the nodes in the tree. */
+    @Override
+    public Iterator<N> iterator() {
+        return new NodeIterator<>(getRoot());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void copy(final BSPTree<P, N> src) {
+        copySubtree(src.getRoot(), getRoot());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void extract(final N node) {
+        // copy downward
+        final N extracted = importSubtree(node);
+
+        // extract upward
+        final N newRoot = extractParentPath(node, extracted);
+
+        // set the root of this tree
+        setRoot(newRoot);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void transform(final Transform<P> transform) {
+        final boolean swapChildren = swapsInsideOutside(transform);
+        transformRecursive(getRoot(), transform, swapChildren);
+
+        invalidate();
+    }
+
+    /** Get a simple string representation of the tree structure. The returned string contains
+     * the tree structure down to the default max depth of {@value #DEFAULT_TREE_STRING_MAX_DEPTH}.
+     * @return a string representation of the tree
+     */
+    public String treeString() {
+        return treeString(DEFAULT_TREE_STRING_MAX_DEPTH);
+    }
+
+    /** Get a simple string representation of the tree structure. The returned string contains
+     * the tree structure down to {@code maxDepth}.
+     * @param maxDepth the maximum depth in the tree to print; nodes below this depth are skipped
+     * @return a string representation of the tree
+     */
+    public String treeString(final int maxDepth) {
+        BSPTreePrinter<P, N> printer = new BSPTreePrinter<>(maxDepth);
+        accept(printer);
+
+        return printer.toString();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return new StringBuilder()
+                .append(getClass().getSimpleName())
+                .append("[count= ")
+                .append(count())
+                .append(", height= ")
+                .append(height())
+                .append("]")
+                .toString();
+    }
+
+    /** Create a new node for this tree.
+     * @return a new node for this tree
+     */
+    protected abstract N createNode();
+
+    /** Copy non-structural node properties from {@code src} to {@code dst}.
+     * Non-structural properties are those properties not directly related
+     * to the structure of the BSP tree, i.e. properties other than parent/child
+     * connections and cut subhyperplanes. Subclasses should override this method
+     * when additional properties are stored on nodes.
+     * @param src source node
+     * @param dst destination node
+     */
+    protected void copyNodeProperties(final N src, final N dst) {
+        // no-op
+    }
+
+    /** Method called to initialize a new child node. Subclasses can use this method to
+     * set initial attributes on the node.
+     * @param parent the parent node
+     * @param child the new child node
+     * @param isPlus true if the child will be assigned as the parent's plus child;
+     *      false if it will be the parent's minus child
+     */
+    protected void initChildNode(final N parent, final N child, final boolean isPlus) {
+        // no-op
+    }
+
+    /** Create a non-structural copy of the given node. Properties such as parent/child
+     * connections and cut subhyperplanes are <em>not</em> copied.
+     * @param src the node to copy; does not need to belong to the current tree
+     * @return the copied node
+     * @see AbstractBSPTree#copyNodeProperties(AbstractNode, AbstractNode)
+     */
+    protected N copyNode(final N src) {
+        final N copy = createNode();
+        copyNodeProperties(src, copy);
+
+        return copy;
+    }
+
+    /** Recursively copy a subtree. The returned node is not attached to the current tree.
+     * Structural <em>and</em> non-structural properties are copied from the source subtree
+     * to the destination subtree. This method does nothing if {@code src} and {@code dst}
+     * reference the same node.
+     * @param src the node representing the source subtree; does not need to belong to the
+     *      current tree
+     * @param dst the node representing the destination subtree
+     * @return the copied node, ie {@code dst}
+     */
+    protected N copySubtree(final N src, final N dst) {
+        // only copy if we're actually switching nodes
+        if (src != dst) {
+            // copy non-structural properties
+            copyNodeProperties(src, dst);
+
+            // copy the subtree structure
+            ConvexSubHyperplane<P> cut = null;
+            N minus = null;
+            N plus = null;
+
+            if (!src.isLeaf()) {
+                final AbstractBSPTree<P, N> dstTree = dst.getTree();
+
+                cut = src.getCut();
+                minus = copySubtree(src.getMinus(), dstTree.createNode());
+                plus = copySubtree(src.getPlus(), dstTree.createNode());
+            }
+
+            dst.setSubtree(cut, minus, plus);
+        }
+
+        return dst;
+    }
+
+    /** Import the subtree represented by the given node into this tree. If the given node
+     * already belongs to this tree, then the node is returned directly without modification.
+     * If the node does <em>not</em> belong to this tree, a new node is created and the src node
+     * subtree is copied into it.
+     *
+     * <p>This method does not modify the current structure of the tree.</p>
+     * @param src node to import
+     * @return the given node if it belongs to this tree, otherwise a new node containing
+     *      a copy of the given node's subtree
+     * @see #copySubtree(AbstractNode, AbstractNode)
+     */
+    protected N importSubtree(final N src) {
+        // create a copy of the node if it's not already in this tree
+        if (src.getTree() != this) {
+            return copySubtree(src, createNode());
+        }
+
+        return src;
+    }
+
+    /** Extract the path from {@code src} to the root of its tree and
+     * set it as the parent path of {@code dst}. Leaf nodes created during
+     * the extraction are given the same node properties as their counterparts
+     * in the source tree but without the cuts and child nodes. The properties
+     * of {@code dst} are not modified, with the exception of its parent node
+     * reference.
+     * @param src the source node to copy the parent path from
+     * @param dst the destination node to place under the extracted path
+     * @return the root node of the extracted path
+     */
+    protected N extractParentPath(final N src, final N dst) {
+        N dstParent = dst;
+        N dstChild;
+
+        N srcChild = src;
+        N srcParent = srcChild.getParent();
+
+        while (srcParent != null) {
+            dstChild = dstParent;
+            dstParent = copyNode(srcParent);
+
+            if (srcChild.isMinus()) {
+                dstParent.setSubtree(
+                        srcParent.getCut(),
+                        dstChild,
+                        copyNode(srcParent.getPlus()));
+            } else {
+                dstParent.setSubtree(
+                        srcParent.getCut(),
+                        copyNode(srcParent.getMinus()),
+                        dstChild);
+            }
+
+            srcChild = srcParent;
+            srcParent = srcChild.getParent();
+        }
+
+        return dstParent;
+    }
+
+    /** Find the smallest node in the tree containing the point, starting
+     * at the given node.
+     * @param start the node to begin the search with
+     * @param pt the point to check
+     * @param cutBehavior value determining the search behavior when the test point
+     *      lies directly on the cut subhyperplane of an internal node
+     * @return the smallest node in the tree containing the point
+     */
+    protected N findNode(final N start, final P pt, final NodeCutRule cutBehavior) {
+        final Hyperplane<P> cutHyper = start.getCutHyperplane();
+        if (cutHyper != null) {
+            final HyperplaneLocation cutLoc = cutHyper.classify(pt);
+
+            final boolean onPlusSide = cutLoc == HyperplaneLocation.PLUS;
+            final boolean onMinusSide = cutLoc == HyperplaneLocation.MINUS;
+            final boolean onCut = !onPlusSide && !onMinusSide;
+
+            if (onMinusSide || (onCut && cutBehavior == NodeCutRule.MINUS)) {
+                return findNode(start.getMinus(), pt, cutBehavior);
+            } else if (onPlusSide || (onCut && cutBehavior == NodeCutRule.PLUS)) {
+                return findNode(start.getPlus(), pt, cutBehavior);
+            }
+        }
+        return start;
+    }
+
+    /** Visit the nodes in a subtree.
+     * @param node the node to begin the visit process
+     * @param visitor the visitor to pass nodes to
+     */
+    protected void acceptVisitor(final N node, BSPTreeVisitor<P, N> visitor) {
+        if (node.isLeaf()) {
+            visitor.visit(node);
+        } else {
+            final Order order = visitor.visitOrder(node);
+
+            if (order != null) {
+
+                switch (order) {
+                case PLUS_MINUS_NODE:
+                    acceptVisitor(node.getPlus(), visitor);
+                    acceptVisitor(node.getMinus(), visitor);
+                    visitor.visit(node);
+                    break;
+                case PLUS_NODE_MINUS:
+                    acceptVisitor(node.getPlus(), visitor);
+                    visitor.visit(node);
+                    acceptVisitor(node.getMinus(), visitor);
+                    break;
+                case MINUS_PLUS_NODE:
+                    acceptVisitor(node.getMinus(), visitor);
+                    acceptVisitor(node.getPlus(), visitor);
+                    visitor.visit(node);
+                    break;
+                case MINUS_NODE_PLUS:
+                    acceptVisitor(node.getMinus(), visitor);
+                    visitor.visit(node);
+                    acceptVisitor(node.getPlus(), visitor);
+                    break;
+                case NODE_PLUS_MINUS:
+                    visitor.visit(node);
+                    acceptVisitor(node.getPlus(), visitor);
+                    acceptVisitor(node.getMinus(), visitor);
+                    break;
+                default: // NODE_MINUS_PLUS:
+                    visitor.visit(node);
+                    acceptVisitor(node.getMinus(), visitor);
+                    acceptVisitor(node.getPlus(), visitor);
+                    break;
+                }
+            }
+        }
+    }
+
+    /** Cut a node with a hyperplane. The algorithm proceeds are follows:
+     * <ol>
+     *      <li>The hyperplane is trimmed by splitting it with each cut hyperplane on the
+     *      path from the given node to the root of the tree.</li>
+     *      <li>If the remaining portion of the hyperplane is <em>not</em> empty, then
+     *          <ul>
+     *              <li>the remaining portion becomes the cut subhyperplane for the node,</li>
+     *              <li>two new child nodes are created and initialized with
+     *              {@link #initChildNode(AbstractNode, AbstractNode, boolean)}, and</li>
+     *              <li>true is returned.</li>
+     *          </ul>
+ *          </li>
+     *      <li>If the remaining portion of the hyperplane <em>is</em> empty (ie, the
+     *      cutting hyperplane does not intersect the node's region), then
+     *          <ul>
+     *              <li>the node is converted to a leaf node (meaning that previous
+     *              child nodes are lost), and</li>
+     *              <li>false is returned.</li>
+     *          </ul>
+     *      </li>
+     * </ol>
+     *
+     * <p>It is important to note that since this method uses the path from given node
+     * to the tree root, it must only be used on nodes that are already inserted into
+     * the tree.</p>
+     *
+     * <p>This method always calls {@link #invalidate()} to invalidate cached tree properties.</p>
+     *
+     * @param node the node to cut
+     * @param cutter the hyperplane to cut the node with
+     * @return true if the node was cut and two new child nodes were created;
+     *      otherwise false
+     * @see #trimToNode(AbstractNode, ConvexSubHyperplane)
+     * @see #cutNode(AbstractNode, ConvexSubHyperplane)
+     * @see #invalidate()
+     */
+    protected boolean insertNodeCut(final N node, final Hyperplane<P> cutter) {
+        // cut the hyperplane using all hyperplanes from this node up
+        // to the root
+        final ConvexSubHyperplane<P> cut = trimToNode(node, cutter.span());
+        if (cut == null || cut.isEmpty()) {
+            // insertion failed; the node was not cut
+            cutNode(node, null);
+            return false;
+        }
+
+        cutNode(node, cut);
+        return true;
+    }
+
+    /** Trim the given subhyperplane to the region defined by the given node. This method cuts the
+     * subhyperplane with the cut hyperplanes (binary partitioners) of all parent nodes up to
+     * the root and returns the trimmed subhyperplane or {@code null} if the subhyperplane lies
+     * outside of the region defined by the node.
+     *
+     * <p>If the subhyperplane is directly coincident with a binary partitioner of a parent node,
+     * then the relative orientations of the associated hyperplanes are used to determine the behavior,
+     * as described below.
+     * <ul>
+     *      <li>If the orientations are <strong>similar</strong>, then the subhyperplane is determined to
+     *      lie <em>outside</em> of the node's region and {@code null} is returned.</li>
+     *      <li>If the orientations are <strong>different</strong> (ie, opposite), then the subhyperplane
+     *      is determined to lie <em>inside</em> of the node's region and the fit operation continues
+     *      with the remaining parent nodes.</li>
+     * </ul>
+     * These rules are designed to allow the creation of trees with node regions that are the thickness
+     * of a single hyperplane. For example, in two dimensions, a tree could be constructed with an internal
+     * node containing a cut along the x-axis in the positive direction and with a child node containing a
+     * cut along the x-axis in the opposite direction. If the nodes in the tree are given inside and outside
+     * attributes, then this tree could be used to represent a region consisting of a single line or a region
+     * consisting of the entire space except for the single line. This would not be possible if nodes were not
+     * able to have cut hyperplanes that were coincident with parent cuts but in opposite directions.
+     *
+     * <p>
+     * Another way of looking at the rules above is that inserting a hyperplane into the tree that exactly
+     * matches the hyperplane of a parent node does not add any information to the tree. However, adding a
+     * hyperplane to the tree that is coincident with a parent node but with the opposite orientation,
+     * <em>does</em> add information to the tree.
+     *
+     * @param node the node representing the region to fit the subhyperplane to
+     * @param sub the subhyperplane to trim to the node's region
+     * @return the trimmed subhyperplane or null if the given subhyperplane does not intersect
+     *      the node's region
+     */
+    protected ConvexSubHyperplane<P> trimToNode(final N node, final ConvexSubHyperplane<P> sub) {
+
+        ConvexSubHyperplane<P> result = sub;
+
+        N parentNode = node.getParent();
+        N currentNode = node;
+
+        while (parentNode != null && result != null) {
+            final Split<? extends ConvexSubHyperplane<P>> split = result.split(parentNode.getCutHyperplane());
+
+            if (split.getLocation() == SplitLocation.NEITHER) {
+                // if we're directly on the splitter and have the same orientation, then
+                // we say the subhyperplane does not lie in the node's region (no new information
+                // is added to the tree in this case)
+                if (result.getHyperplane().similarOrientation(parentNode.getCutHyperplane())) {
+                    result = null;
+                }
+            } else {
+                result = currentNode.isPlus() ? split.getPlus() : split.getMinus();
+            }
+
+            currentNode = parentNode;
+            parentNode = parentNode.getParent();
+        }
+
+        return result;
+    }
+
+    /** Remove the cut from the given node. Returns true if the node had a cut before
+     * the call to this method. Any previous child nodes are lost.
+     * @param node the node to remove the cut from
+     * @return true if the node previously had a cut
+     */
+    protected boolean removeNodeCut(final N node) {
+        boolean hadCut = node.getCut() != null;
+        cutNode(node, null);
+
+        return hadCut;
+    }
+
+    /** Set the cut subhyperplane for the given node. If {@code cut} is {@code null} then any
+     * existing child nodes are removed. If {@code cut} is not {@code null}, two new child
+     * nodes are created and initialized with
+     * {@link AbstractBSPTree#initChildNode(AbstractNode, AbstractNode, boolean)}.
+     *
+     * <p>This method performs absolutely <em>no</em> validation on the given cut
+     * subhyperplane. It is the responsibility of the caller to ensure that the
+     * subhyperplane fits the region represented by the node.</p>
+     *
+     * <p>This method always calls {@link #invalidate()} to invalidate cached tree properties.</p>
+     * @param node the node to cut
+     * @param cut the convex subhyperplane to set as the node cut
+     */
+    protected void cutNode(final N node, final ConvexSubHyperplane<P> cut) {
+        N plus = null;
+        N minus = null;
+
+        if (cut != null) {
+            minus = createNode();
+            initChildNode(node, minus, false);
+
+            plus = createNode();
+            initChildNode(node, plus, true);
+        }
+
+        node.setSubtree(cut, minus, plus);
+
+        invalidate();
+    }
+
+    /** Return true if the given transform swaps the inside and outside of
+     * the region.
+     *
+     * <p>The default behavior of this method is to return true if the transform
+     * does not preserve spatial orientation (ie, {@link Transform#preservesOrientation()}
+     * is false). Subclasses may need to override this method to implement the correct
+     * behavior for their space and dimension.</p>
+     * @param transform transform to check
+     * @return true if the given transform swaps the interior and exterior of
+     *      the region
+     */
+    protected boolean swapsInsideOutside(final Transform<P> transform) {
+        return !transform.preservesOrientation();
+    }
+
+    /** Recursively insert a subhyperplane into the tree at the given node.
+     * @param node the node to begin insertion with
+     * @param insert the subhyperplane to insert
+     * @param trimmed subhyperplane containing the result of splitting the entire
+     *      space with each hyperplane from this node to the root
+     */
+    private void insertRecursive(final N node, final ConvexSubHyperplane<P> insert,
+            final ConvexSubHyperplane<P> trimmed) {
+        if (node.isLeaf()) {
+            cutNode(node, trimmed);
+        } else {
+            final Split<? extends ConvexSubHyperplane<P>> insertSplit = insert.split(node.getCutHyperplane());
+
+            final ConvexSubHyperplane<P> minus = insertSplit.getMinus();
+            final ConvexSubHyperplane<P> plus = insertSplit.getPlus();
+
+            if (minus != null || plus != null) {
+                final Split<? extends ConvexSubHyperplane<P>> trimmedSplit = trimmed.split(node.getCutHyperplane());
+
+                if (minus != null) {
+                    insertRecursive(node.getMinus(), minus, trimmedSplit.getMinus());
+                }
+                if (plus != null) {
+                    insertRecursive(node.getPlus(), plus, trimmedSplit.getPlus());
+                }
+            }
+        }
+    }
+
+    /** Transform the subtree rooted as {@code node} recursively.
+     * @param node the root node of the subtree to transform
+     * @param t the transform to apply
+     * @param swapChildren if true, the plus and minus child nodes of each internal node
+     *      will be swapped; this should be the case when the transform is a reflection
+     */
+    private void transformRecursive(final N node, final Transform<P> t, final boolean swapChildren) {
+        if (node.isInternal()) {
+            // transform our cut
+            final ConvexSubHyperplane<P> transformedCut = node.getCut().transform(t);
+
+            // transform our children
+            transformRecursive(node.getMinus(), t, swapChildren);
+            transformRecursive(node.getPlus(), t, swapChildren);
+
+            final N transformedMinus = swapChildren ? node.getPlus() : node.getMinus();
+            final N transformedPlus = swapChildren ? node.getMinus() : node.getPlus();
+
+            // set our new state
+            node.setSubtree(transformedCut, transformedMinus, transformedPlus);
+        }
+    }
+
+    /** Split this tree with the given hyperplane, placing the split contents into the given
+     * target trees. One of the given trees may be null, in which case that portion of the split
+     * will not be exported. The current tree is not modified.
+     * @param splitter splitting hyperplane
+     * @param minus tree that will contain the portion of the tree on the minus side of the splitter
+     * @param plus tree that will contain the portion of the tree on the plus side of the splitter
+     */
+    protected void splitIntoTrees(final Hyperplane<P> splitter,
+            final AbstractBSPTree<P, N> minus, final AbstractBSPTree<P, N> plus) {
+
+        AbstractBSPTree<P, N> temp = (minus != null) ? minus : plus;
+
+        N splitRoot = temp.splitSubtree(this.getRoot(), splitter.span());
+
+        if (minus != null) {
+            if (plus != null) {
+                plus.extract(splitRoot.getPlus());
+            }
+            minus.extract(splitRoot.getMinus());
+        } else {
+            plus.extract(splitRoot.getPlus());
+        }
+    }
+
+    /** Split the subtree rooted at the given node by a partitioning convex subhyperplane defined
+     * on the same region as the node. The subtree rooted at {@code node} is imported into
+     * this tree, meaning that if it comes from a different tree, the other tree is not
+     * modified.
+     * @param node the root node of the subtree to split; may come from a different tree,
+     *      in which case the other tree is not modified
+     * @param partitioner partitioning convex subhyperplane
+     * @return node containing the split subtree
+     */
+    protected N splitSubtree(final N node, final ConvexSubHyperplane<P> partitioner) {
+        if (node.isLeaf()) {
+            return splitLeafNode(node, partitioner);
+        }
+        return splitInternalNode(node, partitioner);
+    }
+
+    /** Split the given leaf node by a partitioning convex subhyperplane defined on the
+     * same region and import it into this tree.
+     * @param node the leaf node to split
+     * @param partitioner partitioning convex subhyperplane
+     * @return node containing the split subtree
+     */
+    private N splitLeafNode(final N node, final ConvexSubHyperplane<P> partitioner) {
+        // in this case, we just create a new parent node with the partitioner as its
+        // cut and two copies of the original node as children
+        final N parent = createNode();
+        parent.setSubtree(partitioner, copyNode(node), copyNode(node));
+
+        return parent;
+    }
+
+    /** Split the given internal node by a partitioning convex subhyperplane defined on the same region
+     * as the node and import it into this tree.
+     * @param node the internal node to split
+     * @param partitioner partitioning convex subhyperplane
+     * @return node containing the split subtree
+     */
+    private N splitInternalNode(final N node, final ConvexSubHyperplane<P> partitioner) {
+        // split the partitioner and node cut with each other's hyperplanes to determine their relative positions
+        final Split<? extends ConvexSubHyperplane<P>> partitionerSplit = partitioner.split(node.getCutHyperplane());
+        final Split<? extends ConvexSubHyperplane<P>> nodeCutSplit = node.getCut().split(partitioner.getHyperplane());
+
+        final SplitLocation partitionerSplitSide = partitionerSplit.getLocation();
+        final SplitLocation nodeCutSplitSide = nodeCutSplit.getLocation();
+
+        final N result = createNode();
+
+        N resultMinus;
+        N resultPlus;
+
+        if (partitionerSplitSide == SplitLocation.PLUS) {
+            if (nodeCutSplitSide == SplitLocation.PLUS) {
+                // partitioner is on node cut plus side, node cut is on partitioner plus side
+                final N nodePlusSplit = splitSubtree(node.getPlus(), partitioner);
+
+                resultMinus = nodePlusSplit.getMinus();
+
+                resultPlus = copyNode(node);
+                resultPlus.setSubtree(node.getCut(), importSubtree(node.getMinus()), nodePlusSplit.getPlus());
+            } else {
+                // partitioner is on node cut plus side, node cut is on partitioner minus side
+                final N nodePlusSplit = splitSubtree(node.getPlus(), partitioner);
+
+                resultMinus = copyNode(node);
+                resultMinus.setSubtree(node.getCut(), importSubtree(node.getMinus()), nodePlusSplit.getMinus());
+
+                resultPlus = nodePlusSplit.getPlus();
+            }
+        } else if (partitionerSplitSide == SplitLocation.MINUS) {
+            if (nodeCutSplitSide == SplitLocation.MINUS) {
+                // partitioner is on node cut minus side, node cut is on partitioner minus side
+                final N nodeMinusSplit = splitSubtree(node.getMinus(), partitioner);
+
+                resultMinus = copyNode(node);
+                resultMinus.setSubtree(node.getCut(), nodeMinusSplit.getMinus(), importSubtree(node.getPlus()));
+
+                resultPlus = nodeMinusSplit.getPlus();
+            } else {
+                // partitioner is on node cut minus side, node cut is on partitioner plus side
+                final N nodeMinusSplit = splitSubtree(node.getMinus(), partitioner);
+
+                resultMinus = nodeMinusSplit.getMinus();
+
+                resultPlus = copyNode(node);
+                resultPlus.setSubtree(node.getCut(), nodeMinusSplit.getPlus(), importSubtree(node.getPlus()));
+            }
+        } else if (partitionerSplitSide == SplitLocation.BOTH) {
+            // partitioner and node cut split each other
+            final N nodeMinusSplit = splitSubtree(node.getMinus(), partitionerSplit.getMinus());
+            final N nodePlusSplit = splitSubtree(node.getPlus(), partitionerSplit.getPlus());
+
+            resultMinus = copyNode(node);
+            resultMinus.setSubtree(nodeCutSplit.getMinus(), nodeMinusSplit.getMinus(), nodePlusSplit.getMinus());
+
+            resultPlus = copyNode(node);
+            resultPlus.setSubtree(nodeCutSplit.getPlus(), nodeMinusSplit.getPlus(), nodePlusSplit.getPlus());
+        } else {
+            // partitioner and node cut are parallel or anti-parallel
+            final boolean sameOrientation = partitioner.getHyperplane().similarOrientation(node.getCutHyperplane());
+
+            resultMinus = importSubtree(sameOrientation ? node.getMinus() : node.getPlus());
+            resultPlus = importSubtree(sameOrientation ? node.getPlus() : node.getMinus());
+        }
+
+        result.setSubtree(partitioner, resultMinus, resultPlus);
+
+        return result;
+    }
+
+    /** Invalidate any previously computed properties that rely on the internal structure of the tree.
+     * This method must be called any time the tree's internal structure changes in order to force cacheable
+     * tree and node properties to be recomputed the next time they are requested.
+     *
+     * <p>This method increments the tree's {@link #version} property.</p>
+     * @see #getVersion()
+     */
+    protected void invalidate() {
+        version = Math.max(0, version + 1); // positive values only
+    }
+
+    /** Get the current structural version of the tree. This is incremented each time the
+     * tree structure is changes and can be used by nodes to allow caching of computed values.
+     * @return the current version of the tree structure
+     * @see #invalidate()
+     */
+    protected int getVersion() {
+        return version;
+    }
+
+    /** Abstract implementation of {@link BSPTree.Node}. This class is intended for use with
+     * {@link AbstractBSPTree} and delegates tree mutation methods back to the parent tree object.
+     * @param <P> Point implementation type
+     * @param <N> BSP tree node implementation type
+     */
+    public abstract static class AbstractNode<P extends Point<P>, N extends AbstractNode<P, N>>
+        implements BSPTree.Node<P, N>, Serializable {
+
+        /** Serializable UID. */
+        private static final long serialVersionUID = 20190225L;
+
+        /** The owning tree instance. */
+        private final AbstractBSPTree<P, N> tree;
+
+        /** The parent node; this will be null for the tree root node. */
+        private N parent;
+
+        /** The subhyperplane cutting the node's region; this will be null for leaf nodes. */
+        private ConvexSubHyperplane<P> cut;
+
+        /** The node lying on the minus side of the cut subhyperplane; this will be null
+         * for leaf nodes.
+         */
+        private N minus;
+
+        /** The node lying on the plus side of the cut subhyperplane; this will be null
+         * for leaf nodes.
+         */
+        private N plus;
+
+        /** The current version of the node. This is set to track the tree's version
+         * and is used to detect when certain values need to be recomputed due to
+         * structural changes in the tree.
+         */
+        private int nodeVersion = -1;
+
+        /** The depth of this node in the tree. This will be zero for the root node and
+         * {@link AbstractBSPTree#UNKNOWN_VALUE} when the value needs to be computed.
+         */
+        private int depth = UNKNOWN_VALUE;
+
+        /** The total number of nodes in the subtree rooted at this node. This will be
+         * set to {@link AbstractBSPTree#UNKNOWN_VALUE} when the value needs
+         * to be computed.
+         */
+        private int count = UNKNOWN_VALUE;
+
+        /** The height of the subtree rooted at this node. This will
+         * be set to {@link AbstractBSPTree#UNKNOWN_VALUE} when the value needs
+         * to be computed.
+         */
+        private int height = UNKNOWN_VALUE;
+
+        /** Simple constructor.
+         * @param tree the tree instance that owns this node
+         */
+        protected AbstractNode(final AbstractBSPTree<P, N> tree) {
+            this.tree = tree;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public AbstractBSPTree<P, N> getTree() {
+            return tree;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int depth() {
+            if (depth == UNKNOWN_VALUE) {
+                // calculate our depth based on our parent's depth, if
+                // possible
+                if (parent != null) {
+                    final int parentDepth = parent.depth();
+                    if (parentDepth != UNKNOWN_VALUE) {
+                        depth = parentDepth + 1;
+                    }
+                }
+            }
+            return depth;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int height() {
+            checkValid();
+
+            if (height == UNKNOWN_VALUE) {
+                if (isLeaf()) {
+                    height = 0;
+                } else {
+                    height = Math.max(getMinus().height(), getPlus().height()) + 1;
+                }
+            }
+
+            return height;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int count() {
+            checkValid();
+
+            if (count == UNKNOWN_VALUE) {
+                count = 1;
+
+                if (!isLeaf()) {
+                    count += minus.count() + plus.count();
+                }
+            }
+
+            return count;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Iterator<N> iterator() {
+            return new NodeIterator<P, N>(getSelf());
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void accept(final BSPTreeVisitor<P, N> visitor) {
+            tree.acceptVisitor(getSelf(), visitor);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public N getParent() {
+            return parent;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public boolean isLeaf() {
+            return cut == null;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public boolean isInternal() {
+            return cut != null;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public boolean isPlus() {
+            return parent != null && parent.getPlus() == this;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public boolean isMinus() {
+            return parent != null && parent.getMinus() == this;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public ConvexSubHyperplane<P> getCut() {
+            return cut;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Hyperplane<P> getCutHyperplane() {
+            return (cut != null) ? cut.getHyperplane() : null;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public N getPlus() {
+            return plus;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public N getMinus() {
+            return minus;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public boolean insertCut(final Hyperplane<P> cutter) {
+            return tree.insertNodeCut(getSelf(), cutter);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public boolean clearCut() {
+            return tree.removeNodeCut(getSelf());
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public N cut(final Hyperplane<P> cutter) {
+            this.insertCut(cutter);
+
+            return getSelf();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder();
+            sb.append(this.getClass().getSimpleName())
+                .append("[cut= ")
+                .append(getCut())
+                .append(']');
+
+            return sb.toString();
+        }
+
+        /** Set the parameters for the subtree rooted at this node. The arguments should either be
+         * all null (representing a leaf node) or all non-null (representing an internal node).
+         *
+         * <p>Absolutely no validation is performed on the arguments. Callers are responsible for
+         * ensuring that any given subhyperplane fits the region defined by the node and that
+         * any child nodes belong to this tree and are correctly initialized.</p>
+         *
+         * @param newCut the new cut subhyperplane for the node
+         * @param newMinus the new minus child for the node
+         * @param newPlus the new plus child for the node
+         */
+        protected void setSubtree(final ConvexSubHyperplane<P> newCut, final N newMinus, final N newPlus) {
+            this.cut = newCut;
+
+            final N self = getSelf();
+
+            // cast for access to private member
+            AbstractNode<P, N> minusNode = newMinus;
+            AbstractNode<P, N> plusNode = newPlus;
+
+            // get the child depth now if we know it offhand, otherwise set it to the unknown value
+            // and have the child pull it when needed
+            final int childDepth = (depth != UNKNOWN_VALUE) ? depth + 1 : UNKNOWN_VALUE;
+
+            if (newMinus != null) {
+                minusNode.parent = self;
+                minusNode.depth = childDepth;
+            }
+            this.minus = newMinus;
+
+            if (newPlus != null) {
+                plusNode.parent = self;
+                plusNode.depth = childDepth;
+            }
+            this.plus = newPlus;
+        }
+
+        /**
+         * Make this node a root node, detaching it from its parent and settings its depth to zero.
+         * Any previous parent node will be left in an invalid state since one of its children now
+         * does not have a reference back to it.
+         */
+        protected void makeRoot() {
+            parent = null;
+            depth = 0;
+        }
+
+        /** Check if cached node properties are valid, meaning that no structural updates have
+         * occurred in the tree since the last call to this method. If updates have occurred, the
+         * {@link #nodeInvalidated()} method is called to clear the cached properties. This method
+         * should be called at the beginning of any method that fetches cacheable properties
+         * to ensure that no stale values are returned.
+         */
+        protected void checkValid() {
+            final int treeVersion = tree.getVersion();
+
+            if (nodeVersion != treeVersion) {
+                // the tree structure changed somewhere
+                nodeInvalidated();
+
+                // store the current version
+                nodeVersion = treeVersion;
+            }
+        }
+
+        /** Method called from {@link #checkValid()} when updates
+         * are detected in the tree. This method should clear out any
+         * computed properties that rely on the structure of the tree
+         * and prepare them for recalculation.
+         */
+        protected void nodeInvalidated() {
+            count = UNKNOWN_VALUE;
+            height = UNKNOWN_VALUE;
+        }
+
+        /** Get a reference to the current instance, cast to type N.
+         * @return a reference to the current instance, as type N.
+         */
+        protected abstract N getSelf();
+    }
+
+    /** Class for iterating through the nodes in a BSP subtree.
+     * @param <P> Point implementation type
+     * @param <N> Node implementation type
+     */
+    public static class NodeIterator<P extends Point<P>, N extends AbstractNode<P, N>> implements Iterator<N> {
+
+        /** The current node stack. */
+        private final Deque<N> stack = new LinkedList<>();
+
+        /** Create a new instance for iterating over the nodes in the given subtree.
+         * @param subtreeRoot the root node of the subtree to iterate
+         */
+        public NodeIterator(final N subtreeRoot) {
+            stack.push(subtreeRoot);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public boolean hasNext() {
+            return !stack.isEmpty();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public N next() {
+            if (stack.isEmpty()) {
+                throw new NoSuchElementException();
+            }
+
+            final N result = stack.pop();
+
+            if (result != null && !result.isLeaf()) {
+                stack.push(result.getPlus());
+                stack.push(result.getMinus());
+            }
+
+            return result;
+        }
+    }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperator.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperator.java
new file mode 100644
index 0000000..8cfaa46
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperator.java
@@ -0,0 +1,147 @@
+/*
+ * 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.bsp;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree.AbstractNode;
+
+/** Class containing the basic algorithm for merging two {@link AbstractBSPTree}
+ * instances. Subclasses must override the
+ * {@link #mergeLeaf(AbstractBSPTree.AbstractNode, AbstractBSPTree.AbstractNode)} method
+ * to implement the merging logic for their particular use case. The remainder of the
+ * algorithm is independent of the use case.
+ *
+ * <p>This class does not expose any public methods so that subclasses can present their own
+ * public API, tailored to the specific types being worked with. In particular, most subclasses
+ * will want to restrict the tree types used with the algorithm, which is difficult to implement
+ * cleanly at this level.</p>
+ *
+ * <p>This class maintains state during the merging process and is therefore
+ * <em>not</em> thread-safe.</p>
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+public abstract class AbstractBSPTreeMergeOperator<P extends Point<P>, N extends AbstractNode<P, N>> {
+
+    /** The tree that the merge operation output will be written to. All existing content
+     * in this tree is overwritten.
+     */
+    private AbstractBSPTree<P, N> outputTree;
+
+    /** Set the tree used as output for this instance.
+     * @param outputTree the tree used as output for this instance
+     */
+    protected void setOutputTree(final AbstractBSPTree<P, N> outputTree) {
+        this.outputTree = outputTree;
+    }
+
+    /** Get the tree used as output for this instance.
+     * @return the tree used as output for this instance
+     */
+    protected AbstractBSPTree<P, N> getOutputTree() {
+        return outputTree;
+    }
+
+    /** Perform a merge operation with the two input trees and store the result in the output tree. The
+     * output tree may be one of the input trees, in which case, the tree is modified in place.
+     * @param input1 first input tree
+     * @param input2 second input tree
+     * @param output output tree all previous content in this tree is overwritten
+     */
+    protected void performMerge(final AbstractBSPTree<P, N> input1, final AbstractBSPTree<P, N> input2,
+            final AbstractBSPTree<P, N> output) {
+
+        setOutputTree(output);
+
+        final N root1 = input1.getRoot();
+        final N root2 = input2.getRoot();
+
+        final N outputRoot = performMergeRecursive(root1, root2);
+
+        getOutputTree().setRoot(outputRoot);
+    }
+
+    /** Recursively merge two nodes.
+     * @param node1 node from the first input tree
+     * @param node2 node from the second input tree
+     * @return a merged node
+     */
+    private N performMergeRecursive(final N node1, final N node2) {
+
+        if (node1.isLeaf() || node2.isLeaf()) {
+            // delegate to the mergeLeaf method if we can no longer continue
+            // merging recursively
+            final N merged = mergeLeaf(node1, node2);
+
+            // copy the merged node to the output if needed (in case mergeLeaf
+            // returned one of the input nodes directly)
+            return outputTree.importSubtree(merged);
+        } else {
+            final N partitioned = outputTree.splitSubtree(node2, node1.getCut());
+
+            final N minus = performMergeRecursive(node1.getMinus(), partitioned.getMinus());
+
+            final N plus = performMergeRecursive(node1.getPlus(), partitioned.getPlus());
+
+            final N outputNode = outputTree.copyNode(node1);
+            outputNode.setSubtree(node1.getCut(), minus, plus);
+
+            return outputNode;
+        }
+    }
+
+    /** Create a new node in the output tree. The node is associated with the output tree but
+     * is not attached to a parent node.
+     * @return a new node associated with the output tree but not yet attached to a parent
+     */
+    protected N outputNode() {
+        return outputTree.createNode();
+    }
+
+    /** Create a new node in the output tree with the same non-structural properties as the given
+     * node. Non-structural properties are properties other than parent, children, or cut. The
+     * returned node is associated with the output tree but is not attached to a parent node.
+     * Note that this method only copies the given node and <strong>not</strong> any of its children.
+     * @param node the input node to copy properties from
+     * @return a new node in the output tree
+     */
+    protected N outputNode(final N node) {
+        return outputTree.copyNode(node);
+    }
+
+    /** Place the subtree rooted at the given input node into the output tree. The subtree
+     * is copied if needed.
+     * @param node the root of the subtree to copy
+     * @return a subtree in the output tree
+     */
+    protected N outputSubtree(final N node) {
+        return outputTree.importSubtree(node);
+    }
+
+    /** Merge a leaf node from one input with a subtree from another.
+     * <p>When this method is called, one or both of the given nodes will be a leaf node.
+     * This method is expected to return a node representing the merger of the two given
+     * nodes. The way that the returned node is determined defines the overall behavior of
+     * the merge operation.
+     * </p>
+     * <p>The return value can be one of the two input nodes or a completely different one.</p>
+     * @param node1 node from the first input tree
+     * @param node2 node from the second input tree
+     * @return node representing the merger of the two input nodes
+     */
+    protected abstract N mergeLeaf(N node1, N node2);
+}
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
new file mode 100644
index 0000000..9a867a8
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTree.java
@@ -0,0 +1,966 @@
+/*
+ * 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.bsp;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.RegionLocation;
+import org.apache.commons.geometry.core.internal.IteratorTransform;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SplitLocation;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.ClosestFirstVisitor;
+
+/** {@link BSPTree} specialized for representing regions of space. For example, this
+ * class can be used to represent polygons in Euclidean 2D space and polyhedrons
+ * in Euclidean 3D space.
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+public abstract class AbstractRegionBSPTree<
+        P extends Point<P>,
+        N extends AbstractRegionBSPTree.AbstractRegionNode<P, N>>
+    extends AbstractBSPTree<P, N> implements HyperplaneBoundedRegion<P> {
+
+    /** Serializable UID. */
+    private static final long serialVersionUID = 1L;
+
+    /** Value used to indicate an unknown size. */
+    private static final double UNKNOWN_SIZE = -1.0;
+
+    /** The region boundary size; this is computed when requested and then cached. */
+    private double boundarySize = UNKNOWN_SIZE;
+
+    /** The current size properties for the region. */
+    private RegionSizeProperties<P> regionSizeProperties;
+
+    /** Construct a new region will the given boolean determining whether or not the
+     * region will be full (including the entire space) or empty (excluding the entire
+     * space).
+     * @param full if true, the region will cover the entire space, otherwise it will
+     *      be empty
+     */
+    protected AbstractRegionBSPTree(final boolean full) {
+        getRoot().setLocation(full ? RegionLocation.INSIDE : RegionLocation.OUTSIDE);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isEmpty() {
+        return !hasNodeWithLocationRecursive(getRoot(), RegionLocation.INSIDE);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isFull() {
+        return !hasNodeWithLocationRecursive(getRoot(), RegionLocation.OUTSIDE);
+    }
+
+    /** Return true if any node in the subtree rooted at the given node has a location with the
+     * given value.
+     * @param node the node at the root of the subtree to search
+     * @param location the location to find
+     * @return true if any node in the subtree has the given location
+     */
+    private boolean hasNodeWithLocationRecursive(final AbstractRegionNode<P, N> node, final RegionLocation location) {
+        if (node == null) {
+            return false;
+        }
+
+        return node.getLocation() == location ||
+                hasNodeWithLocationRecursive(node.getMinus(), location) ||
+                hasNodeWithLocationRecursive(node.getPlus(), location);
+    }
+
+    /** Modify this instance so that it contains the entire space.
+     * @see #isFull()
+     */
+    public void setFull() {
+        final N root = getRoot();
+
+        root.clearCut();
+        root.setLocation(RegionLocation.INSIDE);
+    }
+
+    /** Modify this instance so that is is completely empty.
+     * @see #isEmpty()
+     */
+    public void setEmpty() {
+        final N root = getRoot();
+
+        root.clearCut();
+        root.setLocation(RegionLocation.OUTSIDE);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getSize() {
+        return getRegionSizeProperties().getSize();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getBoundarySize() {
+        if (boundarySize < 0.0) {
+            double sum = 0.0;
+
+            RegionCutBoundary<P> boundary;
+            for (final AbstractRegionNode<P, N> node : this) {
+                boundary = node.getCutBoundary();
+                if (boundary != null) {
+                    sum += boundary.getInsideFacing().getSize();
+                    sum += boundary.getOutsideFacing().getSize();
+                }
+            }
+
+            boundarySize = sum;
+        }
+
+        return boundarySize;
+    }
+
+    /** Return an {@link Iterable} for iterating over the boundaries of the region.
+     * Each boundary is oriented such that its plus side points to the outside of the
+     * region. The exact ordering of the boundaries is determined by the internal structure
+     * of the tree.
+     * @return an {@link Iterable} for iterating over the boundaries of the region
+     * @see #getBoundaries()
+     */
+    public Iterable<? extends ConvexSubHyperplane<P>> boundaries() {
+        return createBoundaryIterable(Function.identity());
+    }
+
+    /** Internal method for creating the iterable instances used to iterate the region boundaries.
+     * @param typeConverter function to convert the generic convex subhyperplane type into
+     *      the type specific for this tree
+     * @param <C> ConvexSubhyperplane implementation type
+     * @return an iterable to iterating the region boundaries
+     */
+    protected <C extends ConvexSubHyperplane<P>> Iterable<C> createBoundaryIterable(
+            final Function<ConvexSubHyperplane<P>, C> typeConverter) {
+
+        return new Iterable<C>() {
+
+            @Override
+            public Iterator<C> iterator() {
+                final NodeIterator<P, N> nodeIterator = new NodeIterator<>(getRoot());
+                return new RegionBoundaryIterator<>(nodeIterator, typeConverter);
+            }
+        };
+    }
+
+    /** Return a list containing the boundaries of the region. Each boundary is oriented such
+     * that its plus side points to the outside of the region. The exact ordering of
+     * the boundaries is determined by the internal structure of the tree.
+     * @return a list of the boundaries of the region
+     */
+    public List<? extends ConvexSubHyperplane<P>> getBoundaries() {
+        return createBoundaryList(Function.identity());
+    }
+
+    /** Iternal method for creating a list of the region boundaries.
+     * @param typeConverter function to convert the generic convex subhyperplane type into
+     *      the type specific for this tree
+     * @param <C> ConvexSubhyperplane implementation type
+     * @return a list of the region boundaries
+     */
+    protected <C extends ConvexSubHyperplane<P>> List<C> createBoundaryList(
+            final Function<ConvexSubHyperplane<P>, C> typeConverter) {
+
+        final List<C> result = new ArrayList<>();
+
+        final RegionBoundaryIterator<P, C, N> it = new RegionBoundaryIterator<>(iterator(), typeConverter);
+        it.forEachRemaining(result::add);
+
+        return result;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public P project(P pt) {
+        final BoundaryProjector<P, N> projector = new BoundaryProjector<>(pt);
+        accept(projector);
+
+        return projector.getProjected();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public P getBarycenter() {
+        return getRegionSizeProperties().getBarycenter();
+    }
+
+    /** Helper method implementing the algorithm for splitting a tree by a hyperplane. Subclasses
+     * should call this method with two instantiated trees of the correct type.
+     * @param splitter splitting hyperplane
+     * @param minus tree that will contain the minus side of the split result
+     * @param plus tree that will contain the plus side of the split result
+     * @param <T> Tree implementation type
+     * @return result of splitting this tree with the given hyperplane
+     */
+    protected <T extends AbstractRegionBSPTree<P, N>> Split<T> split(final Hyperplane<P> splitter,
+            final T minus, final T plus) {
+
+        splitIntoTrees(splitter, minus, plus);
+
+        T splitMinus = null;
+        T splitPlus = null;
+
+        if (minus != null) {
+            minus.getRoot().getPlus().setLocation(RegionLocation.OUTSIDE);
+            minus.condense();
+
+            splitMinus = minus.isEmpty() ? null : minus;
+        }
+        if (plus != null) {
+            plus.getRoot().getMinus().setLocation(RegionLocation.OUTSIDE);
+            plus.condense();
+
+            splitPlus = plus.isEmpty() ? null : plus;
+        }
+
+        return new Split<T>(splitMinus, splitPlus);
+    }
+
+    /** Get the size-related properties for the region. The value is computed
+     * lazily and cached.
+     * @return the size-related properties for the region
+     */
+    protected RegionSizeProperties<P> getRegionSizeProperties() {
+        if (regionSizeProperties == null) {
+            regionSizeProperties = computeRegionSizeProperties();
+        }
+
+        return regionSizeProperties;
+    }
+
+    /** Compute the size-related properties of the region.
+     * @return object containing size properties for the region
+     */
+    protected abstract RegionSizeProperties<P> computeRegionSizeProperties();
+
+    /** {@inheritDoc}
+     *
+     * <p>If the point is {@link org.apache.commons.geometry.core.Spatial#isNaN() NaN}, then
+     * {@link RegionLocation#OUTSIDE} is returned.</p>
+     */
+    @Override
+    public RegionLocation classify(final P point) {
+        if (point.isNaN()) {
+            return RegionLocation.OUTSIDE;
+        }
+
+        return classifyRecursive(getRoot(), point);
+    }
+
+    /** Recursively classify a point with respect to the region.
+     * @param node the node to classify against
+     * @param point the point to classify
+     * @return the classification of the point with respect to the region rooted
+     *      at the given node
+     */
+    private RegionLocation classifyRecursive(final AbstractRegionNode<P, N> node, final P point) {
+        if (node.isLeaf()) {
+            // the point is in a leaf, so the classification is just the leaf location
+            return node.getLocation();
+        } else {
+            final HyperplaneLocation cutLoc = node.getCutHyperplane().classify(point);
+
+            if (cutLoc == HyperplaneLocation.MINUS) {
+                return classifyRecursive(node.getMinus(), point);
+            } else if (cutLoc == HyperplaneLocation.PLUS) {
+                return classifyRecursive(node.getPlus(), point);
+            } else {
+                // the point is on the cut boundary; classify against both child
+                // subtrees and see if we end up with the same result or not
+                RegionLocation minusLoc = classifyRecursive(node.getMinus(), point);
+                RegionLocation plusLoc = classifyRecursive(node.getPlus(), point);
+
+                if (minusLoc == plusLoc) {
+                    return minusLoc;
+                }
+                return RegionLocation.BOUNDARY;
+            }
+        }
+    }
+
+    /** Change this region into its complement. All inside nodes become outside
+     * nodes and vice versa. The orientation of the cut subhyperplanes is not modified.
+     */
+    public void complement() {
+        complementRecursive(getRoot());
+    }
+
+    /** Set this instance to be the complement of the given tree. The argument
+     * is not modified.
+     * @param tree the tree to become the complement of
+     */
+    public void complement(final AbstractRegionBSPTree<P, N> tree) {
+        copySubtree(tree.getRoot(), getRoot());
+        complementRecursive(getRoot());
+    }
+
+    /** Recursively switch all inside nodes to outside nodes and vice versa.
+     * @param node the node at the root of the subtree to switch
+     */
+    private void complementRecursive(final AbstractRegionNode<P, N> node) {
+        if (node != null) {
+            final RegionLocation newLoc = (node.getLocationValue() == RegionLocation.INSIDE) ?
+                    RegionLocation.OUTSIDE :
+                    RegionLocation.INSIDE;
+
+            node.setLocation(newLoc);
+
+            complementRecursive(node.getMinus());
+            complementRecursive(node.getPlus());
+        }
+    }
+
+    /** Compute the union of this instance and the given region, storing the result back in
+     * this instance. The argument is not modified.
+     * @param other the tree to compute the union with
+     */
+    public void union(final AbstractRegionBSPTree<P, N> other) {
+        new UnionOperator<P, N>().apply(this, other, this);
+    }
+
+    /** Compute the union of the two regions passed as arguments and store the result in
+     * this instance. Any nodes currently existing in this instance are removed.
+     * @param a first argument to the union operation
+     * @param b second argument to the union operation
+     */
+    public void union(final AbstractRegionBSPTree<P, N> a, final AbstractRegionBSPTree<P, N> b) {
+        new UnionOperator<P, N>().apply(a, b, this);
+    }
+
+    /** Compute the intersection of this instance and the given region, storing the result back in
+     * this instance. The argument is not modified.
+     * @param other the tree to compute the intersection with
+     */
+    public void intersection(final AbstractRegionBSPTree<P, N> other) {
+        new IntersectionOperator<P, N>().apply(this, other, this);
+    }
+
+    /** Compute the intersection of the two regions passed as arguments and store the result in
+     * this instance. Any nodes currently existing in this instance are removed.
+     * @param a first argument to the intersection operation
+     * @param b second argument to the intersection operation
+     */
+    public void intersection(final AbstractRegionBSPTree<P, N> a, final AbstractRegionBSPTree<P, N> b) {
+        new IntersectionOperator<P, N>().apply(a, b, this);
+    }
+
+    /** Compute the difference of this instance and the given region, storing the result back in
+     * this instance. The argument is not modified.
+     * @param other the tree to compute the difference with
+     */
+    public void difference(final AbstractRegionBSPTree<P, N> other) {
+        new DifferenceOperator<P, N>().apply(this, other, this);
+    }
+
+    /** Compute the difference of the two regions passed as arguments and store the result in
+     * this instance. Any nodes currently existing in this instance are removed.
+     * @param a first argument to the difference operation
+     * @param b second argument to the difference operation
+     */
+    public void difference(final AbstractRegionBSPTree<P, N> a, final AbstractRegionBSPTree<P, N> b) {
+        new DifferenceOperator<P, N>().apply(a, b, this);
+    }
+
+    /** Compute the symmetric difference (xor) of this instance and the given region, storing the result back in
+     * this instance. The argument is not modified.
+     * @param other the tree to compute the symmetric difference with
+     */
+    public void xor(final AbstractRegionBSPTree<P, N> other) {
+        new XorOperator<P, N>().apply(this, other, this);
+    }
+
+    /** Compute the symmetric difference (xor) of the two regions passed as arguments and store the result in
+     * this instance. Any nodes currently existing in this instance are removed.
+     * @param a first argument to the symmetric difference operation
+     * @param b second argument to the symmetric difference operation
+     */
+    public void xor(final AbstractRegionBSPTree<P, N> a, final AbstractRegionBSPTree<P, N> b) {
+        new XorOperator<P, N>().apply(a, b, this);
+    }
+
+    /** Condense this tree by removing redundant subtrees.
+     *
+     * <p>This operation can be used to reduce the total number of nodes in the
+     * tree after performing node manipulations. For example, if two sibling leaf
+     * nodes both represent the same {@link RegionLocation}, then there is no reason
+     * from the perspective of the geometric region to retain both nodes. They are
+     * therefore both merged into their parent node. This method performs this
+     * simplification process.
+     * </p>
+     */
+    protected void condense() {
+        condenseRecursive(getRoot());
+    }
+
+    /** Recursively condense nodes that have children with homogenous location attributes
+     * (eg, both inside, both outside) into single nodes.
+     * @param node the root of the subtree to condense
+     * @return the location of the successfully condensed subtree or null if no condensing was
+     *      able to be performed
+     */
+    private RegionLocation condenseRecursive(final N node) {
+        if (node.isLeaf()) {
+            return node.getLocation();
+        }
+
+        final RegionLocation minusLocation = condenseRecursive(node.getMinus());
+        final RegionLocation plusLocation = condenseRecursive(node.getPlus());
+
+        if (minusLocation != null && plusLocation != null && minusLocation == plusLocation) {
+            node.setLocation(minusLocation);
+            node.clearCut();
+
+            return minusLocation;
+        }
+
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void copyNodeProperties(final N src, final N dst) {
+        dst.setLocation(src.getLocationValue());
+    }
+
+    /** Compute the portion of the node's cut subhyperplane that lies on the boundary of
+     * the region.
+     * @param node the node to compute the cut subhyperplane boundary of
+     * @return object representing the portions of the node's cut subhyperplane that lie
+     *      on the region's boundary
+     */
+    private RegionCutBoundary<P> computeBoundary(final N node) {
+        if (node.isLeaf()) {
+            // no boundary for leaf nodes; they are either entirely in or
+            // entirely out
+            return null;
+        }
+
+        ConvexSubHyperplane<P> sub = node.getCut();
+
+        // find the portions of the node cut sub-hyperplane that touch inside and
+        // outside cells in the minus sub-tree
+        SubHyperplane.Builder<P> minusInBuilder = sub.builder();
+        SubHyperplane.Builder<P> minusOutBuilder = sub.builder();
+
+        characterizeSubHyperplane(sub, node.getMinus(), minusInBuilder, minusOutBuilder);
+
+        List<? extends ConvexSubHyperplane<P>> minusIn = minusInBuilder.build().toConvex();
+        List<? extends ConvexSubHyperplane<P>> minusOut = minusOutBuilder.build().toConvex();
+
+        // create the result boundary builders
+        SubHyperplane.Builder<P> insideFacing = sub.builder();
+        SubHyperplane.Builder<P> outsideFacing = sub.builder();
+
+        if (!minusIn.isEmpty()) {
+            // Add to the boundary anything that touches an inside cell in the minus sub-tree
+            // and an outside cell in the plus sub-tree. These portions are oriented with their
+            // plus side pointing to the outside of the region.
+            for (ConvexSubHyperplane<P> minusInFragment : minusIn) {
+                characterizeSubHyperplane(minusInFragment, node.getPlus(), null, outsideFacing);
+            }
+        }
+
+        if (!minusOut.isEmpty()) {
+            // Add to the boundary anything that touches an outside cell in the minus sub-tree
+            // and an inside cell in the plus sub-tree. These portions are oriented with their
+            // plus side pointing to the inside of the region.
+            for (ConvexSubHyperplane<P> minusOutFragment : minusOut) {
+                characterizeSubHyperplane(minusOutFragment, node.getPlus(), insideFacing, null);
+            }
+        }
+
+        return new RegionCutBoundary<P>(insideFacing.build(), outsideFacing.build());
+    }
+
+    /** Recursive method to characterize a convex subhyperplane with respect to the region's
+     * boundaries.
+     * @param sub the subhyperplane to characterize
+     * @param node the node to characterize the subhyperplane against
+     * @param in the builder that will receive the portions of the subhyperplane that lie in the inside
+     *      of the region; may be null
+     * @param out the builder that will receive the portions of the subhyperplane that lie on the outside
+     *      of the region; may be null
+     */
+    private void characterizeSubHyperplane(final ConvexSubHyperplane<P> sub, final AbstractRegionNode<P, N> node,
+            final SubHyperplane.Builder<P> in, final SubHyperplane.Builder<P> out) {
+
+        if (sub != null) {
+            if (node.isLeaf()) {
+                if (node.isInside() && in != null) {
+                    in.add(sub);
+                } else if (node.isOutside() && out != null) {
+                    out.add(sub);
+                }
+            } else {
+                final Split<? extends ConvexSubHyperplane<P>> split = sub.split(node.getCutHyperplane());
+
+                // Continue further on down the subtree with the same subhyperplane if the
+                // subhyperplane lies directly on the current node's cut
+                if (split.getLocation() == SplitLocation.NEITHER) {
+                    characterizeSubHyperplane(sub, node.getPlus(), in, out);
+                    characterizeSubHyperplane(sub, node.getMinus(), in, out);
+                } else {
+                    characterizeSubHyperplane(split.getPlus(), node.getPlus(), in, out);
+                    characterizeSubHyperplane(split.getMinus(), node.getMinus(), in, out);
+                }
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void initChildNode(final N parent, final N child, final boolean isPlus) {
+        super.initChildNode(parent, child, isPlus);
+
+        child.setLocation(isPlus ? RegionLocation.OUTSIDE : RegionLocation.INSIDE);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void invalidate() {
+        super.invalidate();
+
+        // clear cached region properties
+        boundarySize = UNKNOWN_SIZE;
+        regionSizeProperties = null;
+    }
+
+    /** {@link BSPTree.Node} implementation for use with {@link AbstractRegionBSPTree}s.
+     * @param <P> Point implementation type
+     * @param <N> BSP tree node implementation type
+     */
+    public abstract static class AbstractRegionNode<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+        extends AbstractBSPTree.AbstractNode<P, N> {
+
+        /** Serializable UID. */
+        private static final long serialVersionUID = 1L;
+
+        /** The location for the node. This will only be set on leaf nodes. */
+        private RegionLocation location;
+
+        /** Object representing the part of the node cut subhyperplane that lies on the
+         * region boundary. This is calculated lazily and is only present on internal nodes.
+         */
+        private RegionCutBoundary<P> cutBoundary;
+
+        /** Simple constructor.
+         * @param tree owning tree instance
+         */
+        protected AbstractRegionNode(AbstractBSPTree<P, N> tree) {
+            super(tree);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public AbstractRegionBSPTree<P, N> getTree() {
+            // cast to our parent tree type
+            return (AbstractRegionBSPTree<P, N>) super.getTree();
+        }
+
+        /** Get the location of the node. This value will only be non-null for
+         * leaf nodes.
+         * @return the location of the node; will be null for internal nodes
+         */
+        public RegionLocation getLocation() {
+            return isLeaf() ? location : null;
+        }
+
+        /** True if the node is a leaf node and has a location of {@link RegionLocation#INSIDE}.
+         * @return true if the node is a leaf node and has a location of
+         *      {@link RegionLocation#INSIDE}
+         */
+        public boolean isInside() {
+            return getLocation() == RegionLocation.INSIDE;
+        }
+
+        /** True if the node is a leaf node and has a location of {@link RegionLocation#OUTSIDE}.
+         * @return true if the node is a leaf node and has a location of
+         *      {@link RegionLocation#OUTSIDE}
+         */
+        public boolean isOutside() {
+            return getLocation() == RegionLocation.OUTSIDE;
+        }
+
+        /** Get the portion of the node's cut subhyperplane that lies on the boundary of the
+         * region.
+         * @return the portion of the node's cut subhyperplane that lies on the boundary of
+         *      the region
+         */
+        public RegionCutBoundary<P> getCutBoundary() {
+            if (!isLeaf()) {
+                checkValid();
+
+                if (cutBoundary == null) {
+                    cutBoundary = getTree().computeBoundary(getSelf());
+                }
+            }
+
+            return cutBoundary;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder();
+            sb.append(this.getClass().getSimpleName())
+                .append("[cut= ")
+                .append(getCut())
+                .append(", location= ")
+                .append(getLocation())
+                .append("]");
+
+            return sb.toString();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected void nodeInvalidated() {
+            super.nodeInvalidated();
+
+            // null any computed boundary value since it is no longer valid
+            cutBoundary = null;
+        }
+
+        /** Set the location attribute for the node.
+         * @param location the location attribute for the node
+         */
+        protected void setLocation(final RegionLocation location) {
+            this.location = location;
+        }
+
+        /** Get the value of the location property, unmodified based on the
+         * node's leaf state.
+         * @return the value of the location property
+         */
+        protected RegionLocation getLocationValue() {
+            return location;
+        }
+    }
+
+    /** Class containing the basic algorithm for merging region BSP trees.
+     * @param <P> Point implementation type
+     * @param <N> BSP tree node implementation type
+     */
+    public abstract static class RegionMergeOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+        extends AbstractBSPTreeMergeOperator<P, N> {
+
+        /** Merge two input trees, storing the output in the third. The output tree can be one of the
+         * input trees. The output tree is condensed before the method returns.
+         * @param inputTree1 first input tree
+         * @param inputTree2 second input tree
+         * @param outputTree the tree that will contain the result of the merge; may be one
+         *      of the input trees
+         */
+        public void apply(final AbstractRegionBSPTree<P, N> inputTree1, final AbstractRegionBSPTree<P, N> inputTree2,
+                final AbstractRegionBSPTree<P, N> outputTree) {
+
+            this.performMerge(inputTree1, inputTree2, outputTree);
+
+            outputTree.condense();
+        }
+    }
+
+    /** Class for performing boolean union operations on region trees.
+     * @param <P> Point implementation type
+     * @param <N> BSP tree node implementation type
+     */
+    public static class UnionOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+        extends RegionMergeOperator<P, N> {
+
+        /** {@inheritDoc} */
+        @Override
+        protected N mergeLeaf(final N node1, final N node2) {
+            if (node1.isLeaf()) {
+                return node1.isInside() ? node1 : node2;
+            }
+
+            // call again with flipped arguments
+            return mergeLeaf(node2, node1);
+        }
+    }
+
+    /** Class for performing boolean intersection operations on region trees.
+     * @param <P> Point implementation type
+     * @param <N> BSP tree node implementation type
+     */
+    public static class IntersectionOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+        extends RegionMergeOperator<P, N> {
+
+        /** {@inheritDoc} */
+        @Override
+        protected N mergeLeaf(final N node1, final N node2) {
+            if (node1.isLeaf()) {
+                return node1.isInside() ? node2 : node1;
+            }
+
+            // call again with flipped arguments
+            return mergeLeaf(node2, node1);
+        }
+    }
+
+    /** Class for performing boolean difference operations on region trees.
+     * @param <P> Point implementation type
+     * @param <N> BSP tree node implementation type
+     */
+    public static class DifferenceOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+        extends RegionMergeOperator<P, N> {
+
+        /** {@inheritDoc} */
+        @Override
+        protected N mergeLeaf(final N node1, final N node2) {
+            // a region is included if it belongs in tree1 and is not in tree2
+
+            if (node1.isInside()) {
+                // this region is inside of tree1, so only include subregions that are
+                // not in tree2, ie include everything in node2's complement
+                final N output = outputSubtree(node2);
+                output.getTree().complementRecursive(output);
+
+                return output;
+            } else if (node2.isInside()) {
+                // this region is inside of tree2 and so cannot be in the result region
+                final N output = outputNode();
+                output.setLocation(RegionLocation.OUTSIDE);
+
+                return output;
+            }
+
+            // this region is not in tree2, so we can include everything in tree1
+            return node1;
+        }
+    }
+
+    /** Class for performing boolean symmetric difference (xor) operations on region trees.
+     * @param <P> Point implementation type
+     * @param <N> BSP tree node implementation type
+     */
+    public static class XorOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+        extends RegionMergeOperator<P, N> {
+
+        /** {@inheritDoc} */
+        @Override
+        protected N mergeLeaf(final N node1, final N node2) {
+            // a region is included if it belongs in tree1 and is not in tree2 OR
+            // it belongs in tree2 and is not in tree1
+
+            if (node1.isLeaf()) {
+                if (node1.isInside()) {
+                    // this region is inside node1, so only include subregions that are
+                    // not in node2, ie include everything in node2's complement
+                    final N output = outputSubtree(node2);
+                    output.getTree().complementRecursive(output);
+
+                    return output;
+                } else {
+                    // this region is not in node1, so only include subregions that
+                    // in node2
+                    return node2;
+                }
+            }
+
+            // the operation is symmetric, so perform the same operation but with the
+            // nodes flipped
+            return mergeLeaf(node2, node1);
+        }
+    }
+
+    /** Class used to compute the point on the region's boundary that is closest to a target point.
+     * @param <P> Point implementation type
+     * @param <N> BSP tree node implementation type
+     */
+    protected static class BoundaryProjector<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+        extends ClosestFirstVisitor<P, N> {
+
+        /** Serializable UID. */
+        private static final long serialVersionUID = 20190504L;
+
+        /** The projected point. */
+        private P projected;
+
+        /** The current closest distance to the boundary found. */
+        private double minDist = -1.0;
+
+        /** Simple constructor.
+         * @param point the point to project onto the region's boundary
+         */
+        public BoundaryProjector(final P point) {
+            super(point);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void visit(final N node) {
+            final P point = getTarget();
+
+            if (node.isInternal() && (minDist < 0.0 || isPossibleClosestCut(node.getCut(), point, minDist))) {
+                final RegionCutBoundary<P> boundary = node.getCutBoundary();
+                final P boundaryPt = boundary.closest(point);
+
+                final double dist = boundaryPt.distance(point);
+                final int cmp = Double.compare(dist, minDist);
+
+                if (minDist < 0.0 || cmp < 0) {
+                    projected = boundaryPt;
+                    minDist = dist;
+                } else if (cmp == 0) {
+                    // the two points are the _exact_ same distance from the reference point, so use
+                    // a separate method to disambiguate them
+                    projected = disambiguateClosestPoint(point, projected, boundaryPt);
+                }
+            }
+        }
+
+        /** Return true if the given node cut subhyperplane is a possible candidate for containing the
+         * closest region boundary point to the target.
+         * @param cut the node cut subhyperplane to test
+         * @param target the target point being projected
+         * @param currentMinDist the smallest distance found so far to a region boundary; this value is guaranteed
+         *      to be non-negative
+         * @return true if the subhyperplane is a possible candidate for containing the closest region
+         *      boundary point to the target
+         */
+        protected boolean isPossibleClosestCut(final SubHyperplane<P> cut, final P target,
+                final double currentMinDist) {
+            return Math.abs(cut.getHyperplane().offset(target)) <= currentMinDist;
+        }
+
+        /** Method used to determine which of points {@code a} and {@code b} should be considered
+         * as the "closest" point to {@code target} when the points are exactly equidistant.
+         * @param target the target point
+         * @param a first point to consider
+         * @param b second point to consider
+         * @return which of {@code a} or {@code b} should be considered as the one closest to
+         *      {@code target}
+         */
+        protected P disambiguateClosestPoint(final P target, final P a, final P b) {
+            return a;
+        }
+
+        /** Get the projected point on the region's boundary, or null if no point could be found.
+         * @return the projected point on the region's boundary
+         */
+        public P getProjected() {
+            return projected;
+        }
+    }
+
+    /** Class containing the primary size-related properties of a region. These properties
+     * are typically computed at the same time, so this class serves to encapsulate the result
+     * of the combined computation.
+     * @param <P> Point implementation type
+     */
+    protected static class RegionSizeProperties<P extends Point<P>> implements Serializable {
+
+        /** Serializable UID. */
+        private static final long serialVersionUID = 20190428L;
+
+        /** The size of the region. */
+        private final double size;
+
+        /** The barycenter of the region. */
+        private final P barycenter;
+
+        /** Simple constructor.
+         * @param size the region size
+         * @param barycenter the region barycenter
+         */
+        public RegionSizeProperties(final double size, final P barycenter) {
+            this.size = size;
+            this.barycenter = barycenter;
+        }
+
+        /** Get the size of the region.
+         * @return the size of the region
+         */
+        public double getSize() {
+            return size;
+        }
+
+        /** Get the barycenter of the region.
+         * @return the barycenter of the region
+         */
+        public P getBarycenter() {
+            return barycenter;
+        }
+    }
+
+    /** Class that iterates over the boundary convex subhyperplanes from a set of region nodes.
+     * @param <P> Point implementation type
+     * @param <C> Boundary convex subhyperplane implementation type
+     * @param <N> BSP tree node implementation type
+     */
+    protected static final class RegionBoundaryIterator<
+            P extends Point<P>,
+            C extends ConvexSubHyperplane<P>,
+            N extends AbstractRegionNode<P, N>>
+        extends IteratorTransform<N, C> {
+
+        /** Function that converts from the convex subhyperplane type to the output type. */
+        private final Function<ConvexSubHyperplane<P>, C> typeConverter;
+
+        /** Simple constructor.
+         * @param inputIterator iterator that will provide all nodes in the tree
+         * @param typeConverter function that converts from the convex subhyperplane type to the output type
+         */
+        private RegionBoundaryIterator(final Iterator<N> inputIterator,
+                final Function<ConvexSubHyperplane<P>, C> typeConverter) {
+            super(inputIterator);
+
+            this.typeConverter = typeConverter;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected void acceptInput(final N input) {
+            if (input.isInternal()) {
+                final RegionCutBoundary<P> cutBoundary = input.getCutBoundary();
+
+                final SubHyperplane<P> outsideFacing = cutBoundary.getOutsideFacing();
+                final SubHyperplane<P> insideFacing = cutBoundary.getInsideFacing();
+
+                if (outsideFacing != null && !outsideFacing.isEmpty()) {
+                    for (ConvexSubHyperplane<P> boundary : outsideFacing.toConvex()) {
+
+                        addOutput(typeConverter.apply(boundary));
+                    }
+                }
+                if (insideFacing != null && !insideFacing.isEmpty()) {
+                    for (ConvexSubHyperplane<P> boundary : insideFacing.toConvex()) {
+                        ConvexSubHyperplane<P> reversed = boundary.reverse();
+
+                        addOutput(typeConverter.apply(reversed));
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AttributeBSPTree.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AttributeBSPTree.java
new file mode 100644
index 0000000..d7fecd6
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AttributeBSPTree.java
@@ -0,0 +1,144 @@
+/*
+ * 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.bsp;
+
+import org.apache.commons.geometry.core.Point;
+
+/** Simple {@link BSPTree} implementation allowing arbitrary values to be
+ * associated with each node.
+ * @param <P> Point implementation type
+ * @param <T> Tree node attribute type
+ */
+public class AttributeBSPTree<P extends Point<P>, T>
+    extends AbstractBSPTree<P, AttributeBSPTree.AttributeNode<P, T>> {
+
+    /** Serializable UID. */
+    private static final long serialVersionUID = 20190306L;
+
+    /** The initial attribute value to use for newly created nodes. */
+    private final T initialNodeAttribute;
+
+    /** Create a new tree instance. New nodes in the tree are given an attribute
+     * of null.
+     */
+    public AttributeBSPTree() {
+        this(null);
+    }
+
+    /** Create a new tree instance. New nodes in the tree are assigned the given
+     * initial attribute value.
+     * @param initialNodeAttribute The attribute value to assign to newly created nodes.
+     */
+    public AttributeBSPTree(T initialNodeAttribute) {
+        this.initialNodeAttribute = initialNodeAttribute;
+
+        this.getRoot().setAttribute(initialNodeAttribute);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected AttributeNode<P, T> createNode() {
+        return new AttributeNode<P, T>(this);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void initChildNode(final AttributeNode<P, T> parent, final AttributeNode<P, T> child,
+            final boolean isPlus) {
+        super.initChildNode(parent, child, isPlus);
+
+        child.setAttribute(initialNodeAttribute);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void copyNodeProperties(final AttributeNode<P, T> src, final AttributeNode<P, T> dst) {
+        dst.setAttribute(src.getAttribute());
+    }
+
+    /** {@link BSPTree.Node} implementation for use with {@link AttributeBSPTree}s.
+     * @param <P> Point implementation type
+     * @param <T> Tree node attribute type
+     */
+    public static class AttributeNode<P extends Point<P>, T>
+        extends AbstractBSPTree.AbstractNode<P, AttributeNode<P, T>> {
+
+        /** Serializable UID. */
+        private static final long serialVersionUID = 1L;
+
+        /** The node attribute. */
+        private T attribute;
+
+        /** Simple constructor.
+         * @param tree the owning tree; this must be an instance of {@link AttributeBSPTree}
+         */
+        protected AttributeNode(AbstractBSPTree<P, AttributeNode<P, T>> tree) {
+            super(tree);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public AttributeBSPTree<P, T> getTree() {
+            // cast to our parent tree type
+            return (AttributeBSPTree<P, T>) super.getTree();
+        }
+
+        /** Get the attribute associated with this node.
+         * @return the attribute associated with this node
+         */
+        public T getAttribute() {
+            return attribute;
+        }
+
+        /** Set the attribute associated with this node.
+         * @param attribute the attribute to associate with this node
+         */
+        public void setAttribute(T attribute) {
+            this.attribute = attribute;
+        }
+
+        /** Set the attribute for this node. The node is returned.
+         * @param attributeValue attribute to set for the node
+         * @return the node instance
+         */
+        public AttributeNode<P, T> attr(final T attributeValue) {
+            setAttribute(attributeValue);
+
+            return this;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder();
+            sb.append(this.getClass().getSimpleName())
+                .append("[cut= ")
+                .append(getCut())
+                .append(", attribute= ")
+                .append(attribute)
+                .append("]");
+
+            return sb.toString();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected AttributeNode<P, T> getSelf() {
+            return this;
+        }
+    }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPSubtree.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPSubtree.java
new file mode 100644
index 0000000..5943765
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPSubtree.java
@@ -0,0 +1,53 @@
+/*
+ * 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.bsp;
+
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import org.apache.commons.geometry.core.Point;
+
+/** Interface for types that form the root of BSP subtrees. This includes trees
+ * themselves as well as each node in a tree.
+ * @param <P> Point implementation type
+ * @param <N> Node implementation type
+ */
+public interface BSPSubtree<P extends Point<P>, N extends BSPTree.Node<P, N>> extends Iterable<N> {
+
+    /** Return the total number of nodes in the subtree.
+     * @return the total number of nodes in the subtree.
+     */
+    int count();
+
+    /** The height of the subtree, ie the length of the longest downward path from
+     * the subtree root to a leaf node. A leaf node has a height of 0.
+     * @return the height of the subtree.
+     */
+    int height();
+
+    /** Accept a visitor instance, calling it with each node from the subtree.
+     * @param visitor visitor called with each subtree node
+     */
+    void accept(BSPTreeVisitor<P, N> visitor);
+
+    /** Create a stream over the nodes in this subtree.
+     * @return a stream for accessing the nodes in this subtree
+     */
+    default Stream<N> stream() {
+        return StreamSupport.stream(spliterator(), false);
+    }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTree.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTree.java
new file mode 100644
index 0000000..758ca90
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTree.java
@@ -0,0 +1,225 @@
+/*
+ * 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.bsp;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+
+/** Interface for Binary Space Partitioning (BSP) trees.
+ * @param <P> Point implementation type
+ * @param <N> Node implementation type
+ */
+public interface BSPTree<P extends Point<P>, N extends BSPTree.Node<P, N>>
+    extends BSPSubtree<P, N> {
+
+    /** Enum specifying possible behaviors when a point used to locate a node
+     * falls directly on the cut subhyperplane of an internal node.
+     */
+    enum NodeCutRule {
+
+        /** Choose the minus child of the internal node and continue searching.
+         * This behavior will result in a leaf node always being returned by the
+         * node search.
+         */
+        MINUS,
+
+        /** Choose the plus child of the internal node and continue searching.
+         * This behavior will result in a leaf node always being returned by the
+         * node search.
+         */
+        PLUS,
+
+        /** Choose the internal node and stop searching. This behavior may result
+         * in non-leaf nodes being returned by the node search.
+         */
+        NODE
+    }
+
+    /** Get the root node of the tree.
+     * @return the root node of the tree
+     */
+    N getRoot();
+
+    /** Find a node in this subtree containing the given point or its interior or boundary.
+     * When a point lies directly on the cut of an internal node, the minus child of the
+     * cut is chosen. This is equivalent to {@code subtree.findNode(pt, NodeCutRule.MINUS)}
+     * and always returns a leaf node.
+     * @param pt test point used to locate a node in the tree
+     * @return leaf node containing the point on its interior or boundary
+     * @see #findNode(Point, NodeCutRule)
+     */
+    default N findNode(P pt) {
+        return findNode(pt, NodeCutRule.MINUS);
+    }
+
+    /** Find a node in this subtree containing the given point on it interior or boundary. The
+     * search should always return a leaf node except in the cases where the given point lies
+     * exactly on the cut subhyperplane of an internal node. In this case, it is unclear whether
+     * the search should continue with the minus child, the plus child, or end with the internal
+     * node. The {@code cutRule} argument specifies what should happen in this case.
+     * <ul>
+     *      <li>{@link NodeCutRule#MINUS} - continue the search in the minus subtree</li>
+     *      <li>{@link NodeCutRule#PLUS} - continue the search in the plus subtree</li>
+     *      <li>{@link NodeCutRule#NODE} - stop the search and return the internal node</li>
+     * </ul>
+     * @param pt test point used to locate a node in the tree
+     * @param cutRule value used to determine the search behavior when the test point lies
+     *      exactly on the cut subhyperplane of an internal node
+     * @return node containing the point on its interior or boundary
+     * @see #findNode(Point)
+     */
+    N findNode(P pt, NodeCutRule cutRule);
+
+    /** Insert a subhyperplane into the tree.
+     * @param sub the subhyperplane to insert into the tree
+     */
+    void insert(SubHyperplane<P> sub);
+
+    /** Insert a convex subhyperplane into the tree.
+     * @param convexSub the convex subhyperplane to insert into the tree
+     */
+    void insert(ConvexSubHyperplane<P> convexSub);
+
+    /** Insert a set of convex subhyperplanes into the tree.
+     * @param convexSubs iterable containing a collection of subhyperplanes
+     *      to insert into the tree
+     */
+    void insert(Iterable<? extends ConvexSubHyperplane<P>> convexSubs);
+
+    /** Make the current instance a deep copy of the argument.
+     * @param src the tree to copy
+     */
+    void copy(BSPTree<P, N> src);
+
+    /** Set this instance to the region represented by the given node. The
+     * given node could have come from this tree or a different tree.
+     * @param node the node to extract
+     */
+    void extract(N node);
+
+    /** Transform this tree. Each cut subhyperplane in the tree is transformed in place using
+     * the given {@link Transform}.
+     * @param transform the transform to apply
+     */
+    void transform(Transform<P> transform);
+
+    /** Interface for Binary Space Partitioning (BSP) tree nodes.
+     * @param <P> Point type
+     * @param <N> BSP tree node implementation type
+     */
+    interface Node<P extends Point<P>, N extends Node<P, N>> extends BSPSubtree<P, N> {
+
+        /** Get the {@link BSPTree} that owns the node.
+         * @return the owning tree
+         */
+        BSPTree<P, N> getTree();
+
+        /** Get the depth of the node in the tree. The root node of the tree
+         * has a depth of 0.
+         * @return the depth of the node in the tree
+         */
+        int depth();
+
+        /** Get the parent of the node. This will be null if the node is the
+         * root of the tree.
+         * @return the parent node for this instance
+         */
+        N getParent();
+
+        /** Get the cut for the node. This is a convex subhyperplane that splits
+         * the region for the cell into two disjoint regions, namely the plus and
+         * minus regions. This will be null for leaf nodes.
+         * @see #getPlus()
+         * @see #getMinus()
+         * @return the cut subhyperplane for the cell
+         */
+        ConvexSubHyperplane<P> getCut();
+
+        /** Get the hyperplane belonging to the node cut, if it exists.
+         * @return the hyperplane belonging to the node cut, or null if
+         *      the node does not have a cut
+         * @see #getCut()
+         */
+        Hyperplane<P> getCutHyperplane();
+
+        /** Get the node for the minus region of the cell. This will be null if the
+         * node has not been cut, ie if it is a leaf node.
+         * @return the node for the minus region of the cell
+         */
+        N getMinus();
+
+        /** Get the node for the plus region of the cell. This will be null if the
+         * node has not been cut, ie if it is a leaf node.
+         * @return the node for the plus region of the cell
+         */
+        N getPlus();
+
+        /** Return true if the node is a leaf node, meaning that it has no
+         * binary partitioner (aka "cut") and therefore no child nodes.
+         * @return true if the node is a leaf node
+         */
+        boolean isLeaf();
+
+        /** Return true if the node is an internal node, meaning that is
+         * has a binary partitioner (aka "cut") and therefore two child nodes.
+         * @return true if the node is an internal node
+         */
+        boolean isInternal();
+
+        /** Return true if the node has a parent and is the parent's minus
+         * child.
+         * @return true if the node is the minus child of its parent
+         */
+        boolean isMinus();
+
+        /** Return true if the node has a parent and is the parent's plus
+         * child.
+         * @return true if the node is the plus child of its parent
+         */
+        boolean isPlus();
+
+        /** Insert a cut into this node. If the given hyperplane intersects
+         * this node's region, then the node's cut is set to the {@link ConvexSubHyperplane}
+         * representing the intersection, new plus and minus child leaf nodes
+         * are assigned, and true is returned. If the hyperplane does not intersect
+         * the node's region, then the node's cut and plus and minus child references
+         * are all set to null (ie, it becomes a leaf node) and false is returned. In
+         * either case, any existing cut and/or child nodes are removed by this method.
+         * @param cutter the hyperplane to cut the node's region with
+         * @return true if the cutting hyperplane intersected the node's region, resulting
+         *      in the creation of new child nodes
+         */
+        boolean insertCut(Hyperplane<P> cutter);
+
+        /** Remove the cut from this node. Returns true if the node previously had a cut.
+         * @return true if the node had a cut before the call to this method
+         */
+        boolean clearCut();
+
+        /** Cut this node with the given hyperplane. The same node is returned, regardless of
+         * the outcome of the cut operation. If the operation succeeded, then the node will
+         * have plus and minus child nodes.
+         * @param cutter the hyperplane to cut the node's region with
+         * @return this node
+         * @see #insertCut(Hyperplane)
+         */
+        N cut(Hyperplane<P> cutter);
+    }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreePrinter.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreePrinter.java
new file mode 100644
index 0000000..b5df252
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreePrinter.java
@@ -0,0 +1,118 @@
+/*
+ * 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.bsp;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTree.Node;
+
+/** Internal class for creating simple string representations of BSP trees.
+ * @param <P> Point implementation type
+ * @param <N> Node implementation type
+ */
+final class BSPTreePrinter<P extends Point<P>, N extends Node<P, N>>
+    implements BSPTreeVisitor<P, N> {
+
+    /** Line indent string. */
+    private static final String INDENT = "    ";
+
+    /** New line character. */
+    private static final String NEW_LINE = "\n";
+
+    /** Entry prefix for nodes on the minus side of their parent. */
+    private static final String MINUS_CHILD = "[-] ";
+
+    /** Entry prefix for nodes on the plus side of their parent. */
+    private static final String PLUS_CHILD = "[+] ";
+
+    /** Ellipsis for truncated representations. */
+    private static final String ELLIPSIS = "...";
+
+    /** Maximum depth of nodes that will be printed. */
+    private final int maxDepth;
+
+    /** Contains the string output. */
+    private final StringBuilder output = new StringBuilder();
+
+    /** Simple constructor.
+     * @param maxDepth maximum depth of nodes to be printed
+     */
+    BSPTreePrinter(final int maxDepth) {
+        this.maxDepth = maxDepth;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void visit(final N node) {
+        final int depth = node.depth();
+
+        if (depth <= maxDepth) {
+            startLine(node);
+            writeNode(node);
+        } else if (depth == maxDepth + 1 && node.isPlus()) {
+            startLine(node);
+            write(ELLIPSIS);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Order visitOrder(final N node) {
+        return Order.NODE_MINUS_PLUS;
+    }
+
+    /** Start a line for the given node.
+     * @param node the node to begin a line for
+     */
+    private void startLine(final N node) {
+        if (node.getParent() != null) {
+            write(NEW_LINE);
+        }
+
+        final int depth = node.depth();
+        for (int i = 0; i < depth; ++i) {
+            write(INDENT);
+        }
+    }
+
+    /** Writes the given node to the output.
+     * @param node the node to write
+     */
+    private void writeNode(final N node) {
+        if (node.getParent() != null) {
+            if (node.isMinus()) {
+                write(MINUS_CHILD);
+            } else {
+                write(PLUS_CHILD);
+            }
+        }
+
+        write(node.toString());
+    }
+
+    /** Add the given string to the output.
+     * @param str the string to add
+     */
+    private void write(String str) {
+        output.append(str);
+    }
+
+    /** Return the string representation of the visited tree. */
+    @Override
+    public String toString() {
+        return output.toString();
+    }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitor.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitor.java
new file mode 100644
index 0000000..8fc0ffd
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitor.java
@@ -0,0 +1,173 @@
+/*
+ * 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.bsp;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Point;
+
+/** Interface for visiting the nodes in a {@link BSPTree} or {@link BSPSubtree}.
+ * @param <P> Point implementation type
+ * @param <N> BSP tree node implementation type
+ */
+@FunctionalInterface
+public interface BSPTreeVisitor<P extends Point<P>, N extends BSPTree.Node<P, N>> {
+
+    /** Enum used to specify the order in which visitors should visit the nodes
+     * in the tree.
+     */
+    enum Order {
+
+        /** Indicates that the visitor should first visit the plus sub-tree, then
+         * the minus sub-tree and then the current node.
+         */
+        PLUS_MINUS_NODE,
+
+        /** Indicates that the visitor should first visit the plus sub-tree, then
+         * the current node, and then the minus sub-tree.
+         */
+        PLUS_NODE_MINUS,
+
+        /** Indicates that the visitor should first visit the minus sub-tree, then
+         * the plus sub-tree, and then the current node.
+         */
+        MINUS_PLUS_NODE,
+
+        /** Indicates that the visitor should first visit the minus sub-tree, then the
+         * current node, and then the plus sub-tree.
+         */
+        MINUS_NODE_PLUS,
+
+        /** Indicates that the visitor should first visit the current node, then the
+         * plus sub-tree, and then the minus sub-tree.
+         */
+        NODE_PLUS_MINUS,
+
+        /** Indicates that the visitor should first visit the current node, then the
+         * minus sub-tree, and then the plus sub-tree.
+         */
+        NODE_MINUS_PLUS;
+    }
+
+    /** Visit a node in a BSP tree. This method is called for both internal nodes and
+     * leaf nodes.
+     * @param node the node being visited
+     */
+    void visit(N node);
+
+    /** Determine the visit order for the given internal node. This is called for each
+     * internal node before {@link #visit(BSPTree.Node)} is called. Returning null from
+     * this method skips the subtree rooted at the given node. This method is not called
+     * on leaf nodes.
+     * @param internalNode the internal node to determine the visit order for
+     * @return the order that the subtree rooted at the given node should be visited
+     */
+    default Order visitOrder(final N internalNode) {
+        return Order.NODE_MINUS_PLUS;
+    }
+
+    /** Abstract class for {@link BSPTreeVisitor} implementations that base their visit
+     * ordering on a target point.
+     * @param <P> Point implementation type
+     * @param <N> BSP tree node implementation type
+     */
+    abstract class TargetPointVisitor<P extends Point<P>, N extends BSPTree.Node<P, N>>
+        implements BSPTreeVisitor<P, N>, Serializable {
+
+        /** Serializable UID. */
+        private static final long serialVersionUID = 20190504L;
+
+        /** Point serving as the target of the traversal. */
+        private final P target;
+
+        /** Simple constructor.
+         * @param target the point serving as the target for the tree traversal
+         */
+        public TargetPointVisitor(final P target) {
+            this.target = target;
+        }
+
+        /** Get the target point for the tree traversal.
+         * @return the target point for the tree traversal
+         */
+        public P getTarget() {
+            return target;
+        }
+    }
+
+    /** {@link BSPTreeVisitor} base class that orders tree nodes so that nodes closest to the target point are
+     * visited first. This is done by choosing {@link Order#MINUS_NODE_PLUS}
+     * when the target point lies on the minus side of the node's cut hyperplane and {@link Order#PLUS_NODE_MINUS}
+     * when it lies on the plus side. The order {@link Order#MINUS_NODE_PLUS} order is used when
+     * the target point lies directly on the node's cut hyerplane and no child node is closer than the other.
+     * @param <P> Point implementation type
+     * @param <N> BSP tree node implementation type
+     */
+    abstract class ClosestFirstVisitor<P extends Point<P>, N extends BSPTree.Node<P, N>>
+        extends TargetPointVisitor<P, N> {
+
+        /** Serializable UID. */
+        private static final long serialVersionUID = 20190504L;
+
+        /** Simple constructor.
+         * @param target the point serving as the target for the traversal
+         */
+        public ClosestFirstVisitor(final P target) {
+            super(target);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Order visitOrder(N node) {
+            if (node.getCutHyperplane().offset(getTarget()) > 0.0) {
+                return Order.PLUS_NODE_MINUS;
+            }
+            return Order.MINUS_NODE_PLUS;
+        }
+    }
+
+    /** {@link BSPTreeVisitor} base class that orders tree nodes so that nodes farthest from the target point
+     * are traversed first. This is done by choosing {@link Order#PLUS_NODE_MINUS}
+     * when the target point lies on the minus side of the node's cut hyperplane and {@link Order#MINUS_NODE_PLUS}
+     * when it lies on the plus side. The order {@link Order#MINUS_NODE_PLUS} order is used when
+     * the target point lies directly on the node's cut hyerplane and no child node is closer than the other.
+     * @param <P> Point implementation type
+     * @param <N> BSP tree node implementation type
+     */
+    abstract class FarthestFirstVisitor<P extends Point<P>, N extends BSPTree.Node<P, N>>
+        extends TargetPointVisitor<P, N> {
+
+        /** Serializable UID. */
+        private static final long serialVersionUID = 20190504L;
+
+        /** Simple constructor.
+         * @param target the point serving as the target for the traversal
+         */
+        public FarthestFirstVisitor(final P target) {
+            super(target);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Order visitOrder(N node) {
+            if (node.getCutHyperplane().offset(getTarget()) < 0.0) {
+                return Order.PLUS_NODE_MINUS;
+            }
+            return Order.MINUS_NODE_PLUS;
+        }
+    }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/RegionCutBoundary.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/RegionCutBoundary.java
new file mode 100644
index 0000000..a165381
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/RegionCutBoundary.java
@@ -0,0 +1,109 @@
+/*
+ * 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.bsp;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+
+/** Class representing the portion of an
+ * {@link AbstractRegionBSPTree.AbstractRegionNode AbstractRegionNode}'s cut subhyperplane that
+ * lies on the boundary of the region. Portions of this subhyperplane
+ * may be oriented so that the plus side of the subhyperplane points toward
+ * the outside of the region ({@link #getOutsideFacing()}) and other portions
+ * of the same subhyperplane may be oriented so that the plus side points
+ * toward the inside of the region ({@link #getInsideFacing()}).
+ *
+ * @param <P> Point implementation type
+ */
+public final class RegionCutBoundary<P extends Point<P>> implements Serializable {
+
+    /** Serializable UID. */
+    private static final long serialVersionUID = 20190310L;
+
+    /** Portion of the region cut subhyperplane with its plus side facing the
+     * inside of the region.
+     */
+    private final SubHyperplane<P> insideFacing;
+
+    /** Portion of the region cut subhyperplane with its plus side facing the
+     * outside of the region.
+     */
+    private final SubHyperplane<P> outsideFacing;
+
+    /** Simple constructor.
+     * @param insideFacing portion of the region cut subhyperplane with its plus side facing the
+     *      inside of the region
+     * @param outsideFacing portion of the region cut subhyperplane with its plus side facing the
+     *      outside of the region
+     */
+    public RegionCutBoundary(final SubHyperplane<P> insideFacing, final SubHyperplane<P> outsideFacing) {
+        this.insideFacing = insideFacing;
+        this.outsideFacing = outsideFacing;
+    }
+
+    /** Get the portion of the region cut subhyperplane with its plus side facing the
+     * inside of the region.
+     * @return the portion of the region cut subhyperplane with its plus side facing the
+     *      inside of the region
+     */
+    public SubHyperplane<P> getInsideFacing() {
+        return insideFacing;
+    }
+
+    /** Get the portion of the region cut subhyperplane with its plus side facing the
+     * outside of the region.
+     * @return the portion of the region cut subhyperplane with its plus side facing the
+     *      outside of the region
+     */
+    public SubHyperplane<P> getOutsideFacing() {
+        return outsideFacing;
+    }
+
+    /** Return the closest point to the argument in the inside and outside facing
+     * portions of the cut boundary.
+     * @param pt the reference point
+     * @return the point in the cut boundary closest to the reference point
+     * @see SubHyperplane#closest(Point)
+     */
+    public P closest(final P pt) {
+        final P insideFacingPt = (insideFacing != null) ? insideFacing.closest(pt) : null;
+        final P outsideFacingPt = (outsideFacing != null) ? outsideFacing.closest(pt) : null;
+
+        if (insideFacingPt != null && outsideFacingPt != null) {
+            if (pt.distance(insideFacingPt) < pt.distance(outsideFacingPt)) {
+                return insideFacingPt;
+            }
+            return outsideFacingPt;
+        } else if (insideFacingPt != null) {
+            return insideFacingPt;
+        }
+        return outsideFacingPt;
+    }
+
+    /** Return true if the given point is contained in the boundary, in either the
+     * inside facing portion or the outside facing portion.
+     * @param pt point to test
+     * @return true if the point is contained in the boundary
+     * @see SubHyperplane#contains(Point)
+     */
+    public boolean contains(final P pt) {
+        return (insideFacing != null && insideFacing.contains(pt)) ||
+                (outsideFacing != null && outsideFacing.contains(pt));
+    }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/package-info.java
similarity index 63%
rename from commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
rename to commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/package-info.java
index 046defe..b94442a 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/package-info.java
@@ -14,23 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.commons.geometry.core.partitioning;
-
-/** Enumerate representing the location of an element with respect to an
- * {@link Hyperplane hyperplane} of a space.
+/**
+ *
+ * <p>
+ * This package contains classes related to Binary Space Partitioning (BSP) trees. BSP
+ * tree are data structures that allow arbitrary partitioning of spaces using hyperplanes.
+ * </p>
  */
-public enum Side {
-
-    /** Code for the plus side of the hyperplane. */
-    PLUS,
-
-    /** Code for the minus side of the hyperplane. */
-    MINUS,
-
-    /** Code for elements crossing the hyperplane from plus to minus side. */
-    BOTH,
-
-    /** Code for the hyperplane itself. */
-    HYPER;
-
-}
+package org.apache.commons.geometry.core.partitioning.bsp;
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/package-info.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/package-info.java
index cfb55c0..b0e4967 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/package-info.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/package-info.java
@@ -16,99 +16,8 @@
  */
 /**
  *
- * This package provides classes to implement Binary Space Partition trees.
- *
- * <p>
- * {@link org.apache.commons.geometry.core.partitioning.BSPTree BSP trees}
- * are an efficient way to represent parts of space and in particular
- * polytopes (line segments in 1D, polygons in 2D and polyhedrons in 3D)
- * and to operate on them. The main principle is to recursively subdivide
- * the space using simple hyperplanes (points in 1D, lines in 2D, planes
- * in 3D).
- * </p>
- *
- * <p>
- * We start with a tree composed of a single node without any cut
- * hyperplane: it represents the complete space, which is a convex
- * part. If we add a cut hyperplane to this node, this represents a
- * partition with the hyperplane at the node level and two half spaces at
- * each side of the cut hyperplane. These half-spaces are represented by
- * two child nodes without any cut hyperplanes associated, the plus child
- * which represents the half space on the plus side of the cut hyperplane
- * and the minus child on the other side. Continuing the subdivisions, we
- * end up with a tree having internal nodes that are associated with a
- * cut hyperplane and leaf nodes without any hyperplane which correspond
- * to convex parts.
- * </p>
- *
- * <p>
- * When BSP trees are used to represent polytopes, the convex parts are
- * known to be completely inside or outside the polytope as long as there
- * is no facet in the part (which is obviously the case if the cut
- * hyperplanes have been chosen as the underlying hyperplanes of the
- * facets (this is called an autopartition) and if the subdivision
- * process has been continued until all facets have been processed. It is
- * important to note that the polytope is <em>not</em> defined by a
- * single part, but by several convex ones. This is the property that
- * allows BSP-trees to represent non-convex polytopes despites all parts
- * are convex. The {@link
- * org.apache.commons.geometry.core.partitioning.Region Region} class is
- * devoted to this representation, it is build on top of the {@link
- * org.apache.commons.geometry.core.partitioning.BSPTree BSPTree} class using
- * boolean objects as the leaf nodes attributes to represent the
- * inside/outside property of each leaf part, and also adds various
- * methods dealing with boundaries (i.e. the separation between the
- * inside and the outside parts).
- * </p>
- *
  * <p>
- * Rather than simply associating the internal nodes with an hyperplane,
- * we consider <em>sub-hyperplanes</em> which correspond to the part of
- * the hyperplane that is inside the convex part defined by all the
- * parent nodes (this implies that the sub-hyperplane at root node is in
- * fact a complete hyperplane, because there is no parent to bound
- * it). Since the parts are convex, the sub-hyperplanes are convex, in
- * 3D the convex parts are convex polyhedrons, and the sub-hyperplanes
- * are convex polygons that cut these polyhedrons in two
- * sub-polyhedrons. Using this definition, a BSP tree completely
- * partitions the space. Each point either belongs to one of the
- * sub-hyperplanes in an internal node or belongs to one of the leaf
- * convex parts.
+ * This package contains code related to partitioning of spaces by hyperplanes.
  * </p>
- *
- * <p>
- * In order to determine where a point is, it is sufficient to check its
- * position with respect to the root cut hyperplane, to select the
- * corresponding child tree and to repeat the procedure recursively,
- * until either the point appears to be exactly on one of the hyperplanes
- * in the middle of the tree or to be in one of the leaf parts. For
- * this operation, it is sufficient to consider the complete hyperplanes,
- * there is no need to check the points with the boundary of the
- * sub-hyperplanes, because this check has in fact already been realized
- * by the recursive descent in the tree. This is very easy to do and very
- * efficient, especially if the tree is well balanced (the cost is
- * <code>O(log(n))</code> where <code>n</code> is the number of facets)
- * or if the first tree levels close to the root discriminate large parts
- * of the total space.
- * </p>
- *
- * <p>
- * One of the main sources for the development of this package was Bruce
- * Naylor, John Amanatides and William Thibault paper <a
- * href="http://www.cs.yorku.ca/~amana/research/bsptSetOp.pdf">Merging
- * BSP Trees Yields Polyhedral Set Operations</a> Proc. Siggraph '90,
- * Computer Graphics 24(4), August 1990, pp 115-124, published by the
- * Association for Computing Machinery (ACM). The same paper can also be
- * found <a
- * href="http://www.cs.utexas.edu/users/fussell/courses/cs384g/bsp_treemerge.pdf">here</a>.
- * </p>
- *
- * <p>
- * Note that the interfaces defined in this package are <em>not</em> intended to
- * be implemented by Apache Commons Math users, they are only intended to be
- * implemented within the library itself. New methods may be added even for
- * minor versions, which breaks compatibility for external implementations.
- * </p>
- *
  */
 package org.apache.commons.geometry.core.partitioning;
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/precision/DoublePrecisionContext.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/precision/DoublePrecisionContext.java
index 6773837..7ca33ab 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/precision/DoublePrecisionContext.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/precision/DoublePrecisionContext.java
@@ -23,7 +23,7 @@ import java.util.Comparator;
  */
 public abstract class DoublePrecisionContext implements Comparator<Double>, Serializable {
 
-    /** Serializable identifier */
+    /** Serializable identifier. */
     private static final long serialVersionUID = 20190121L;
 
     /** Return true if the given values are considered equal to each other.
@@ -85,6 +85,22 @@ public abstract class DoublePrecisionContext implements Comparator<Double>, Seri
         return compare(a, b) >= 0;
     }
 
+    /** Return the sign of the argument: 0 if the value is considered equal to
+     * zero, -1 if less than 0, and +1 if greater than 0.
+     * @param a number to determine the sign of
+     * @return 0 if the number is considered equal to 0, -1 if less than
+     *      0, and +1 if greater than 0
+     */
+    public int sign(final double a) {
+        final int cmp = compare(a, 0.0);
+        if (cmp < 0) {
+            return -1;
+        } else if (cmp > 0) {
+            return 1;
+        }
+        return 0;
+    }
+
     /** {@inheritDoc} */
     @Override
     public int compare(final Double a, final Double b) {
@@ -110,7 +126,7 @@ public abstract class DoublePrecisionContext implements Comparator<Double>, Seri
      *      first is smaller than the second, {@code 1} is the first is larger
      *      than the second or either value is NaN.
      */
-    public abstract int compare(final double a, final double b);
+    public abstract int compare(double a, double b);
 
     /** Get the largest positive double value that is still considered equal
      * to zero by this instance.
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/precision/EpsilonDoublePrecisionContext.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/precision/EpsilonDoublePrecisionContext.java
index c26f502..0d69c8b 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/precision/EpsilonDoublePrecisionContext.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/precision/EpsilonDoublePrecisionContext.java
@@ -30,7 +30,7 @@ import org.apache.commons.numbers.core.Precision;
  */
 public class EpsilonDoublePrecisionContext extends DoublePrecisionContext {
 
-    /** Serializable identifier */
+    /** Serializable identifier. */
     private static final long serialVersionUID = 20190119L;
 
     /** Epsilon value. */
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
new file mode 100644
index 0000000..f15fcc8
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/EmbeddingTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.core;
+
+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.junit.Test;
+
+import org.junit.Assert;
+
+public class EmbeddingTest {
+
+    @Test
+    public void testToSubspace_collection_emptyInput() {
+        // arrange
+        TestLine line = TestLine.Y_AXIS;
+
+        // act
+        List<TestPoint1D> result = line.toSubspace(new ArrayList<>());
+
+        // assert
+        Assert.assertEquals(0, result.size());
+    }
+
+    @Test
+    public void testToSubspace_collection() {
+        // arrange
+        List<TestPoint2D> pts = Arrays.asList(
+                    new TestPoint2D(0, 0),
+                    new TestPoint2D(1, 0.25),
+                    new TestPoint2D(0.5, 1)
+                );
+
+        TestLine line = TestLine.Y_AXIS;
+
+        // act
+        List<TestPoint1D> result = line.toSubspace(pts);
+
+        // assert
+        Assert.assertEquals(3, result.size());
+        Assert.assertEquals(0, result.get(0).getX(), PartitionTestUtils.EPS);
+        Assert.assertEquals(0.25, result.get(1).getX(), PartitionTestUtils.EPS);
+        Assert.assertEquals(1, result.get(2).getX(), PartitionTestUtils.EPS);
+    }
+
+    @Test
+    public void testToSpace_collection_emptyInput() {
+        // arrange
+        TestLine line = TestLine.Y_AXIS;
+
+        // act
+        List<TestPoint2D> result = line.toSpace(new ArrayList<>());
+
+        // assert
+        Assert.assertEquals(0, result.size());
+    }
+
+    @Test
+    public void testToSpace_collection() {
+        // arrange
+        List<TestPoint1D> pts = Arrays.asList(
+                    new TestPoint1D(0),
+                    new TestPoint1D(1),
+                    new TestPoint1D(0.5)
+                );
+
+        TestLine line = TestLine.Y_AXIS;
+
+        // act
+        List<TestPoint2D> result = line.toSpace(pts);
+
+        // assert
+        Assert.assertEquals(3, result.size());
+        PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0), result.get(0));
+        PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 1), result.get(1));
+        PartitionTestUtils.assertPointsEqual(new TestPoint2D(0, 0.5), result.get(2));
+    }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/GeometryTestUtils.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/GeometryTestUtils.java
index 60bdeea..242e2d9 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/GeometryTestUtils.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/GeometryTestUtils.java
@@ -20,6 +20,7 @@ import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
+import java.util.regex.Pattern;
 
 import org.junit.Assert;
 
@@ -50,7 +51,7 @@ public class GeometryTestUtils {
      * @param exceptionType the expected exception type
      */
     public static void assertThrows(Runnable r, Class<?> exceptionType) {
-        assertThrows(r, exceptionType, null);
+        assertThrows(r, exceptionType, (String) null);
     }
 
     /** Asserts that the given Runnable throws an exception of the given type. If
@@ -77,6 +78,42 @@ public class GeometryTestUtils {
         }
     }
 
+    /** Asserts that the given Runnable throws an exception of the given type. If
+     * {@code pattern} is not null, the exception message is asserted to match the
+     * given regex.
+     * @param r the Runnable instance
+     * @param exceptionType the expected exception type
+     * @param pattern regex pattern to match; ignored if null
+     */
+    public static void assertThrows(Runnable r, Class<?> exceptionType, Pattern pattern) {
+        try {
+            r.run();
+            Assert.fail("Operation should have thrown an exception");
+        }
+        catch (Exception exc) {
+            Class<?> actualType = exc.getClass();
+
+            Assert.assertTrue("Expected exception of type " + exceptionType.getName() + " but was " + actualType.getName(),
+                    exceptionType.isAssignableFrom(actualType));
+
+            if (pattern != null) {
+                String message = exc.getMessage();
+
+                String err = "Expected exception message to match /" + pattern + "/ but was [" + message + "]";
+                Assert.assertTrue(err, pattern.matcher(message).matches());
+            }
+        }
+    }
+
+    /** Assert that a string contains a given substring value.
+     * @param substr
+     * @param actual
+     */
+    public static void assertContains(String substr, String actual) {
+        String msg = "Expected string to contain [" + substr + "] but was [" + actual + "]";
+        Assert.assertTrue(msg, actual.contains(substr));
+    }
+
     /**
      * Serializes and then recovers an object from a byte array. Returns the deserialized object.
      *
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/IteratorTransformTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/IteratorTransformTest.java
new file mode 100644
index 0000000..8eaab17
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/internal/IteratorTransformTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class IteratorTransformTest {
+
+    @Test
+    public void testIteration() {
+        // arrange
+        List<Integer> input = Arrays.asList(1, 2, 3, 4, 12, 13);
+
+        // act
+        List<String> result = toList(new EvenCharIterator(input.iterator()));
+
+        // assert
+        Assert.assertEquals(Arrays.asList("2", "4", "1", "2"), result);
+    }
+
+    @Test(expected = NoSuchElementException.class)
+    public void testThrowsNoSuchElement() {
+        // arrange
+        List<Integer> input = Arrays.asList();
+        EvenCharIterator it = new EvenCharIterator(input.iterator());
+
+        // act/assert
+        Assert.assertFalse(it.hasNext());
+        it.next();
+    }
+
+    private static <T> List<T> toList(Iterator<T> it) {
+        List<T> result = new ArrayList<>();
+        while (it.hasNext()) {
+            result.add(it.next());
+        }
+
+        return result;
+    }
+
+    private static class EvenCharIterator extends IteratorTransform<Integer, String>{
+
+        public EvenCharIterator(final Iterator<Integer> inputIterator) {
+            super(inputIterator);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected void acceptInput(Integer input) {
+            // filter out odd integers
+            int value = input.intValue();
+            if (value % 2 == 0) {
+                char[] chars = (value + "").toCharArray();
+
+                if (chars.length > 1) {
+                    List<String> strs = new ArrayList<>();
+                    for (char ch : chars) {
+                        strs.add(ch + "");
+                    }
+
+                    addAllOutput(strs);
+                }
+                else if (chars.length == 1) {
+                    addOutput(chars[0] + "");
+                }
+            }
+        }
+    }
+}
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/partition/test/PartitionTestUtils.java
new file mode 100644
index 0000000..107e5a7
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/PartitionTestUtils.java
@@ -0,0 +1,115 @@
+/*
+ * 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.partition.test;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTree;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTree.Node;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.junit.Assert;
+
+/** Class containing utility methods for tests related to the
+ * partition package.
+ */
+public class PartitionTestUtils {
+
+    public static final double EPS = 1e-6;
+
+    public static final DoublePrecisionContext PRECISION =
+            new EpsilonDoublePrecisionContext(EPS);
+
+    /**
+     * Asserts that corresponding values in the given points are equal.
+     * @param expected
+     * @param actual
+     */
+    public static void assertPointsEqual(TestPoint2D expected, TestPoint2D actual) {
+        String msg = "Expected points to equal " + expected + " but was " + actual + ";";
+        Assert.assertEquals(msg, expected.getX(), actual.getX(), EPS);
+        Assert.assertEquals(msg, expected.getY(), actual.getY(), EPS);
+    }
+
+    public static void assertSegmentsEqual(TestLineSegment expected, TestLineSegment actual) {
+        String msg = "Expected line segment to equal " + expected + " but was " + actual;
+
+        Assert.assertEquals(msg, expected.getStartPoint().getX(),
+                actual.getStartPoint().getX(), EPS);
+        Assert.assertEquals(msg, expected.getStartPoint().getY(),
+                actual.getStartPoint().getY(), EPS);
+
+        Assert.assertEquals(msg, expected.getEndPoint().getX(),
+                actual.getEndPoint().getX(), EPS);
+        Assert.assertEquals(msg, expected.getEndPoint().getY(),
+                actual.getEndPoint().getY(), EPS);
+    }
+
+    public static void assertIsInternalNode(Node<?, ?> node) {
+        Assert.assertNotNull(node.getCut());
+        Assert.assertNotNull(node.getMinus());
+        Assert.assertNotNull(node.getPlus());
+
+        Assert.assertTrue(node.isInternal());
+        Assert.assertFalse(node.isLeaf());
+    }
+
+    public static void assertIsLeafNode(Node<?, ?> node) {
+        Assert.assertNull(node.getCut());
+        Assert.assertNull(node.getMinus());
+        Assert.assertNull(node.getPlus());
+
+        Assert.assertFalse(node.isInternal());
+        Assert.assertTrue(node.isLeaf());
+    }
+
+    /** Assert that the given tree for has a valid, consistent internal structure. This checks that all nodes
+     * in the tree are owned by the tree, that the node depth values are correct, and the cut nodes have children
+     * and non-cut nodes do not.
+     * @param tree tree to check
+     */
+    public static <P extends Point<P>, N extends BSPTree.Node<P, N>> void assertTreeStructure(final BSPTree<P, N> tree) {
+        assertTreeStructureRecursive(tree, tree.getRoot(), 0);
+    }
+
+    /** Recursive method to assert that a tree has a valid internal structure.
+     * @param tree tree to check
+     * @param node node to check
+     * @param expectedDepth the expected depth of the node in the tree
+     */
+    private static <P extends Point<P>, N extends BSPTree.Node<P, N>> void assertTreeStructureRecursive(
+            final BSPTree<P, N> tree, final BSPTree.Node<P, N> node, final int expectedDepth) {
+
+        Assert.assertSame("Node has an incorrect owning tree", tree, node.getTree());
+        Assert.assertEquals("Node has an incorrect depth property", node.depth(), expectedDepth);
+
+        if (node.getCut() == null) {
+            String msg = "Node without cut cannot have children";
+
+            Assert.assertNull(msg, node.getMinus());
+            Assert.assertNull(msg, node.getPlus());
+        }
+        else {
+            String msg = "Node with cut must have children";
+
+            Assert.assertNotNull(msg, node.getMinus());
+            Assert.assertNotNull(msg, node.getPlus());
+
+            assertTreeStructureRecursive(tree, node.getPlus(), expectedDepth + 1);
+            assertTreeStructureRecursive(tree, node.getMinus(), expectedDepth + 1);
+        }
+    }
+}
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/partition/test/TestBSPTree.java
new file mode 100644
index 0000000..b04ca59
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestBSPTree.java
@@ -0,0 +1,64 @@
+/*
+ * 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.partition.test;
+
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
+
+/** BSP Tree implementation class for testing purposes.
+ */
+public class TestBSPTree extends AbstractBSPTree<TestPoint2D, TestBSPTree.TestNode> {
+
+    /** Serializable UID */
+    private static final long serialVersionUID = 20190225L;
+
+    /** {@inheritDoc} */
+    @Override
+    protected TestNode createNode() {
+        return new TestNode(this);
+    }
+
+    /** {@inheritDoc}
+     *
+     * <p>Exposed as public for testing.</p>
+     */
+    @Override
+    public void splitIntoTrees(Hyperplane<TestPoint2D> splitter,
+            final AbstractBSPTree<TestPoint2D, TestBSPTree.TestNode> minus,
+            final AbstractBSPTree<TestPoint2D, TestBSPTree.TestNode> plus) {
+
+        super.splitIntoTrees(splitter, minus, plus);
+    }
+
+    /** BSP Tree node class for {@link TestBSPTree}.
+     */
+    public static class TestNode extends AbstractBSPTree.AbstractNode<TestPoint2D,TestNode> {
+
+        /** Serializable UID */
+        private static final long serialVersionUID = 20190225L;
+
+        public TestNode(AbstractBSPTree<TestPoint2D, TestNode> tree) {
+            super(tree);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected TestNode getSelf() {
+            return this;
+        }
+    }
+}
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/partition/test/TestLine.java
new file mode 100644
index 0000000..8a19ed5
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLine.java
@@ -0,0 +1,280 @@
+/*
+ * 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.partition.test;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.EmbeddingHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
+
+/** Class representing a line in two dimensional Euclidean space. This
+ * class should only be used for testing purposes.
+ */
+public class TestLine implements EmbeddingHyperplane<TestPoint2D, TestPoint1D>, Serializable {
+
+    /** Line pointing along the positive x-axis. */
+    public static final TestLine X_AXIS = new TestLine(0, 0, 1, 0);
+
+    /** Line pointing along the positive y-axis. */
+    public static final TestLine Y_AXIS = new TestLine(0, 0, 0, 1);
+
+    /** Serializable UID */
+    private static final long serialVersionUID = 20190224L;
+
+    /** X value of the normalized line direction. */
+    private final double directionX;
+
+    /** Y value of the normalized line direction. */
+    private final double directionY;
+
+    /** The distance between the origin and the line. */
+    private final double originOffset;
+
+    /** Construct a line from two points. The line points in the direction from
+     * {@code p1} to {@code p2}.
+     * @param p1 first point
+     * @param p2 second point
+     */
+    public TestLine(final TestPoint2D p1, final TestPoint2D p2) {
+        this(p1.getX(), p1.getY(), p2.getX(), p2.getY());
+    }
+
+    /** Construct a line from two point, given by their components.
+     * @param x1 x coordinate of first point
+     * @param y1 y coordinate of first point
+     * @param x2 x coordinate of second point
+     * @param y2 y coordinate of second point
+     */
+    public TestLine(final double x1, final double y1, final double x2, final double y2) {
+        double vecX = x2 - x1;
+        double vecY = y2 - y1;
+
+        double norm = norm(vecX, vecY);
+
+        vecX /= norm;
+        vecY /= norm;
+
+        if (!Double.isFinite(vecX) || !Double.isFinite(vecY)) {
+            throw new IllegalStateException("Unable to create line between points: (" +
+                    x1 + ", " + y1 + "), (" + x2 + ", " + y2 + ")");
+        }
+
+        this.directionX = vecX;
+        this.directionY = vecY;
+
+        this.originOffset = signedArea(vecX, vecY, x1, y1);
+    }
+
+    /** Get the line origin, meaning the projection of the 2D origin onto the line.
+     * @return line origin
+     */
+    public TestPoint2D getOrigin() {
+        return toSpace(0);
+    }
+
+    /** Get the x component of the line direction.
+     * @return x component of the line direction.
+     */
+    public double getDirectionX() {
+        return directionX;
+    }
+
+    /** Get the y component of the line direction.
+     * @return y component of the line direction.
+     */
+    public double getDirectionY() {
+        return directionY;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double offset(TestPoint2D point) {
+        return originOffset - signedArea(directionX, directionY, point.getX(), point.getY());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public HyperplaneLocation classify(TestPoint2D point) {
+        final double offset = offset(point);
+        final double cmp = PartitionTestUtils.PRECISION.compare(offset, 0.0);
+        if (cmp == 0) {
+            return HyperplaneLocation.ON;
+        }
+        return cmp < 0 ? HyperplaneLocation.MINUS : HyperplaneLocation.PLUS;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean contains(TestPoint2D point) {
+        return classify(point) == HyperplaneLocation.ON;
+    }
+
+    /** Get the location of the given 2D point in the 1D space of the line.
+     * @param point point to project into the line's 1D space
+     * @return location of the point in the line's 1D space
+     */
+    public double toSubspaceValue(TestPoint2D point) {
+        return (directionX * point.getX()) + (directionY * point.getY());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public TestPoint1D toSubspace(TestPoint2D point) {
+        return new TestPoint1D(toSubspaceValue(point));
+    }
+
+    /** Get the 2D location of the given 1D location in the line's 1D space.
+     * @param abscissa location in the line's 1D space.
+     * @return the location of the given 1D point in 2D space
+     */
+    public TestPoint2D toSpace(final double abscissa) {
+        if (Double.isInfinite(abscissa)) {
+            int dirXCmp = PartitionTestUtils.PRECISION.sign(directionX);
+            int dirYCmp = PartitionTestUtils.PRECISION.sign(directionY);
+
+            double x;
+            if (dirXCmp == 0) {
+                // vertical line
+                x = getOrigin().getX();
+            }
+            else {
+                x = (dirXCmp < 0 ^ abscissa < 0) ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY;
+            }
+
+            double y;
+            if (dirYCmp == 0) {
+                // horizontal line
+                y = getOrigin().getY();
+            }
+            else {
+                y = (dirYCmp < 0 ^ abscissa < 0) ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY;
+            }
+
+            return new TestPoint2D(x, y);
+        }
+
+        final double ptX = (abscissa * directionX) + (-originOffset * directionY);
+        final double ptY = (abscissa * directionY) + (originOffset * directionX);
+
+        return new TestPoint2D(ptX, ptY);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public TestPoint2D toSpace(TestPoint1D point) {
+        return toSpace(point.getX());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public TestPoint2D project(final TestPoint2D point) {
+        return toSpace(toSubspaceValue(point));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public TestLine reverse() {
+        TestPoint2D pt = getOrigin();
+        return new TestLine(pt.getX(), pt.getY(), pt.getX() - directionX, pt.getY() - directionY);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public TestLine transform(Transform<TestPoint2D> transform) {
+        TestPoint2D p1 = transform.apply(toSpace(0));
+        TestPoint2D p2 = transform.apply(toSpace(1));
+
+        return new TestLine(p1, p2);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean similarOrientation(Hyperplane<TestPoint2D> other) {
+        final TestLine otherLine = (TestLine) other;
+        final double dot = (directionX * otherLine.directionX) + (directionY * otherLine.directionY);
+        return dot >= 0.0;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public TestLineSegment span() {
+        return new TestLineSegment(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, this);
+    }
+
+    /** Get the intersection point of the instance and another line.
+     * @param other other line
+     * @return intersection point of the instance and the other line
+     *      or null if there is no unique intersection point (ie, the lines
+     *      are parallel or coincident)
+     */
+    public TestPoint2D intersection(final TestLine other) {
+        final double area = signedArea(directionX, directionY, other.directionX, other.directionY);
+        if (PartitionTestUtils.PRECISION.eqZero(area)) {
+            // lines are parallel
+            return null;
+        }
+
+        final double x = ((other.directionX * originOffset) +
+                (-directionX * other.originOffset)) / area;
+
+        final double y = ((other.directionY * originOffset) +
+                (-directionY * other.originOffset)) / area;
+
+        return new TestPoint2D(x, y);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(this.getClass().getSimpleName())
+            .append("[origin= ")
+            .append(getOrigin())
+            .append(", direction= (")
+            .append(directionX)
+            .append(", ")
+            .append(directionY)
+            .append(")]");
+
+        return sb.toString();
+    }
+
+    /** Compute the signed area of the parallelogram with sides defined by the given
+     * vectors.
+     * @param x1 x coordinate of first vector
+     * @param y1 y coordinate of first vector
+     * @param x2 x coordinate of second vector
+     * @param y2 y coordinate of second vector
+     * @return the signed are of the parallelogram with side defined by the given
+     *      vectors
+     */
+    private static double signedArea(final double x1, final double y1,
+            final double x2, final double y2) {
+        return (x1 * y2) + (-y1 * x2);
+    }
+
+    /** Compute the Euclidean norm.
+     * @param x x coordinate value
+     * @param y y coordinate value
+     * @return Euclidean norm
+     */
+    public static double norm(final double x, final double y) {
+        return Math.sqrt((x * x) + (y * y));
+    }
+}
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/partition/test/TestLineSegment.java
new file mode 100644
index 0000000..aa20dd7
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegment.java
@@ -0,0 +1,340 @@
+/*
+ * 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.partition.test;
+
+import java.io.Serializable;
+import java.util.Arrays;
+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.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+
+/** Class representing a line segment in two dimensional Euclidean space. This
+ * class should only be used for testing purposes.
+ */
+public class TestLineSegment implements ConvexSubHyperplane<TestPoint2D>, Serializable {
+
+    /** Serializable UID */
+    private static final long serialVersionUID = 20190224L;
+
+    /** Abscissa of the line segment start point. */
+    private final double start;
+
+    /** Abscissa of the line segment end point. */
+    private final double end;
+
+    /** The underlying line for the line segment. */
+    private final TestLine line;
+
+    /** Construct a line segment between two points.
+     * @param start start point
+     * @param end end point
+     */
+    public TestLineSegment(final TestPoint2D start, final TestPoint2D end) {
+        this.line = new TestLine(start, end);
+
+        final double startValue = line.toSubspaceValue(start);
+        final double endValue = line.toSubspaceValue(end);
+
+        this.start = Math.min(startValue, endValue);
+        this.end = Math.max(startValue, endValue);
+    }
+
+    /** Construct a line segment between two points.
+     * @param x1 x coordinate of first point
+     * @param y1 y coordinate of first point
+     * @param x2 x coordinate of second point
+     * @param y2 y coordinate of second point
+     */
+    public TestLineSegment(final double x1, final double y1, final double x2, final double y2) {
+        this(new TestPoint2D(x1, y1), new TestPoint2D(x2, y2));
+    }
+
+    /** Construct a line segment based on an existing line.
+     * @param start abscissa of the line segment start point
+     * @param end abscissa of the line segment end point
+     * @param line the underyling line
+     */
+    public TestLineSegment(final double start, final double end, final TestLine line) {
+        this.start = Math.min(start, end);
+        this.end = Math.max(start, end);
+        this.line = line;
+    }
+
+    /** Get the start abscissa value.
+     * @return
+     */
+    public double getStart() {
+        return start;
+    }
+
+    /** Get the end abscissa value.
+     * @return
+     */
+    public double getEnd() {
+        return end;
+    }
+
+    /** Get the start point of the line segment.
+     * @return the start point of the line segment
+     */
+    public TestPoint2D getStartPoint() {
+        return line.toSpace(start);
+    }
+
+    /** Get the end point of the line segment.
+     * @return the end point of the line segment
+     */
+    public TestPoint2D getEndPoint() {
+        return line.toSpace(end);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public TestLine getHyperplane() {
+        return line;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isFull() {
+        return start < end && Double.isInfinite(start) && Double.isInfinite(end);
+
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isEmpty() {
+        return PartitionTestUtils.PRECISION.eqZero(getSize());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isInfinite() {
+        return Double.isInfinite(getSize());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isFinite() {
+        return !isInfinite();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getSize() {
+        return Math.abs(start - end);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public RegionLocation classify(TestPoint2D point) {
+        if (line.contains(point)) {
+            final double value = line.toSubspaceValue(point);
+
+            final int startCmp = PartitionTestUtils.PRECISION.compare(value, start);
+            final int endCmp = PartitionTestUtils.PRECISION.compare(value, end);
+
+            if (startCmp == 0 || endCmp == 0) {
+                return RegionLocation.BOUNDARY;
+            }
+            else if (startCmp > 0 && endCmp < 0) {
+                return RegionLocation.INSIDE;
+            }
+        }
+
+        return RegionLocation.OUTSIDE;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public TestPoint2D closest(TestPoint2D point) {
+        double value = line.toSubspaceValue(point);
+        value = Math.max(Math.min(value, end), start);
+
+        return line.toSpace(value);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public List<ConvexSubHyperplane<TestPoint2D>> toConvex() {
+        return Arrays.asList(this);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public TestLineSegment reverse() {
+        TestLine rLine = line.reverse();
+        return new TestLineSegment(-end, -start, rLine);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Split<TestLineSegment> split(Hyperplane<TestPoint2D> splitter) {
+        final TestLine splitterLine = (TestLine) splitter;
+
+        if (isInfinite()) {
+            return splitInfinite(splitterLine);
+        }
+        return splitFinite(splitterLine);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public SubHyperplane.Builder<TestPoint2D> builder() {
+        return new TestLineSegmentCollectionBuilder(line);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public ConvexSubHyperplane<TestPoint2D> transform(Transform<TestPoint2D> transform) {
+        if (!isInfinite()) {
+            // simple case; just transform the points directly
+            TestPoint2D p1 = transform.apply(getStartPoint());
+            TestPoint2D p2 = transform.apply(getEndPoint());
+
+            return new TestLineSegment(p1, p2);
+        }
+
+        // determine how the line has transformed
+        TestPoint2D p0 = transform.apply(line.toSpace(0));
+        TestPoint2D p1 = transform.apply(line.toSpace(1));
+
+        TestLine tLine = new TestLine(p0, p1);
+        double translation = tLine.toSubspaceValue(p0);
+        double scale = tLine.toSubspaceValue(p1);
+
+        double tStart = (start * scale) + translation;
+        double tEnd = (end * scale) + translation;
+
+        return new TestLineSegment(tStart, tEnd, tLine);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(this.getClass().getSimpleName())
+            .append("[start= ")
+            .append(getStartPoint())
+            .append(", end= ")
+            .append(getEndPoint())
+            .append("]");
+
+        return sb.toString();
+    }
+
+    /** Method used to split the instance with the given line when the instance has
+     * infinite size.
+     * @param splitter the splitter line
+     * @return the split convex subhyperplane
+     */
+    private Split<TestLineSegment> splitInfinite(TestLine splitter) {
+        final TestPoint2D intersection = splitter.intersection(line);
+
+        if (intersection == null) {
+            // the lines are parallel
+            final double originOffset = splitter.offset(line.getOrigin());
+
+            final int sign = PartitionTestUtils.PRECISION.sign(originOffset);
+            if (sign < 0) {
+                return new Split<TestLineSegment>(this, null);
+            }
+            else if (sign > 0) {
+                return new Split<TestLineSegment>(null, this);
+            }
+            return new Split<TestLineSegment>(null, null);
+        }
+        else {
+            // the lines intersect
+            final double intersectionAbscissa = line.toSubspaceValue(intersection);
+
+            TestLineSegment startSegment = null;
+            TestLineSegment endSegment = null;
+
+            if (start < intersectionAbscissa) {
+                startSegment = new TestLineSegment(start, intersectionAbscissa, line);
+            }
+            if (intersectionAbscissa < end) {
+                endSegment = new TestLineSegment(intersectionAbscissa, end, line);
+            }
+
+            final double startOffset = splitter.offset(line.toSpace(intersectionAbscissa - 1));
+            final double startCmp = PartitionTestUtils.PRECISION.sign(startOffset);
+
+            final TestLineSegment minus = (startCmp > 0) ? endSegment: startSegment;
+            final TestLineSegment plus = (startCmp > 0) ? startSegment : endSegment;
+
+            return new Split<TestLineSegment>(minus, plus);
+        }
+    }
+
+    /** Method used to split the instance with the given line when the instance has
+     * finite size.
+     * @param splitter the splitter line
+     * @return the split convex subhyperplane
+     */
+    private Split<TestLineSegment> splitFinite(TestLine splitter) {
+
+        final double startOffset = splitter.offset(line.toSpace(start));
+        final double endOffset = splitter.offset(line.toSpace(end));
+
+        final int startCmp = PartitionTestUtils.PRECISION.sign(startOffset);
+        final int endCmp = PartitionTestUtils.PRECISION.sign(endOffset);
+
+        // startCmp |   endCmp  |   result
+        // --------------------------------
+        // 0        |   0       |   hyper
+        // 0        |   < 0     |   minus
+        // 0        |   > 0     |   plus
+        // < 0      |   0       |   minus
+        // < 0      |   < 0     |   minus
+        // < 0      |   > 0     |   SPLIT
+        // > 0      |   0       |   plus
+        // > 0      |   < 0     |   SPLIT
+        // > 0      |   > 0     |   plus
+
+        if (startCmp == 0 && endCmp == 0) {
+            // the entire line segment is directly on the splitter line
+            return new Split<TestLineSegment>(null, null);
+        }
+        else if (startCmp < 1 && endCmp < 1) {
+            // the entire line segment is on the minus side
+            return new Split<TestLineSegment>(this, null);
+        }
+        else if (startCmp > -1 && endCmp > -1) {
+            // the entire line segment is on the plus side
+            return new Split<TestLineSegment>(null, this);
+        }
+
+        // we need to split the line
+        final TestPoint2D intersection = splitter.intersection(line);
+        final double intersectionAbscissa = line.toSubspaceValue(intersection);
+
+        final TestLineSegment startSegment = new TestLineSegment(start, intersectionAbscissa, line);
+        final TestLineSegment endSegment = new TestLineSegment(intersectionAbscissa, end, line);
+
+        final TestLineSegment minus = (startCmp > 0) ? endSegment: startSegment;
+        final TestLineSegment plus = (startCmp > 0) ? startSegment : endSegment;
+
+        return new Split<TestLineSegment>(minus, plus);
+    }
+}
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/partition/test/TestLineSegmentCollection.java
new file mode 100644
index 0000000..a541a5d
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partition/test/TestLineSegmentCollection.java
@@ -0,0 +1,197 @@
+/*
+ * 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.partition.test;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+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.ConvexSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Split;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+
+/** Class containing a collection line segments. This class should only be used for
+ * testing purposes.
+ */
+public class TestLineSegmentCollection implements SubHyperplane<TestPoint2D>, Serializable {
+
+    /** Serializable UID */
+    private static final long serialVersionUID = 20190303L;
+
+    /** The collection of line-segments making up the subhyperplane.
+     */
+    private final List<TestLineSegment> segments;
+
+    /** Create a new instance with the given line segments. The segments
+     * are all assumed to lie on the same hyperplane.
... 64761 lines suppressed ...