Merge branch 'GEOMETRY-69__Matt'

Closes #42.
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 e204e2c..d2dc7b0 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
@@ -29,7 +29,6 @@
 import org.apache.commons.geometry.core.partitioning.Split;
 import org.apache.commons.geometry.core.partitioning.SplitLocation;
 import org.apache.commons.geometry.core.partitioning.SubHyperplane;
-import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.Order;
 
 /** Abstract class for Binary Space Partitioning (BSP) tree implementations.
  * @param <P> Point implementation type
@@ -88,7 +87,7 @@
     /** {@inheritDoc} */
     @Override
     public void accept(final BSPTreeVisitor<P, N> visitor) {
-        acceptVisitor(getRoot(), visitor);
+        accept(getRoot(), visitor);
     }
 
     /** {@inheritDoc} */
@@ -350,51 +349,68 @@
      * @param node the node to begin the visit process
      * @param visitor the visitor to pass nodes to
      */
-    protected void acceptVisitor(final N node, BSPTreeVisitor<P, N> visitor) {
+    protected void accept(final N node, final BSPTreeVisitor<P, N> visitor) {
+        acceptRecursive(node, visitor);
+    }
+
+    /** Recursively visit the nodes in the subtree rooted at the given node.
+     * @param node the node located at the root of the subtree to visit
+     * @param visitor the visitor to pass nodes to
+     * @return true if the visit operation should continue
+     */
+    private boolean acceptRecursive(final N node, final BSPTreeVisitor<P, N> visitor) {
         if (node.isLeaf()) {
-            visitor.visit(node);
+            return shouldContinueVisit(visitor.visit(node));
         } else {
-            final Order order = visitor.visitOrder(node);
+            final BSPTreeVisitor.Order order = visitor.visitOrder(node);
 
             if (order != null) {
 
                 switch (order) {
                 case PLUS_MINUS_NODE:
-                    acceptVisitor(node.getPlus(), visitor);
-                    acceptVisitor(node.getMinus(), visitor);
-                    visitor.visit(node);
-                    break;
+                    return acceptRecursive(node.getPlus(), visitor) &&
+                            acceptRecursive(node.getMinus(), visitor) &&
+                            shouldContinueVisit(visitor.visit(node));
                 case PLUS_NODE_MINUS:
-                    acceptVisitor(node.getPlus(), visitor);
-                    visitor.visit(node);
-                    acceptVisitor(node.getMinus(), visitor);
-                    break;
+                    return acceptRecursive(node.getPlus(), visitor) &&
+                            shouldContinueVisit(visitor.visit(node)) &&
+                            acceptRecursive(node.getMinus(), visitor);
                 case MINUS_PLUS_NODE:
-                    acceptVisitor(node.getMinus(), visitor);
-                    acceptVisitor(node.getPlus(), visitor);
-                    visitor.visit(node);
-                    break;
+                    return acceptRecursive(node.getMinus(), visitor) &&
+                            acceptRecursive(node.getPlus(), visitor) &&
+                            shouldContinueVisit(visitor.visit(node));
                 case MINUS_NODE_PLUS:
-                    acceptVisitor(node.getMinus(), visitor);
-                    visitor.visit(node);
-                    acceptVisitor(node.getPlus(), visitor);
-                    break;
+                    return acceptRecursive(node.getMinus(), visitor) &&
+                            shouldContinueVisit(visitor.visit(node)) &&
+                            acceptRecursive(node.getPlus(), visitor);
                 case NODE_PLUS_MINUS:
-                    visitor.visit(node);
-                    acceptVisitor(node.getPlus(), visitor);
-                    acceptVisitor(node.getMinus(), visitor);
-                    break;
-                default: // NODE_MINUS_PLUS:
-                    visitor.visit(node);
-                    acceptVisitor(node.getMinus(), visitor);
-                    acceptVisitor(node.getPlus(), visitor);
+                    return shouldContinueVisit(visitor.visit(node)) &&
+                            acceptRecursive(node.getPlus(), visitor) &&
+                            acceptRecursive(node.getMinus(), visitor);
+                case  NODE_MINUS_PLUS:
+                    return shouldContinueVisit(visitor.visit(node)) &&
+                            acceptRecursive(node.getMinus(), visitor) &&
+                            acceptRecursive(node.getPlus(), visitor);
+                default: // NONE
                     break;
                 }
             }
+
+            return true;
         }
     }
 
-    /** Cut a node with a hyperplane. The algorithm proceeds are follows:
+    /** Return true if the given BSP tree visit result indicates that the current visit
+     * operation should continue.
+     * @param result visit result from BSP tree node visit operation
+     * @return true if the visit operation should continue with remaining nodes in the
+     *      BSP tree
+     */
+    private boolean shouldContinueVisit(final BSPTreeVisitor.Result result) {
+        return result == BSPTreeVisitor.Result.CONTINUE;
+    }
+
+    /** Cut a node with a hyperplane. The algorithm proceeds as follows:
      * <ol>
      *      <li>The hyperplane is trimmed by splitting it with each cut hyperplane on the
      *      path from the given node to the root of the tree.</li>
@@ -885,7 +901,7 @@
         /** {@inheritDoc} */
         @Override
         public void accept(final BSPTreeVisitor<P, N> visitor) {
-            tree.acceptVisitor(getSelf(), visitor);
+            tree.accept(getSelf(), visitor);
         }
 
         /** {@inheritDoc} */
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 93191bc..8500396 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
@@ -805,7 +805,7 @@
 
         /** {@inheritDoc} */
         @Override
-        public void visit(final N node) {
+        public Result visit(final N node) {
             final P point = getTarget();
 
             if (node.isInternal() && (minDist < 0.0 || isPossibleClosestCut(node.getCut(), point, minDist))) {
@@ -824,6 +824,8 @@
                     projected = disambiguateClosestPoint(point, projected, boundaryPt);
                 }
             }
+
+            return Result.CONTINUE;
         }
 
         /** Return true if the given node cut subhyperplane is a possible candidate for containing the
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreePrinter.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreePrinter.java
index b5df252..b0cb9a0 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreePrinter.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreePrinter.java
@@ -56,7 +56,7 @@
 
     /** {@inheritDoc} */
     @Override
-    public void visit(final N node) {
+    public Result visit(final N node) {
         final int depth = node.depth();
 
         if (depth <= maxDepth) {
@@ -66,11 +66,16 @@
             startLine(node);
             write(ELLIPSIS);
         }
+
+        return Result.CONTINUE;
     }
 
     /** {@inheritDoc} */
     @Override
     public Order visitOrder(final N node) {
+        if (node.depth() > maxDepth + 1) {
+            return Order.NONE;
+        }
         return Order.NODE_MINUS_PLUS;
     }
 
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitor.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitor.java
index 6e5d689..db8a1a6 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitor.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitor.java
@@ -58,19 +58,38 @@
         /** Indicates that the visitor should first visit the current node, then the
          * minus sub-tree, and then the plus sub-tree.
          */
-        NODE_MINUS_PLUS;
+        NODE_MINUS_PLUS,
+
+        /** Indicates that the visitor should not visit any of the nodes in this subtree. */
+        NONE;
+    }
+
+    /** Enum representing the result of a BSP tree node visit operation.
+     */
+    enum Result {
+
+        /** Indicates that the visit operation should continue with the remaining nodes in
+         * the BSP tree.
+         */
+        CONTINUE,
+
+        /** Indicates that the visit operation should terminate and not visit any further
+         * nodes in the tree.
+         */
+        TERMINATE
     }
 
     /** Visit a node in a BSP tree. This method is called for both internal nodes and
      * leaf nodes.
      * @param node the node being visited
+     * @return the result of the visit operation
      */
-    void visit(N node);
+    Result visit(N node);
 
     /** Determine the visit order for the given internal node. This is called for each
-     * internal node before {@link #visit(BSPTree.Node)} is called. Returning null from
-     * this method skips the subtree rooted at the given node. This method is not called
-     * on leaf nodes.
+     * internal node before {@link #visit(BSPTree.Node)} is called. Returning null
+     * or {@link Order#NONE}from this method skips the subtree rooted at the given node.
+     * This method is not called on leaf nodes.
      * @param internalNode the internal node to determine the visit order for
      * @return the order that the subtree rooted at the given node should be visited
      */
@@ -122,7 +141,7 @@
 
         /** {@inheritDoc} */
         @Override
-        public Order visitOrder(N node) {
+        public Order visitOrder(final N node) {
             if (node.getCutHyperplane().offset(getTarget()) > 0.0) {
                 return Order.PLUS_NODE_MINUS;
             }
@@ -149,7 +168,7 @@
 
         /** {@inheritDoc} */
         @Override
-        public Order visitOrder(N node) {
+        public Order visitOrder(final N node) {
             if (node.getCutHyperplane().offset(getTarget()) < 0.0) {
                 return Order.PLUS_NODE_MINUS;
             }
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 46a88da..50a9781 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
@@ -33,7 +33,6 @@
 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.bsp.BSPTree.NodeCutRule;
-import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.Order;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -814,7 +813,10 @@
         List<TestNode> nodes = new ArrayList<>();
 
         // act
-        tree.accept(node -> nodes.add(node));
+        tree.accept(node -> {
+            nodes.add(node);
+            return BSPTreeVisitor.Result.CONTINUE;
+        });
 
         // assert
         Assert.assertEquals(
@@ -836,37 +838,37 @@
         TestNode minusPlus = minus.getPlus();
 
         // act/assert
-        TestVisitor plusMinusNode = new TestVisitor(Order.PLUS_MINUS_NODE);
+        TestVisitor plusMinusNode = new TestVisitor(BSPTreeVisitor.Order.PLUS_MINUS_NODE);
         tree.accept(plusMinusNode);
         Assert.assertEquals(
                 Arrays.asList(plus, minusPlus, minusMinus, minus, root),
                 plusMinusNode.getVisited());
 
-        TestVisitor plusNodeMinus = new TestVisitor(Order.PLUS_NODE_MINUS);
+        TestVisitor plusNodeMinus = new TestVisitor(BSPTreeVisitor.Order.PLUS_NODE_MINUS);
         tree.accept(plusNodeMinus);
         Assert.assertEquals(
                 Arrays.asList(plus, root, minusPlus, minus, minusMinus),
                 plusNodeMinus.getVisited());
 
-        TestVisitor minusPlusNode = new TestVisitor(Order.MINUS_PLUS_NODE);
+        TestVisitor minusPlusNode = new TestVisitor(BSPTreeVisitor.Order.MINUS_PLUS_NODE);
         tree.accept(minusPlusNode);
         Assert.assertEquals(
                 Arrays.asList(minusMinus, minusPlus, minus, plus, root),
                 minusPlusNode.getVisited());
 
-        TestVisitor minusNodePlus = new TestVisitor(Order.MINUS_NODE_PLUS);
+        TestVisitor minusNodePlus = new TestVisitor(BSPTreeVisitor.Order.MINUS_NODE_PLUS);
         tree.accept(minusNodePlus);
         Assert.assertEquals(
                 Arrays.asList(minusMinus, minus, minusPlus, root, plus),
                 minusNodePlus.getVisited());
 
-        TestVisitor nodeMinusPlus = new TestVisitor(Order.NODE_MINUS_PLUS);
+        TestVisitor nodeMinusPlus = new TestVisitor(BSPTreeVisitor.Order.NODE_MINUS_PLUS);
         tree.accept(nodeMinusPlus);
         Assert.assertEquals(
                 Arrays.asList(root, minus, minusMinus, minusPlus, plus),
                 nodeMinusPlus.getVisited());
 
-        TestVisitor nodePlusMinus = new TestVisitor(Order.NODE_PLUS_MINUS);
+        TestVisitor nodePlusMinus = new TestVisitor(BSPTreeVisitor.Order.NODE_PLUS_MINUS);
         tree.accept(nodePlusMinus);
         Assert.assertEquals(
                 Arrays.asList(root, plus, minus, minusPlus, minusMinus),
@@ -881,23 +883,126 @@
             .getMinus().cut(TestLine.Y_AXIS);
 
         TestNode root = tree.getRoot();
+        TestNode plus = root.getPlus();
         TestNode minus = root.getMinus();
-        TestNode minusMinus = minus.getMinus();
-        TestNode minusPlus = minus.getPlus();
 
-        TestVisitor visitor = new TestVisitor(Order.NODE_MINUS_PLUS);
+        TestVisitor visitor = new TestVisitor(BSPTreeVisitor.Order.NODE_MINUS_PLUS) {
+            @Override
+            public Order visitOrder(TestNode node) {
+                if (node == minus) {
+                    return null;
+                }
+                return super.visitOrder(node);
+            }
+        };
 
         // act
-        minus.accept(visitor);
+        tree.accept(visitor);
 
         // assert
         Assert.assertEquals(
-                Arrays.asList(minus, minusMinus, minusPlus),
+                Arrays.asList(root, plus),
                 visitor.getVisited());
     }
 
     @Test
-    public void testVisit_visitNode() {
+    public void testVisit_noneVisitOrderSkipsSubtree() {
+        // arrange
+        TestBSPTree tree = new TestBSPTree();
+        tree.getRoot().cut(TestLine.X_AXIS)
+            .getMinus().cut(TestLine.Y_AXIS);
+
+        TestNode root = tree.getRoot();
+        TestNode plus = root.getPlus();
+        TestNode minus = root.getMinus();
+
+        TestVisitor visitor = new TestVisitor(BSPTreeVisitor.Order.NODE_MINUS_PLUS) {
+            @Override
+            public Order visitOrder(TestNode node) {
+                if (node == minus) {
+                    return Order.NONE;
+                }
+                return super.visitOrder(node);
+            }
+        };
+
+        // act
+        tree.accept(visitor);
+
+        // assert
+        Assert.assertEquals(
+                Arrays.asList(root, plus),
+                visitor.getVisited());
+    }
+
+    @Test
+    public void testVisit_visitorReturnsNull_terminatesEarly() {
+        // arrange
+        TestBSPTree tree = new TestBSPTree();
+        tree.getRoot().cut(TestLine.X_AXIS)
+            .getMinus().cut(TestLine.Y_AXIS);
+
+        TestNode root = tree.getRoot();
+        TestNode minus = root.getMinus();
+        TestNode minusMinus = minus.getMinus();
+        TestNode minusPlus = minus.getPlus();
+
+        TestVisitor visitor = new TestVisitor(BSPTreeVisitor.Order.MINUS_PLUS_NODE) {
+            @Override
+            public Result visit(TestNode node) {
+                super.visit(node);
+
+                if (node == minus) {
+                    return null;
+                }
+                return Result.CONTINUE;
+            }
+        };
+
+        // act
+        tree.accept(visitor);
+
+        // assert
+        Assert.assertEquals(
+                Arrays.asList(minusMinus, minusPlus, minus),
+                visitor.getVisited());
+    }
+
+    @Test
+    public void testVisit_visitorReturnsTerminate_terminatesEarly() {
+        // arrange
+        TestBSPTree tree = new TestBSPTree();
+        tree.getRoot().cut(TestLine.X_AXIS)
+            .getMinus().cut(TestLine.Y_AXIS);
+
+        TestNode root = tree.getRoot();
+        TestNode minus = root.getMinus();
+        TestNode minusMinus = minus.getMinus();
+        TestNode minusPlus = minus.getPlus();
+
+        TestVisitor visitor = new TestVisitor(BSPTreeVisitor.Order.MINUS_PLUS_NODE) {
+            @Override
+            public Result visit(TestNode node) {
+                super.visit(node);
+
+                if (node == minus) {
+                    return Result.TERMINATE;
+                }
+                return Result.CONTINUE;
+            }
+        };
+
+        // act
+        tree.accept(visitor);
+
+        // assert
+        Assert.assertEquals(
+                Arrays.asList(minusMinus, minusPlus, minus),
+                visitor.getVisited());
+    }
+
+    @Test
+    public void testVisit_earlyTerminationPermutations() {
         // arrange
         TestBSPTree tree = new TestBSPTree();
         tree.getRoot().cut(TestLine.X_AXIS)
@@ -909,14 +1014,67 @@
         TestNode minusMinus = minus.getMinus();
         TestNode minusPlus = minus.getPlus();
 
+        // act/assert
+        TestVisitor plusMinusNode = new TestVisitor(BSPTreeVisitor.Order.PLUS_MINUS_NODE).withTerminationNode(minus);
+        tree.accept(plusMinusNode);
+        Assert.assertEquals(
+                Arrays.asList(plus, minusPlus, minusMinus, minus),
+                plusMinusNode.getVisited());
+
+        TestVisitor plusNodeMinus = new TestVisitor(BSPTreeVisitor.Order.PLUS_NODE_MINUS).withTerminationNode(minus);
+        tree.accept(plusNodeMinus);
+        Assert.assertEquals(
+                Arrays.asList(plus, root, minusPlus, minus),
+                plusNodeMinus.getVisited());
+
+        TestVisitor minusPlusNode = new TestVisitor(BSPTreeVisitor.Order.MINUS_PLUS_NODE).withTerminationNode(minus);
+        tree.accept(minusPlusNode);
+        Assert.assertEquals(
+                Arrays.asList(minusMinus, minusPlus, minus),
+                minusPlusNode.getVisited());
+
+        TestVisitor minusNodePlus = new TestVisitor(BSPTreeVisitor.Order.MINUS_NODE_PLUS).withTerminationNode(minus);
+        tree.accept(minusNodePlus);
+        Assert.assertEquals(
+                Arrays.asList(minusMinus, minus),
+                minusNodePlus.getVisited());
+
+        TestVisitor nodeMinusPlus = new TestVisitor(BSPTreeVisitor.Order.NODE_MINUS_PLUS).withTerminationNode(minus);
+        tree.accept(nodeMinusPlus);
+        Assert.assertEquals(
+                Arrays.asList(root, minus),
+                nodeMinusPlus.getVisited());
+
+        TestVisitor nodePlusMinus = new TestVisitor(BSPTreeVisitor.Order.NODE_PLUS_MINUS).withTerminationNode(minus);
+        tree.accept(nodePlusMinus);
+        Assert.assertEquals(
+                Arrays.asList(root, plus, minus),
+                nodePlusMinus.getVisited());
+    }
+
+    @Test
+    public void testVisit_visitNode() {
+        // arrange
+        TestBSPTree tree = new TestBSPTree();
+        tree.getRoot().cut(TestLine.X_AXIS)
+            .getMinus().cut(TestLine.Y_AXIS);
+
+        TestNode root = tree.getRoot();
+        TestNode minus = root.getMinus();
+        TestNode minusMinus = minus.getMinus();
+        TestNode minusPlus = minus.getPlus();
+
         List<TestNode> nodes = new ArrayList<>();
 
         // act
-        tree.accept(node -> nodes.add(node));
+        minus.accept(node -> {
+            nodes.add(node);
+            return BSPTreeVisitor.Result.CONTINUE;
+        });
 
         // assert
         Assert.assertEquals(
-                Arrays.asList(root, minus, minusMinus, minusPlus, plus),
+                Arrays.asList(minus, minusMinus, minusPlus),
                 nodes);
     }
 
@@ -1783,15 +1941,27 @@
 
         private final Order order;
 
+        private TestNode terminationNode;
+
         private final List<TestNode> visited = new ArrayList<>();
 
         public TestVisitor(Order order) {
             this.order = order;
         }
 
+        public TestVisitor withTerminationNode(TestNode terminationNode) {
+            this.terminationNode = terminationNode;
+
+            return this;
+        }
+
         @Override
-        public void visit(TestNode node) {
+        public Result visit(TestNode node) {
             visited.add(node);
+
+            return (terminationNode == node) ?
+                    Result.TERMINATE :
+                    Result.CONTINUE;
         }
 
         @Override
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 32471b7..53ad322 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
@@ -2201,8 +2201,6 @@
 
     private static class TestRegionBSPTree extends AbstractRegionBSPTree<TestPoint2D, TestRegionNode> {
 
-        private static final long serialVersionUID = 20190405L;
-
         TestRegionBSPTree() {
             this(true);
         }
@@ -2243,8 +2241,6 @@
 
     private static class TestRegionNode extends AbstractRegionNode<TestPoint2D, TestRegionNode> {
 
-        private static final long serialVersionUID = 20190405L;
-
         protected TestRegionNode(AbstractBSPTree<TestPoint2D, TestRegionNode> tree) {
             super(tree);
         }
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitorTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitorTest.java
index 2e692ae..cd58ca1 100644
--- a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitorTest.java
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/bsp/BSPTreeVisitorTest.java
@@ -18,12 +18,10 @@
 
 import org.apache.commons.geometry.core.partition.test.TestBSPTree;
 import org.apache.commons.geometry.core.partition.test.TestBSPTree.TestNode;
-import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor;
-import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.ClosestFirstVisitor;
-import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.FarthestFirstVisitor;
-import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.Order;
 import org.apache.commons.geometry.core.partition.test.TestLine;
 import org.apache.commons.geometry.core.partition.test.TestPoint2D;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.ClosestFirstVisitor;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.FarthestFirstVisitor;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -32,10 +30,10 @@
     @Test
     public void testDefaultVisitOrder() {
         // arrange
-        BSPTreeVisitor<TestPoint2D, TestNode> visitor = n -> {};
+        BSPTreeVisitor<TestPoint2D, TestNode> visitor = n -> BSPTreeVisitor.Result.CONTINUE;
 
         // act/assert
-        Assert.assertEquals(Order.NODE_MINUS_PLUS, visitor.visitOrder(null));
+        Assert.assertEquals(BSPTreeVisitor.Order.NODE_MINUS_PLUS, visitor.visitOrder(null));
     }
 
     @Test
@@ -48,25 +46,25 @@
         root.getPlus().cut(TestLine.Y_AXIS);
 
         // act
-        checkClosestFirst(new TestPoint2D(1, 1), root, Order.MINUS_NODE_PLUS);
-        checkClosestFirst(new TestPoint2D(1, 1), root.getMinus(), Order.PLUS_NODE_MINUS);
-        checkClosestFirst(new TestPoint2D(1, 1), root.getPlus(), Order.PLUS_NODE_MINUS);
+        checkClosestFirst(new TestPoint2D(1, 1), root, BSPTreeVisitor.Order.MINUS_NODE_PLUS);
+        checkClosestFirst(new TestPoint2D(1, 1), root.getMinus(), BSPTreeVisitor.Order.PLUS_NODE_MINUS);
+        checkClosestFirst(new TestPoint2D(1, 1), root.getPlus(), BSPTreeVisitor.Order.PLUS_NODE_MINUS);
 
-        checkClosestFirst(new TestPoint2D(-1, 1), root, Order.MINUS_NODE_PLUS);
-        checkClosestFirst(new TestPoint2D(-1, 1), root.getMinus(), Order.MINUS_NODE_PLUS);
-        checkClosestFirst(new TestPoint2D(-1, 1), root.getPlus(), Order.MINUS_NODE_PLUS);
+        checkClosestFirst(new TestPoint2D(-1, 1), root, BSPTreeVisitor.Order.MINUS_NODE_PLUS);
+        checkClosestFirst(new TestPoint2D(-1, 1), root.getMinus(), BSPTreeVisitor.Order.MINUS_NODE_PLUS);
+        checkClosestFirst(new TestPoint2D(-1, 1), root.getPlus(), BSPTreeVisitor.Order.MINUS_NODE_PLUS);
 
-        checkClosestFirst(new TestPoint2D(-1, -1), root, Order.PLUS_NODE_MINUS);
-        checkClosestFirst(new TestPoint2D(-1, -1), root.getMinus(), Order.MINUS_NODE_PLUS);
-        checkClosestFirst(new TestPoint2D(-1, -1), root.getPlus(), Order.MINUS_NODE_PLUS);
+        checkClosestFirst(new TestPoint2D(-1, -1), root, BSPTreeVisitor.Order.PLUS_NODE_MINUS);
+        checkClosestFirst(new TestPoint2D(-1, -1), root.getMinus(), BSPTreeVisitor.Order.MINUS_NODE_PLUS);
+        checkClosestFirst(new TestPoint2D(-1, -1), root.getPlus(), BSPTreeVisitor.Order.MINUS_NODE_PLUS);
 
-        checkClosestFirst(new TestPoint2D(1, -1), root, Order.PLUS_NODE_MINUS);
-        checkClosestFirst(new TestPoint2D(1, -1), root.getMinus(), Order.PLUS_NODE_MINUS);
-        checkClosestFirst(new TestPoint2D(1, -1), root.getPlus(), Order.PLUS_NODE_MINUS);
+        checkClosestFirst(new TestPoint2D(1, -1), root, BSPTreeVisitor.Order.PLUS_NODE_MINUS);
+        checkClosestFirst(new TestPoint2D(1, -1), root.getMinus(), BSPTreeVisitor.Order.PLUS_NODE_MINUS);
+        checkClosestFirst(new TestPoint2D(1, -1), root.getPlus(), BSPTreeVisitor.Order.PLUS_NODE_MINUS);
 
-        checkClosestFirst(TestPoint2D.ZERO, root.getPlus(), Order.MINUS_NODE_PLUS);
-        checkClosestFirst(TestPoint2D.ZERO, root.getPlus(), Order.MINUS_NODE_PLUS);
-        checkClosestFirst(TestPoint2D.ZERO, root.getPlus(), Order.MINUS_NODE_PLUS);
+        checkClosestFirst(TestPoint2D.ZERO, root.getPlus(), BSPTreeVisitor.Order.MINUS_NODE_PLUS);
+        checkClosestFirst(TestPoint2D.ZERO, root.getPlus(), BSPTreeVisitor.Order.MINUS_NODE_PLUS);
+        checkClosestFirst(TestPoint2D.ZERO, root.getPlus(), BSPTreeVisitor.Order.MINUS_NODE_PLUS);
     }
 
     @Test
@@ -79,35 +77,35 @@
         root.getPlus().cut(TestLine.Y_AXIS);
 
         // act
-        checkFarthestFirst(new TestPoint2D(1, 1), root, Order.PLUS_NODE_MINUS);
-        checkFarthestFirst(new TestPoint2D(1, 1), root.getMinus(), Order.MINUS_NODE_PLUS);
-        checkFarthestFirst(new TestPoint2D(1, 1), root.getPlus(), Order.MINUS_NODE_PLUS);
+        checkFarthestFirst(new TestPoint2D(1, 1), root, BSPTreeVisitor.Order.PLUS_NODE_MINUS);
+        checkFarthestFirst(new TestPoint2D(1, 1), root.getMinus(), BSPTreeVisitor.Order.MINUS_NODE_PLUS);
+        checkFarthestFirst(new TestPoint2D(1, 1), root.getPlus(), BSPTreeVisitor.Order.MINUS_NODE_PLUS);
 
-        checkFarthestFirst(new TestPoint2D(-1, 1), root, Order.PLUS_NODE_MINUS);
-        checkFarthestFirst(new TestPoint2D(-1, 1), root.getMinus(), Order.PLUS_NODE_MINUS);
-        checkFarthestFirst(new TestPoint2D(-1, 1), root.getPlus(), Order.PLUS_NODE_MINUS);
+        checkFarthestFirst(new TestPoint2D(-1, 1), root, BSPTreeVisitor.Order.PLUS_NODE_MINUS);
+        checkFarthestFirst(new TestPoint2D(-1, 1), root.getMinus(), BSPTreeVisitor.Order.PLUS_NODE_MINUS);
+        checkFarthestFirst(new TestPoint2D(-1, 1), root.getPlus(), BSPTreeVisitor.Order.PLUS_NODE_MINUS);
 
-        checkFarthestFirst(new TestPoint2D(-1, -1), root, Order.MINUS_NODE_PLUS);
-        checkFarthestFirst(new TestPoint2D(-1, -1), root.getMinus(), Order.PLUS_NODE_MINUS);
-        checkFarthestFirst(new TestPoint2D(-1, -1), root.getPlus(), Order.PLUS_NODE_MINUS);
+        checkFarthestFirst(new TestPoint2D(-1, -1), root, BSPTreeVisitor.Order.MINUS_NODE_PLUS);
+        checkFarthestFirst(new TestPoint2D(-1, -1), root.getMinus(), BSPTreeVisitor.Order.PLUS_NODE_MINUS);
+        checkFarthestFirst(new TestPoint2D(-1, -1), root.getPlus(), BSPTreeVisitor.Order.PLUS_NODE_MINUS);
 
-        checkFarthestFirst(new TestPoint2D(1, -1), root, Order.MINUS_NODE_PLUS);
-        checkFarthestFirst(new TestPoint2D(1, -1), root.getMinus(), Order.MINUS_NODE_PLUS);
-        checkFarthestFirst(new TestPoint2D(1, -1), root.getPlus(), Order.MINUS_NODE_PLUS);
+        checkFarthestFirst(new TestPoint2D(1, -1), root, BSPTreeVisitor.Order.MINUS_NODE_PLUS);
+        checkFarthestFirst(new TestPoint2D(1, -1), root.getMinus(), BSPTreeVisitor.Order.MINUS_NODE_PLUS);
+        checkFarthestFirst(new TestPoint2D(1, -1), root.getPlus(), BSPTreeVisitor.Order.MINUS_NODE_PLUS);
 
-        checkFarthestFirst(TestPoint2D.ZERO, root.getPlus(), Order.MINUS_NODE_PLUS);
-        checkFarthestFirst(TestPoint2D.ZERO, root.getPlus(), Order.MINUS_NODE_PLUS);
-        checkFarthestFirst(TestPoint2D.ZERO, root.getPlus(), Order.MINUS_NODE_PLUS);
+        checkFarthestFirst(TestPoint2D.ZERO, root.getPlus(), BSPTreeVisitor.Order.MINUS_NODE_PLUS);
+        checkFarthestFirst(TestPoint2D.ZERO, root.getPlus(), BSPTreeVisitor.Order.MINUS_NODE_PLUS);
+        checkFarthestFirst(TestPoint2D.ZERO, root.getPlus(), BSPTreeVisitor.Order.MINUS_NODE_PLUS);
     }
 
-    private static void checkClosestFirst(TestPoint2D target, TestNode node, Order order) {
+    private static void checkClosestFirst(TestPoint2D target, TestNode node, BSPTreeVisitor.Order order) {
         ClosestFirstStubVisitor visitor = new ClosestFirstStubVisitor(target);
 
         Assert.assertSame(target, visitor.getTarget());
         Assert.assertEquals(order, visitor.visitOrder(node));
     }
 
-    private static void checkFarthestFirst(TestPoint2D target, TestNode node, Order order) {
+    private static void checkFarthestFirst(TestPoint2D target, TestNode node, BSPTreeVisitor.Order order) {
         FarthestFirstStubVisitor visitor = new FarthestFirstStubVisitor(target);
 
         Assert.assertSame(target, visitor.getTarget());
@@ -116,27 +114,25 @@
 
     private static class ClosestFirstStubVisitor extends ClosestFirstVisitor<TestPoint2D, TestNode> {
 
-        private static final long serialVersionUID = 1L;
-
         public ClosestFirstStubVisitor(TestPoint2D target) {
             super(target);
         }
 
         @Override
-        public void visit(TestNode node) {
+        public Result visit(TestNode node) {
+            return Result.CONTINUE;
         }
     }
 
     private static class FarthestFirstStubVisitor extends FarthestFirstVisitor<TestPoint2D, TestNode> {
 
-        private static final long serialVersionUID = 1L;
-
         public FarthestFirstStubVisitor(TestPoint2D target) {
             super(target);
         }
 
         @Override
-        public void visit(TestNode node) {
+        public Result visit(TestNode node) {
+            return Result.CONTINUE;
         }
     }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractSubPlane.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractSubPlane.java
index c1647f0..cea56a6 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractSubPlane.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AbstractSubPlane.java
@@ -80,7 +80,8 @@
             .append("[plane= ")
             .append(getPlane())
             .append(", subspaceRegion= ")
-            .append(getSubspaceRegion());
+            .append(getSubspaceRegion())
+            .append(']');
 
 
         return sb.toString();
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 451046a..bf9848d 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
@@ -30,8 +30,8 @@
 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.RegionBSPTree2D;
 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
@@ -138,77 +138,10 @@
      *      intersection exists
      */
     public ConvexSubPlane raycastFirst(final Segment3D ray) {
-        return raycastFirstRecursive(getRoot(), ray);
-    }
+        final RaycastIntersectionVisitor visitor = new RaycastIntersectionVisitor(ray);
+        getRoot().accept(visitor);
 
-    /** Recursive method used to find the first intersection of the given ray/line segment
-     * with the boundary of the region.
-     * @param node current BSP tree node
-     * @param ray the ray used for the raycast operation
-     * @return the node cut subhyperplane containing the intersection or null if no
-     *      intersection exists
-     */
-    private ConvexSubPlane raycastFirstRecursive(final RegionNode3D node, final Segment3D ray) {
-        if (node.isLeaf()) {
-            // no boundary to intersect with on leaf nodes
-            return null;
-        }
-
-        // establish search order
-        final Plane cut = (Plane) node.getCutHyperplane();
-        final Line3D line = ray.getLine();
-
-        final boolean plusIsNear = line.getDirection().dot(cut.getNormal()) < 0;
-
-        final RegionNode3D nearNode = plusIsNear ? node.getPlus() : node.getMinus();
-        final RegionNode3D farNode = plusIsNear ? node.getMinus() : node.getPlus();
-
-        // check the near node
-        final ConvexSubPlane nearResult = raycastFirstRecursive(nearNode, ray);
-        if (nearResult != null) {
-            return nearResult;
-        }
-
-        // check ourselves
-        final Vector3D intersection = computeRegionCutBoundaryIntersection(node, ray);
-        if (intersection != null) {
-            // we intersect, so our cut is the answer
-            return (ConvexSubPlane) node.getCut();
-        }
-
-        // check the far node
-        final ConvexSubPlane farResult = raycastFirstRecursive(farNode, ray);
-        if (farResult != null) {
-            return farResult;
-        }
-
-        return null;
-    }
-
-    /** Compute the intersection point between the region cut boundary and the given line segment.
-     * @param node BSP tree node to compute the region cut boundary intersection for
-     * @param segment line segment to compute the intersection for
-     * @return the intersection point between the region cut boundary and the given line segment or
-     *      null if one does not exist.
-     */
-    private Vector3D computeRegionCutBoundaryIntersection(final RegionNode3D node, final Segment3D segment) {
-        if (node.isInternal()) {
-            final Line3D line = segment.getLine();
-            final Vector3D intersection = ((Plane) node.getCutHyperplane()).intersection(line);
-
-            if (intersection != null && segment.contains(intersection)) {
-
-                final RegionCutBoundary<Vector3D> boundary = node.getCutBoundary();
-
-                if ((boundary.getInsideFacing() != null && boundary.getInsideFacing().contains(intersection)) ||
-                        boundary.getOutsideFacing() != null && boundary.getOutsideFacing().contains(intersection)) {
-
-                    return intersection;
-                }
-            }
-        }
-
-        return null;
+        return visitor.getIntersectionCut();
     }
 
     /** {@inheritDoc} */
@@ -616,12 +549,14 @@
 
         /** {@inheritDoc} */
         @Override
-        public void visit(final RegionNode3D node) {
+        public Result visit(final RegionNode3D node) {
             if (node.isInternal()) {
                 RegionCutBoundary<Vector3D> boundary = node.getCutBoundary();
                 addFacetContribution(boundary.getOutsideFacing(), false);
                 addFacetContribution(boundary.getInsideFacing(), true);
             }
+
+            return Result.CONTINUE;
         }
 
         /** Return the computed size properties for the visited region.
@@ -683,4 +618,72 @@
             }
         }
     }
+
+    /** BSP tree visitor that locates the node cut subhyperplane for the first intersection between a
+     * given line segment and BSP tree region boundary.
+     */
+    private static final class RaycastIntersectionVisitor implements BSPTreeVisitor<Vector3D, RegionNode3D> {
+
+        /** The line segment to intersect with the BSP tree. */
+        private final Segment3D segment;
+
+        /** The node cut subhyperplane containing the first boundary intersection. */
+        private ConvexSubPlane intersectionCut;
+
+        /** Create a new instance that locates the first boundary intersection between the given line segment and
+         * the visited BSP tree.
+         * @param segment segment to intersect with the BSP tree region boundary
+         */
+        RaycastIntersectionVisitor(final Segment3D segment) {
+            this.segment = segment;
+        }
+
+        /** Get the node cut subhyperplane containing the first intersection between the configured line segment
+         * and the BSP tree region boundary. This must be called after the tree nodes have been visited.
+         * @return the node cut subhyperplane containing the first intersection between the configured line segment
+         *      and the BSP tree region boundary or null if no such intersection was found
+         */
+        public ConvexSubPlane getIntersectionCut() {
+            return intersectionCut;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Order visitOrder(final RegionNode3D internalNode) {
+            final Plane cut = (Plane) internalNode.getCutHyperplane();
+            final Line3D line = segment.getLine();
+
+            final boolean plusIsNear = line.getDirection().dot(cut.getNormal()) < 0;
+
+            return plusIsNear ?
+                    Order.PLUS_NODE_MINUS :
+                    Order.MINUS_NODE_PLUS;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Result visit(final RegionNode3D node) {
+            if (node.isInternal()) {
+                // check if the line segment intersects the cut subhyperplane
+                final Line3D line = segment.getLine();
+                final Vector3D intersection = ((Plane) node.getCutHyperplane()).intersection(line);
+
+                if (intersection != null && segment.contains(intersection)) {
+
+                    final RegionCutBoundary<Vector3D> boundary = node.getCutBoundary();
+
+                    // check if the intersection point lies on the region boundary
+                    if ((boundary.getInsideFacing() != null && boundary.getInsideFacing().contains(intersection)) ||
+                            boundary.getOutsideFacing() != null && boundary.getOutsideFacing().contains(intersection)) {
+
+                        intersectionCut = (ConvexSubPlane) node.getCut();
+
+                        return Result.TERMINATE;
+                    }
+                }
+            }
+
+            return Result.CONTINUE;
+        }
+    }
 }
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Vector1DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Vector1DTest.java
index 393fce4..3df8825 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Vector1DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Vector1DTest.java
@@ -652,14 +652,25 @@
     }
 
     @Test
-    public void testNormalize_static() {
+    public void testUnitFrom_coordinates() {
         // act/assert
         checkVector(Vector1D.Unit.from(2.0), 1);
         checkVector(Vector1D.Unit.from(-4.0), -1);
     }
 
     @Test
-    public void testNormalize_static_illegalNorm() {
+    public void testUnitFrom_vector() {
+        // arrange
+        Vector1D vec = Vector1D.of(2);
+        Vector1D unitVec = Vector1D.Unit.from(2);
+
+        // act/assert
+        checkVector(Vector1D.Unit.from(vec), 1);
+        Assert.assertSame(unitVec, Vector1D.Unit.from(unitVec));
+    }
+
+    @Test
+    public void testUnitFrom_illegalNorm() {
         GeometryTestUtils.assertThrows(() -> Vector1D.Unit.from(0.0),
                 IllegalNormException.class);
         GeometryTestUtils.assertThrows(() -> Vector1D.Unit.from(Double.NaN),
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlaneTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlaneTest.java
index b4063cb..a3ff41c 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlaneTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/ConvexSubPlaneTest.java
@@ -20,6 +20,7 @@
 import java.util.List;
 
 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.Transform;
 import org.apache.commons.geometry.core.partitioning.Split;
@@ -597,6 +598,23 @@
         Assert.assertSame(sp, split.getPlus());
     }
 
+    @Test
+    public void testToString() {
+        // arrange
+        ConvexSubPlane sp = ConvexSubPlane.fromVertexLoop(Arrays.asList(
+                    Vector3D.ZERO,
+                    Vector3D.Unit.PLUS_X,
+                    Vector3D.Unit.PLUS_Y
+                ), TEST_PRECISION);
+
+        // act
+        String str = sp.toString();
+
+        // assert
+        GeometryTestUtils.assertContains("plane= Plane[", str);
+        GeometryTestUtils.assertContains("subspaceRegion= ConvexArea[", str);
+    }
+
     private static void checkPlane(Plane plane, Vector3D origin, Vector3D u, Vector3D v) {
         u = u.normalize();
         v = v.normalize();
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 390afc0..1ca6474 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
@@ -19,7 +19,6 @@
 import java.util.Arrays;
 import java.util.List;
 
-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.Transform;
@@ -38,6 +37,7 @@
 import org.apache.commons.geometry.euclidean.twod.Line;
 import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
 import org.apache.commons.geometry.euclidean.twod.Vector2D;
+import org.apache.commons.numbers.angle.PlaneAngleRadians;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -394,6 +394,19 @@
     }
 
     @Test
+    public void testToString() {
+        // arrange
+        SubPlane sp = new SubPlane(Plane.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION));
+
+        // act
+        String str = sp.toString();
+
+        // assert
+        GeometryTestUtils.assertContains("plane= Plane[", str);
+        GeometryTestUtils.assertContains("subspaceRegion= RegionBSPTree2D[", str);
+    }
+
+    @Test
     public void testBuilder() {
         // arrange
         Plane mainPlane = Plane.fromPointAndPlaneVectors(
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java
index 23e2e75..b163f78 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java
@@ -1177,7 +1177,7 @@
     }
 
     @Test
-    public void testNormalize_static() {
+    public void testUnitFrom_coordinates() {
         // arrange
         double invSqrt3 = 1.0 / Math.sqrt(3.0);
 
@@ -1187,7 +1187,19 @@
     }
 
     @Test
-    public void testNormalize_static_illegalNorm() {
+    public void testUnitFrom_vector() {
+        // arrange
+        double invSqrt3 = 1.0 / Math.sqrt(3.0);
+        Vector3D vec = Vector3D.of(2.0, -2.0, 2.0);
+        Vector3D.Unit unitVec = Vector3D.Unit.from(2.0, -2.0, 2.0);
+
+        // act/assert
+        checkVector(Vector3D.Unit.from(vec), invSqrt3, -invSqrt3, invSqrt3);
+        Assert.assertSame(unitVec, Vector3D.Unit.from(unitVec));
+    }
+
+    @Test
+    public void testUnitFrom_static_illegalNorm() {
         GeometryTestUtils.assertThrows(() -> Vector3D.Unit.from(0.0, 0.0, 0.0),
                 IllegalNormException.class);
         GeometryTestUtils.assertThrows(() -> Vector3D.Unit.from(Double.NaN, 1.0, 1.0),
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java
index e61326b..1e76c86 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java
@@ -989,7 +989,7 @@
     }
 
     @Test
-    public void testNormalize_static() {
+    public void testUnitFrom_coordinates() {
         // arrange
         double invSqrt2 = 1.0 / Math.sqrt(2.0);
 
@@ -999,7 +999,19 @@
     }
 
     @Test
-    public void testNormalize_static_illegalNorm() {
+    public void testUnitFrom_vector() {
+        // arrange
+        double invSqrt2 = 1.0 / Math.sqrt(2.0);
+        Vector2D vec = Vector2D.of(2.0, -2.0);
+        Vector2D.Unit unitVec = Vector2D.Unit.from(2.0, -2.0);
+
+        // act/assert
+        checkVector(Vector2D.Unit.from(vec), invSqrt2, -invSqrt2);
+        Assert.assertSame(unitVec, Vector2D.Unit.from(unitVec));
+    }
+
+    @Test
+    public void testUnitFrom_illegalNorm() {
         GeometryTestUtils.assertThrows(() -> Vector2D.Unit.from(0.0, 0.0),
                 IllegalNormException.class);
         GeometryTestUtils.assertThrows(() -> Vector2D.Unit.from(Double.NaN, 1.0),