GEOMETRY-72: adding BoundarySource interface and implementations in Euclidean 2D, 3D and spherical 2D; making a number of internal BSP tree classes private
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.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 @@
         }
     }
 
-    /** Return an iterator over the nodes in the tree. */
+    /** {@inheritDoc} */
     @Override
-    public Iterator<N> iterator() {
-        return new NodeIterator<>(getRoot());
+    public void insert(final BoundarySource<? extends ConvexSubHyperplane<P>> boundarySrc) {
+        try (Stream<? extends ConvexSubHyperplane<P>> stream = boundarySrc.boundaryStream()) {
+            stream.forEach(this::insert);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Iterable<N> nodes() {
+        return () -> new NodeIterator<>(getRoot());
     }
 
     /** {@inheritDoc} */
@@ -894,8 +904,8 @@
 
         /** {@inheritDoc} */
         @Override
-        public Iterator<N> iterator() {
-            return new NodeIterator<>(getSelf());
+        public Iterable<N> nodes() {
+            return () -> new NodeIterator<>(getSelf());
         }
 
         /** {@inheritDoc} */
@@ -1075,7 +1085,7 @@
      * @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 @@
         /** 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 @@
             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 @@
     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 @@
 
         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,133 +645,6 @@
         }
     }
 
-    /** Class containing the basic algorithm for merging region BSP trees.
-     * @param <P> Point implementation type
-     * @param <N> BSP tree node implementation type
-     */
-    public abstract static class RegionMergeOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
-        extends AbstractBSPTreeMergeOperator<P, N> {
-
-        /** Merge two input trees, storing the output in the third. The output tree can be one of the
-         * input trees. The output tree is condensed before the method returns.
-         * @param inputTree1 first input tree
-         * @param inputTree2 second input tree
-         * @param outputTree the tree that will contain the result of the merge; may be one
-         *      of the input trees
-         */
-        public void apply(final AbstractRegionBSPTree<P, N> inputTree1, final AbstractRegionBSPTree<P, N> inputTree2,
-                final AbstractRegionBSPTree<P, N> outputTree) {
-
-            this.performMerge(inputTree1, inputTree2, outputTree);
-
-            outputTree.condense();
-        }
-    }
-
-    /** Class for performing boolean union operations on region trees.
-     * @param <P> Point implementation type
-     * @param <N> BSP tree node implementation type
-     */
-    public static class UnionOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
-        extends RegionMergeOperator<P, N> {
-
-        /** {@inheritDoc} */
-        @Override
-        protected N mergeLeaf(final N node1, final N node2) {
-            if (node1.isLeaf()) {
-                return node1.isInside() ? node1 : node2;
-            }
-
-            // call again with flipped arguments
-            return mergeLeaf(node2, node1);
-        }
-    }
-
-    /** Class for performing boolean intersection operations on region trees.
-     * @param <P> Point implementation type
-     * @param <N> BSP tree node implementation type
-     */
-    public static class IntersectionOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
-        extends RegionMergeOperator<P, N> {
-
-        /** {@inheritDoc} */
-        @Override
-        protected N mergeLeaf(final N node1, final N node2) {
-            if (node1.isLeaf()) {
-                return node1.isInside() ? node2 : node1;
-            }
-
-            // call again with flipped arguments
-            return mergeLeaf(node2, node1);
-        }
-    }
-
-    /** Class for performing boolean difference operations on region trees.
-     * @param <P> Point implementation type
-     * @param <N> BSP tree node implementation type
-     */
-    public static class DifferenceOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
-        extends RegionMergeOperator<P, N> {
-
-        /** {@inheritDoc} */
-        @Override
-        protected N mergeLeaf(final N node1, final N node2) {
-            // a region is included if it belongs in tree1 and is not in tree2
-
-            if (node1.isInside()) {
-                // this region is inside of tree1, so only include subregions that are
-                // not in tree2, ie include everything in node2's complement
-                final N output = outputSubtree(node2);
-                output.getTree().complementRecursive(output);
-
-                return output;
-            } else if (node2.isInside()) {
-                // this region is inside of tree2 and so cannot be in the result region
-                final N output = outputNode();
-                output.setLocation(RegionLocation.OUTSIDE);
-
-                return output;
-            }
-
-            // this region is not in tree2, so we can include everything in tree1
-            return node1;
-        }
-    }
-
-    /** Class for performing boolean symmetric difference (xor) operations on region trees.
-     * @param <P> Point implementation type
-     * @param <N> BSP tree node implementation type
-     */
-    public static class XorOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
-        extends RegionMergeOperator<P, N> {
-
-        /** {@inheritDoc} */
-        @Override
-        protected N mergeLeaf(final N node1, final N node2) {
-            // a region is included if it belongs in tree1 and is not in tree2 OR
-            // it belongs in tree2 and is not in tree1
-
-            if (node1.isLeaf()) {
-                if (node1.isInside()) {
-                    // this region is inside node1, so only include subregions that are
-                    // not in node2, ie include everything in node2's complement
-                    final N output = outputSubtree(node2);
-                    output.getTree().complementRecursive(output);
-
-                    return output;
-                } else {
-                    // this region is not in node1, so only include subregions that
-                    // in node2
-                    return node2;
-                }
-            }
-
-            // the operation is symmetric, so perform the same operation but with the
-            // nodes flipped
-            return mergeLeaf(node2, node1);
-        }
-    }
-
     /** Class used to compute the point on the region's boundary that is closest to a target point.
      * @param <P> Point implementation type
      * @param <N> BSP tree node implementation type
@@ -887,12 +759,139 @@
         }
     }
 
+    /** Class containing the basic algorithm for merging region BSP trees.
+     * @param <P> Point implementation type
+     * @param <N> BSP tree node implementation type
+     */
+    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
+         * input trees. The output tree is condensed before the method returns.
+         * @param inputTree1 first input tree
+         * @param inputTree2 second input tree
+         * @param outputTree the tree that will contain the result of the merge; may be one
+         *      of the input trees
+         */
+        public void apply(final AbstractRegionBSPTree<P, N> inputTree1, final AbstractRegionBSPTree<P, N> inputTree2,
+                final AbstractRegionBSPTree<P, N> outputTree) {
+
+            this.performMerge(inputTree1, inputTree2, outputTree);
+
+            outputTree.condense();
+        }
+    }
+
+    /** Class for performing boolean union operations on region trees.
+     * @param <P> Point implementation type
+     * @param <N> BSP tree node implementation type
+     */
+    private static final class UnionOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+        extends RegionMergeOperator<P, N> {
+
+        /** {@inheritDoc} */
+        @Override
+        protected N mergeLeaf(final N node1, final N node2) {
+            if (node1.isLeaf()) {
+                return node1.isInside() ? node1 : node2;
+            }
+
+            // call again with flipped arguments
+            return mergeLeaf(node2, node1);
+        }
+    }
+
+    /** Class for performing boolean intersection operations on region trees.
+     * @param <P> Point implementation type
+     * @param <N> BSP tree node implementation type
+     */
+    private static final class IntersectionOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+        extends RegionMergeOperator<P, N> {
+
+        /** {@inheritDoc} */
+        @Override
+        protected N mergeLeaf(final N node1, final N node2) {
+            if (node1.isLeaf()) {
+                return node1.isInside() ? node2 : node1;
+            }
+
+            // call again with flipped arguments
+            return mergeLeaf(node2, node1);
+        }
+    }
+
+    /** Class for performing boolean difference operations on region trees.
+     * @param <P> Point implementation type
+     * @param <N> BSP tree node implementation type
+     */
+    private static final class DifferenceOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+        extends RegionMergeOperator<P, N> {
+
+        /** {@inheritDoc} */
+        @Override
+        protected N mergeLeaf(final N node1, final N node2) {
+            // a region is included if it belongs in tree1 and is not in tree2
+
+            if (node1.isInside()) {
+                // this region is inside of tree1, so only include subregions that are
+                // not in tree2, ie include everything in node2's complement
+                final N output = outputSubtree(node2);
+                output.getTree().complementRecursive(output);
+
+                return output;
+            } else if (node2.isInside()) {
+                // this region is inside of tree2 and so cannot be in the result region
+                final N output = outputNode();
+                output.setLocation(RegionLocation.OUTSIDE);
+
+                return output;
+            }
+
+            // this region is not in tree2, so we can include everything in tree1
+            return node1;
+        }
+    }
+
+    /** Class for performing boolean symmetric difference (xor) operations on region trees.
+     * @param <P> Point implementation type
+     * @param <N> BSP tree node implementation type
+     */
+    private static final class XorOperator<P extends Point<P>, N extends AbstractRegionNode<P, N>>
+        extends RegionMergeOperator<P, N> {
+
+        /** {@inheritDoc} */
+        @Override
+        protected N mergeLeaf(final N node1, final N node2) {
+            // a region is included if it belongs in tree1 and is not in tree2 OR
+            // it belongs in tree2 and is not in tree1
+
+            if (node1.isLeaf()) {
+                if (node1.isInside()) {
+                    // this region is inside node1, so only include subregions that are
+                    // not in node2, ie include everything in node2's complement
+                    final N output = outputSubtree(node2);
+                    output.getTree().complementRecursive(output);
+
+                    return output;
+                } else {
+                    // this region is not in node1, so only include subregions that
+                    // in node2
+                    return node2;
+                }
+            }
+
+            // the operation is symmetric, so perform the same operation but with the
+            // nodes flipped
+            return mergeLeaf(node2, node1);
+        }
+    }
+
     /** Class 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 @@
          * @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 @@
  * @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 @@
      */
     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 @@
 
 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 @@
      */
     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 @@
             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.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.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 @@
     }
 
     @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 @@
     }
 
     @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 @@
     }
 
     @Test
