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/12/25 12:57:09 UTC

[commons-geometry] branch master updated (4fafb06 -> de91b1a)

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

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


    from 4fafb06  adding back portion of userguide lost in merge; fixing table of contents link
     new d65ea8f  GEOMETRY-72: adding BoundarySource interface and implementations in Euclidean 2D, 3D and spherical 2D; making a number of internal BSP tree classes private
     new a314aa6  Merge branch 'GEOMETRY-72__Matt'
     new de91b1a  Upgrade CP.

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


Summary of changes:
 .../{Splittable.java => BoundarySource.java}       |  18 +-
 .../core/partitioning/bsp/AbstractBSPTree.java     |  24 +-
 .../partitioning/bsp/AbstractRegionBSPTree.java    | 253 ++++++-----
 .../geometry/core/partitioning/bsp/BSPSubtree.java |  15 +-
 .../geometry/core/partitioning/bsp/BSPTree.java    |   7 +
 .../bsp/AbstractBSPTreeMergeOperatorTest.java      |   6 +-
 .../core/partitioning/bsp/AbstractBSPTreeTest.java | 154 +++----
 .../bsp/AbstractRegionBSPTreeTest.java             |   4 +-
 .../partitioning/bsp/AttributeBSPTreeTest.java     |   8 +-
 .../geometry/euclidean/oned/RegionBSPTree1D.java   |   2 +-
 .../geometry/euclidean/threed/Boundaries3D.java    |  86 ++++
 .../{Transform3D.java => BoundarySource3D.java}    |  20 +-
 .../geometry/euclidean/threed/ConvexVolume.java    |  18 +-
 .../geometry/euclidean/threed/RegionBSPTree3D.java | 291 +-----------
 .../geometry/euclidean/twod/Boundaries2D.java      |  71 +++
 .../{Transform2D.java => BoundarySource2D.java}    |  20 +-
 .../geometry/euclidean/twod/ConvexArea.java        |  26 +-
 .../commons/geometry/euclidean/twod/Polyline.java  |  26 +-
 .../geometry/euclidean/twod/RegionBSPTree2D.java   | 196 +-------
 .../euclidean/DocumentationExamplesTest.java       |   7 +-
 .../euclidean/threed/Boundaries3DTest.java         | 135 ++++++
 .../euclidean/threed/ConvexVolumeTest.java         |  32 +-
 .../euclidean/threed/RegionBSPTree3DTest.java      | 491 +++++++--------------
 .../geometry/euclidean/threed/SubPlaneTest.java    |  13 +-
 .../geometry/euclidean/twod/Boundaries2DTest.java  | 101 +++++
 .../geometry/euclidean/twod/ConvexAreaTest.java    |  30 ++
 .../geometry/euclidean/twod/PolylineTest.java      |  29 +-
 .../euclidean/twod/RegionBSPTree2DTest.java        | 216 ++++-----
 .../geometry/spherical/oned/RegionBSPTree1S.java   |   2 +-
 .../{package-info.java => BoundarySource2S.java}   |  31 +-
 .../geometry/spherical/twod/ConvexArea2S.java      |  17 +-
 .../geometry/spherical/twod/GreatArcPath.java      |  26 +-
 .../geometry/spherical/twod/RegionBSPTree2S.java   |  27 +-
 .../geometry/spherical/twod/ConvexArea2STest.java  |  27 ++
 .../geometry/spherical/twod/GreatArcPathTest.java  |  33 +-
 .../spherical/twod/RegionBSPTree2STest.java        |  53 ++-
 pom.xml                                            |   2 +-
 src/site/xdoc/index.xml                            |   6 +-
 38 files changed, 1205 insertions(+), 1318 deletions(-)
 copy commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/{Splittable.java => BoundarySource.java} (66%)
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Boundaries3D.java
 copy commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/{Transform3D.java => BoundarySource3D.java} (61%)
 create mode 100644 commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Boundaries2D.java
 copy commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/{Transform2D.java => BoundarySource2D.java} (62%)
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Boundaries3DTest.java
 create mode 100644 commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Boundaries2DTest.java
 copy commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/{package-info.java => BoundarySource2S.java} (61%)


[commons-geometry] 03/03: Upgrade CP.

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

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

commit de91b1a8fa90e31c7049a6469ca5ef1aeb44a822
Author: Gilles Sadowski <gi...@harfang.homelinux.org>
AuthorDate: Wed Dec 25 13:55:13 2019 +0100

    Upgrade CP.
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index f7a6aca..2727081 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>org.apache.commons</groupId>
     <artifactId>commons-parent</artifactId>
-    <version>49</version>
+    <version>50</version>
   </parent>
 
   <groupId>org.apache.commons</groupId>


[commons-geometry] 01/03: GEOMETRY-72: adding BoundarySource interface and implementations in Euclidean 2D, 3D and spherical 2D; making a number of internal BSP tree classes private

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

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

commit d65ea8f1c63921933f3c1c79bb0a6b221b088966
Author: Matt Juntunen <ma...@hotmail.com>
AuthorDate: Tue Dec 24 17:42:09 2019 -0500

    GEOMETRY-72: adding BoundarySource interface and implementations in Euclidean 2D, 3D and spherical 2D; making a number of internal BSP tree classes private
---
 .../geometry/core/partitioning/BoundarySource.java |  34 ++
 .../core/partitioning/bsp/AbstractBSPTree.java     |  24 +-
 .../partitioning/bsp/AbstractRegionBSPTree.java    | 253 ++++++-----
 .../geometry/core/partitioning/bsp/BSPSubtree.java |  15 +-
 .../geometry/core/partitioning/bsp/BSPTree.java    |   7 +
 .../bsp/AbstractBSPTreeMergeOperatorTest.java      |   6 +-
 .../core/partitioning/bsp/AbstractBSPTreeTest.java | 154 +++----
 .../bsp/AbstractRegionBSPTreeTest.java             |   4 +-
 .../partitioning/bsp/AttributeBSPTreeTest.java     |   8 +-
 .../geometry/euclidean/oned/RegionBSPTree1D.java   |   2 +-
 .../geometry/euclidean/threed/Boundaries3D.java    |  86 ++++
 .../euclidean/threed/BoundarySource3D.java         |  35 ++
 .../geometry/euclidean/threed/ConvexVolume.java    |  18 +-
 .../geometry/euclidean/threed/RegionBSPTree3D.java | 291 +-----------
 .../geometry/euclidean/twod/Boundaries2D.java      |  71 +++
 .../geometry/euclidean/twod/BoundarySource2D.java  |  35 ++
 .../geometry/euclidean/twod/ConvexArea.java        |  26 +-
 .../commons/geometry/euclidean/twod/Polyline.java  |  26 +-
 .../geometry/euclidean/twod/RegionBSPTree2D.java   | 196 +-------
 .../euclidean/DocumentationExamplesTest.java       |   7 +-
 .../euclidean/threed/Boundaries3DTest.java         | 135 ++++++
 .../euclidean/threed/ConvexVolumeTest.java         |  32 +-
 .../euclidean/threed/RegionBSPTree3DTest.java      | 491 +++++++--------------
 .../geometry/euclidean/threed/SubPlaneTest.java    |  13 +-
 .../geometry/euclidean/twod/Boundaries2DTest.java  | 101 +++++
 .../geometry/euclidean/twod/ConvexAreaTest.java    |  30 ++
 .../geometry/euclidean/twod/PolylineTest.java      |  29 +-
 .../euclidean/twod/RegionBSPTree2DTest.java        | 216 ++++-----
 .../geometry/spherical/oned/RegionBSPTree1S.java   |   2 +-
 .../geometry/spherical/twod/BoundarySource2S.java  |  35 ++
 .../geometry/spherical/twod/ConvexArea2S.java      |  17 +-
 .../geometry/spherical/twod/GreatArcPath.java      |  26 +-
 .../geometry/spherical/twod/RegionBSPTree2S.java   |  27 +-
 .../geometry/spherical/twod/ConvexArea2STest.java  |  27 ++
 .../geometry/spherical/twod/GreatArcPathTest.java  |  33 +-
 .../spherical/twod/RegionBSPTree2STest.java        |  53 ++-
 src/site/xdoc/index.xml                            |   6 +-
 37 files changed, 1291 insertions(+), 1280 deletions(-)

diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundarySource.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundarySource.java
new file mode 100644
index 0000000..6dffc6b
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundarySource.java
@@ -0,0 +1,34 @@
+/*
+ * 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.stream.Stream;
+
+import org.apache.commons.geometry.core.Point;
+
+/** Interface representing an object that can produce region boundaries as a stream
+ * of convex subhyperplanes.
+ * @param <C> Convex subhyperplane implementation type
+ */
+@FunctionalInterface
+public interface BoundarySource<C extends ConvexSubHyperplane<? extends Point<?>>> {
+
+    /** Return a stream containing the boundaries for this instance.
+     * @return a stream containing the boundaries for this instance
+     */
+    Stream<C> boundaryStream();
+}
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
index d2dc7b0..060f995 100644
--- 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
@@ -20,9 +20,11 @@ import java.util.Deque;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.NoSuchElementException;
+import java.util.stream.Stream;
 
 import org.apache.commons.geometry.core.Point;
 import org.apache.commons.geometry.core.Transform;
+import org.apache.commons.geometry.core.partitioning.BoundarySource;
 import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
 import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
@@ -117,10 +119,18 @@ public abstract class AbstractBSPTree<P extends Point<P>, N extends AbstractBSPT
         }
     }
 
-    /** Return an iterator over the nodes in the tree. */
+    /** {@inheritDoc} */
+    @Override
+    public void insert(final BoundarySource<? extends ConvexSubHyperplane<P>> boundarySrc) {
+        try (Stream<? extends ConvexSubHyperplane<P>> stream = boundarySrc.boundaryStream()) {
+            stream.forEach(this::insert);
+        }
+    }
+
+    /** {@inheritDoc} */
     @Override
