GEOMETRY-69: adding return value to BSPTreeVisitor.visit(BSPTree.Node) so that visit operations can terminate early
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..c06c59d 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,8 @@
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;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.VisitOrder;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.VisitResult;
/** Abstract class for Binary Space Partitioning (BSP) tree implementations.
* @param <P> Point implementation type
@@ -350,51 +351,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 acceptVisitor(final N node, final BSPTreeVisitor<P, N> visitor) {
+ acceptVisitorRecursive(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 acceptVisitorRecursive(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 VisitOrder 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 acceptVisitorRecursive(node.getPlus(), visitor) &&
+ acceptVisitorRecursive(node.getMinus(), visitor) &&
+ shouldContinueVisit(visitor.visit(node));
case PLUS_NODE_MINUS:
- acceptVisitor(node.getPlus(), visitor);
- visitor.visit(node);
- acceptVisitor(node.getMinus(), visitor);
- break;
+ return acceptVisitorRecursive(node.getPlus(), visitor) &&
+ shouldContinueVisit(visitor.visit(node)) &&
+ acceptVisitorRecursive(node.getMinus(), visitor);
case MINUS_PLUS_NODE:
- acceptVisitor(node.getMinus(), visitor);
- acceptVisitor(node.getPlus(), visitor);
- visitor.visit(node);
- break;
+ return acceptVisitorRecursive(node.getMinus(), visitor) &&
+ acceptVisitorRecursive(node.getPlus(), visitor) &&
+ shouldContinueVisit(visitor.visit(node));
case MINUS_NODE_PLUS:
- acceptVisitor(node.getMinus(), visitor);
- visitor.visit(node);
- acceptVisitor(node.getPlus(), visitor);
- break;
+ return acceptVisitorRecursive(node.getMinus(), visitor) &&
+ shouldContinueVisit(visitor.visit(node)) &&
+ acceptVisitorRecursive(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)) &&
+ acceptVisitorRecursive(node.getPlus(), visitor) &&
+ acceptVisitorRecursive(node.getMinus(), visitor);
+ case NODE_MINUS_PLUS:
+ return shouldContinueVisit(visitor.visit(node)) &&
+ acceptVisitorRecursive(node.getMinus(), visitor) &&
+ acceptVisitorRecursive(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 VisitResult result) {
+ return result == VisitResult.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>
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..402c080 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 VisitResult 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 VisitResult.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..464d5a0 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 VisitResult visit(final N node) {
final int depth = node.depth();
if (depth <= maxDepth) {
@@ -66,12 +66,17 @@
startLine(node);
write(ELLIPSIS);
}
+
+ return VisitResult.CONTINUE;
}
/** {@inheritDoc} */
@Override
- public Order visitOrder(final N node) {
- return Order.NODE_MINUS_PLUS;
+ public VisitOrder visitOrder(final N node) {
+ if (node.depth() > maxDepth + 1) {
+ return VisitOrder.NONE;
+ }
+ return VisitOrder.NODE_MINUS_PLUS;
}
/** Start a line for the given node.
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..f17b25d 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
@@ -28,7 +28,7 @@
/** Enum used to specify the order in which visitors should visit the nodes
* in the tree.
*/
- enum Order {
+ enum VisitOrder {
/** Indicates that the visitor should first visit the plus sub-tree, then
* the minus sub-tree and then the current node.
@@ -58,24 +58,43 @@
/** 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 VisitResult {
+
+ /** 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);
+ VisitResult 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 VisitOrder#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
*/
- default Order visitOrder(final N internalNode) {
- return Order.NODE_MINUS_PLUS;
+ default VisitOrder visitOrder(final N internalNode) {
+ return VisitOrder.NODE_MINUS_PLUS;
}
/** Abstract class for {@link BSPTreeVisitor} implementations that base their visit
@@ -104,9 +123,9 @@
}
/** {@link BSPTreeVisitor} base class that orders tree nodes so that nodes closest to the target point are
- * visited first. This is done by choosing {@link Order#MINUS_NODE_PLUS}
- * when the target point lies on the minus side of the node's cut hyperplane and {@link Order#PLUS_NODE_MINUS}
- * when it lies on the plus side. The order {@link Order#MINUS_NODE_PLUS} order is used when
+ * visited first. This is done by choosing {@link VisitOrder#MINUS_NODE_PLUS}
+ * when the target point lies on the minus side of the node's cut hyperplane and {@link VisitOrder#PLUS_NODE_MINUS}
+ * when it lies on the plus side. The order {@link VisitOrder#MINUS_NODE_PLUS} order is used when
* the target point lies directly on the node's cut hyerplane and no child node is closer than the other.
* @param <P> Point implementation type
* @param <N> BSP tree node implementation type
@@ -122,18 +141,18 @@
/** {@inheritDoc} */
@Override
- public Order visitOrder(N node) {
+ public VisitOrder visitOrder(N node) {
if (node.getCutHyperplane().offset(getTarget()) > 0.0) {
- return Order.PLUS_NODE_MINUS;
+ return VisitOrder.PLUS_NODE_MINUS;
}
- return Order.MINUS_NODE_PLUS;
+ return VisitOrder.MINUS_NODE_PLUS;
}
}
/** {@link BSPTreeVisitor} base class that orders tree nodes so that nodes farthest from the target point
- * are traversed first. This is done by choosing {@link Order#PLUS_NODE_MINUS}
- * when the target point lies on the minus side of the node's cut hyperplane and {@link Order#MINUS_NODE_PLUS}
- * when it lies on the plus side. The order {@link Order#MINUS_NODE_PLUS} order is used when
+ * are traversed first. This is done by choosing {@link VisitOrder#PLUS_NODE_MINUS}
+ * when the target point lies on the minus side of the node's cut hyperplane and {@link VisitOrder#MINUS_NODE_PLUS}
+ * when it lies on the plus side. The order {@link VisitOrder#MINUS_NODE_PLUS} order is used when
* the target point lies directly on the node's cut hyerplane and no child node is closer than the other.
* @param <P> Point implementation type
* @param <N> BSP tree node implementation type
@@ -149,11 +168,11 @@
/** {@inheritDoc} */
@Override
- public Order visitOrder(N node) {
+ public VisitOrder visitOrder(N node) {
if (node.getCutHyperplane().offset(getTarget()) < 0.0) {
- return Order.PLUS_NODE_MINUS;
+ return VisitOrder.PLUS_NODE_MINUS;
}
- return Order.MINUS_NODE_PLUS;
+ return VisitOrder.MINUS_NODE_PLUS;
}
}
}
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..c9b5e60 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,8 @@
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.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.VisitOrder;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.VisitResult;
import org.junit.Assert;
import org.junit.Test;
@@ -814,7 +815,10 @@
List<TestNode> nodes = new ArrayList<>();
// act
- tree.accept(node -> nodes.add(node));
+ tree.accept(node -> {
+ nodes.add(node);
+ return VisitResult.CONTINUE;
+ });
// assert
Assert.assertEquals(
@@ -836,37 +840,37 @@
TestNode minusPlus = minus.getPlus();
// act/assert
- TestVisitor plusMinusNode = new TestVisitor(Order.PLUS_MINUS_NODE);
+ TestVisitor plusMinusNode = new TestVisitor(VisitOrder.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(VisitOrder.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(VisitOrder.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(VisitOrder.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(VisitOrder.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(VisitOrder.NODE_PLUS_MINUS);
tree.accept(nodePlusMinus);
Assert.assertEquals(
Arrays.asList(root, plus, minus, minusPlus, minusMinus),
@@ -881,23 +885,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(VisitOrder.NODE_MINUS_PLUS) {
+ @Override
+ public VisitOrder 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(VisitOrder.NODE_MINUS_PLUS) {
+ @Override
+ public VisitOrder visitOrder(TestNode node) {
+ if (node == minus) {
+ return VisitOrder.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(VisitOrder.MINUS_PLUS_NODE) {
+ @Override
+ public VisitResult visit(TestNode node) {
+ super.visit(node);
+
+ if (node == minus) {
+ return null;
+ }
+ return VisitResult.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(VisitOrder.MINUS_PLUS_NODE) {
+ @Override
+ public VisitResult visit(TestNode node) {
+ super.visit(node);
+
+ if (node == minus) {
+ return VisitResult.TERMINATE;
+ }
+ return VisitResult.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 +1016,67 @@
TestNode minusMinus = minus.getMinus();
TestNode minusPlus = minus.getPlus();
+ // act/assert
+ TestVisitor plusMinusNode = new TestVisitor(VisitOrder.PLUS_MINUS_NODE).withTerminationNode(minus);
+ tree.accept(plusMinusNode);
+ Assert.assertEquals(
+ Arrays.asList(plus, minusPlus, minusMinus, minus),
+ plusMinusNode.getVisited());
+
+ TestVisitor plusNodeMinus = new TestVisitor(VisitOrder.PLUS_NODE_MINUS).withTerminationNode(minus);
+ tree.accept(plusNodeMinus);
+ Assert.assertEquals(
+ Arrays.asList(plus, root, minusPlus, minus),
+ plusNodeMinus.getVisited());
+
+ TestVisitor minusPlusNode = new TestVisitor(VisitOrder.MINUS_PLUS_NODE).withTerminationNode(minus);
+ tree.accept(minusPlusNode);
+ Assert.assertEquals(
+ Arrays.asList(minusMinus, minusPlus, minus),
+ minusPlusNode.getVisited());
+
+ TestVisitor minusNodePlus = new TestVisitor(VisitOrder.MINUS_NODE_PLUS).withTerminationNode(minus);
+ tree.accept(minusNodePlus);
+ Assert.assertEquals(
+ Arrays.asList(minusMinus, minus),
+ minusNodePlus.getVisited());
+
+ TestVisitor nodeMinusPlus = new TestVisitor(VisitOrder.NODE_MINUS_PLUS).withTerminationNode(minus);
+ tree.accept(nodeMinusPlus);
+ Assert.assertEquals(
+ Arrays.asList(root, minus),
+ nodeMinusPlus.getVisited());
+
+ TestVisitor nodePlusMinus = new TestVisitor(VisitOrder.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 VisitResult.CONTINUE;
+ });
// assert
Assert.assertEquals(
- Arrays.asList(root, minus, minusMinus, minusPlus, plus),
+ Arrays.asList(minus, minusMinus, minusPlus),
nodes);
}
@@ -1781,21 +1941,33 @@
private static class TestVisitor implements BSPTreeVisitor<TestPoint2D, TestNode> {
- private final Order order;
+ private final VisitOrder order;
+
+ private TestNode terminationNode;
private final List<TestNode> visited = new ArrayList<>();
- public TestVisitor(Order order) {
+ public TestVisitor(VisitOrder order) {
this.order = order;
}
- @Override
- public void visit(TestNode node) {
- visited.add(node);
+ public TestVisitor withTerminationNode(TestNode terminationNode) {
+ this.terminationNode = terminationNode;
+
+ return this;
}
@Override
- public Order visitOrder(TestNode node) {
+ public VisitResult visit(TestNode node) {
+ visited.add(node);
+
+ return (terminationNode == node) ?
+ VisitResult.TERMINATE :
+ VisitResult.CONTINUE;
+ }
+
+ @Override
+ public VisitOrder visitOrder(TestNode node) {
return order;
}
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..f70f450 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,12 @@
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.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.VisitOrder;
+import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor.VisitResult;
import org.junit.Assert;
import org.junit.Test;
@@ -32,10 +32,10 @@
@Test
public void testDefaultVisitOrder() {
// arrange
- BSPTreeVisitor<TestPoint2D, TestNode> visitor = n -> {};
+ BSPTreeVisitor<TestPoint2D, TestNode> visitor = n -> VisitResult.CONTINUE;
// act/assert
- Assert.assertEquals(Order.NODE_MINUS_PLUS, visitor.visitOrder(null));
+ Assert.assertEquals(VisitOrder.NODE_MINUS_PLUS, visitor.visitOrder(null));
}
@Test
@@ -48,25 +48,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, VisitOrder.MINUS_NODE_PLUS);
+ checkClosestFirst(new TestPoint2D(1, 1), root.getMinus(), VisitOrder.PLUS_NODE_MINUS);
+ checkClosestFirst(new TestPoint2D(1, 1), root.getPlus(), VisitOrder.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, VisitOrder.MINUS_NODE_PLUS);
+ checkClosestFirst(new TestPoint2D(-1, 1), root.getMinus(), VisitOrder.MINUS_NODE_PLUS);
+ checkClosestFirst(new TestPoint2D(-1, 1), root.getPlus(), VisitOrder.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, VisitOrder.PLUS_NODE_MINUS);
+ checkClosestFirst(new TestPoint2D(-1, -1), root.getMinus(), VisitOrder.MINUS_NODE_PLUS);
+ checkClosestFirst(new TestPoint2D(-1, -1), root.getPlus(), VisitOrder.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, VisitOrder.PLUS_NODE_MINUS);
+ checkClosestFirst(new TestPoint2D(1, -1), root.getMinus(), VisitOrder.PLUS_NODE_MINUS);
+ checkClosestFirst(new TestPoint2D(1, -1), root.getPlus(), VisitOrder.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(), VisitOrder.MINUS_NODE_PLUS);
+ checkClosestFirst(TestPoint2D.ZERO, root.getPlus(), VisitOrder.MINUS_NODE_PLUS);
+ checkClosestFirst(TestPoint2D.ZERO, root.getPlus(), VisitOrder.MINUS_NODE_PLUS);
}
@Test
@@ -79,35 +79,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, VisitOrder.PLUS_NODE_MINUS);
+ checkFarthestFirst(new TestPoint2D(1, 1), root.getMinus(), VisitOrder.MINUS_NODE_PLUS);
+ checkFarthestFirst(new TestPoint2D(1, 1), root.getPlus(), VisitOrder.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, VisitOrder.PLUS_NODE_MINUS);
+ checkFarthestFirst(new TestPoint2D(-1, 1), root.getMinus(), VisitOrder.PLUS_NODE_MINUS);
+ checkFarthestFirst(new TestPoint2D(-1, 1), root.getPlus(), VisitOrder.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, VisitOrder.MINUS_NODE_PLUS);
+ checkFarthestFirst(new TestPoint2D(-1, -1), root.getMinus(), VisitOrder.PLUS_NODE_MINUS);
+ checkFarthestFirst(new TestPoint2D(-1, -1), root.getPlus(), VisitOrder.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, VisitOrder.MINUS_NODE_PLUS);
+ checkFarthestFirst(new TestPoint2D(1, -1), root.getMinus(), VisitOrder.MINUS_NODE_PLUS);
+ checkFarthestFirst(new TestPoint2D(1, -1), root.getPlus(), VisitOrder.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(), VisitOrder.MINUS_NODE_PLUS);
+ checkFarthestFirst(TestPoint2D.ZERO, root.getPlus(), VisitOrder.MINUS_NODE_PLUS);
+ checkFarthestFirst(TestPoint2D.ZERO, root.getPlus(), VisitOrder.MINUS_NODE_PLUS);
}
- private static void checkClosestFirst(TestPoint2D target, TestNode node, Order order) {
+ private static void checkClosestFirst(TestPoint2D target, TestNode node, VisitOrder 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, VisitOrder order) {
FarthestFirstStubVisitor visitor = new FarthestFirstStubVisitor(target);
Assert.assertSame(target, visitor.getTarget());
@@ -116,27 +116,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 VisitResult visit(TestNode node) {
+ return VisitResult.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 VisitResult visit(TestNode node) {
+ return VisitResult.CONTINUE;
}
}
}
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..07e8df6 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 VisitResult visit(final RegionNode3D node) {
if (node.isInternal()) {
RegionCutBoundary<Vector3D> boundary = node.getCutBoundary();
addFacetContribution(boundary.getOutsideFacing(), false);
addFacetContribution(boundary.getInsideFacing(), true);
}
+
+ return VisitResult.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 VisitOrder 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 ?
+ VisitOrder.PLUS_NODE_MINUS :
+ VisitOrder.MINUS_NODE_PLUS;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public VisitResult 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 VisitResult.TERMINATE;
+ }
+ }
+ }
+
+ return VisitResult.CONTINUE;
+ }
+ }
}