RegionBSPTree3D.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.geometry.euclidean.threed;

  18. import java.util.ArrayList;
  19. import java.util.List;
  20. import java.util.stream.Stream;
  21. import java.util.stream.StreamSupport;

  22. import org.apache.commons.geometry.core.partitioning.Hyperplane;
  23. import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset;
  24. import org.apache.commons.geometry.core.partitioning.HyperplaneSubset;
  25. import org.apache.commons.geometry.core.partitioning.Split;
  26. import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
  27. import org.apache.commons.geometry.core.partitioning.bsp.AbstractPartitionedRegionBuilder;
  28. import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
  29. import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor;
  30. import org.apache.commons.geometry.core.partitioning.bsp.RegionCutBoundary;
  31. import org.apache.commons.geometry.euclidean.threed.line.Line3D;
  32. import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
  33. import org.apache.commons.geometry.euclidean.threed.line.LinecastPoint3D;
  34. import org.apache.commons.numbers.core.Precision;

  35. /** Binary space partitioning (BSP) tree representing a region in three dimensional
  36.  * Euclidean space.
  37.  */
  38. public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, RegionBSPTree3D.RegionNode3D>
  39.     implements BoundarySource3D {

  40.     /** Create a new, empty region. */
  41.     public RegionBSPTree3D() {
  42.         this(false);
  43.     }

  44.     /** Create a new region. If {@code full} is true, then the region will
  45.      * represent the entire 3D space. Otherwise, it will be empty.
  46.      * @param full whether or not the region should contain the entire
  47.      *      3D space or be empty
  48.      */
  49.     public RegionBSPTree3D(final boolean full) {
  50.         super(full);
  51.     }

  52.     /** Return a deep copy of this instance.
  53.      * @return a deep copy of this instance.
  54.      * @see #copy(org.apache.commons.geometry.core.partitioning.bsp.BSPTree)
  55.      */
  56.     public RegionBSPTree3D copy() {
  57.         final RegionBSPTree3D result = RegionBSPTree3D.empty();
  58.         result.copy(this);

  59.         return result;
  60.     }

  61.     /** {@inheritDoc} */
  62.     @Override
  63.     public Iterable<PlaneConvexSubset> boundaries() {
  64.         return createBoundaryIterable(PlaneConvexSubset.class::cast);
  65.     }

  66.     /** {@inheritDoc} */
  67.     @Override
  68.     public Stream<PlaneConvexSubset> boundaryStream() {
  69.         return StreamSupport.stream(boundaries().spliterator(), false);
  70.     }

  71.     /** {@inheritDoc} */
  72.     @Override
  73.     public List<PlaneConvexSubset> getBoundaries() {
  74.         return createBoundaryList(PlaneConvexSubset.class::cast);
  75.     }

  76.     /** Return a list of {@link ConvexVolume}s representing the same region
  77.      * as this instance. One convex volume is returned for each interior leaf
  78.      * node in the tree.
  79.      * @return a list of convex volumes representing the same region as this
  80.      *      instance
  81.      */
  82.     public List<ConvexVolume> toConvex() {
  83.         final List<ConvexVolume> result = new ArrayList<>();

  84.         toConvexRecursive(getRoot(), ConvexVolume.full(), result);

  85.         return result;
  86.     }

  87.     /** Recursive method to compute the convex volumes of all inside leaf nodes in the subtree rooted at the given
  88.      * node. The computed convex volumes are added to the given list.
  89.      * @param node root of the subtree to compute the convex volumes for
  90.      * @param nodeVolume the convex volume for the current node; this will be split by the node's cut hyperplane to
  91.      *      form the convex volumes for any child nodes
  92.      * @param result list containing the results of the computation
  93.      */
  94.     private void toConvexRecursive(final RegionNode3D node, final ConvexVolume nodeVolume,
  95.             final List<? super ConvexVolume> result) {

  96.         if (node.isLeaf()) {
  97.             // base case; only add to the result list if the node is inside
  98.             if (node.isInside()) {
  99.                 result.add(nodeVolume);
  100.             }
  101.         } else {
  102.             // recurse
  103.             final Split<ConvexVolume> split = nodeVolume.split(node.getCutHyperplane());

  104.             toConvexRecursive(node.getMinus(), split.getMinus(), result);
  105.             toConvexRecursive(node.getPlus(), split.getPlus(), result);
  106.         }
  107.     }

  108.     /** {@inheritDoc} */
  109.     @Override
  110.     public Split<RegionBSPTree3D> split(final Hyperplane<Vector3D> splitter) {
  111.         return split(splitter, RegionBSPTree3D.empty(), RegionBSPTree3D.empty());
  112.     }

  113.     /** {@inheritDoc} */
  114.     @Override
  115.     public Vector3D project(final Vector3D pt) {
  116.         // use our custom projector so that we can disambiguate points that are
  117.         // actually equidistant from the target point
  118.         final BoundaryProjector3D projector = new BoundaryProjector3D(pt);
  119.         accept(projector);

  120.         return projector.getProjected();
  121.     }

  122.     /** Return the current instance.
  123.      */
  124.     @Override
  125.     public RegionBSPTree3D toTree() {
  126.         return this;
  127.     }

  128.     /** {@inheritDoc} */
  129.     @Override
  130.     public List<LinecastPoint3D> linecast(final LineConvexSubset3D subset) {
  131.         final LinecastVisitor visitor = new LinecastVisitor(subset, false);
  132.         accept(visitor);

  133.         return visitor.getResults();
  134.     }

  135.     /** {@inheritDoc} */
  136.     @Override
  137.     public LinecastPoint3D linecastFirst(final LineConvexSubset3D subset) {
  138.         final LinecastVisitor visitor = new LinecastVisitor(subset, true);
  139.         accept(visitor);

  140.         return visitor.getFirstResult();
  141.     }

  142.     /** {@inheritDoc} */
  143.     @Override
  144.     protected RegionSizeProperties<Vector3D> computeRegionSizeProperties() {
  145.         // handle simple cases
  146.         if (isFull()) {
  147.             return new RegionSizeProperties<>(Double.POSITIVE_INFINITY, null);
  148.         } else if (isEmpty()) {
  149.             return new RegionSizeProperties<>(0, null);
  150.         }

  151.         final RegionSizePropertiesVisitor visitor = new RegionSizePropertiesVisitor();
  152.         accept(visitor);

  153.         return visitor.getRegionSizeProperties();
  154.     }

  155.     /** {@inheritDoc} */
  156.     @Override
  157.     protected RegionNode3D createNode() {
  158.         return new RegionNode3D(this);
  159.     }

  160.     /** Return a new instance containing all of 3D space.
  161.      * @return a new instance containing all of 3D space.
  162.      */
  163.     public static RegionBSPTree3D full() {
  164.         return new RegionBSPTree3D(true);
  165.     }

  166.     /** Return a new, empty instance. The represented region is completely empty.
  167.      * @return a new, empty instance.
  168.      */
  169.     public static RegionBSPTree3D empty() {
  170.         return new RegionBSPTree3D(false);
  171.     }

  172.     /** Construct a new tree from the given boundaries. If no boundaries
  173.      * are present, the returned tree is empty.
  174.      * @param boundaries boundaries to construct the tree from
  175.      * @return a new tree instance constructed from the given boundaries
  176.      * @see #from(Iterable, boolean)
  177.      */
  178.     public static RegionBSPTree3D from(final Iterable<? extends PlaneConvexSubset> boundaries) {
  179.         return from(boundaries, false);
  180.     }

  181.     /** Construct a new tree from the given boundaries. If {@code full} is true, then
  182.      * the initial tree before boundary insertion contains the entire space. Otherwise,
  183.      * it is empty.
  184.      * @param boundaries boundaries to construct the tree from
  185.      * @param full if true, the initial tree will contain the entire space
  186.      * @return a new tree instance constructed from the given boundaries
  187.      */
  188.     public static RegionBSPTree3D from(final Iterable<? extends PlaneConvexSubset> boundaries, final boolean full) {
  189.         final RegionBSPTree3D tree = new RegionBSPTree3D(full);
  190.         tree.insert(boundaries);

  191.         return tree;
  192.     }

  193.     /** Create a new {@link PartitionedRegionBuilder3D} instance which can be used to build balanced
  194.      * BSP trees from region boundaries.
  195.      * @return a new {@link PartitionedRegionBuilder3D} instance
  196.      */
  197.     public static PartitionedRegionBuilder3D partitionedRegionBuilder() {
  198.         return new PartitionedRegionBuilder3D();
  199.     }

  200.     /** BSP tree node for three dimensional Euclidean space.
  201.      */
  202.     public static final class RegionNode3D extends AbstractRegionBSPTree.AbstractRegionNode<Vector3D, RegionNode3D> {
  203.         /** Simple constructor.
  204.          * @param tree the owning tree instance
  205.          */
  206.         RegionNode3D(final AbstractBSPTree<Vector3D, RegionNode3D> tree) {
  207.             super(tree);
  208.         }

  209.         /** Get the region represented by this node. The returned region contains
  210.          * the entire area contained in this node, regardless of the attributes of
  211.          * any child nodes.
  212.          * @return the region represented by this node
  213.          */
  214.         public ConvexVolume getNodeRegion() {
  215.             ConvexVolume volume = ConvexVolume.full();

  216.             RegionNode3D child = this;
  217.             RegionNode3D parent;

  218.             while ((parent = child.getParent()) != null) {
  219.                 final Split<ConvexVolume> split = volume.split(parent.getCutHyperplane());

  220.                 volume = child.isMinus() ? split.getMinus() : split.getPlus();

  221.                 child = parent;
  222.             }

  223.             return volume;
  224.         }

  225.         /** {@inheritDoc} */
  226.         @Override
  227.         protected RegionNode3D getSelf() {
  228.             return this;
  229.         }
  230.     }

  231.     /** Class used to build regions in Euclidean 3D space by inserting boundaries into a BSP
  232.      * tree containing "partitions", i.e. structural cuts where both sides of the cut have the same region location.
  233.      * When partitions are chosen that effectively divide the region boundaries at each partition level, the
  234.      * constructed tree is shallower and more balanced than one constructed from the region boundaries alone,
  235.      * resulting in improved performance. For example, consider a mesh approximation of a sphere. The region is
  236.      * convex so each boundary has all of the other boundaries on its minus side; the plus sides are all empty.
  237.      * When these boundaries are inserted directly into a tree, the tree degenerates into a simple linked list of
  238.      * nodes with a height directly proportional to the number of boundaries. This means that many operations on the
  239.      * tree, such as inside/outside testing of points, involve iterating through each and every region boundary. In
  240.      * contrast, if a partition is first inserted that passes through the sphere center, the first BSP tree node
  241.      * contains region nodes on its plus <em>and</em> minus sides, cutting the height of the tree in half. Operations
  242.      * such as inside/outside testing are then able to skip half of the tree nodes with a single test on the
  243.      * root node, resulting in drastically improved performance. Insertion of additional partitions (using a grid
  244.      * layout, for example) can produce even shallower trees, although there is a point unique to each boundary set at
  245.      * which the addition of more partitions begins to decrease instead of increase performance.
  246.      *
  247.      * <h2>Usage</h2>
  248.      * <p>Usage of this class consists of two phases: (1) <em>partition insertion</em> and (2) <em>boundary
  249.      * insertion</em>. Instances begin in the <em>partition insertion</em> phase. Here, partitions can be inserted
  250.      * into the empty tree using {@link PartitionedRegionBuilder3D#insertPartition(PlaneConvexSubset) insertPartition}
  251.      * or similar methods. The {@link org.apache.commons.geometry.core.partitioning.bsp.RegionCutRule#INHERIT INHERIT}
  252.      * cut rule is used internally to insert the cut so the represented region remains empty even as partitions are
  253.      * inserted.
  254.      * </p>
  255.      *
  256.      * <p>The instance moves into the <em>boundary insertion</em> phase when the caller inserts the first region
  257.      * boundary, using {@link PartitionedRegionBuilder3D#insertBoundary(PlaneConvexSubset) insertBoundary} or
  258.      * similar methods. Attempting to insert a partition after this point results in an {@code IllegalStateException}.
  259.      * This ensures that partitioning cuts are always located higher up the tree than boundary cuts.</p>
  260.      *
  261.      * <p>After all boundaries are inserted, the {@link PartitionedRegionBuilder3D#build() build} method is used
  262.      * to perform final processing and return the computed tree.</p>
  263.      */
  264.     public static final class PartitionedRegionBuilder3D
  265.         extends AbstractPartitionedRegionBuilder<Vector3D, RegionNode3D> {

  266.         /** Construct a new builder instance.
  267.          */
  268.         private PartitionedRegionBuilder3D() {
  269.             super(RegionBSPTree3D.empty());
  270.         }

  271.         /** Insert a partition plane.
  272.          * @param partition partition to insert
  273.          * @return this instance
  274.          * @throws IllegalStateException if a boundary has previously been inserted
  275.          */
  276.         public PartitionedRegionBuilder3D insertPartition(final Plane partition) {
  277.             return insertPartition(partition.span());
  278.         }

  279.         /** Insert a plane convex subset as a partition.
  280.          * @param partition partition to insert
  281.          * @return this instance
  282.          * @throws IllegalStateException if a boundary has previously been inserted
  283.          */
  284.         public PartitionedRegionBuilder3D insertPartition(final PlaneConvexSubset partition) {
  285.             insertPartitionInternal(partition);

  286.             return this;
  287.         }

  288.         /** Insert a set of three axis aligned planes intersecting at the given point as partitions.
  289.          * The planes all contain the {@code center} point and have the normals {@code +x}, {@code +y},
  290.          * and {@code +z} in that order. If inserted into an empty tree, this will partition the space
  291.          * into 8 sections.
  292.          * @param center center point for the partitions; all 3 inserted planes intersect at this point
  293.          * @param precision precision context used to construct the planes
  294.          * @return this instance
  295.          * @throws IllegalStateException if a boundary has previously been inserted
  296.          */
  297.         public PartitionedRegionBuilder3D insertAxisAlignedPartitions(final Vector3D center,
  298.                 final Precision.DoubleEquivalence precision) {

  299.             insertPartition(Planes.fromPointAndNormal(center, Vector3D.Unit.PLUS_X, precision));
  300.             insertPartition(Planes.fromPointAndNormal(center, Vector3D.Unit.PLUS_Y, precision));
  301.             insertPartition(Planes.fromPointAndNormal(center, Vector3D.Unit.PLUS_Z, precision));

  302.             return this;
  303.         }

  304.         /** Insert a 3D grid of partitions. The partitions are constructed recursively: at each level a set of
  305.          * three axis-aligned partitioning planes are inserted using
  306.          * {@link #insertAxisAlignedPartitions(Vector3D, Precision.DoubleEquivalence) insertAxisAlignedPartitions}.
  307.          * The algorithm then recurses using bounding boxes from the min point to the center and from the center
  308.          * point to the max. Note that this means no partitions are ever inserted directly on the boundaries of
  309.          * the given bounding box. This is intentional and done to allow this method to be called directly with the
  310.          * bounding box from a set of boundaries to be inserted without unnecessarily adding partitions that will
  311.          * never have region boundaries on both sides.
  312.          * @param bounds bounding box for the grid
  313.          * @param level recursion level for the grid; each level subdivides each grid cube into 8 sections, making the
  314.          *      total number of grid cubes equal to {@code 8 ^ level}
  315.          * @param precision precision context used to construct the partition planes
  316.          * @return this instance
  317.          * @throws IllegalStateException if a boundary has previously been inserted
  318.          */
  319.         public PartitionedRegionBuilder3D insertAxisAlignedGrid(final Bounds3D bounds, final int level,
  320.                 final Precision.DoubleEquivalence precision) {

  321.             insertAxisAlignedGridRecursive(bounds.getMin(), bounds.getMax(), level, precision);

  322.             return this;
  323.         }

  324.         /** Recursively insert axis-aligned grid partitions.
  325.          * @param min min point for the grid cube to partition
  326.          * @param max max point for the grid cube to partition
  327.          * @param level current recursion level
  328.          * @param precision precision context used to construct the partition planes
  329.          */
  330.         private void insertAxisAlignedGridRecursive(final Vector3D min, final Vector3D max, final int level,
  331.                 final Precision.DoubleEquivalence precision) {
  332.             if (level > 0) {
  333.                 final Vector3D center = min.lerp(max, 0.5);

  334.                 insertAxisAlignedPartitions(center, precision);

  335.                 final int nextLevel = level - 1;
  336.                 insertAxisAlignedGridRecursive(min, center, nextLevel, precision);
  337.                 insertAxisAlignedGridRecursive(center, max, nextLevel, precision);
  338.             }
  339.         }

  340.         /** Insert a region boundary.
  341.          * @param boundary region boundary to insert
  342.          * @return this instance
  343.          */
  344.         public PartitionedRegionBuilder3D insertBoundary(final PlaneConvexSubset boundary) {
  345.             insertBoundaryInternal(boundary);

  346.             return this;
  347.         }

  348.         /** Insert a collection of region boundaries.
  349.          * @param boundaries boundaries to insert
  350.          * @return this instance
  351.          */
  352.         public PartitionedRegionBuilder3D insertBoundaries(final Iterable<? extends PlaneConvexSubset> boundaries) {
  353.             for (final PlaneConvexSubset boundary : boundaries) {
  354.                 insertBoundaryInternal(boundary);
  355.             }

  356.             return this;
  357.         }

  358.         /** Insert all boundaries from the given source.
  359.          * @param boundarySrc source of boundaries to insert
  360.          * @return this instance
  361.          */
  362.         public PartitionedRegionBuilder3D insertBoundaries(final BoundarySource3D boundarySrc) {
  363.             try (Stream<PlaneConvexSubset> stream = boundarySrc.boundaryStream()) {
  364.                 stream.forEach(this::insertBoundaryInternal);
  365.             }

  366.             return this;
  367.         }

  368.         /** Build and return the region BSP tree.
  369.          * @return the region BSP tree
  370.          */
  371.         public RegionBSPTree3D build() {
  372.             return (RegionBSPTree3D) buildInternal();
  373.         }
  374.     }

  375.     /** Class used to project points onto the 3D region boundary.
  376.      */
  377.     private static final class BoundaryProjector3D extends BoundaryProjector<Vector3D, RegionNode3D> {
  378.         /** Simple constructor.
  379.          * @param point the point to project onto the region's boundary
  380.          */
  381.         private BoundaryProjector3D(final Vector3D point) {
  382.             super(point);
  383.         }

  384.         /** {@inheritDoc} */
  385.         @Override
  386.         protected Vector3D disambiguateClosestPoint(final Vector3D target, final Vector3D a, final Vector3D b) {
  387.             // return the point with the smallest coordinate values
  388.             final int cmp = Vector3D.COORDINATE_ASCENDING_ORDER.compare(a, b);
  389.             return cmp < 0 ? a : b;
  390.         }
  391.     }

  392.     /** Visitor for computing geometric properties for 3D BSP tree instances.
  393.      *  The volume of the region is computed using the equation
  394.      *  <code>V = (1/3)*&Sigma;<sub>F</sub>[(C<sub>F</sub>&sdot;N<sub>F</sub>)*area(F)]</code>,
  395.      *  where <code>F</code> represents each face in the region, <code>C<sub>F</sub></code>
  396.      *  represents the centroid of the face, and <code>N<sub>F</sub></code> represents the
  397.      *  normal of the face. (More details can be found in the article
  398.      *  <a href="https://en.wikipedia.org/wiki/Polyhedron#Volume">here</a>.)
  399.      *  This essentially splits up the region into pyramids with a 2D face forming
  400.      *  the base of each pyramid. The centroid is computed in a similar way. The centroid
  401.      *  of each pyramid is calculated using the fact that it is located 3/4 of the way along the
  402.      *  line from the apex to the base. The region centroid then becomes the volume-weighted
  403.      *  average of these pyramid centers.
  404.      *  @see <a href="https://en.wikipedia.org/wiki/Polyhedron#Volume">Polyhedron#Volume</a>
  405.      */
  406.     private static final class RegionSizePropertiesVisitor implements BSPTreeVisitor<Vector3D, RegionNode3D> {

  407.         /** Accumulator for boundary volume contributions. */
  408.         private double volumeSum;

  409.         /** Centroid contribution x coordinate accumulator. */
  410.         private double sumX;

  411.         /** Centroid contribution y coordinate accumulator. */
  412.         private double sumY;

  413.         /** Centroid contribution z coordinate accumulator. */
  414.         private double sumZ;

  415.         /** {@inheritDoc} */
  416.         @Override
  417.         public Result visit(final RegionNode3D node) {
  418.             if (node.isInternal()) {
  419.                 final RegionCutBoundary<Vector3D> boundary = node.getCutBoundary();

  420.                 for (final HyperplaneConvexSubset<Vector3D> outsideFacing : boundary.getOutsideFacing()) {
  421.                     addBoundaryContribution(outsideFacing, false);
  422.                 }

  423.                 for (final HyperplaneConvexSubset<Vector3D> insideFacing : boundary.getInsideFacing()) {
  424.                     addBoundaryContribution(insideFacing, true);
  425.                 }
  426.             }

  427.             return Result.CONTINUE;
  428.         }

  429.         /** Return the computed size properties for the visited region.
  430.          * @return the computed size properties for the visited region.
  431.          */
  432.         public RegionSizeProperties<Vector3D> getRegionSizeProperties() {
  433.             double size = Double.POSITIVE_INFINITY;
  434.             Vector3D centroid = null;

  435.             // we only have a finite size if the volume sum is finite and positive
  436.             // (negative indicates a finite outside surrounded by an infinite inside)
  437.             if (Double.isFinite(volumeSum) && volumeSum > 0.0) {
  438.                 // apply the 1/3 pyramid volume scaling factor
  439.                 size = volumeSum / 3.0;

  440.                 // Since the volume we used when adding together the boundary contributions
  441.                 // was 3x the actual pyramid size, we'll multiply by 1/4 here instead
  442.                 // of 3/4 to adjust for the actual centroid position in each pyramid.
  443.                 final double centroidScale = 1.0 / (4 * size);
  444.                 centroid =  Vector3D.of(
  445.                         sumX * centroidScale,
  446.                         sumY * centroidScale,
  447.                         sumZ * centroidScale);
  448.             }

  449.             return new RegionSizeProperties<>(size, centroid);
  450.         }

  451.         /** Add the contribution of the given node cut boundary. If {@code reverse} is true,
  452.          * the volume of the contribution is reversed before being added to the total.
  453.          * @param boundary node cut boundary
  454.          * @param reverse if true, the boundary contribution is reversed before being added to the total.
  455.          */
  456.         private void addBoundaryContribution(final HyperplaneSubset<Vector3D> boundary, final boolean reverse) {
  457.             final PlaneSubset boundarySubset = (PlaneSubset) boundary;

  458.             final Plane boundaryPlane = boundarySubset.getPlane();
  459.             final double boundaryArea = boundarySubset.getSize();
  460.             final Vector3D boundaryCentroid = boundarySubset.getCentroid();

  461.             if (Double.isInfinite(boundaryArea)) {
  462.                 volumeSum = Double.POSITIVE_INFINITY;
  463.             } else if (boundaryCentroid != null) {
  464.                 // the volume here is actually 3x the actual pyramid volume; we'll apply
  465.                 // the final scaling all at once at the end
  466.                 double scaledVolume = boundaryArea * boundaryCentroid.dot(boundaryPlane.getNormal());
  467.                 if (reverse) {
  468.                     scaledVolume = -scaledVolume;
  469.                 }

  470.                 volumeSum += scaledVolume;

  471.                 sumX += scaledVolume * boundaryCentroid.getX();
  472.                 sumY += scaledVolume * boundaryCentroid.getY();
  473.                 sumZ += scaledVolume * boundaryCentroid.getZ();
  474.             }
  475.         }
  476.     }

  477.     /** BSP tree visitor that performs a linecast operation against the boundaries of the visited tree.
  478.      */
  479.     private static final class LinecastVisitor implements BSPTreeVisitor<Vector3D, RegionNode3D> {

  480.         /** The line subset to intersect with the boundaries of the BSP tree. */
  481.         private final LineConvexSubset3D linecastSubset;

  482.         /** If true, the visitor will stop visiting the tree once the first linecast
  483.          * point is determined.
  484.          */
  485.         private final boolean firstOnly;

  486.         /** The minimum abscissa found during the search. */
  487.         private double minAbscissa = Double.POSITIVE_INFINITY;

  488.         /** List of results from the linecast operation. */
  489.         private final List<LinecastPoint3D> results = new ArrayList<>();

  490.         /** Create a new instance with the given intersecting line convex subset.
  491.          * @param linecastSubset line subset to intersect with the BSP tree region boundary
  492.          * @param firstOnly if true, the visitor will stop visiting the tree once the first
  493.          *      linecast point is determined
  494.          */
  495.         LinecastVisitor(final LineConvexSubset3D linecastSubset, final boolean firstOnly) {
  496.             this.linecastSubset = linecastSubset;
  497.             this.firstOnly = firstOnly;
  498.         }

  499.         /** Get the first {@link org.apache.commons.geometry.euclidean.twod.LinecastPoint2D}
  500.          * resulting from the linecast operation.
  501.          * @return the first linecast result point
  502.          */
  503.         public LinecastPoint3D getFirstResult() {
  504.             final List<LinecastPoint3D> sortedResults = getResults();

  505.             return sortedResults.isEmpty() ?
  506.                     null :
  507.                     sortedResults.get(0);
  508.         }

  509.         /** Get a list containing the results of the linecast operation. The list is
  510.          * sorted and filtered.
  511.          * @return list of sorted and filtered results from the linecast operation
  512.          */
  513.         public List<LinecastPoint3D> getResults() {
  514.             LinecastPoint3D.sortAndFilter(results);

  515.             return results;
  516.         }

  517.         /** {@inheritDoc} */
  518.         @Override
  519.         public Order visitOrder(final RegionNode3D internalNode) {
  520.             final Plane cut = (Plane) internalNode.getCutHyperplane();
  521.             final Line3D line = linecastSubset.getLine();

  522.             final boolean plusIsNear = line.getDirection().dot(cut.getNormal()) < 0;

  523.             return plusIsNear ?
  524.                     Order.PLUS_NODE_MINUS :
  525.                     Order.MINUS_NODE_PLUS;
  526.         }

  527.         /** {@inheritDoc} */
  528.         @Override
  529.         public Result visit(final RegionNode3D node) {
  530.             if (node.isInternal()) {
  531.                 // check if the line subset intersects the node cut hyperplane
  532.                 final Line3D line = linecastSubset.getLine();
  533.                 final Vector3D pt = ((Plane) node.getCutHyperplane()).intersection(line);

  534.                 if (pt != null) {
  535.                     if (firstOnly && !results.isEmpty() &&
  536.                             line.getPrecision().compare(minAbscissa, line.abscissa(pt)) < 0) {
  537.                         // we have results and we are now sure that no other intersection points will be
  538.                         // found that are closer or at the same position on the intersecting line.
  539.                         return Result.TERMINATE;
  540.                     } else if (linecastSubset.contains(pt)) {
  541.                         // we've potentially found a new linecast point; add it to the list of potential
  542.                         // results
  543.                         final LinecastPoint3D potentialResult = computeLinecastPoint(pt, node);
  544.                         if (potentialResult != null) {
  545.                             results.add(potentialResult);

  546.                             // update the min abscissa
  547.                             minAbscissa = Math.min(minAbscissa, potentialResult.getAbscissa());
  548.                         }
  549.                     }
  550.                 }
  551.             }

  552.             return Result.CONTINUE;
  553.         }

  554.         /** Compute the linecast point for the given intersection point and tree node, returning null
  555.          * if the point does not actually lie on the region boundary.
  556.          * @param pt intersection point
  557.          * @param node node containing the cut that the linecast line intersected with
  558.          * @return a new linecast point instance or null if the intersection point does not lie
  559.          *      on the region boundary
  560.          */
  561.         private LinecastPoint3D computeLinecastPoint(final Vector3D pt, final RegionNode3D node) {
  562.             final Plane cut = (Plane) node.getCutHyperplane();
  563.             final RegionCutBoundary<Vector3D> boundary = node.getCutBoundary();

  564.             boolean onBoundary = false;
  565.             boolean negateNormal = false;

  566.             if (boundary.containsInsideFacing(pt)) {
  567.                 // on inside-facing boundary
  568.                 onBoundary = true;
  569.                 negateNormal = true;
  570.             } else  if (boundary.containsOutsideFacing(pt)) {
  571.                 // on outside-facing boundary
  572.                 onBoundary = true;
  573.             }

  574.             if (onBoundary) {
  575.                 Vector3D normal = cut.getNormal();
  576.                 if (negateNormal) {
  577.                     normal = normal.negate();
  578.                 }

  579.                 return new LinecastPoint3D(pt, normal, linecastSubset.getLine());
  580.             }

  581.             return null;
  582.         }
  583.     }
  584. }