-    public Iterator<N> iterator() {
-        return new NodeIterator<>(getRoot());
+    public Iterable<N> nodes() {
+        return () -> new NodeIterator<>(getRoot());
     }
 
     /** {@inheritDoc} */
@@ -894,8 +904,8 @@ public abstract class AbstractBSPTree<P extends Point<P>, N extends AbstractBSPT
 
         /** {@inheritDoc} */
         @Override
-        public Iterator<N> iterator() {
-            return new NodeIterator<>(getSelf());
+        public Iterable<N> nodes() {
+            return () -> new NodeIterator<>(getSelf());
         }
 
         /** {@inheritDoc} */
@@ -1075,7 +1085,7 @@ public abstract class AbstractBSPTree<P extends Point<P>, N extends AbstractBSPT
      * @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> {
+    private static final 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<>();
@@ -1083,7 +1093,7 @@ public abstract class AbstractBSPTree<P extends Point<P>, N extends AbstractBSPT
         /** 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) {
+        NodeIterator(final N subtreeRoot) {
             stack.push(subtreeRoot);
         }
 
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTree.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTree.java
index 19faf35..15a1680 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTree.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTree.java
@@ -123,7 +123,7 @@ public abstract class AbstractRegionBSPTree<
             double sum = 0.0;
 
             RegionCutBoundary<P> boundary;
-            for (final AbstractRegionNode<P, N> node : this) {
+            for (final AbstractRegionNode<P, N> node : nodes()) {
                 boundary = node.getCutBoundary();
                 if (boundary != null) {
                     sum += boundary.getInsideFacing().getSize();
@@ -157,10 +157,9 @@ public abstract class AbstractRegionBSPTree<
     protected <C extends ConvexSubHyperplane<P>> Iterable<C> createBoundaryIterable(
             final Function<ConvexSubHyperplane<P>, C> typeConverter) {
 
-        return () -> {
-            final NodeIterator<P, N> nodeIterator = new NodeIterator<>(getRoot());
-            return new RegionBoundaryIterator<>(nodeIterator, typeConverter);
-        };
+        return () -> new RegionBoundaryIterator<>(
+                getRoot().nodes().iterator(),
+                typeConverter);
     }
 
     /** Return a list containing the boundaries of the region. Each boundary is oriented such
@@ -183,7 +182,7 @@ public abstract class AbstractRegionBSPTree<
 
         final List<C> result = new ArrayList<>();
 
-        final RegionBoundaryIterator<P, C, N> it = new RegionBoundaryIterator<>(iterator(), typeConverter);
+        final RegionBoundaryIterator<P, C, N> it = new RegionBoundaryIterator<>(nodes().iterator(), typeConverter);
         it.forEachRemaining(result::add);
 
         return result;
@@ -646,11 +645,125 @@ public abstract class AbstractRegionBSPTree<
         }
     }
 
+    /** 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> {
+        /** 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 Result 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 Result.CONTINUE;
+        }
+
+        /** 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>> {
+        /** 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 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>>
+    private 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
@@ -673,7 +786,7 @@ public abstract class AbstractRegionBSPTree<
      * @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>>
+    private static final class UnionOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
         extends RegionMergeOperator<P, N> {
 
         /** {@inheritDoc} */
@@ -692,7 +805,7 @@ public abstract class AbstractRegionBSPTree<
      * @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>>
+    private static final class IntersectionOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
         extends RegionMergeOperator<P, N> {
 
         /** {@inheritDoc} */
@@ -711,7 +824,7 @@ public abstract class AbstractRegionBSPTree<
      * @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>>
+    private static final class DifferenceOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
         extends RegionMergeOperator<P, N> {
 
         /** {@inheritDoc} */
@@ -743,7 +856,7 @@ public abstract class AbstractRegionBSPTree<
      * @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>>
+    private static final class XorOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
         extends RegionMergeOperator<P, N> {
 
         /** {@inheritDoc} */
@@ -773,126 +886,12 @@ public abstract class AbstractRegionBSPTree<
         }
     }
 
-    /** 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> {
-        /** 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 Result 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 Result.CONTINUE;
-        }
-
-        /** 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>> {
-        /** 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<
+    private static final class RegionBoundaryIterator<
             P extends Point<P>,
             C extends ConvexSubHyperplane<P>,
             N extends AbstractRegionNode<P, N>>
@@ -905,7 +904,7 @@ public abstract class AbstractRegionBSPTree<
          * @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,
+        RegionBoundaryIterator(final Iterator<N> inputIterator,
                 final Function<ConvexSubHyperplane<P>, C> typeConverter) {
             super(inputIterator);
 
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
index 5943765..a09eff3 100644
--- 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
@@ -16,9 +16,6 @@
  */
 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
@@ -26,7 +23,7 @@ import org.apache.commons.geometry.core.Point;
  * @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> {
+public interface BSPSubtree<P extends Point<P>, N extends BSPTree.Node<P, N>> {
 
     /** Return the total number of nodes in the subtree.
      * @return the total number of nodes in the subtree.
@@ -44,10 +41,10 @@ public interface BSPSubtree<P extends Point<P>, N extends BSPTree.Node<P, N>> ex
      */
     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
+    /** Get an iterable for accessing the nodes in this subtree. This provides a simple
+     * alternative to {@link #accept(BSPTreeVisitor)} for accessing tree nodes but is not
+     * as powerful or flexible since the node iteration order is fixed.
+     * @return an iterable for accessing the nodes in this subtree
      */
-    default Stream<N> stream() {
-        return StreamSupport.stream(spliterator(), false);
-    }
+    Iterable<N> nodes();
 }
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
index 758ca90..50dd742 100644
--- 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
@@ -18,6 +18,7 @@ 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.BoundarySource;
 import org.apache.commons.geometry.core.partitioning.ConvexSubHyperplane;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
 import org.apache.commons.geometry.core.partitioning.SubHyperplane;
@@ -103,6 +104,12 @@ public interface BSPTree<P extends Point<P>, N extends BSPTree.Node<P, N>>
      */
     void insert(Iterable<? extends ConvexSubHyperplane<P>> convexSubs);
 
+    /** Insert all convex subhyperplanes from the given source into the tree.
+     * @param boundarySrc source of boundary convex subhyperplanes to insert
+     *      into the tree
+     */
+    void insert(BoundarySource<? extends ConvexSubHyperplane<P>> boundarySrc);
+
     /** Make the current instance a deep copy of the argument.
      * @param src the tree to copy
      */
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperatorTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperatorTest.java
index d3c2f91..8e25253 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperatorTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeMergeOperatorTest.java
@@ -16,6 +16,8 @@
  */
 package org.apache.commons.geometry.core.partitioning.bsp;
 
+import java.util.stream.StreamSupport;
+
 import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
 import org.apache.commons.geometry.core.partition.test.TestLine;
 import org.apache.commons.geometry.core.partition.test.TestPoint2D;
@@ -550,7 +552,9 @@ public class AbstractBSPTreeMergeOperatorTest {
             String attr = leaf.getAttribute();
 
             AttributeNode<TestPoint2D, String> output = outputSubtree(subtree);
-            output.stream().filter(BSPTree.Node::isLeaf).forEach(n -> n.setAttribute(attr + n.getAttribute()));
+            StreamSupport.stream(output.nodes().spliterator(), false)
+                .filter(BSPTree.Node::isLeaf)
+                .forEach(n -> n.setAttribute(attr + n.getAttribute()));
 
             return output;
         }
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeTest.java
index 18e5a48..39e6a11 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractBSPTreeTest.java
@@ -24,6 +24,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.partition.test.PartitionTestUtils;
@@ -34,6 +35,7 @@ import org.apache.commons.geometry.core.partition.test.TestLineSegment;
 import org.apache.commons.geometry.core.partition.test.TestLineSegmentCollection;
 import org.apache.commons.geometry.core.partition.test.TestPoint2D;
 import org.apache.commons.geometry.core.partition.test.TestTransform2D;
+import org.apache.commons.geometry.core.partitioning.BoundarySource;
 import org.apache.commons.geometry.core.partitioning.bsp.BSPTree.NodeCutRule;
 import org.junit.Assert;
 import org.junit.Test;
@@ -713,6 +715,59 @@ public class AbstractBSPTreeTest {
     }
 
     @Test
+    public void testInsert_boundarySource() {
+        // arrange
+        TestBSPTree tree = new TestBSPTree();
+
+        TestLineSegment a = new TestLineSegment(-1, -1, 1, -1);
+        TestLineSegment b = new TestLineSegment(1, -1, 0, 0);
+        TestLineSegment c = new TestLineSegment(0, 0, 1, 1);
+        TestLineSegment d = new TestLineSegment(1, 1, -1, 1);
+        TestLineSegment e = new TestLineSegment(-1, 1, -1, -1);
+
+        BoundarySource<TestLineSegment> src = () -> Arrays.asList(a, b, c, d, e).stream();
+
+        // act
+        tree.insert(src);
+
+        // assert
+        List<TestLineSegment> segments = getLineSegments(tree);
+
+        Assert.assertEquals(5, segments.size());
+
+        PartitionTestUtils.assertSegmentsEqual(
+                new TestLineSegment(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, new TestLine(-1, -1, 1, -1)),
+                segments.get(0));
+        PartitionTestUtils.assertSegmentsEqual(
+                new TestLineSegment(-Math.sqrt(2), Double.POSITIVE_INFINITY, new TestLine(1, -1, 0, 0)),
+                segments.get(1));
+        PartitionTestUtils.assertSegmentsEqual(
+                new TestLineSegment(-1, 1, -1, -1),
+                segments.get(2));
+
+        PartitionTestUtils.assertSegmentsEqual(
+                new TestLineSegment(0, Double.POSITIVE_INFINITY, new TestLine(0, 0, 1, 1)),
+                segments.get(3));
+        PartitionTestUtils.assertSegmentsEqual(
+                new TestLineSegment(1, 1, -1, 1),
+                segments.get(4));
+    }
+
+    @Test
+    public void testInsert_boundarySource_emptySource() {
+        // arrange
+        TestBSPTree tree = new TestBSPTree();
+
+        BoundarySource<TestLineSegment> src = () -> new ArrayList<TestLineSegment>().stream();
+
+        // act
+        tree.insert(src);
+
+        // assert
+        Assert.assertEquals(1, tree.count());
+    }
+
+    @Test
     public void testCount() {
         // arrange
         TestBSPTree tree = new TestBSPTree();
@@ -1081,13 +1136,13 @@ public class AbstractBSPTreeTest {
     }
 
     @Test
-    public void testIterable_emptyTree() {
+    public void testNodesIterable_emptyTree() {
         // arrange
         TestBSPTree tree = new TestBSPTree();
         List<TestNode> nodes = new ArrayList<>();
 
         // act
-        for (TestNode node : tree) {
+        for (TestNode node : tree.nodes()) {
             nodes.add(node);
         }
 
@@ -1097,7 +1152,7 @@ public class AbstractBSPTreeTest {
     }
 
     @Test
-    public void testIterable_multipleNodes() {
+    public void testNodesIterable_multipleNodes() {
         // arrange
         TestBSPTree tree = new TestBSPTree();
 
@@ -1112,7 +1167,7 @@ public class AbstractBSPTreeTest {
         List<TestNode> nodes = new ArrayList<>();
 
         // act
-        for (TestNode node : tree) {
+        for (TestNode node : tree.nodes()) {
             nodes.add(node);
         }
 
@@ -1131,11 +1186,11 @@ public class AbstractBSPTreeTest {
 
 
     @Test
-    public void testIterable_iteratorThrowsNoSuchElementExceptionAtEnd() {
+    public void testNodesIterable_iteratorThrowsNoSuchElementExceptionAtEnd() {
         // arrange
         TestBSPTree tree = new TestBSPTree();
 
-        Iterator<TestNode> it = tree.iterator();
+        Iterator<TestNode> it = tree.nodes().iterator();
         it.next();
 
         // act
@@ -1148,49 +1203,7 @@ public class AbstractBSPTreeTest {
     }
 
     @Test
-    public void testStream_emptyTree() {
-        // arrange
-        TestBSPTree tree = new TestBSPTree();
-
-        // act
-        List<TestNode> nodes = tree.stream().collect(Collectors.toList());
-
-        // assert
-        Assert.assertEquals(1, nodes.size());
-        Assert.assertSame(tree.getRoot(), nodes.get(0));
-    }
-
-    @Test
-    public void testStream_multipleNodes() {
-        // arrange
-        TestBSPTree tree = new TestBSPTree();
-
-        TestNode root = tree.getRoot();
-        root.cut(TestLine.X_AXIS)
-                .getMinus()
-                    .cut(TestLine.Y_AXIS)
-                 .getParent()
-                 .getPlus()
-                     .cut(TestLine.Y_AXIS);
-
-        // act
-        List<TestNode> nodes = tree.stream().collect(Collectors.toList());
-
-        // assert
-        Assert.assertEquals(7, nodes.size());
-        Assert.assertSame(root, nodes.get(0));
-
-        Assert.assertSame(root.getMinus(), nodes.get(1));
-        Assert.assertSame(root.getMinus().getMinus(), nodes.get(2));
-        Assert.assertSame(root.getMinus().getPlus(), nodes.get(3));
-
-        Assert.assertSame(root.getPlus(), nodes.get(4));
-        Assert.assertSame(root.getPlus().getMinus(), nodes.get(5));
-        Assert.assertSame(root.getPlus().getPlus(), nodes.get(6));
-    }
-
-    @Test
-    public void testNodeIterable_singleNodeSubtree() {
+    public void testSubtreeNodesIterable_singleNodeSubtree() {
         // arrange
         TestBSPTree tree = new TestBSPTree();
         TestNode node = tree.getRoot().cut(TestLine.X_AXIS)
@@ -1200,7 +1213,7 @@ public class AbstractBSPTreeTest {
 
         List<TestNode> nodes = new ArrayList<>();
         // act
-        for (TestNode n : node) {
+        for (TestNode n : node.nodes()) {
             nodes.add(n);
         }
 
@@ -1210,7 +1223,7 @@ public class AbstractBSPTreeTest {
     }
 
     @Test
-    public void testNodeIterable_multipleNodeSubtree() {
+    public void testSubtreeNodesIterable_multipleNodeSubtree() {
         // arrange
         TestBSPTree tree = new TestBSPTree();
         TestNode node = tree.getRoot().cut(TestLine.X_AXIS)
@@ -1219,7 +1232,7 @@ public class AbstractBSPTreeTest {
 
         List<TestNode> nodes = new ArrayList<>();
         // act
-        for (TestNode n : node) {
+        for (TestNode n : node.nodes()) {
             nodes.add(n);
         }
 
@@ -1231,41 +1244,6 @@ public class AbstractBSPTreeTest {
     }
 
     @Test
-    public void testNodeStream_singleNodeSubtree() {
-        // arrange
-        TestBSPTree tree = new TestBSPTree();
-        TestNode node = tree.getRoot().cut(TestLine.X_AXIS)
-            .getMinus()
-                .cut(TestLine.Y_AXIS)
-                .getMinus();
-
-        // act
-        List<TestNode> nodes = node.stream().collect(Collectors.toList());
-
-        // assert
-        Assert.assertEquals(1, nodes.size());
-        Assert.assertSame(node, nodes.get(0));
-    }
-
-    @Test
-    public void testNodeStream_multipleNodeSubtree() {
-        // arrange
-        TestBSPTree tree = new TestBSPTree();
-        TestNode node = tree.getRoot().cut(TestLine.X_AXIS)
-            .getMinus()
-                .cut(TestLine.Y_AXIS);
-
-        // act
-        List<TestNode> nodes = node.stream().collect(Collectors.toList());
-
-        // assert
-        Assert.assertEquals(3, nodes.size());
-        Assert.assertSame(node, nodes.get(0));
-        Assert.assertSame(node.getMinus(), nodes.get(1));
-        Assert.assertSame(node.getPlus(), nodes.get(2));
-    }
-
-    @Test
     public void testCopy_rootOnly() {
         // arrange
         TestBSPTree tree = new TestBSPTree();
@@ -1909,7 +1887,7 @@ public class AbstractBSPTreeTest {
     }
 
     private static List<TestLineSegment> getLineSegments(TestBSPTree tree) {
-        return tree.stream()
+        return StreamSupport.stream(tree.nodes().spliterator(), false)
             .filter(BSPTree.Node::isInternal)
             .map(n -> (TestLineSegment) n.getCut())
             .collect(Collectors.toList());
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java
index d78b268..a634ed2 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AbstractRegionBSPTreeTest.java
@@ -1036,10 +1036,10 @@ public class AbstractRegionBSPTreeTest {
         Assert.assertEquals(tree.count(), copy.count());
 
         List<RegionLocation> origLocations = new ArrayList<>();
-        tree.forEach(n -> origLocations.add(n.getLocationValue()));
+        tree.nodes().forEach(n -> origLocations.add(n.getLocationValue()));
 
         List<RegionLocation> copyLocations = new ArrayList<>();
-        copy.forEach(n -> copyLocations.add(n.getLocationValue()));
+        copy.nodes().forEach(n -> copyLocations.add(n.getLocationValue()));
 
         Assert.assertEquals(origLocations, copyLocations);
     }
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AttributeBSPTreeTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AttributeBSPTreeTest.java
index 6fe05dd..99b1606 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AttributeBSPTreeTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/AttributeBSPTreeTest.java
@@ -157,11 +157,11 @@ public class AttributeBSPTreeTest {
         root.getMinus().attr("A");
         root.getPlus().attr("B");
 
-        root.getMinus().getMinus().forEach(n -> n.attr("a"));
-        root.getMinus().getPlus().forEach(n -> n.attr("b"));
+        root.getMinus().getMinus().nodes().forEach(n -> n.attr("a"));
+        root.getMinus().getPlus().nodes().forEach(n -> n.attr("b"));
 
-        root.getPlus().getPlus().forEach(n -> n.attr("c"));
-        root.getPlus().getMinus().forEach(n -> n.attr("d"));
+        root.getPlus().getPlus().nodes().forEach(n -> n.attr("c"));
+        root.getPlus().getMinus().nodes().forEach(n -> n.attr("d"));
 
         AttributeBSPTree<TestPoint2D, String> result = new AttributeBSPTree<>();
 
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1D.java
index 38c8fef..b326213 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/RegionBSPTree1D.java
@@ -239,7 +239,7 @@ public final class RegionBSPTree1D extends AbstractRegionBSPTree<Vector1D, Regio
      *      insides node's convex region
      */
     private void visitInsideIntervals(final BiConsumer<OrientedPoint, OrientedPoint> visitor) {
-        for (RegionNode1D node : this) {
+        for (RegionNode1D node : nodes()) {
             if (node.isInside()) {
                 node.visitNodeInterval(visitor);
             }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Boundaries3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Boundaries3D.java
new file mode 100644
index 0000000..6e5a2ae
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Boundaries3D.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.threed;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+
+/** Utility class for constructing {@link BoundarySource3D} objects to produce common
+ * shapes.
+ */
+public final class Boundaries3D {
+
+    /** Private constructor. */
+    private Boundaries3D() {
+    }
+
+    /** Return a {@link BoundarySource3D} instance defining an axis-aligned rectangular prism. The points {@code a}
+     * and {@code b} are taken to represent opposite corner points in the prism and may be specified in
+     * any order.
+     * @param a first corner point in the prism (opposite of {@code b})
+     * @param b second corner point in the prism (opposite of {@code a})
+     * @param precision precision context used to construct boundary instances
+     * @return a boundary source defining the boundaries of the rectangular prism
+     * @throws IllegalArgumentException if the width, height, or depth of the defined prism is zero
+     *      as evaluated by the precision context.
+     */
+    public static BoundarySource3D rect(final Vector3D a, final Vector3D b, final DoublePrecisionContext precision) {
+
+        final double minX = Math.min(a.getX(), b.getX());
+        final double maxX = Math.max(a.getX(), b.getX());
+
+        final double minY = Math.min(a.getY(), b.getY());
+        final double maxY = Math.max(a.getY(), b.getY());
+
+        final double minZ = Math.min(a.getZ(), b.getZ());
+        final double maxZ = Math.max(a.getZ(), b.getZ());
+
+        if (precision.eq(minX, maxX) || precision.eq(minY, maxY) || precision.eq(minZ, maxZ)) {
+            throw new IllegalArgumentException("Rectangular prism has zero size: " + a + ", " + b + ".");
+        }
+
+        final Vector3D[] vertices = {
+            Vector3D.of(minX, minY, minZ),
+            Vector3D.of(maxX, minY, minZ),
+            Vector3D.of(maxX, maxY, minZ),
+            Vector3D.of(minX, maxY, minZ),
+
+            Vector3D.of(minX, minY, maxZ),
+            Vector3D.of(maxX, minY, maxZ),
+            Vector3D.of(maxX, maxY, maxZ),
+            Vector3D.of(minX, maxY, maxZ)
+        };
+
+        List<ConvexSubPlane> facets = Arrays.asList(
+            // -z and +z sides
+            ConvexSubPlane.fromVertexLoop(Arrays.asList(vertices[0], vertices[3], vertices[2], vertices[1]), precision),
+            ConvexSubPlane.fromVertexLoop(Arrays.asList(vertices[4], vertices[5], vertices[6], vertices[7]), precision),
+
+            // -x and +x sides
+            ConvexSubPlane.fromVertexLoop(Arrays.asList(vertices[0], vertices[4], vertices[7], vertices[3]), precision),
+            ConvexSubPlane.fromVertexLoop(Arrays.asList(vertices[5], vertices[1], vertices[2], vertices[6]), precision),
+
+            // -y and +y sides
+            ConvexSubPlane.fromVertexLoop(Arrays.asList(vertices[0], vertices[1], vertices[5], vertices[4]), precision),
+            ConvexSubPlane.fromVertexLoop(Arrays.asList(vertices[3], vertices[7], vertices[6], vertices[2]), precision)
+        );
+
+        return () -> facets.stream();
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3D.java
new file mode 100644
index 0000000..556a559
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/BoundarySource3D.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.threed;
+
+import org.apache.commons.geometry.core.partitioning.BoundarySource;
+
+/** Extension of the {@link BoundarySource} interface for Euclidean 3D
+ * space.
+ */
+public interface BoundarySource3D extends BoundarySource<ConvexSubPlane> {
+
+    /** Construct a new BSP tree from the boundaries contained in this
+     * instance.
+     * @return a new BSP tree constructed from the boundaries in this
+     *      instance
+     * @see RegionBSPTree3D#from(BoundarySource)
+     */
+    default RegionBSPTree3D toTree() {
+        return RegionBSPTree3D.from(this);
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexVolume.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexVolume.java
index 74ded24..1089027 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexVolume.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/ConvexVolume.java
@@ -19,6 +19,7 @@ package org.apache.commons.geometry.euclidean.threed;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Stream;
 
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.partitioning.AbstractConvexHyperplaneBoundedRegion;
@@ -30,7 +31,9 @@ import org.apache.commons.geometry.euclidean.twod.ConvexArea;
 /** Class representing a finite or infinite convex volume in Euclidean 3D space.
  * The boundaries of this area, if any, are composed of convex subplanes.
  */
-public final class ConvexVolume extends AbstractConvexHyperplaneBoundedRegion<Vector3D, ConvexSubPlane> {
+public final class ConvexVolume extends AbstractConvexHyperplaneBoundedRegion<Vector3D, ConvexSubPlane>
+    implements BoundarySource3D {
+
     /** Instance representing the full 3D volume. */
     private static final ConvexVolume FULL = new ConvexVolume(Collections.emptyList());
 
@@ -44,6 +47,12 @@ public final class ConvexVolume extends AbstractConvexHyperplaneBoundedRegion<Ve
 
     /** {@inheritDoc} */
     @Override
+    public Stream<ConvexSubPlane> boundaryStream() {
+        return getBoundaries().stream();
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public double getSize() {
         if (isFull()) {
             return Double.POSITIVE_INFINITY;
@@ -134,13 +143,6 @@ public final class ConvexVolume extends AbstractConvexHyperplaneBoundedRegion<Ve
         return transformInternal(transform, this, ConvexSubPlane.class, ConvexVolume::new);
     }
 
-    /** Return a BSP tree instance representing the same region as the current instance.
-     * @return a BSP tree instance representing the same region as the current instance
-     */
-    public RegionBSPTree3D toTree() {
-        return RegionBSPTree3D.from(this);
-    }
-
     /** Return an instance representing the full 3D volume.
      * @return an instance representing the full 3D volume.
      */
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java
index 081d396..db238bc 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3D.java
@@ -17,10 +17,11 @@
 package org.apache.commons.geometry.euclidean.threed;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
-import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 
+import org.apache.commons.geometry.core.partitioning.BoundarySource;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
 import org.apache.commons.geometry.core.partitioning.Split;
 import org.apache.commons.geometry.core.partitioning.SubHyperplane;
@@ -28,15 +29,15 @@ import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
 import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
 import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor;
 import org.apache.commons.geometry.core.partitioning.bsp.RegionCutBoundary;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
-import org.apache.commons.geometry.euclidean.twod.Polyline;
 import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
 import org.apache.commons.geometry.euclidean.twod.Vector2D;
 
 /** Binary space partitioning (BSP) tree representing a region in three dimensional
  * Euclidean space.
  */
-public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, RegionBSPTree3D.RegionNode3D> {
+public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, RegionBSPTree3D.RegionNode3D>
+    implements BoundarySource3D {
+
     /** Create a new, empty region. */
     public RegionBSPTree3D() {
         this(false);
@@ -70,6 +71,12 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
 
     /** {@inheritDoc} */
     @Override
+    public Stream<ConvexSubPlane> boundaryStream() {
+        return StreamSupport.stream(boundaries().spliterator(), false);
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public List<ConvexSubPlane> getBoundaries() {
         return createBoundaryList(b -> (ConvexSubPlane) b);
     }
@@ -179,26 +186,19 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
         return new RegionBSPTree3D(false);
     }
 
-    /** Create a new BSP tree instance representing the same region as the argument.
-     * @param volume convex volume instance
-     * @return a new BSP tree instance representing the same region as the argument
+    /** Construct a new tree from the boundaries in the given boundary source. If no boundaries
+     * are present in the given source, their the returned tree contains the full space.
+     * @param boundarySrc boundary source to construct a tree from
+     * @return a new tree instance constructed from the boundaries in the
+     *      given source
      */
-    public static RegionBSPTree3D from(final ConvexVolume volume) {
+    public static RegionBSPTree3D from(final BoundarySource<ConvexSubPlane> boundarySrc) {
         RegionBSPTree3D tree = RegionBSPTree3D.full();
-        tree.insert(volume.getBoundaries());
+        tree.insert(boundarySrc);
 
         return tree;
     }
 
-    /** Create a new {@link RegionBSPTree3D.Builder} instance for creating BSP
-     * trees from boundary representations.
-     * @param precision precision context to use for floating point comparisons.
-     * @return a new builder instance
-     */
-    public static Builder builder(final DoublePrecisionContext precision) {
-        return new Builder(precision);
-    }
-
     /** BSP tree node for three dimensional Euclidean space.
      */
     public static final class RegionNode3D extends AbstractRegionBSPTree.AbstractRegionNode<Vector3D, RegionNode3D> {
@@ -238,259 +238,6 @@ public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, Regio
         }
     }
 
-    /** Class used to construct {@link RegionBSPTree3D} instances from boundary representations.
-     */
-    public static final class Builder {
-
-        /** Precision object used to perform floating point comparisons. This object is
-         * used when constructing geometric types.
-         */
-        private final DoublePrecisionContext precision;
-
-        /** The BSP tree being constructed. */
-        private final RegionBSPTree3D tree = RegionBSPTree3D.empty();
-
-        /** List of vertices to use for indexed facet operations. */
-        private List<Vector3D> vertexList;
-
-        /** Create a new builder instance. The given precision context will be used when
-         * constructing geometric types.
-         * @param precision precision object used to perform floating point comparisons
-         */
-        public Builder(final DoublePrecisionContext precision) {
-            this.precision = precision;
-        }
-
-        /** Set the list of vertices to use for indexed facet operations.
-         * @param vertices array of vertices
-         * @return this builder instance
-         * @see #addIndexedFacet(int...)
-         * @see #addIndexedFacet(List)
-         * @see #addIndexedFacets(int[][])
-         */
-        public Builder withVertexList(final Vector3D... vertices) {
-            return withVertexList(Arrays.asList(vertices));
-        }
-
-        /** Set the list of vertices to use for indexed facet operations.
-         * @param vertices list of vertices
-         * @return this builder instance
-         * @see #addIndexedFacet(int...)
-         * @see #addIndexedFacet(List)
-         * @see #addIndexedFacets(int[][])
-         */
-        public Builder withVertexList(final List<Vector3D> vertices) {
-            this.vertexList = vertices;
-            return this;
-        }
-
-        /** Add a subplane to the tree.
-         * @param subplane subplane to add
-         * @return this builder instance
-         */
-        public Builder add(final SubPlane subplane) {
-            tree.insert(subplane);
-            return this;
-        }
-
-        /** Add a convex subplane to the tree.
-         * @param convex convex subplane to add
-         * @return this builder instance
-         */
-        public Builder add(final ConvexSubPlane convex) {
-            tree.insert(convex);
-            return this;
-        }
-
-        /** Add a facet defined by the given array of vertices. The vertices
-         * are considered to form a loop, even if the first vertex is not included
-         * again at the end of the array.
-         * @param vertices array of vertices defining the facet
-         * @return this builder instance
-         */
-        public Builder addFacet(final Vector3D... vertices) {
-            return addFacet(Arrays.asList(vertices));
-        }
-
-        /** Add a facet defined by the given list of vertices. The vertices
-         * are considered to form a loop, even if the first vertex is not included
-         * again at the end of the list.
-         * @param vertices list of vertices defining the facet
-         * @return this builder instance
-         */
-        public Builder addFacet(final List<Vector3D> vertices) {
-            // if there are only 3 vertices, then we know for certain that the area is convex
-            if (vertices.size() < 4) {
-                return add(ConvexSubPlane.fromVertexLoop(vertices, precision));
-            }
-
-            final Plane plane = Plane.fromPoints(vertices, precision);
-            final List<Vector2D> subspaceVertices = plane.toSubspace(vertices);
-
-            final Polyline path = Polyline.fromVertexLoop(subspaceVertices, precision);
-            return add(new SubPlane(plane, path.toTree()));
-        }
-
-        /** Add multiple facets, each one defined by an array of indices into the current
-         * vertex list.
-         * @param facets array of facet definitions, where each definition consists of an
-         *      array of indices into the current vertex list
-         * @return this builder instance
-         * @see #withVertexList(List)
-         */
-        public Builder addIndexedFacets(int[][] facets) {
-            for (int[] facet : facets) {
-                addIndexedFacet(facet);
-            }
-
-            return this;
-        }
-
-        /** Add a facet defined by an array of indices into the current vertex list.
-         * @param vertexIndices indices into the current vertex list, defining the vertices
-         *      for the facet
-         * @return this builder instance
-         * @see #withVertexList(List)
-         */
-        public Builder addIndexedFacet(int... vertexIndices) {
-            final Vector3D[] vertices = new Vector3D[vertexIndices.length];
-            for (int i = 0; i < vertexIndices.length; ++i) {
-                vertices[i] = vertexList.get(vertexIndices[i]);
-            }
-
-            return addFacet(vertices);
-        }
-
-        /** Add a facet defined by a list of indices into the current vertex list.
-         * @param vertexIndices indices into the current vertex list, defining the vertices
-         *      for the facet
-         * @return this builder instance
-         * @see #withVertexList(List)
-         */
-        public Builder addIndexedFacet(final List<Integer> vertexIndices) {
-            final List<Vector3D> vertices = vertexIndices.stream()
-                    .map(idx -> vertexList.get(idx)).collect(Collectors.toList());
-
-            return addFacet(vertices);
-        }
-
-        /** Add an axis-oriented cube with the given dimensions to this instance.
-        * @param center the cube center point
-        * @param size the size of the cube
-        * @return this builder instance
-        * @throws IllegalArgumentException if the width, height, or depth of the defined region is zero
-        *      as evaluated by the precision context.
-        */
-        public Builder addCenteredCube(final Vector3D center, final double size) {
-            return addCenteredRect(center, size, size, size);
-        }
-
-        /** Add an axis-oriented cube with the given dimensions to this instance.
-         * @param corner a corner of the cube
-         * @param size the size of the cube
-         * @return this builder instance
-         * @throws IllegalArgumentException if the width, height, or depth of the defined region is zero
-         *      as evaluated by the precision context.
-         */
-        public Builder addCube(final Vector3D corner, final double size) {
-            return addRect(corner, size, size, size);
-        }
-
-        /** Add an axis-oriented rectangular prism to this instance. The prism is centered at the given point and
-         * has the specified dimensions.
-         * @param center center point for the rectangular prism
-         * @param xSize size of the prism along the x-axis
-         * @param ySize size of the prism along the y-axis
-         * @param zSize size of the prism along the z-axis
-         * @return this builder instance
-         * @throws IllegalArgumentException if the width, height, or depth of the defined region is zero
-         *      as evaluated by the precision context.
-         */
-        public Builder addCenteredRect(final Vector3D center, final double xSize, final double ySize,
-                final double zSize) {
-
-            return addRect(Vector3D.of(
-                        center.getX() - (xSize * 0.5),
-                        center.getY() - (ySize * 0.5),
-                        center.getZ() - (zSize * 0.5)
-                    ), xSize, ySize, zSize);
-        }
-
-        /** Add an axis-oriented rectangular prism to this instance. The prism
-         * is constructed by taking {@code pt} as one corner of the region and adding {@code xDelta},
-         * {@code yDelta}, and {@code zDelta} to its components to create the opposite corner.
-         * @param pt point lying in a corner of the region
-         * @param xDelta distance to move along the x axis to place the other points in the
-         *      prism; this value may be negative.
-         * @param yDelta distance to move along the y axis to place the other points in the
-         *      prism; this value may be negative.
-         * @param zDelta distance to move along the z axis to place the other points in the
-         *      prism; this value may be negative.
-         * @return this builder instance
-         * @throws IllegalArgumentException if the width, height, or depth of the defined region is zero
-         *      as evaluated by the precision context.
-         */
-        public Builder addRect(final Vector3D pt, final double xDelta, final double yDelta, final double zDelta) {
-            return addRect(pt, Vector3D.of(
-                    pt.getX() + xDelta,
-                    pt.getY() + yDelta,
-                    pt.getZ() + zDelta));
-        }
-
-        /** Add an axis-oriented rectangular prism to this instance. The points {@code a} and {@code b}
-         * are taken to represent opposite corner points in the prism and may be specified in any order.
-         * @param a first corner point in the rectangular prism (opposite of {@code b})
-         * @param b second corner point in the rectangular prism (opposite of {@code a})
-         * @return this builder instance
-         * @throws IllegalArgumentException if the width, height, or depth of the defined region is zero
-         *      as evaluated by the precision context.
-         */
-        public Builder addRect(final Vector3D a, final Vector3D b) {
-            final double minX = Math.min(a.getX(), b.getX());
-            final double maxX = Math.max(a.getX(), b.getX());
-
-            final double minY = Math.min(a.getY(), b.getY());
-            final double maxY = Math.max(a.getY(), b.getY());
-
-            final double minZ = Math.min(a.getZ(), b.getZ());
-            final double maxZ = Math.max(a.getZ(), b.getZ());
-
-            if (precision.eq(minX, maxX) || precision.eq(minY, maxY) || precision.eq(minZ, maxZ)) {
-                throw new IllegalArgumentException("Rectangular prism has zero size: " + a + ", " + b + ".");
-            }
-
-            final Vector3D[] vertices = {
-                Vector3D.of(minX, minY, minZ),
-                Vector3D.of(maxX, minY, minZ),
-                Vector3D.of(maxX, maxY, minZ),
-                Vector3D.of(minX, maxY, minZ),
-
-                Vector3D.of(minX, minY, maxZ),
-                Vector3D.of(maxX, minY, maxZ),
-                Vector3D.of(maxX, maxY, maxZ),
-                Vector3D.of(minX, maxY, maxZ)
-            };
-
-            addFacet(vertices[0], vertices[3], vertices[2], vertices[1]);
-            addFacet(vertices[4], vertices[5], vertices[6], vertices[7]);
-
-            addFacet(vertices[5], vertices[1], vertices[2], vertices[6]);
-            addFacet(vertices[0], vertices[4], vertices[7], vertices[3]);
-
-            addFacet(vertices[0], vertices[1], vertices[5], vertices[4]);
-            addFacet(vertices[3], vertices[7], vertices[6], vertices[2]);
-
-            return this;
-        }
-
-        /** Get the created BSP tree.
-         * @return the created BSP tree
-         */
-        public RegionBSPTree3D build() {
-            return tree;
-        }
-    }
-
     /** Class used to project points onto the 3D region boundary.
      */
     private static final class BoundaryProjector3D extends BoundaryProjector<Vector3D, RegionNode3D> {
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Boundaries2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Boundaries2D.java
new file mode 100644
index 0000000..37de050
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Boundaries2D.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.twod;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+
+/** Utility class for creating {@link BoundarySource2D} instances for generating common
+ * shapes.
+ */
+public final class Boundaries2D {
+
+    /** Private constructor. */
+    private Boundaries2D() {
+    }
+
+    /** Create a {@link BoundarySource2D} defining an axis-aligned rectangular region. The points {@code a}
+     * and {@code b} are taken to represent opposite corner points in the rectangle and may be specified in
+     * any order.
+     * @param a first corner point in the rectangle (opposite of {@code b})
+     * @param b second corner point in the rectangle (opposite of {@code a})
+     * @param precision precision context used to construct prism instances
+     * @return a boundary source defining the boundaries of the rectangular region
+     * @throws IllegalArgumentException if the width or height of the defined rectangle is zero
+     *      as evaluated by the precision context.
+     */
+    public static BoundarySource2D rect(final Vector2D a, final Vector2D b,
+            final DoublePrecisionContext precision) {
+
+        final double minX = Math.min(a.getX(), b.getX());
+        final double maxX = Math.max(a.getX(), b.getX());
+
+        final double minY = Math.min(a.getY(), b.getY());
+        final double maxY = Math.max(a.getY(), b.getY());
+
+        if (precision.eq(minX, maxX) || precision.eq(minY, maxY)) {
+            throw new IllegalArgumentException("Rectangle has zero size: " + a + ", " + b + ".");
+        }
+
+        final Vector2D lowerLeft = Vector2D.of(minX, minY);
+        final Vector2D upperLeft = Vector2D.of(minX, maxY);
+
+        final Vector2D upperRight = Vector2D.of(maxX, maxY);
+        final Vector2D lowerRight = Vector2D.of(maxX, minY);
+
+        List<Segment> segments = Arrays.asList(
+                Segment.fromPoints(lowerLeft, lowerRight, precision),
+                Segment.fromPoints(upperRight, upperLeft, precision),
+                Segment.fromPoints(lowerRight, upperRight, precision),
+                Segment.fromPoints(upperLeft, lowerLeft, precision)
+            );
+
+        return () -> segments.stream();
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2D.java
new file mode 100644
index 0000000..ade62b3
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/BoundarySource2D.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.twod;
+
+import org.apache.commons.geometry.core.partitioning.BoundarySource;
+
+/** Extension of the {@link BoundarySource} interface for Euclidean 2D
+ * space.
+ */
+public interface BoundarySource2D extends BoundarySource<Segment> {
+
+    /** Construct a new BSP tree from the boundaries contained in this
+     * instance.
+     * @return a new BSP tree constructed from the boundaries in this
+     *      instance
+     * @see RegionBSPTree2D#from(BoundarySource)
+     */
+    default RegionBSPTree2D toTree() {
+        return RegionBSPTree2D.from(this);
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ConvexArea.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ConvexArea.java
index bc5acc3..87594f9 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ConvexArea.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/ConvexArea.java
@@ -21,6 +21,8 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.partitioning.AbstractConvexHyperplaneBoundedRegion;
@@ -32,7 +34,9 @@ import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 /** Class representing a finite or infinite convex area in Euclidean 2D space.
  * The boundaries of this area, if any, are composed of line segments.
  */
-public final class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vector2D, Segment> {
+public final class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vector2D, Segment>
+    implements BoundarySource2D {
+
     /** Instance representing the full 2D plane. */
     private static final ConvexArea FULL = new ConvexArea(Collections.emptyList());
 
@@ -44,6 +48,12 @@ public final class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vect
         super(boundaries);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public Stream<Segment> boundaryStream() {
+        return getBoundaries().stream();
+    }
+
     /** Get the connected line segment paths comprising the boundary of the area. The
      * segments are oriented so that their minus sides point toward the interior of the
      * region. The size of the returned list is
@@ -155,13 +165,6 @@ public final class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vect
         return splitInternal(splitter, this, Segment.class, ConvexArea::new);
     }
 
-    /** Return a BSP tree instance representing the same region as the current instance.
-     * @return a BSP tree instance representing the same region as the current instance
-     */
-    public RegionBSPTree2D toTree() {
-        return RegionBSPTree2D.from(this);
-    }
-
     /** Return an instance representing the full 2D area.
      * @return an instance representing the full 2D area.
      */
@@ -247,10 +250,9 @@ public final class ConvexArea extends AbstractConvexHyperplaneBoundedRegion<Vect
      * @return a convex area constructed from the lines in the given path
      */
     public static ConvexArea fromPath(final Polyline path) {
-        final List<Line> lines = new ArrayList<>();
-        for (Segment segment : path) {
-            lines.add(segment.getLine());
-        }
+        final List<Line> lines = path.boundaryStream()
+                .map(Segment::getLine)
+                .collect(Collectors.toList());
 
         return fromBounds(lines);
     }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Polyline.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Polyline.java
index 9ef8a1c..9ceb1b6 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Polyline.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Polyline.java
@@ -21,9 +21,9 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Iterator;
 import java.util.List;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.apache.commons.geometry.core.Transform;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
@@ -36,7 +36,7 @@ import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
  * <p>Instances of this class are guaranteed to be immutable.</p>
  * @see <a href="https://en.wikipedia.org/wiki/Polygonal_chain">Polygonal chain</a>
  */
-public class Polyline implements Iterable<Segment> {
+public class Polyline implements BoundarySource2D {
     /** Polyline instance containing no segments. */
     private static final Polyline EMPTY = new Polyline(Collections.emptyList());
 
@@ -50,6 +50,12 @@ public class Polyline implements Iterable<Segment> {
         this.segments = Collections.unmodifiableList(segments);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public Stream<Segment> boundaryStream() {
+        return getSegments().stream();
+    }
+
     /** Get the line segments comprising the polyline.
      * @return the line segments comprising the polyline
      */
@@ -198,16 +204,6 @@ public class Polyline implements Iterable<Segment> {
         return this;
     }
 
-    /** Construct a {@link RegionBSPTree2D} from the line segments in this instance.
-     * @return a bsp tree constructed from the line segments in this instance
-     */
-    public RegionBSPTree2D toTree() {
-        RegionBSPTree2D tree = RegionBSPTree2D.empty();
-        tree.insert(this);
-
-        return tree;
-    }
-
     /** Simplify this polyline, if possible, by combining adjacent segments that lie on the
      * same line (as determined by {@link Line#equals(Object)}).
      * @return a simplified instance
@@ -261,12 +257,6 @@ public class Polyline implements Iterable<Segment> {
         return new SimplifiedPolyline(simplified);
     }
 
-    /** {@inheritDoc} */
-    @Override
-    public Iterator<Segment> iterator() {
-        return segments.iterator();
-    }
-
     /** Return a string representation of the segment polyline.
      *
      * <p>In order to keep the string representation short but useful, the exact format of the return
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java
index 25812ed..8aa61b7 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2D.java
@@ -20,17 +20,21 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 
+import org.apache.commons.geometry.core.partitioning.BoundarySource;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
 import org.apache.commons.geometry.core.partitioning.Split;
 import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
 import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
-import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 
 /** Binary space partitioning (BSP) tree representing a region in two dimensional
  * Euclidean space.
  */
-public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, RegionBSPTree2D.RegionNode2D> {
+public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, RegionBSPTree2D.RegionNode2D>
+    implements BoundarySource2D {
+
     /** List of line segment paths comprising the region boundary. */
     private List<Polyline> boundaryPaths;
 
@@ -68,6 +72,12 @@ public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, Regio
 
     /** {@inheritDoc} */
     @Override
+    public Stream<Segment> boundaryStream() {
+        return StreamSupport.stream(boundaries().spliterator(), false);
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public List<Segment> getBoundaries() {
         return createBoundaryList(b -> (Segment) b);
     }
@@ -244,27 +254,19 @@ public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, Regio
         return new RegionBSPTree2D(false);
     }
 
-    /** Construct a tree from a convex area.
-     * @param area the area to construct a tree from
-     * @return tree instance representing the same area as the given
-     *      convex area
+    /** Construct a new tree from the boundaries in the given boundary source. If no boundaries
+     * are present in the given source, their the returned tree contains the full space.
+     * @param boundarySrc boundary source to construct a tree from
+     * @return a new tree instance constructed from the boundaries in the
+     *      given source
      */
-    public static RegionBSPTree2D from(final ConvexArea area) {
+    public static RegionBSPTree2D from(final BoundarySource<Segment> boundarySrc) {
         final RegionBSPTree2D tree = RegionBSPTree2D.full();
-        tree.insert(area.getBoundaries());
+        tree.insert(boundarySrc);
 
         return tree;
     }
 
-    /** Create a new {@link RegionBSPTree2D.Builder} instance for creating BSP
-     * trees from boundary representations.
-     * @param precision precision context to use for floating point comparisons.
-     * @return a new builder instance
-     */
-    public static Builder builder(final DoublePrecisionContext precision) {
-        return new Builder(precision);
-    }
-
     /** BSP tree node for two dimensional Euclidean space.
      */
     public static final class RegionNode2D extends AbstractRegionBSPTree.AbstractRegionNode<Vector2D, RegionNode2D> {
@@ -304,166 +306,6 @@ public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, Regio
         }
     }
 
-    /** Class used to construct {@link RegionBSPTree2D} instances from boundary representations.
-     */
-    public static final class Builder {
-
-        /** Precision object used to perform floating point comparisons. This object is
-         * used when constructing geometric types.
-         */
-        private final DoublePrecisionContext precision;
-
-        /** The BSP tree being constructed. */
-        private final RegionBSPTree2D tree = RegionBSPTree2D.empty();
-
-        /** Create a new builder instance. The given precision context will be used when
-         * constructing geometric types.
-         * @param precision precision object used to perform floating point comparisons
-         */
-        public Builder(final DoublePrecisionContext precision) {
-            this.precision = precision;
-        }
-
-        /** Add a subline to the tree.
-         * @param subline subline to add
-         * @return this builder instance
-         */
-        public Builder add(final SubLine subline) {
-            tree.insert(subline);
-            return this;
-        }
-
-        /** Add a segment to the tree.
-         * @param segment segment to add
-         * @return this builder instance
-         */
-        public Builder add(final Segment segment) {
-            tree.insert(segment);
-            return this;
-        }
-
-        /** Add the line segments defined in the given segment path.
-         * @param path path containing line segments to add
-         * @return this builder instance
-         */
-        public Builder add(final Polyline path) {
-            for (Segment segment : path) {
-                add(segment);
-            }
-
-            return this;
-        }
-
-        /** Add a segment defined by the given points.
-         * @param start start point
-         * @param end end point
-         * @return this builder instance
-         */
-        public Builder addSegment(final Vector2D start, final Vector2D end) {
-            return add(Segment.fromPoints(start, end, precision));
-        }
-
-        /** Add segments defining an axis-oriented square with the given corner point and size.
-         * @param center center point of the square
-         * @param size the size of the square
-         * @return this builder instance
-         * @throws IllegalArgumentException if the width or height of the defined rectangle is zero
-         *      as evaluated by the precision context.
-         */
-        public Builder addCenteredSquare(final Vector2D center, final double size) {
-            return addCenteredRect(center, size, size);
-        }
-
-        /** Add segments defining an axis-oriented square with the given corner point and size.
-         * @param corner point in the corner of the square
-         * @param size the size of the square
-         * @return this builder instance
-         * @throws IllegalArgumentException if the width or height of the defined rectangle is zero
-         *      as evaluated by the precision context.
-         */
-        public Builder addSquare(final Vector2D corner, final double size) {
-            return addRect(corner, size, size);
-        }
-
-        /** Add segments defining an axis-oriented rectangular region with the given center point and size.
-         * @param center center point for the region
-         * @param xSize size along the x-axis
-         * @param ySize size along the y-axis
-         * @return this builder instance
-         * @throws IllegalArgumentException if the width or height of the defined rectangle is zero
-         *      as evaluated by the precision context.
-         */
-        public Builder addCenteredRect(final Vector2D center, final double xSize, final double ySize) {
-            return addRect(Vector2D.of(
-                        center.getX() - (0.5 * xSize),
-                        center.getY() - (0.5 * ySize)
-                    ), xSize, ySize);
-        }
-
-        /** Add segments defining an axis-oriented rectangular region. The region
-         * is constructed by taking {@code pt} as one corner of the region and adding {@code xDelta}
-         * and {@code yDelta} to its components to create the opposite corner. If {@code xDelta}
-         * and {@code yDelta} are both positive, then the constructed rectangle will have {@code pt}
-         * as its lower-left corner and will have a width and height of {@code xDelta} and {@code yDelta}
-         * respectively.
-         * @param pt point lying in a corner of the region
-         * @param xDelta distance to move along the x axis to place the other points in the
-         *      rectangle; this value may be negative, in which case {@code pt} will lie
-         *      on the right side of the constructed rectangle
-         * @param yDelta distance to move along the y axis to place the other points in the
-         *      rectangle; this value may be negative, in which case {@code pt} will lie
-         *      on the top of the rectangle
-         * @return this builder instance
-         * @throws IllegalArgumentException if the width or height of the defined rectangle is zero
-         *      as evaluated by the precision context.
-         */
-        public Builder addRect(final Vector2D pt, final double xDelta, final double yDelta) {
-            return addRect(pt, Vector2D.of(
-                    pt.getX() + xDelta,
-                    pt.getY() + yDelta));
-        }
-
-        /** Add segments defining an axis-oriented rectangular region. The points {@code a} and {@code b}
-         * are taken to represent opposite corner points in the rectangle and may be specified in any order.
-         * @param a first corner point in the rectangle (opposite of {@code b})
-         * @param b second corner point in the rectangle (opposite of {@code a})
-         * @return this builder instance
-         * @throws IllegalArgumentException if the width or height of the defined rectangle is zero
-         *      as evaluated by the precision context.
-         */
-        public Builder addRect(final Vector2D a, final Vector2D b) {
-            final double minX = Math.min(a.getX(), b.getX());
-            final double maxX = Math.max(a.getX(), b.getX());
-
-            final double minY = Math.min(a.getY(), b.getY());
-            final double maxY = Math.max(a.getY(), b.getY());
-
-            if (precision.eq(minX, maxX) || precision.eq(minY, maxY)) {
-                throw new IllegalArgumentException("Rectangle has zero size: " + a + ", " + b + ".");
-            }
-
-            final Vector2D lowerLeft = Vector2D.of(minX, minY);
-            final Vector2D upperLeft = Vector2D.of(minX, maxY);
-
-            final Vector2D upperRight = Vector2D.of(maxX, maxY);
-            final Vector2D lowerRight = Vector2D.of(maxX, minY);
-
-            addSegment(lowerLeft, lowerRight);
-            addSegment(upperRight, upperLeft);
-            addSegment(lowerRight, upperRight);
-            addSegment(upperLeft, lowerLeft);
-
-            return this;
-        }
-
-        /** Get the created BSP tree.
-         * @return the created BSP tree
-         */
-        public RegionBSPTree2D build() {
-            return tree;
-        }
-    }
-
     /** Class used to project points onto the 2D region boundary.
      */
     private static final class BoundaryProjector2D extends BoundaryProjector<Vector2D, RegionNode2D> {
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
index f77ba4d..cf6818e 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/DocumentationExamplesTest.java
@@ -28,6 +28,7 @@ import org.apache.commons.geometry.euclidean.oned.Interval;
 import org.apache.commons.geometry.euclidean.oned.RegionBSPTree1D;
 import org.apache.commons.geometry.euclidean.oned.Vector1D;
 import org.apache.commons.geometry.euclidean.threed.AffineTransformMatrix3D;
+import org.apache.commons.geometry.euclidean.threed.Boundaries3D;
 import org.apache.commons.geometry.euclidean.threed.ConvexSubPlane;
 import org.apache.commons.geometry.euclidean.threed.Line3D;
 import org.apache.commons.geometry.euclidean.threed.Plane;
@@ -59,9 +60,9 @@ public class DocumentationExamplesTest {
 
         // create a binary space partitioning tree representing the unit cube
         // centered on the origin
-        RegionBSPTree3D region = RegionBSPTree3D.builder(precision)
-                .addRect(Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5))
-                .build();
+        RegionBSPTree3D region = Boundaries3D.rect(
+                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), precision)
+                .toTree();
 
         // create a rotated copy of the region
         Transform3D rotation = QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Z, 0.25 * Math.PI);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Boundaries3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Boundaries3DTest.java
new file mode 100644
index 0000000..469913a
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Boundaries3DTest.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.threed;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class Boundaries3DTest {
+
+    private static final double TEST_EPS = 1e-10;
+
+    private static final DoublePrecisionContext TEST_PRECISION =
+            new EpsilonDoublePrecisionContext(TEST_EPS);
+
+    @Test
+    public void testRect_minFirst() {
+        // act
+        List<ConvexSubPlane> boundaries = Boundaries3D.rect(Vector3D.of(1, 2, 3), Vector3D.of(4, 5, 6), TEST_PRECISION)
+                .boundaryStream()
+                .collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(6, boundaries.size());
+
+        Vector3D b1 = Vector3D.of(1, 2, 3);
+        Vector3D b2 = Vector3D.of(4, 2, 3);
+        Vector3D b3 = Vector3D.of(4, 5, 3);
+        Vector3D b4 = Vector3D.of(1, 5, 3);
+
+        Vector3D t1 = Vector3D.of(1, 2, 6);
+        Vector3D t2 = Vector3D.of(4, 2, 6);
+        Vector3D t3 = Vector3D.of(4, 5, 6);
+        Vector3D t4 = Vector3D.of(1, 5, 6);
+
+        checkVertices(boundaries.get(0), b1, b4, b3, b2, b1);
+        checkVertices(boundaries.get(1), t1, t2, t3, t4, t1);
+
+        checkVertices(boundaries.get(2), b1, t1, t4, b4, b1);
+        checkVertices(boundaries.get(3), t2, b2, b3, t3, t2);
+
+        checkVertices(boundaries.get(4), b1, b2, t2, t1, b1);
+        checkVertices(boundaries.get(5), b4, t4, t3, b3, b4);
+    }
+
+    @Test
+    public void testRect_maxFirst() {
+        // act
+        List<ConvexSubPlane> boundaries = Boundaries3D.rect(Vector3D.of(4, 5, 6), Vector3D.of(1, 2, 3), TEST_PRECISION)
+                .boundaryStream()
+                .collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(6, boundaries.size());
+
+        Vector3D b1 = Vector3D.of(1, 2, 3);
+        Vector3D b2 = Vector3D.of(4, 2, 3);
+        Vector3D b3 = Vector3D.of(4, 5, 3);
+        Vector3D b4 = Vector3D.of(1, 5, 3);
+
+        Vector3D t1 = Vector3D.of(1, 2, 6);
+        Vector3D t2 = Vector3D.of(4, 2, 6);
+        Vector3D t3 = Vector3D.of(4, 5, 6);
+        Vector3D t4 = Vector3D.of(1, 5, 6);
+
+        checkVertices(boundaries.get(0), b1, b4, b3, b2, b1);
+        checkVertices(boundaries.get(1), t1, t2, t3, t4, t1);
+
+        checkVertices(boundaries.get(2), b1, t1, t4, b4, b1);
+        checkVertices(boundaries.get(3), t2, b2, b3, t3, t2);
+
+        checkVertices(boundaries.get(4), b1, b2, t2, t1, b1);
+        checkVertices(boundaries.get(5), b4, t4, t3, b3, b4);
+    }
+
+    @Test
+    public void testRect_toTree() {
+        // arrange
+        BoundarySource3D src = Boundaries3D.rect(Vector3D.of(1, 2, 3), Vector3D.of(4, 5, 6), TEST_PRECISION);
+
+        // act
+        RegionBSPTree3D tree = src.toTree();
+
+        // assert
+        Assert.assertEquals(27, tree.getSize(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(2.5, 3.5, 4.5), tree.getBarycenter(), TEST_EPS);
+    }
+
+    @Test
+    public void testRect_illegalArgs() {
+        // act/assert
+        GeometryTestUtils.assertThrows(() -> {
+            Boundaries3D.rect(Vector3D.of(1, 2, 3), Vector3D.of(1, 5, 6), TEST_PRECISION);
+        }, IllegalArgumentException.class);
+
+        GeometryTestUtils.assertThrows(() -> {
+            Boundaries3D.rect(Vector3D.of(1, 2, 3), Vector3D.of(4, 2, 6), TEST_PRECISION);
+        }, IllegalArgumentException.class);
+
+        GeometryTestUtils.assertThrows(() -> {
+            Boundaries3D.rect(Vector3D.of(1, 2, 3), Vector3D.of(1, 5, 3), TEST_PRECISION);
+        }, IllegalArgumentException.class);
+    }
+
+    private static void checkVertices(ConvexSubPlane sp, Vector3D... pts) {
+        List<Vector3D> actual = sp.getPlane().toSpace(
+                sp.getSubspaceRegion().getBoundaryPaths().get(0).getVertices());
+
+        Assert.assertEquals(pts.length, actual.size());
+
+        for (int i = 0; i < pts.length; ++i) {
+            EuclideanTestUtils.assertCoordinatesEqual(pts[i], actual.get(i), TEST_EPS);
+        }
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexVolumeTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexVolumeTest.java
index 461e9c4..cd748e6 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexVolumeTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexVolumeTest.java
@@ -18,6 +18,7 @@ package org.apache.commons.geometry.euclidean.threed;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.apache.commons.geometry.core.GeometryTestUtils;
 import org.apache.commons.geometry.core.RegionLocation;
@@ -55,7 +56,36 @@ public class ConvexVolumeTest {
     }
 
     @Test
-    public void testTOTree() {
+    public void testBoundaryStream() {
+        // arrange
+        Plane plane = Plane.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION);
+        ConvexVolume volume = ConvexVolume.fromBounds(plane);
+
+        // act
+        List<ConvexSubPlane> boundaries = volume.boundaryStream().collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(1, boundaries.size());
+
+        ConvexSubPlane sp = boundaries.get(0);
+        Assert.assertEquals(0, sp.getSubspaceRegion().getBoundaries().size());
+        Assert.assertSame(plane, sp.getPlane());
+    }
+
+    @Test
+    public void testBoundaryStream_noBoundaries() {
+        // arrange
+        ConvexVolume volume = ConvexVolume.full();
+
+        // act
+        List<ConvexSubPlane> boundaries = volume.boundaryStream().collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(0, boundaries.size());
+    }
+
+    @Test
+    public void testToTree() {
         // arrange
         ConvexVolume volume = ConvexVolume.fromBounds(
                     Plane.fromPointAndNormal(Vector3D.ZERO, Vector3D.Unit.MINUS_X, TEST_PRECISION),
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java
index 63397b0..8dbcc6b 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/RegionBSPTree3DTest.java
@@ -21,6 +21,7 @@ import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.apache.commons.geometry.core.GeometryTestUtils;
 import org.apache.commons.geometry.core.RegionLocation;
@@ -124,9 +125,7 @@ public class RegionBSPTree3DTest {
     @Test
     public void testBoundaries() {
         // arrange
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addRect(Vector3D.ZERO, Vector3D.of(1, 1, 1))
-                .build();
+        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION).toTree();
 
         // act
         List<ConvexSubPlane> subplanes = new ArrayList<>();
@@ -139,9 +138,7 @@ public class RegionBSPTree3DTest {
     @Test
     public void testGetBoundaries() {
         // arrange
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addRect(Vector3D.ZERO, Vector3D.of(1, 1, 1))
-                .build();
+        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION).toTree();
 
         // act
         List<ConvexSubPlane> subplanes = tree.getBoundaries();
@@ -151,6 +148,46 @@ public class RegionBSPTree3DTest {
     }
 
     @Test
+    public void testBoundaryStream() {
+        // arrange
+        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION)
+                .toTree();
+
+        // act
+        List<ConvexSubPlane> subplanes = tree.boundaryStream().collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(6, subplanes.size());
+    }
+
+    @Test
+    public void testBoundaryStream_noBoundaries() {
+        // arrange
+        RegionBSPTree3D tree = RegionBSPTree3D.full();
+
+        // act
+        List<ConvexSubPlane> subplanes = tree.boundaryStream().collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(0, subplanes.size());
+    }
+
+    @Test
+    public void testToTree_returnsNewTree() {
+        // arrange
+        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 2, 1), TEST_PRECISION)
+                .toTree();
+
+        // act
+        RegionBSPTree3D result = tree.toTree();
+
+        // assert
+        Assert.assertEquals(6, result.getBoundaries().size());
+        Assert.assertEquals(2, result.getSize(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 1, 0.5), result.getBarycenter(), TEST_EPS);
+    }
+
+    @Test
     public void testHalfSpace() {
         // act
         RegionBSPTree3D tree = RegionBSPTree3D.empty();
@@ -236,9 +273,8 @@ public class RegionBSPTree3DTest {
     @Test
     public void testRaycastFirstFace() {
         // arrange
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCenteredCube(Vector3D.ZERO, 2)
-                .build();
+        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.of(-1, -1, -1), Vector3D.of(1, 1, 1), TEST_PRECISION)
+                .toTree();
 
         Line3D xPlus = Line3D.fromPoints(Vector3D.ZERO, Vector3D.of(1, 0, 0), TEST_PRECISION);
         Line3D xMinus = Line3D.fromPoints(Vector3D.ZERO, Vector3D.of(-1, 0, 0), TEST_PRECISION);
@@ -289,9 +325,7 @@ public class RegionBSPTree3DTest {
         Vector3D upperCorner = Vector3D.of(1, 1, 1);
         Vector3D center = lowerCorner.lerp(upperCorner, 0.5);
 
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addRect(lowerCorner, upperCorner)
-                .build();
+        RegionBSPTree3D tree = Boundaries3D.rect(lowerCorner, upperCorner, TEST_PRECISION).toTree();
 
         Line3D upDiagonal = Line3D.fromPoints(lowerCorner, upperCorner, TEST_PRECISION);
         Line3D downDiagonal = upDiagonal.reverse();
@@ -321,9 +355,7 @@ public class RegionBSPTree3DTest {
         Vector3D lowerCorner = Vector3D.ZERO;
         Vector3D upperCorner = Vector3D.of(1, 1, 1);
 
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addRect(lowerCorner, upperCorner)
-                .build();
+        RegionBSPTree3D tree = Boundaries3D.rect(lowerCorner, upperCorner, TEST_PRECISION).toTree();
 
         Vector3D firstPointOnLine = Vector3D.of(0.5, -1.0, 0);
         Vector3D secondPointOnLine = Vector3D.of(0.5, 2.0, 0);
@@ -350,9 +382,7 @@ public class RegionBSPTree3DTest {
         Vector3D lowerCorner = Vector3D.ZERO;
         Vector3D upperCorner = Vector3D.of(1, 1, 1);
 
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addRect(lowerCorner, upperCorner)
-                .build();
+        RegionBSPTree3D tree = Boundaries3D.rect(lowerCorner, upperCorner, TEST_PRECISION).toTree();
 
         Vector3D pt = Vector3D.of(0.5, 0.5, 0);
         Line3D intoBoxLine = Line3D.fromPoints(pt, pt.add(Vector3D.Unit.PLUS_Z), TEST_PRECISION);
@@ -374,9 +404,7 @@ public class RegionBSPTree3DTest {
         Vector3D lowerCorner = Vector3D.ZERO;
         Vector3D upperCorner = Vector3D.of(1, 1, 1);
 
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addRect(lowerCorner, upperCorner)
-                .build();
+        RegionBSPTree3D tree = Boundaries3D.rect(lowerCorner, upperCorner, TEST_PRECISION).toTree();
 
         Line3D intoBoxLine = Line3D.fromPoints(lowerCorner, upperCorner, TEST_PRECISION);
         Line3D outOfBoxLine = intoBoxLine.reverse();
@@ -397,9 +425,7 @@ public class RegionBSPTree3DTest {
         Vector3D lowerCorner = Vector3D.ZERO;
         Vector3D upperCorner = Vector3D.of(1, 1, 1);
 
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addRect(lowerCorner, upperCorner)
-                .build();
+        RegionBSPTree3D tree = Boundaries3D.rect(lowerCorner, upperCorner, TEST_PRECISION).toTree();
 
         Line3D line = Line3D.fromPointAndDirection(Vector3D.of(0.5, 0.5, 0.5), Vector3D.Unit.PLUS_X, TEST_PRECISION);
 
@@ -421,9 +447,8 @@ public class RegionBSPTree3DTest {
     @Test
     public void testInvertedRegion() {
         // arrange
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCenteredCube(Vector3D.ZERO, 1)
-                .build();
+        RegionBSPTree3D tree = Boundaries3D.rect(
+                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), TEST_PRECISION).toTree();
 
         // act
         tree.complement();
@@ -448,9 +473,8 @@ public class RegionBSPTree3DTest {
     @Test
     public void testUnitBox() {
         // act
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCenteredCube(Vector3D.ZERO, 1)
-                .build();
+        RegionBSPTree3D tree = Boundaries3D.rect(
+                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), TEST_PRECISION).toTree();
 
         // assert
         Assert.assertFalse(tree.isEmpty());
@@ -511,12 +535,12 @@ public class RegionBSPTree3DTest {
     public void testTwoBoxes_disjoint() {
         // act
         RegionBSPTree3D tree = RegionBSPTree3D.empty();
-        tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCenteredCube(Vector3D.ZERO, 1)
-                .build());
-        tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCenteredCube(Vector3D.of(2, 0, 0), 1)
-                .build());
+        tree.union(Boundaries3D.rect(
+                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), TEST_PRECISION)
+                .toTree());
+        tree.union(Boundaries3D.rect(
+                Vector3D.of(1.5, -0.5, -0.5), Vector3D.of(2.5, 0.5, 0.5), TEST_PRECISION)
+                .toTree());
 
         // assert
         Assert.assertFalse(tree.isEmpty());
@@ -540,12 +564,12 @@ public class RegionBSPTree3DTest {
     public void testTwoBoxes_sharedSide() {
         // act
         RegionBSPTree3D tree = RegionBSPTree3D.empty();
-        tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCenteredCube(Vector3D.ZERO, 1)
-                .build());
-        tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCenteredCube(Vector3D.of(1, 0, 0), 1)
-                .build());
+        tree.union(Boundaries3D.rect(
+                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), TEST_PRECISION)
+                .toTree());
+        tree.union(Boundaries3D.rect(
+                Vector3D.of(0.5, -0.5, -0.5), Vector3D.of(1.5, 0.5, 0.5), TEST_PRECISION)
+                .toTree());
 
         // assert
         Assert.assertFalse(tree.isEmpty());
@@ -572,12 +596,12 @@ public class RegionBSPTree3DTest {
 
         // act
         RegionBSPTree3D tree = RegionBSPTree3D.empty();
-        tree.union(RegionBSPTree3D.builder(precision)
-                .addCenteredCube(Vector3D.ZERO, 1)
-                .build());
-        tree.union(RegionBSPTree3D.builder(precision)
-                .addCenteredCube(Vector3D.of(1 + 1e-7, 0, 0), 1)
-                .build());
+        tree.union(Boundaries3D.rect(
+                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), precision)
+                .toTree());
+        tree.union(Boundaries3D.rect(
+                Vector3D.of(0.5 + 1e-7, -0.5, -0.5), Vector3D.of(1.5 + 1e-7, 0.5, 0.5), precision)
+                .toTree());
 
         // assert
         Assert.assertFalse(tree.isEmpty());
@@ -600,12 +624,12 @@ public class RegionBSPTree3DTest {
     public void testTwoBoxes_sharedEdge() {
         // act
         RegionBSPTree3D tree = RegionBSPTree3D.empty();
-        tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCenteredCube(Vector3D.ZERO, 1)
-                .build());
-        tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCenteredCube(Vector3D.of(1, 1, 0), 1)
-                .build());
+        tree.union(Boundaries3D.rect(
+                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), TEST_PRECISION)
+                .toTree());
+        tree.union(Boundaries3D.rect(
+                Vector3D.of(0.5, 0.5, -0.5), Vector3D.of(1.5, 1.5, 0.5), TEST_PRECISION)
+                .toTree());
 
         // assert
         Assert.assertFalse(tree.isEmpty());
@@ -631,12 +655,12 @@ public class RegionBSPTree3DTest {
     public void testTwoBoxes_sharedPoint() {
         // act
         RegionBSPTree3D tree = RegionBSPTree3D.empty();
-        tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCenteredCube(Vector3D.ZERO, 1)
-                .build());
-        tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCenteredCube(Vector3D.of(1, 1, 1), 1)
-                .build());
+        tree.union(Boundaries3D.rect(
+                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), TEST_PRECISION)
+                .toTree());
+        tree.union(Boundaries3D.rect(
+                Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(1.5, 1.5, 1.5), TEST_PRECISION)
+                .toTree());
 
         // assert
         Assert.assertFalse(tree.isEmpty());
@@ -665,13 +689,16 @@ public class RegionBSPTree3DTest {
         Vector3D vertex3 = Vector3D.of(2, 3, 3);
         Vector3D vertex4 = Vector3D.of(1, 3, 4);
 
+        List<ConvexSubPlane> boundaries = Arrays.asList(
+                ConvexSubPlane.fromVertexLoop(Arrays.asList(vertex3, vertex2, vertex1), TEST_PRECISION),
+                ConvexSubPlane.fromVertexLoop(Arrays.asList(vertex2, vertex3, vertex4), TEST_PRECISION),
+                ConvexSubPlane.fromVertexLoop(Arrays.asList(vertex4, vertex3, vertex1), TEST_PRECISION),
+                ConvexSubPlane.fromVertexLoop(Arrays.asList(vertex1, vertex2, vertex4), TEST_PRECISION)
+            );
+
         // act
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addFacet(vertex3, vertex2, vertex1)
-                .addFacet(vertex2, vertex3, vertex4)
-                .addFacet(vertex4, vertex3, vertex1)
-                .addFacet(vertex1, vertex2, vertex4)
-                .build();
+        RegionBSPTree3D tree = RegionBSPTree3D.full();
+        tree.insert(boundaries);
 
         // assert
         Assert.assertEquals(1.0 / 3.0, tree.getSize(), TEST_EPS);
@@ -735,9 +762,8 @@ public class RegionBSPTree3DTest {
     @Test
     public void testProjectToBoundary() {
         // arrange
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCube(Vector3D.ZERO, 1)
-                .build();
+        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION)
+                .toTree();
 
         // act/assert
         checkProject(tree, Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(0, 0.5, 0.5));
@@ -749,9 +775,8 @@ public class RegionBSPTree3DTest {
     @Test
     public void testProjectToBoundary_invertedRegion() {
         // arrange
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCube(Vector3D.ZERO, 1)
-                .build();
+        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION)
+                .toTree();
 
         tree.complement();
 
@@ -773,9 +798,8 @@ public class RegionBSPTree3DTest {
         double tolerance = 0.05;
         double size = 1.0;
         double radius = size * 0.5;
-        RegionBSPTree3D box = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCube(Vector3D.ZERO, size)
-                .build();
+        RegionBSPTree3D box = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(size, size, size), TEST_PRECISION)
+                .toTree();
         RegionBSPTree3D sphere = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
 
         // act
@@ -855,9 +879,8 @@ public class RegionBSPTree3DTest {
         double tolerance = 0.05;
         double size = 1.0;
         double radius = size * 0.5;
-        RegionBSPTree3D box = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCube(Vector3D.ZERO, size)
-                .build();
+        RegionBSPTree3D box = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(size, size, size), TEST_PRECISION)
+                .toTree();
         RegionBSPTree3D sphere = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
 
         // act
@@ -933,12 +956,11 @@ public class RegionBSPTree3DTest {
     public void testBoolean_xor_twoCubes() throws IOException {
         // arrange
         double size = 1.0;
-        RegionBSPTree3D box1 = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCube(Vector3D.ZERO, size)
-                .build();
-        RegionBSPTree3D box2 = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCube(Vector3D.of(0.5, 0.5, 0.5), size)
-                .build();
+        RegionBSPTree3D box1 = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(size, size, size), TEST_PRECISION)
+                .toTree();
+        RegionBSPTree3D box2 = Boundaries3D.rect(
+                Vector3D.of(0.5, 0.5, 0.5), Vector3D.of(0.5 + size, 0.5 + size, 0.5 + size), TEST_PRECISION)
+                .toTree();
 
         // act
         RegionBSPTree3D result = RegionBSPTree3D.empty();
@@ -975,9 +997,8 @@ public class RegionBSPTree3DTest {
         double tolerance = 0.05;
         double size = 1.0;
         double radius = size * 0.5;
-        RegionBSPTree3D box = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCube(Vector3D.ZERO, size)
-                .build();
+        RegionBSPTree3D box = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(size, size, size), TEST_PRECISION)
+                .toTree();
         RegionBSPTree3D sphere = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
 
         // act
@@ -1053,9 +1074,8 @@ public class RegionBSPTree3DTest {
         double tolerance = 0.05;
         double size = 1.0;
         double radius = size * 0.5;
-        RegionBSPTree3D box = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCube(Vector3D.ZERO, size)
-                .build();
+        RegionBSPTree3D box = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(size, size, size), TEST_PRECISION)
+                .toTree();
         RegionBSPTree3D sphere = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
 
         // act
@@ -1129,9 +1149,8 @@ public class RegionBSPTree3DTest {
         double tolerance = 0.05;
         double size = 1.0;
         double radius = size * 0.5;
-        RegionBSPTree3D box = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCube(Vector3D.ZERO, size)
-                .build();
+        RegionBSPTree3D box = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(size, size, size), TEST_PRECISION)
+                .toTree();
         RegionBSPTree3D sphereToAdd = createSphere(Vector3D.of(size * 0.5, size * 0.5, size), radius, 8, 16);
         RegionBSPTree3D sphereToRemove1 = createSphere(Vector3D.of(size * 0.5, 0, size * 0.5), radius, 8, 16);
         RegionBSPTree3D sphereToRemove2 = createSphere(Vector3D.of(size * 0.5, 1, size * 0.5), radius, 8, 16);
@@ -1180,9 +1199,9 @@ public class RegionBSPTree3DTest {
     @Test
     public void testToConvex_singleBox() {
         // arrange
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCube(Vector3D.of(1, 2, 3), 1)
-                .build();
+        RegionBSPTree3D tree = Boundaries3D.rect(
+                Vector3D.of(1, 2, 3), Vector3D.of(2, 3, 4), TEST_PRECISION)
+                .toTree();
 
         // act
         List<ConvexVolume> result = tree.toConvex();
@@ -1198,12 +1217,10 @@ public class RegionBSPTree3DTest {
     @Test
     public void testToConvex_multipleBoxes() {
         // arrange
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCube(Vector3D.of(4, 5, 6), 1)
-                .build();
-        tree.union(RegionBSPTree3D.builder(TEST_PRECISION)
-                .addRect(Vector3D.ZERO, 2, 1, 1)
-                .build());
+        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.of(4, 5, 6), Vector3D.of(5, 6, 7), TEST_PRECISION)
+                .toTree();
+        tree.union(Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(2, 1, 1), TEST_PRECISION)
+                .toTree());
 
         // act
         List<ConvexVolume> result = tree.toConvex();
@@ -1226,9 +1243,9 @@ public class RegionBSPTree3DTest {
     @Test
     public void testSplit() {
         // arrange
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCube(Vector3D.of(-0.5, -0.5, -0.5), 1)
-                .build();
+        RegionBSPTree3D tree = Boundaries3D.rect(
+                Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), TEST_PRECISION)
+                .toTree();
 
         Plane splitter = Plane.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION);
 
@@ -1250,9 +1267,8 @@ public class RegionBSPTree3DTest {
     @Test
     public void testGetNodeRegion() {
         // arrange
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addRect(Vector3D.ZERO, Vector3D.of(1, 1, 1))
-                .build();
+        RegionBSPTree3D tree = Boundaries3D.rect(Vector3D.ZERO, Vector3D.of(1, 1, 1), TEST_PRECISION)
+                .toTree();
 
         // act/assert
         ConvexVolume rootVol = tree.getRoot().getNodeRegion();
@@ -1268,237 +1284,6 @@ public class RegionBSPTree3DTest {
         EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), centerVol.getBarycenter(), TEST_EPS);
     }
 
-    @Test
-    public void testBuilder_nothingAdded() {
-        // act
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION).build();
-
-        // assert
-        Assert.assertTrue(tree.isEmpty());
-    }
-
-    @Test
-    public void testBuilder_rectMethods() {
-        // act
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-
-                .addRect(Vector3D.ZERO, Vector3D.of(2, 1, 1))
-                .addRect(Vector3D.of(0, 0, -1), Vector3D.of(-1, -2, -2))
-
-                .addRect(Vector3D.of(0, 0, 5), 1, 2, 3)
-                .addRect(Vector3D.of(0, 0, 10), -3, -2, -1)
-
-                .addCenteredRect(Vector3D.of(0, 0, 15), 2, 3, 4)
-                .addCenteredRect(Vector3D.of(0, 0, 20), 4, 3, 2)
-
-                .build();
-
-        // act
-        Assert.assertEquals(2 + 2 + 6 + 6 + 24 + 24, tree.getSize(), TEST_EPS);
-
-        EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.INSIDE,
-                Vector3D.of(1, 0.5, 0.5),
-                Vector3D.of(-0.5, -1, -1.5),
-
-                Vector3D.of(0.5, 1, 6.5),
-                Vector3D.of(-1.5, -1, 9.5),
-
-                Vector3D.of(0, 0, 15),
-                Vector3D.of(0, 0, 20));
-
-        EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.BOUNDARY,
-                Vector3D.of(-1, -1.5, 13),
-                Vector3D.of(1, 1.5, 17),
-
-                Vector3D.of(-2, -1.5, 19),
-                Vector3D.of(2, 1.5, 21));
-    }
-
-    @Test
-    public void testBuilder_rectMethods_invalidDimensions() {
-        // act/assert
-        GeometryTestUtils.assertThrows(() -> {
-            RegionBSPTree3D.builder(TEST_PRECISION).addRect(Vector3D.ZERO, 1e-20, 1, 1);
-        }, IllegalArgumentException.class);
-
-        GeometryTestUtils.assertThrows(() -> {
-            RegionBSPTree3D.builder(TEST_PRECISION).addRect(Vector3D.ZERO, 1, 1e-20, 1);
-        }, IllegalArgumentException.class);
-
-        GeometryTestUtils.assertThrows(() -> {
-            RegionBSPTree3D.builder(TEST_PRECISION).addRect(Vector3D.ZERO, 1, 1, 1e-20);
-        }, IllegalArgumentException.class);
-
-        GeometryTestUtils.assertThrows(() -> {
-            RegionBSPTree3D.builder(TEST_PRECISION).addRect(Vector3D.ZERO, 0, 0, 0);
-        }, IllegalArgumentException.class);
-
-        GeometryTestUtils.assertThrows(() -> {
-            RegionBSPTree3D.builder(TEST_PRECISION).addRect(Vector3D.of(1, 2, 3), Vector3D.of(1, 2, 3));
-        }, IllegalArgumentException.class);
-
-        GeometryTestUtils.assertThrows(() -> {
-            RegionBSPTree3D.builder(TEST_PRECISION).addCenteredRect(Vector3D.of(1, 2, 3), 0, 0, 0);
-        }, IllegalArgumentException.class);
-    }
-
-    @Test
-    public void testBuilder_cubeMethods() {
-        // act
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .addCube(Vector3D.of(1, 0, 0), 2)
-                .addCenteredCube(Vector3D.of(-2, -3, -4), 3)
-                .build();
-
-        // assert
-        Assert.assertEquals(8 + 27, tree.getSize(), TEST_EPS);
-
-        EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.INSIDE,
-                Vector3D.of(2, 1, 1),
-                Vector3D.of(-2, -3, -4));
-
-        EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.BOUNDARY,
-                Vector3D.of(-3.5, -4.5, -5.5),
-                Vector3D.of(-0.5, -1.5, -2.5));
-    }
-
-    @Test
-    public void testBuilder_cubeMethods_invalidDimensions() {
-        // act/assert
-        GeometryTestUtils.assertThrows(() -> {
-            RegionBSPTree3D.builder(TEST_PRECISION).addCube(Vector3D.ZERO, 1e-20);
-        }, IllegalArgumentException.class);
-
-        GeometryTestUtils.assertThrows(() -> {
-            RegionBSPTree3D.builder(TEST_PRECISION).addCube(Vector3D.of(1, 2, 3), 1e-20);
-        }, IllegalArgumentException.class);
-
-        GeometryTestUtils.assertThrows(() -> {
-            RegionBSPTree3D.builder(TEST_PRECISION).addCenteredCube(Vector3D.of(1, 2, 3), 0);
-        }, IllegalArgumentException.class);
-    }
-
-    @Test
-    public void testBuilder_addIndexedFacets_triangles() {
-        // arrange
-        Vector3D[] vertices = {
-            Vector3D.ZERO,
-            Vector3D.of(1, 0, 0),
-            Vector3D.of(1, 1, 0),
-            Vector3D.of(0, 1, 0),
-
-            Vector3D.of(0, 0, 1),
-            Vector3D.of(1, 0, 1),
-            Vector3D.of(1, 1, 1),
-            Vector3D.of(0, 1, 1)
-        };
-
-        int[][] facets = {
-            {0, 3, 2},
-            {0, 2, 1},
-
-            {4, 5, 6},
-            {4, 6, 7},
-
-            {5, 1, 2},
-            {5, 2, 6},
-
-            {4, 7, 3},
-            {4, 3, 0},
-
-            {4, 0, 1},
-            {4, 1, 5},
-
-            {7, 6, 2},
-            {7, 2, 3}
-        };
-
-        // act
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .withVertexList(vertices)
-                .addIndexedFacets(facets)
-                .build();
-
-        // assert
-        Assert.assertFalse(tree.isFull());
-        Assert.assertFalse(tree.isEmpty());
-
-        Assert.assertEquals(1, tree.getSize(), TEST_EPS);
-        Assert.assertEquals(6, tree.getBoundarySize(), TEST_EPS);
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(0.5, 0.5, 0.5), tree.getBarycenter(), TEST_EPS);
-    }
-
-    @Test
-    public void testBuilder_addIndexedFacets_concaveFacets() {
-        // arrange
-        Vector3D[] vertices = {
-                Vector3D.of(-1, 0, 1),
-                Vector3D.of(-1, 0, 0),
-
-                Vector3D.of(0, 2, 1),
-                Vector3D.of(0, 2, 0),
-
-                Vector3D.of(1, 0, 1),
-                Vector3D.of(1, 0, 0),
-
-                Vector3D.of(0, 1, 1),
-                Vector3D.of(0, 1, 0)
-        };
-
-        int[][] facets = {
-                {0, 2, 3, 1},
-                {4, 5, 3, 2},
-                {0, 1, 7, 6},
-                {4, 6, 7, 5},
-                {0, 6, 4, 2},
-                {1, 3, 5, 7}
-        };
-
-        // act
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .withVertexList(vertices)
-                .addIndexedFacets(facets)
-                .build();
-
-        // assert
-        Assert.assertFalse(tree.isFull());
-        Assert.assertFalse(tree.isEmpty());
-
-        Assert.assertTrue(Double.isFinite(tree.getSize()));
-        Assert.assertTrue(Double.isFinite(tree.getBoundarySize()));
-        Assert.assertNotNull(tree.getBarycenter());
-
-        EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.INSIDE, Vector3D.of(0, 1.5, 0.5));
-        EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.OUTSIDE, Vector3D.of(0, 0.5, 0.5));
-    }
-
-    @Test
-    public void testBuilder_addIndexedFacets_multipleVertexLists() {
-        // arrange
-        Vector3D p0 = Vector3D.ZERO;
-        Vector3D p1 = Vector3D.Unit.PLUS_X;
-        Vector3D p2 = Vector3D.Unit.PLUS_Y;
-        Vector3D p3 = Vector3D.Unit.PLUS_Z;
-
-        // act
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .withVertexList(p1, p2, p3)
-                .addIndexedFacet(Arrays.asList(2, 0, 1))
-
-                .withVertexList(p0, p1, p2, p3)
-                .addIndexedFacet(0, 2, 1)
-                .addIndexedFacets(new int[][] {
-                        {0, 1, 3},
-                        {0, 3, 2}
-                })
-                .build();
-
-        // assert
-        Assert.assertEquals(0.5 / 3.0, tree.getSize(), TEST_EPS);
-
-        EuclideanTestUtils.assertRegionLocation(tree, RegionLocation.INSIDE, Vector3D.of(0.25, 0.25, 0.25));
-    }
-
     // GEOMETRY-59
     @Test
     public void testSlightlyConcavePrism() {
@@ -1523,11 +1308,11 @@ public class RegionBSPTree3DTest {
             {3, 0, 4, 7}
         };
 
+        List<ConvexSubPlane> faces = indexedFacetsToBoundaries(vertices, facets);
+
         // act
-        RegionBSPTree3D tree = RegionBSPTree3D.builder(TEST_PRECISION)
-                .withVertexList(vertices)
-                .addIndexedFacets(facets)
-                .build();
+        RegionBSPTree3D tree = RegionBSPTree3D.full();
+        tree.insert(faces);
 
         // assert
         Assert.assertFalse(tree.isFull());
@@ -1540,6 +1325,26 @@ public class RegionBSPTree3DTest {
                 Vector3D.of(-1, 1, 1), Vector3D.of(4, 1, 1));
     }
 
+    private static List<ConvexSubPlane> indexedFacetsToBoundaries(Vector3D[] vertices, int[][] facets) {
+        List<ConvexSubPlane> boundaries = new ArrayList<>();
+
+        List<Vector3D> vertexList = new ArrayList<>();
+
+        for (int i = 0; i < facets.length; ++i) {
+            int[] indices = facets[i];
+
+            for (int j = 0; j < indices.length; ++j) {
+                vertexList.add(vertices[indices[j]]);
+            }
+
+            boundaries.add(ConvexSubPlane.fromVertexLoop(vertexList, TEST_PRECISION));
+
+            vertexList.clear();
+        }
+
+        return boundaries;
+    }
+
     private static RegionBSPTree3D createSphere(final Vector3D center, final double radius, final int stacks, final int slices) {
 
         final List<Plane> planes = new ArrayList<>();
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SubPlaneTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SubPlaneTest.java
index d40811e..99945f9 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SubPlaneTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/SubPlaneTest.java
@@ -32,6 +32,7 @@ import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
 import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
+import org.apache.commons.geometry.euclidean.twod.Boundaries2D;
 import org.apache.commons.geometry.euclidean.twod.ConvexArea;
 import org.apache.commons.geometry.euclidean.twod.Line;
 import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
@@ -181,7 +182,7 @@ public class SubPlaneTest {
     public void testSplit_both() {
         // arrange
         SubPlane sp = new SubPlane(XY_PLANE, false);
-        sp.getSubspaceRegion().union(RegionBSPTree2D.builder(TEST_PRECISION).addRect(Vector2D.of(-1, -1), Vector2D.of(1, 1)).build());
+        sp.getSubspaceRegion().union(Boundaries2D.rect(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
 
         Plane splitter = Plane.fromNormal(Vector3D.Unit.PLUS_X, TEST_PRECISION);
 
@@ -208,7 +209,7 @@ public class SubPlaneTest {
     public void testSplit_intersects_plusOnly() {
         // arrange
         SubPlane sp = new SubPlane(XY_PLANE, false);
-        sp.getSubspaceRegion().union(RegionBSPTree2D.builder(TEST_PRECISION).addRect(Vector2D.of(-1, -1), Vector2D.of(1, 1)).build());
+        sp.getSubspaceRegion().union(Boundaries2D.rect(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
 
         Plane splitter = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0.1, 0, 1), TEST_PRECISION);
 
@@ -226,7 +227,7 @@ public class SubPlaneTest {
     public void testSplit_intersects_minusOnly() {
         // arrange
         SubPlane sp = new SubPlane(XY_PLANE, false);
-        sp.getSubspaceRegion().union(RegionBSPTree2D.builder(TEST_PRECISION).addRect(Vector2D.of(-1, -1), Vector2D.of(1, 1)).build());
+        sp.getSubspaceRegion().union(Boundaries2D.rect(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
 
         Plane splitter = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.of(0.1, 0, -1), TEST_PRECISION);
 
@@ -244,7 +245,7 @@ public class SubPlaneTest {
     public void testSplit_parallel_plusOnly() {
         // arrange
         SubPlane sp = new SubPlane(XY_PLANE, false);
-        sp.getSubspaceRegion().union(RegionBSPTree2D.builder(TEST_PRECISION).addRect(Vector2D.of(-1, -1), Vector2D.of(1, 1)).build());
+        sp.getSubspaceRegion().union(Boundaries2D.rect(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
 
         Plane splitter = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.Unit.PLUS_Z, TEST_PRECISION);
 
@@ -262,7 +263,7 @@ public class SubPlaneTest {
     public void testSplit_parallel_minusOnly() {
         // arrange
         SubPlane sp = new SubPlane(XY_PLANE, false);
-        sp.getSubspaceRegion().union(RegionBSPTree2D.builder(TEST_PRECISION).addRect(Vector2D.of(-1, -1), Vector2D.of(1, 1)).build());
+        sp.getSubspaceRegion().union(Boundaries2D.rect(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
 
         Plane splitter = Plane.fromPointAndNormal(Vector3D.of(0, 0, 1), Vector3D.Unit.MINUS_Z, TEST_PRECISION);
 
@@ -280,7 +281,7 @@ public class SubPlaneTest {
     public void testSplit_coincident() {
         // arrange
         SubPlane sp = new SubPlane(XY_PLANE, false);
-        sp.getSubspaceRegion().union(RegionBSPTree2D.builder(TEST_PRECISION).addRect(Vector2D.of(-1, -1), Vector2D.of(1, 1)).build());
+        sp.getSubspaceRegion().union(Boundaries2D.rect(Vector2D.of(-1, -1), Vector2D.of(1, 1), TEST_PRECISION).toTree());
 
         // act
         Split<SubPlane> split = sp.split(sp.getPlane());
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Boundaries2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Boundaries2DTest.java
new file mode 100644
index 0000000..793497b
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Boundaries2DTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.twod;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class Boundaries2DTest {
+
+    private static final double TEST_EPS = 1e-10;
+
+    private static final DoublePrecisionContext TEST_PRECISION =
+            new EpsilonDoublePrecisionContext(TEST_EPS);
+
+    @Test
+    public void testRect_minFirst() {
+        // act
+        List<Segment> segments = Boundaries2D.rect(Vector2D.of(1, 2), Vector2D.of(3, 4), TEST_PRECISION)
+                .boundaryStream()
+                .collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(4, segments.size());
+
+        assertSegment(segments.get(0), Vector2D.of(1, 2), Vector2D.of(3, 2));
+        assertSegment(segments.get(1), Vector2D.of(3, 4), Vector2D.of(1, 4));
+        assertSegment(segments.get(2), Vector2D.of(3, 2), Vector2D.of(3, 4));
+        assertSegment(segments.get(3), Vector2D.of(1, 4), Vector2D.of(1, 2));
+    }
+
+    @Test
+    public void testRect_maxFirst() {
+        // act
+        List<Segment> segments = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(-1, -2), TEST_PRECISION)
+                .boundaryStream()
+                .collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(4, segments.size());
+
+        assertSegment(segments.get(0), Vector2D.of(-1, -2), Vector2D.of(0, -2));
+        assertSegment(segments.get(1), Vector2D.ZERO, Vector2D.of(-1, 0));
+        assertSegment(segments.get(2), Vector2D.of(0, -2), Vector2D.ZERO);
+        assertSegment(segments.get(3), Vector2D.of(-1, 0), Vector2D.of(-1, -2));
+    }
+
+    @Test
+    public void testRect_toTree() {
+        // act
+        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 4), TEST_PRECISION).toTree();
+
+        // assert
+        Assert.assertFalse(tree.isFull());
+        Assert.assertFalse(tree.isEmpty());
+
+        Assert.assertEquals(4, tree.getSize(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 2), tree.getBarycenter(), TEST_EPS);
+    }
+
+    @Test
+    public void testRect_illegalArgs() {
+        // act/assert
+        GeometryTestUtils.assertThrows(() -> {
+            Boundaries2D.rect(Vector2D.of(1, 1), Vector2D.of(1, 3), TEST_PRECISION);
+        }, IllegalArgumentException.class);
+
+        GeometryTestUtils.assertThrows(() -> {
+            Boundaries2D.rect(Vector2D.of(1, 1), Vector2D.of(3, 1), TEST_PRECISION);
+        }, IllegalArgumentException.class);
+
+        GeometryTestUtils.assertThrows(() -> {
+            Boundaries2D.rect(Vector2D.of(2, 3), Vector2D.of(2, 3), TEST_PRECISION);
+        }, IllegalArgumentException.class);
+    }
+
+    private static void assertSegment(Segment segment, Vector2D start, Vector2D end) {
+        EuclideanTestUtils.assertCoordinatesEqual(start, segment.getStartPoint(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(end, segment.getEndPoint(), TEST_EPS);
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/ConvexAreaTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/ConvexAreaTest.java
index 339af22..a054329 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/ConvexAreaTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/ConvexAreaTest.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.apache.commons.geometry.core.GeometryTestUtils;
 import org.apache.commons.geometry.core.RegionLocation;
@@ -54,6 +55,35 @@ public class ConvexAreaTest {
     }
 
     @Test
+    public void testBoundaryStream() {
+        // arrange
+        Line line = Line.fromPointAndAngle(Vector2D.ZERO, 0, TEST_PRECISION);
+        ConvexArea area = ConvexArea.fromBounds(line);
+
+        // act
+        List<Segment> segments = area.boundaryStream().collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(1, segments.size());
+        Segment segment = segments.get(0);
+        Assert.assertNull(segment.getStartPoint());
+        Assert.assertNull(segment.getEndPoint());
+        Assert.assertSame(line, segment.getLine());
+    }
+
+    @Test
+    public void testBoundaryStream_full() {
+        // arrange
+        ConvexArea area = ConvexArea.full();
+
+        // act
+        List<Segment> segments = area.boundaryStream().collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(0, segments.size());
+    }
+
+    @Test
     public void testToTree() {
         // arrange
         ConvexArea area = ConvexArea.fromBounds(
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolylineTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolylineTest.java
index c3d05fd..e4318ec 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolylineTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/PolylineTest.java
@@ -19,6 +19,7 @@ package org.apache.commons.geometry.euclidean.twod;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.apache.commons.numbers.angle.PlaneAngleRadians;
 import org.apache.commons.geometry.core.GeometryTestUtils;
@@ -544,21 +545,29 @@ public class PolylineTest {
     }
 
     @Test
-    public void testIterable() {
+    public void testBoundaryStream() {
         // arrange
-        Polyline path = Polyline.builder(TEST_PRECISION)
-                .appendVertices(Vector2D.ZERO, Vector2D.Unit.PLUS_X, Vector2D.of(1, 1)).build();
+        Segment seg = Segment.fromPoints(Vector2D.ZERO, Vector2D.of(1, 0), TEST_PRECISION);
+        Polyline path = Polyline.fromSegments(Arrays.asList(seg));
 
         // act
-        List<Segment> segments = new ArrayList<>();
-        for (Segment segment : path) {
-            segments.add(segment);
-        }
+        List<Segment> segments = path.boundaryStream().collect(Collectors.toList());
 
         // assert
-        Assert.assertEquals(2, segments.size());
-        assertFiniteSegment(segments.get(0), Vector2D.Unit.ZERO, Vector2D.Unit.PLUS_X);
-        assertFiniteSegment(segments.get(1), Vector2D.Unit.PLUS_X, Vector2D.of(1, 1));
+        Assert.assertEquals(1, segments.size());
+        Assert.assertSame(seg, segments.get(0));
+    }
+
+    @Test
+    public void testBoundaryStream_empty() {
+        // arrange
+        Polyline path = Polyline.empty();
+
+        // act
+        List<Segment> segments = path.boundaryStream().collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(0, segments.size());
     }
 
     @Test
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java
index d9e5fef..1fbb863 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/RegionBSPTree2DTest.java
@@ -21,6 +21,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.apache.commons.geometry.core.GeometryTestUtils;
 import org.apache.commons.geometry.core.Region;
@@ -30,7 +31,6 @@ import org.apache.commons.geometry.core.partitioning.SplitLocation;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
-import org.apache.commons.geometry.euclidean.oned.Interval;
 import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D.RegionNode2D;
 import org.apache.commons.numbers.angle.PlaneAngleRadians;
 import org.junit.Assert;
@@ -122,9 +122,8 @@ public class RegionBSPTree2DTest {
     @Test
     public void testBoundaries() {
         // arrange
-        RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
-                .addRect(Vector2D.ZERO, Vector2D.of(1, 1))
-                .build();
+        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
+                .toTree();
 
         // act
         List<Segment> segments = new ArrayList<>();
@@ -137,9 +136,8 @@ public class RegionBSPTree2DTest {
     @Test
     public void testGetBoundaries() {
         // arrange
-        RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
-                .addRect(Vector2D.ZERO, Vector2D.of(1, 1))
-                .build();
+        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
+                .toTree();
 
         // act
         List<Segment> segments = tree.getBoundaries();
@@ -149,6 +147,31 @@ public class RegionBSPTree2DTest {
     }
 
     @Test
+    public void testBoundaryStream() {
+        // arrange
+        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION)
+                .toTree();
+
+        // act
+        List<Segment> segments = tree.boundaryStream().collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(4, segments.size());
+    }
+
+    @Test
+    public void testBoundaryStream_noBoundaries() {
+        // arrange
+        RegionBSPTree2D tree = RegionBSPTree2D.full();
+
+        // act
+        List<Segment> segments = tree.boundaryStream().collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(0, segments.size());
+    }
+
+    @Test
     public void testGetBoundaryPaths_cachesResult() {
         // arrange
         RegionBSPTree2D tree = RegionBSPTree2D.empty();
@@ -289,9 +312,8 @@ public class RegionBSPTree2DTest {
     @Test
     public void testToConvex_square() {
         // arrange
-        RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
-                .addSquare(Vector2D.ZERO, 1)
-                .build();
+        RegionBSPTree2D tree = RegionBSPTree2D.from(
+                Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 1), TEST_PRECISION));
 
         // act
         List<ConvexArea> result = tree.toConvex();
@@ -451,9 +473,8 @@ public class RegionBSPTree2DTest {
     @Test
     public void testSplit_bothSides() {
         // arrange
-        RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
-                .addRect(Vector2D.ZERO, 2, 1)
-                .build();
+        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(2, 1), TEST_PRECISION)
+                .toTree();
 
         Line splitter = Line.fromPointAndAngle(Vector2D.ZERO, 0.25 * PlaneAngleRadians.PI, TEST_PRECISION);
 
@@ -477,9 +498,8 @@ public class RegionBSPTree2DTest {
     @Test
     public void testSplit_plusSideOnly() {
         // arrange
-        RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
-                .addRect(Vector2D.ZERO, 2, 1)
-                .build();
+        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(2, 1), TEST_PRECISION)
+                .toTree();
 
         Line splitter = Line.fromPointAndAngle(Vector2D.of(0, 1), 0.25 * PlaneAngleRadians.PI, TEST_PRECISION);
 
@@ -500,9 +520,8 @@ public class RegionBSPTree2DTest {
     @Test
     public void testSplit_minusSideOnly() {
         // arrange
-        RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
-                .addRect(Vector2D.ZERO, 2, 1)
-                .build();
+        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(2, 1), TEST_PRECISION)
+                .toTree();
 
         Line splitter = Line.fromPointAndAngle(Vector2D.of(0, 1), 0.25 * PlaneAngleRadians.PI, TEST_PRECISION)
                 .reverse();
@@ -748,12 +767,10 @@ public class RegionBSPTree2DTest {
     @Test
     public void testGeometricProperties_regionWithHole() {
         // arrange
-        RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
-                .addRect(Vector2D.ZERO, Vector2D.of(3, 3))
-                .build();
-        RegionBSPTree2D inner = RegionBSPTree2D.builder(TEST_PRECISION)
-                .addRect(Vector2D.of(1, 1), Vector2D.of(2, 2))
-                .build();
+        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(3, 3), TEST_PRECISION)
+                .toTree();
+        RegionBSPTree2D inner = Boundaries2D.rect(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION)
+                .toTree();
 
         tree.difference(inner);
 
@@ -789,12 +806,10 @@ public class RegionBSPTree2DTest {
     @Test
     public void testGeometricProperties_complementedRegionWithHole() {
         // arrange
-        RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
-                .addRect(Vector2D.ZERO, Vector2D.of(3, 3))
-                .build();
-        RegionBSPTree2D inner = RegionBSPTree2D.builder(TEST_PRECISION)
-                .addRect(Vector2D.of(1, 1), Vector2D.of(2, 2))
-                .build();
+        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(3, 3), TEST_PRECISION)
+                .toTree();
+        RegionBSPTree2D inner = Boundaries2D.rect(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION)
+                .toTree();
 
         tree.difference(inner);
 
@@ -830,15 +845,28 @@ public class RegionBSPTree2DTest {
     }
 
     @Test
+    public void testFrom_boundarySource_noBoundaries() {
+        // arrange
+        BoundarySource2D src = () -> new ArrayList<Segment>().stream();
+
+        // act
+        RegionBSPTree2D tree = RegionBSPTree2D.from(src);
+
+        // assert
+        Assert.assertNull(tree.getBarycenter());
+        Assert.assertTrue(tree.isFull());
+    }
+
+    @Test
     public void testFromConvexArea_full() {
         // arrange
         ConvexArea area = ConvexArea.full();
 
         // act
         RegionBSPTree2D tree = RegionBSPTree2D.from(area);
-        Assert.assertNull(tree.getBarycenter());
 
         // assert
+        Assert.assertNull(tree.getBarycenter());
         Assert.assertTrue(tree.isFull());
     }
 
@@ -885,100 +913,17 @@ public class RegionBSPTree2DTest {
     }
 
     @Test
-    public void testBuilder_rectMethods() {
-        // act
-        RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
-                .addCenteredRect(Vector2D.ZERO, 1, 2)
-                .addCenteredSquare(Vector2D.of(5, 0), -2)
-
-                .addRect(Vector2D.of(10, 5), -2, -3)
-                .addRect(Vector2D.of(15, 5), Vector2D.of(16, 4))
-
-                .addSquare(Vector2D.of(20, 1), 3)
-
-                .build();
-
-        // assert
-        Assert.assertEquals(2 + 4 + 6 + 1 + 9, tree.getSize(), TEST_EPS);
-
-        checkClassify(tree, RegionLocation.INSIDE,
-                Vector2D.ZERO,
-                Vector2D.of(5, 0),
-                Vector2D.of(9, 3.5),
-                Vector2D.of(15.5, 4.5),
-                Vector2D.of(21.5, 2.5));
-
-        checkClassify(tree, RegionLocation.BOUNDARY,
-                Vector2D.of(-0.5, -1), Vector2D.of(0.5, 1),
-                Vector2D.of(4, -1), Vector2D.of(6, 1));
-    }
-
-    @Test
-    public void testBuilder_rectMethods_zeroSize() {
+    public void testToTree_returnsNewTree() {
         // arrange
-        final RegionBSPTree2D.Builder builder = RegionBSPTree2D.builder(TEST_PRECISION);
-
-        // act/assert
-        GeometryTestUtils.assertThrows(() -> {
-            builder.addRect(Vector2D.of(1, 1), 0, 2);
-        }, IllegalArgumentException.class);
-
-        GeometryTestUtils.assertThrows(() -> {
-            builder.addRect(Vector2D.of(1, 1), 2, 0);
-        }, IllegalArgumentException.class);
-
-        GeometryTestUtils.assertThrows(() -> {
-            builder.addRect(Vector2D.of(2, 3), 0, 0);
-        }, IllegalArgumentException.class);
-
-        GeometryTestUtils.assertThrows(() -> {
-            builder.addRect(Vector2D.of(1, 1), Vector2D.of(1, 3));
-        }, IllegalArgumentException.class);
-
-        GeometryTestUtils.assertThrows(() -> {
-            builder.addRect(Vector2D.of(1, 1), Vector2D.of(3, 1));
-        }, IllegalArgumentException.class);
-
-        GeometryTestUtils.assertThrows(() -> {
-            builder.addRect(Vector2D.of(2, 3), Vector2D.of(2, 3));
-        }, IllegalArgumentException.class);
-
-        GeometryTestUtils.assertThrows(() -> {
-            builder.addCenteredRect(Vector2D.of(2, 3), 0, 1e-20);
-        }, IllegalArgumentException.class);
-
-        GeometryTestUtils.assertThrows(() -> {
-            builder.addSquare(Vector2D.of(2, 3), 1e-20);
-        }, IllegalArgumentException.class);
-
-        GeometryTestUtils.assertThrows(() -> {
-            builder.addCenteredSquare(Vector2D.of(2, 3), 0);
-        }, IllegalArgumentException.class);
-    }
-
-    @Test
-    public void testBuilder_mixedArguments() {
-        // arrange
-        Line minusYAxis = Line.fromPointAndAngle(Vector2D.ZERO, -PlaneAngleRadians.PI_OVER_TWO, TEST_PRECISION);
-
-        Polyline path = Polyline.builder(TEST_PRECISION)
-            .append(Vector2D.Unit.PLUS_X)
-            .append(Vector2D.of(1, 1))
-            .build();
+        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(1, 2), TEST_PRECISION).toTree();
 
         // act
-        RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
-                .add(Segment.fromPoints(Vector2D.ZERO, Vector2D.Unit.PLUS_X, TEST_PRECISION))
-                .add(path)
-                .addSegment(Vector2D.of(1, 1), Vector2D.Unit.PLUS_Y)
-                .add(new SubLine(minusYAxis, Interval.of(-1, 0, TEST_PRECISION).toTree()))
-                .build();
+        RegionBSPTree2D result = tree.toTree();
 
         // assert
-        Assert.assertEquals(1, tree.getSize(), TEST_EPS);
-        Assert.assertEquals(4, tree.getBoundarySize(), TEST_EPS);
-
-        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 0.5), tree.getBarycenter(), TEST_EPS);
+        Assert.assertNotSame(tree, result);
+        Assert.assertEquals(2, result.getSize(), TEST_EPS);
+        EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(0.5, 1), result.getBarycenter(), TEST_EPS);
     }
 
     @Test
@@ -1006,9 +951,8 @@ public class RegionBSPTree2DTest {
     @Test
     public void testProject_rect() {
         // arrange
-        RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
-                .addSquare(Vector2D.of(1, 1), 1)
-                .build();
+        RegionBSPTree2D tree = Boundaries2D.rect(
+                    Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION).toTree();
 
         // act/assert
         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(1, 1), tree.project(Vector2D.ZERO), TEST_EPS);
@@ -1031,9 +975,8 @@ public class RegionBSPTree2DTest {
     @Test
     public void testTransform() {
         // arrange
-        RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
-                .addRect(Vector2D.of(1, 1), 2, 1)
-                .build();
+        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.of(1, 1), Vector2D.of(3, 2), TEST_PRECISION)
+                .toTree();
 
         AffineTransformMatrix2D transform = AffineTransformMatrix2D.createScale(0.5, 2)
                 .rotate(PlaneAngleRadians.PI_OVER_TWO)
@@ -1101,9 +1044,7 @@ public class RegionBSPTree2DTest {
     @Test
     public void testTransform_reflection() {
         // arrange
-        RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
-                .addSquare(Vector2D.of(1, 1), 1)
-                .build();
+        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION).toTree();
 
         Transform2D transform = FunctionTransform2D.from(v -> Vector2D.of(-v.getX(), v.getY()));
 
@@ -1125,9 +1066,8 @@ public class RegionBSPTree2DTest {
     @Test
     public void testTransform_doubleReflection() {
         // arrange
-        RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
-                .addSquare(Vector2D.of(1, 1), 1)
-                .build();
+        RegionBSPTree2D tree = Boundaries2D.rect(
+                    Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION).toTree();
 
         Transform2D transform = FunctionTransform2D.from(Vector2D::negate);
 
@@ -1149,20 +1089,18 @@ public class RegionBSPTree2DTest {
     @Test
     public void testBooleanOperations() {
         // arrange
-        RegionBSPTree2D tree = RegionBSPTree2D.builder(TEST_PRECISION)
-                .addSquare(Vector2D.ZERO, 3)
-                .build();
+        RegionBSPTree2D tree = Boundaries2D.rect(Vector2D.ZERO, Vector2D.of(3, 3), TEST_PRECISION).toTree();
         RegionBSPTree2D temp;
 
         // act
-        temp = RegionBSPTree2D.builder(TEST_PRECISION).addSquare(Vector2D.of(1, 1), 1).build();
+        temp = Boundaries2D.rect(Vector2D.of(1, 1), Vector2D.of(2, 2), TEST_PRECISION).toTree();
         temp.complement();
         tree.intersection(temp);
 
-        temp = RegionBSPTree2D.builder(TEST_PRECISION).addSquare(Vector2D.of(3, 0), 3).build();
+        temp = Boundaries2D.rect(Vector2D.of(3, 0), Vector2D.of(6, 3), TEST_PRECISION).toTree();
         tree.union(temp);
 
-        temp = RegionBSPTree2D.builder(TEST_PRECISION).addRect(Vector2D.of(2, 1), 3, 1).build();
+        temp = Boundaries2D.rect(Vector2D.of(2, 1), Vector2D.of(5, 2), TEST_PRECISION).toTree();
         tree.difference(temp);
 
         temp.setFull();
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1S.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1S.java
index 1c6bc06..b056043 100644
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1S.java
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/oned/RegionBSPTree1S.java
@@ -203,7 +203,7 @@ public class RegionBSPTree1S extends AbstractRegionBSPTree<Point1S, RegionBSPTre
         }
 
         final List<BoundaryPair> insideBoundaryPairs = new ArrayList<>();
-        for (RegionNode1S node : this) {
+        for (RegionNode1S node : nodes()) {
             if (node.isInside()) {
                 insideBoundaryPairs.add(getNodeBoundaryPair(node));
             }
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/BoundarySource2S.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/BoundarySource2S.java
new file mode 100644
index 0000000..f0173c1
--- /dev/null
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/BoundarySource2S.java
@@ -0,0 +1,35 @@
+/*
+ * 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.spherical.twod;
+
+import org.apache.commons.geometry.core.partitioning.BoundarySource;
+
+/** Extension of the {@link BoundarySource} interface for spherical 2D
+ * space.
+ */
+public interface BoundarySource2S extends BoundarySource<GreatArc> {
+
+    /** Construct a new BSP tree from the boundaries contained in this
+     * instance.
+     * @return a new BSP tree constructed from the boundaries in this
+     *      instance
+     * @see RegionBSPTree2S#from(BoundarySource)
+     */
+    default RegionBSPTree2S toTree() {
+        return RegionBSPTree2S.from(this);
+    }
+}
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/ConvexArea2S.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/ConvexArea2S.java
index d77b892..d87dceb 100644
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/ConvexArea2S.java
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/ConvexArea2S.java
@@ -22,6 +22,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.apache.commons.numbers.angle.PlaneAngleRadians;
 import org.apache.commons.geometry.core.Transform;
@@ -35,7 +36,8 @@ import org.apache.commons.geometry.euclidean.threed.Vector3D;
 /** Class representing a convex area in 2D spherical space. The boundaries of this
  * area, if any, are composed of convex great circle arcs.
  */
-public final class ConvexArea2S extends AbstractConvexHyperplaneBoundedRegion<Point2S, GreatArc> {
+public final class ConvexArea2S extends AbstractConvexHyperplaneBoundedRegion<Point2S, GreatArc>
+    implements BoundarySource2S {
     /** Instance representing the full spherical area. */
     private static final ConvexArea2S FULL = new ConvexArea2S(Collections.emptyList());
 
@@ -54,6 +56,12 @@ public final class ConvexArea2S extends AbstractConvexHyperplaneBoundedRegion<Po
         super(boundaries);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public Stream<GreatArc> boundaryStream() {
+        return getBoundaries().stream();
+    }
+
     /** Get a path instance representing the boundary of the area. The path is oriented
      * so that the minus sides of the arcs lie on the inside of the area.
      * @return the boundary path of the area
@@ -166,13 +174,6 @@ public final class ConvexArea2S extends AbstractConvexHyperplaneBoundedRegion<Po
         return (GreatArc) super.trim(convexSubHyperplane);
     }
 
-    /** Return a BSP tree instance representing the same region as the current instance.
-     * @return a BSP tree instance representing the same region as the current instance
-     */
-    public RegionBSPTree2S toTree() {
-        return RegionBSPTree2S.from(this);
-    }
-
     /** Return an instance representing the full spherical 2D space.
      * @return an instance representing the full spherical 2D space.
      */
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatArcPath.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatArcPath.java
index 7d3e7d5..cf10762 100644
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatArcPath.java
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/GreatArcPath.java
@@ -21,14 +21,14 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Iterator;
 import java.util.List;
+import java.util.stream.Stream;
 
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 
 /** Class representing a connected sequence of {@link GreatArc} instances.
  */
-public final class GreatArcPath implements Iterable<GreatArc> {
+public final class GreatArcPath implements BoundarySource2S {
     /** Instance containing no arcs. */
     private static final GreatArcPath EMPTY = new GreatArcPath(Collections.emptyList());
 
@@ -42,6 +42,12 @@ public final class GreatArcPath implements Iterable<GreatArc> {
         this.arcs = Collections.unmodifiableList(arcs);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public Stream<GreatArc> boundaryStream() {
+        return getArcs().stream();
+    }
+
     /** Get the arcs in path.
      * @return the arcs in the path
      */
@@ -140,22 +146,6 @@ public final class GreatArcPath implements Iterable<GreatArc> {
         return false;
     }
 
-    /** {@inheritDoc} */
-    @Override
-    public Iterator<GreatArc> iterator() {
-        return arcs.iterator();
-    }
-
-    /** Construct a {@link RegionBSPTree2S} from the arcs in this instance.
-     * @return a bsp tree constructed from the arcs in this instance
-     */
-    public RegionBSPTree2S toTree() {
-        RegionBSPTree2S tree = RegionBSPTree2S.empty();
-        tree.insert(this);
-
-        return tree;
-    }
-
     /** Return a string representation of this arc path instance.
     *
     * <p>In order to keep the string representation short but useful, the exact format of the return
diff --git a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2S.java b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2S.java
index d519e22..a443c75 100644
--- a/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2S.java
+++ b/commons-geometry-spherical/src/main/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2S.java
@@ -19,18 +19,22 @@ package org.apache.commons.geometry.spherical.twod;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 
-import org.apache.commons.numbers.angle.PlaneAngleRadians;
+import org.apache.commons.geometry.core.partitioning.BoundarySource;
 import org.apache.commons.geometry.core.partitioning.Hyperplane;
 import org.apache.commons.geometry.core.partitioning.Split;
 import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
 import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.numbers.angle.PlaneAngleRadians;
 
 /** BSP tree representing regions in 2D spherical space.
  */
-public class RegionBSPTree2S extends AbstractRegionBSPTree<Point2S, RegionBSPTree2S.RegionNode2S> {
+public class RegionBSPTree2S extends AbstractRegionBSPTree<Point2S, RegionBSPTree2S.RegionNode2S>
+    implements BoundarySource2S {
     /** Constant containing the area of the full spherical space. */
     private static final double FULL_SIZE = 4 * PlaneAngleRadians.PI;
 
@@ -71,6 +75,12 @@ public class RegionBSPTree2S extends AbstractRegionBSPTree<Point2S, RegionBSPTre
 
     /** {@inheritDoc} */
     @Override
+    public Stream<GreatArc> boundaryStream() {
+        return StreamSupport.stream(boundaries().spliterator(), false);
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public List<GreatArc> getBoundaries() {
         return createBoundaryList(b -> (GreatArc) b);
     }
@@ -211,14 +221,15 @@ public class RegionBSPTree2S extends AbstractRegionBSPTree<Point2S, RegionBSPTre
         return new RegionBSPTree2S(true);
     }
 
-    /** Construct a tree from a convex area.
-     * @param area the area to construct a tree from
-     * @return tree instance representing the same area as the given
-     *      convex area
+    /** Construct a new tree from the boundaries in the given boundary source. If no boundaries
+     * are present in the given source, their the returned tree contains the full space.
+     * @param boundarySrc boundary source to construct a tree from
+     * @return a new tree instance constructed from the boundaries in the
+     *      given source
      */
-    public static RegionBSPTree2S from(final ConvexArea2S area) {
+    public static RegionBSPTree2S from(final BoundarySource<GreatArc> boundarySrc) {
         final RegionBSPTree2S tree = RegionBSPTree2S.full();
-        tree.insert(area.getBoundaries());
+        tree.insert(boundarySrc);
 
         return tree;
     }
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/ConvexArea2STest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/ConvexArea2STest.java
index 9380c4e..0307804 100644
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/ConvexArea2STest.java
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/ConvexArea2STest.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.apache.commons.numbers.angle.PlaneAngleRadians;
 import org.apache.commons.geometry.core.GeometryTestUtils;
@@ -540,6 +541,32 @@ public class ConvexArea2STest {
     }
 
     @Test
+    public void testBoundaryStream() {
+        // arrange
+        GreatCircle circle = GreatCircle.fromPole(Vector3D.Unit.PLUS_X, TEST_PRECISION);
+        ConvexArea2S area = ConvexArea2S.fromBounds(circle);
+
+        // act
+        List<GreatArc> arcs = area.boundaryStream().collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(1, arcs.size());
+        Assert.assertSame(circle, arcs.get(0).getCircle());
+    }
+
+    @Test
+    public void testBoundaryStream_noBoundaries() {
+        // arrange
+        ConvexArea2S area = ConvexArea2S.full();
+
+        // act
+        List<GreatArc> arcs = area.boundaryStream().collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(0, arcs.size());
+    }
+
+    @Test
     public void testGetInteriorAngles_noAngles() {
         // act/assert
         Assert.assertEquals(0, ConvexArea2S.full().getInteriorAngles().length);
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcPathTest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcPathTest.java
index 52c13a3..626701b 100644
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcPathTest.java
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/GreatArcPathTest.java
@@ -16,21 +16,21 @@
  */
 package org.apache.commons.geometry.spherical.twod;
 
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
-import org.apache.commons.numbers.angle.PlaneAngleRadians;
 import org.apache.commons.geometry.core.GeometryTestUtils;
 import org.apache.commons.geometry.core.RegionLocation;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
 import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 import org.apache.commons.geometry.spherical.SphericalTestUtils;
+import org.apache.commons.numbers.angle.PlaneAngleRadians;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -284,22 +284,29 @@ public class GreatArcPathTest {
     }
 
     @Test
-    public void testIterator() {
+    public void testBoundaryStream() {
         // arrange
-        GreatArc a = GreatArc.fromPoints(Point2S.PLUS_I, Point2S.PLUS_J, TEST_PRECISION);
-        GreatArc b = GreatArc.fromPoints(Point2S.PLUS_J, Point2S.PLUS_K, TEST_PRECISION);
+        GreatArc fullArc = GreatCircle.fromPole(Vector3D.Unit.PLUS_X, TEST_PRECISION).span();
+        GreatArcPath path = GreatArcPath.fromArcs(fullArc);
 
-        GreatArcPath path = GreatArcPath.fromArcs(Arrays.asList(a, b));
+        // act
+        List<GreatArc> arcs = path.boundaryStream().collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(1, arcs.size());
+        Assert.assertSame(fullArc, arcs.get(0));
+    }
 
-        List<GreatArc> arcs = new ArrayList<>();
+    @Test
+    public void testBoundaryStream_noBoundaries() {
+        // arrange
+        GreatArcPath path = GreatArcPath.empty();
 
         // act
-        for (GreatArc arc : path) {
-            arcs.add(arc);
-        }
+        List<GreatArc> arcs = path.boundaryStream().collect(Collectors.toList());
 
         // assert
-        Assert.assertEquals(arcs, Arrays.asList(a, b));
+        Assert.assertEquals(0, arcs.size());
     }
 
     @Test
@@ -308,8 +315,8 @@ public class GreatArcPathTest {
         RegionBSPTree2S tree = GreatArcPath.empty().toTree();
 
         // assert
-        Assert.assertFalse(tree.isFull());
-        Assert.assertTrue(tree.isEmpty());
+        Assert.assertTrue(tree.isFull());
+        Assert.assertFalse(tree.isEmpty());
     }
 
     @Test
diff --git a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2STest.java b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2STest.java
index 4b15a22..0f0b22e 100644
--- a/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2STest.java
+++ b/commons-geometry-spherical/src/test/java/org/apache/commons/geometry/spherical/twod/RegionBSPTree2STest.java
@@ -111,7 +111,7 @@ public class RegionBSPTree2STest {
     }
 
     @Test
-    public void testFromConvexArea() {
+    public void testFromBoundarySource() {
         // arrange
         ConvexArea2S area = ConvexArea2S.fromVertexLoop(Arrays.asList(
                     Point2S.of(0.1, 0.1), Point2S.of(0, 0.5),
@@ -131,6 +131,18 @@ public class RegionBSPTree2STest {
     }
 
     @Test
+    public void testFromBoundarySource_noBoundaries() {
+        // arrange
+        BoundarySource2S src = () -> new ArrayList<GreatArc>().stream();
+
+        // act
+        RegionBSPTree2S tree = RegionBSPTree2S.from(src);
+
+        // assert
+        Assert.assertTrue(tree.isFull());
+    }
+
+    @Test
     public void testCopy() {
         // arrange
         RegionBSPTree2S tree = new RegionBSPTree2S(true);
@@ -172,6 +184,45 @@ public class RegionBSPTree2STest {
     }
 
     @Test
+    public void testBoundaryStream() {
+        // arrange
+        RegionBSPTree2S tree = RegionBSPTree2S.empty();
+        insertPositiveQuadrant(tree);
+
+        // act
+        List<GreatArc> arcs = tree.boundaryStream().collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(3, arcs.size());
+    }
+
+    @Test
+    public void testBoundaryStream_noBoundaries() {
+        // arrange
+        RegionBSPTree2S tree = RegionBSPTree2S.empty();
+
+        // act
+        List<GreatArc> arcs = tree.boundaryStream().collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(0, arcs.size());
+    }
+
+    @Test
+    public void testToTree_returnsNewInstance() {
+        // arrange
+        RegionBSPTree2S tree = RegionBSPTree2S.empty();
+        insertPositiveQuadrant(tree);
+
+        // act
+        RegionBSPTree2S result = tree.toTree();
+
+        // assert
+        Assert.assertNotSame(tree, result);
+        Assert.assertEquals(3, result.getBoundaries().size());
+    }
+
+    @Test
     public void testGetBoundaryPaths_cachesResult() {
         // arrange
         RegionBSPTree2S tree = RegionBSPTree2S.empty();
diff --git a/src/site/xdoc/index.xml b/src/site/xdoc/index.xml
index af2ea39..3edcb1d 100644
--- a/src/site/xdoc/index.xml
+++ b/src/site/xdoc/index.xml
@@ -46,9 +46,9 @@ DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-6);
 
 // create a binary space partitioning tree representing the unit cube
 // centered on the origin
-RegionBSPTree3D region = RegionBSPTree3D.builder(precision)
-        .addRect(Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5))
-        .build();
+RegionBSPTree3D region = Boundaries3D.rect(
+        Vector3D.of(-0.5, -0.5, -0.5), Vector3D.of(0.5, 0.5, 0.5), precision)
+        .toTree();
 
 // create a rotated copy of the region
 Transform3D rotation = QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Z, 0.25 * Math.PI);


[commons-geometry] 02/03: Merge branch 'GEOMETRY-72__Matt'

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

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

commit a314aa6886df9478c965ca5de497f7b51c834818
Merge: 4fafb06 d65ea8f
Author: Gilles Sadowski <gi...@harfang.homelinux.org>
AuthorDate: Wed Dec 25 13:47:09 2019 +0100

    Merge branch 'GEOMETRY-72__Matt'
    
    Closes #48.

 .../geometry/core/partitioning/BoundarySource.java |  34 ++
 .../core/partitioning/bsp/AbstractBSPTree.java     |  24 +-
 .../partitioning/bsp/AbstractRegionBSPTree.java    | 253 ++++++-----
 .../geometry/core/partitioning/bsp/BSPSubtree.java |  15 +-
 .../geometry/core/partitioning/bsp/BSPTree.java    |   7 +
 .../bsp/AbstractBSPTreeMergeOperatorTest.java      |   6 +-
 .../core/partitioning/bsp/AbstractBSPTreeTest.java | 154 +++----
 .../bsp/AbstractRegionBSPTreeTest.java             |   4 +-
 .../partitioning/bsp/AttributeBSPTreeTest.java     |   8 +-
 .../geometry/euclidean/oned/RegionBSPTree1D.java   |   2 +-
 .../geometry/euclidean/threed/Boundaries3D.java    |  86 ++++
 .../euclidean/threed/BoundarySource3D.java         |  35 ++
 .../geometry/euclidean/threed/ConvexVolume.java    |  18 +-
 .../geometry/euclidean/threed/RegionBSPTree3D.java | 291 +-----------
 .../geometry/euclidean/twod/Boundaries2D.java      |  71 +++
 .../geometry/euclidean/twod/BoundarySource2D.java  |  35 ++
 .../geometry/euclidean/twod/ConvexArea.java        |  26 +-
 .../commons/geometry/euclidean/twod/Polyline.java  |  26 +-
 .../geometry/euclidean/twod/RegionBSPTree2D.java   | 196 +-------
 .../euclidean/DocumentationExamplesTest.java       |   7 +-
 .../euclidean/threed/Boundaries3DTest.java         | 135 ++++++
 .../euclidean/threed/ConvexVolumeTest.java         |  32 +-
 .../euclidean/threed/RegionBSPTree3DTest.java      | 491 +++++++--------------
 .../geometry/euclidean/threed/SubPlaneTest.java    |  13 +-
 .../geometry/euclidean/twod/Boundaries2DTest.java  | 101 +++++
 .../geometry/euclidean/twod/ConvexAreaTest.java    |  30 ++
 .../geometry/euclidean/twod/PolylineTest.java      |  29 +-
 .../euclidean/twod/RegionBSPTree2DTest.java        | 216 ++++-----
 .../geometry/spherical/oned/RegionBSPTree1S.java   |   2 +-
 .../geometry/spherical/twod/BoundarySource2S.java  |  35 ++
 .../geometry/spherical/twod/ConvexArea2S.java      |  17 +-
 .../geometry/spherical/twod/GreatArcPath.java      |  26 +-
 .../geometry/spherical/twod/RegionBSPTree2S.java   |  27 +-
 .../geometry/spherical/twod/ConvexArea2STest.java  |  27 ++
 .../geometry/spherical/twod/GreatArcPathTest.java  |  33 +-
 .../spherical/twod/RegionBSPTree2STest.java        |  53 ++-
 src/site/xdoc/index.xml                            |   6 +-
 37 files changed, 1291 insertions(+), 1280 deletions(-)