-    public void testIterable_multipleNodes() {
+    public void testNodesIterable_multipleNodes() {
         // arrange
         TestBSPTree tree = new TestBSPTree();
 
@@ -1112,7 +1167,7 @@
         List<TestNode> nodes = new ArrayList<>();
 
         // act
-        for (TestNode node : tree) {
+        for (TestNode node : tree.nodes()) {
             nodes.add(node);
         }
 
@@ -1131,11 +1186,11 @@
 
 
     @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 @@
     }
 
     @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 @@
 
         List<TestNode> nodes = new ArrayList<>();
         // act
-        for (TestNode n : node) {
+        for (TestNode n : node.nodes()) {
             nodes.add(n);
         }
 
@@ -1210,7 +1223,7 @@
     }
 
     @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 @@
 
         List<TestNode> nodes = new ArrayList<>();
         // act
-        for (TestNode n : node) {
+        for (TestNode n : node.nodes()) {
             nodes.add(n);
         }
 
@@ -1231,41 +1244,6 @@
     }
 
     @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 @@
     }
 
     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 @@
         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 @@
         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 @@
      *      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 @@
 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 @@
 /** 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 @@
 
     /** {@inheritDoc} */
     @Override
+    public Stream<ConvexSubPlane> boundaryStream() {
+        return getBoundaries().stream();
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public double getSize() {
         if (isFull()) {
             return Double.POSITIVE_INFINITY;
@@ -134,13 +143,6 @@
         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.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 @@
 
     /** {@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 @@
         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 @@
         }
     }
 
-    /** 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.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 @@
 /** 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 @@
         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 @@
         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 @@
      * @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.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 @@
  * <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 @@
         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 @@
         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 @@
         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.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 @@
 
     /** {@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 @@
         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 @@
         }
     }
 
-    /** 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.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 @@
 
         // 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 @@
 
 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 @@
     }
 
     @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.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 @@
     @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 @@
     @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 @@
     }
 
     @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 @@
     @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 @@
         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 @@
         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 @@
         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 @@
         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 @@
         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 @@
     @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 @@
     @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 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 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 @@
 
         // 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 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 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 @@
         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 @@
     @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 @@
     @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 @@
         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 @@
         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 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 @@
         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 @@
         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 @@
         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 @@
     @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 @@
     @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 @@
     @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 @@
     @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 @@
         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 @@
             {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 @@
                 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.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 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 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 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 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 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 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.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 @@
     }
 
     @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 @@
 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 @@
     }
 
     @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.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.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 @@
     @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 @@
     @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 @@
     }
 
     @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 @@
     @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 @@
     @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 @@
     @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 @@
     @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 @@
     @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 @@
     @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 @@
     }
 
     @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 @@
     }
 
     @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 @@
     @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 @@
     @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 @@
     @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 @@
     @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 @@
     @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 @@
         }
 
         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.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 @@
 /** 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 @@
         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 @@
         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.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 @@
         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 @@
         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 @@
 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 @@
 
     /** {@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 @@
         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.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 @@
     }
 
     @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 @@
     }
 
     @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);
-
-        GreatArcPath path = GreatArcPath.fromArcs(Arrays.asList(a, b));
-
-        List<GreatArc> arcs = new ArrayList<>();
+        GreatArc fullArc = GreatCircle.fromPole(Vector3D.Unit.PLUS_X, TEST_PRECISION).span();
+        GreatArcPath path = GreatArcPath.fromArcs(fullArc);
 
         // 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(1, arcs.size());
+        Assert.assertSame(fullArc, arcs.get(0));
+    }
+
+    @Test
+    public void testBoundaryStream_noBoundaries() {
+        // arrange
+        GreatArcPath path = GreatArcPath.empty();
+
+        // act
+        List<GreatArc> arcs = path.boundaryStream().collect(Collectors.toList());
+
+        // assert
+        Assert.assertEquals(0, arcs.size());
     }
 
     @Test
@@ -308,8 +315,8 @@
         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 @@
     }
 
     @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 @@
     }
 
     @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 @@
     }
 
     @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 @@
 
 // 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);