001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.geometry.euclidean.threed; 018 019import java.util.ArrayList; 020import java.util.List; 021import java.util.stream.Stream; 022import java.util.stream.StreamSupport; 023 024import org.apache.commons.geometry.core.partitioning.Hyperplane; 025import org.apache.commons.geometry.core.partitioning.HyperplaneConvexSubset; 026import org.apache.commons.geometry.core.partitioning.HyperplaneSubset; 027import org.apache.commons.geometry.core.partitioning.Split; 028import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree; 029import org.apache.commons.geometry.core.partitioning.bsp.AbstractPartitionedRegionBuilder; 030import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree; 031import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor; 032import org.apache.commons.geometry.core.partitioning.bsp.RegionCutBoundary; 033import org.apache.commons.geometry.euclidean.threed.line.Line3D; 034import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D; 035import org.apache.commons.geometry.euclidean.threed.line.LinecastPoint3D; 036import org.apache.commons.numbers.core.Precision; 037 038/** Binary space partitioning (BSP) tree representing a region in three dimensional 039 * Euclidean space. 040 */ 041public final class RegionBSPTree3D extends AbstractRegionBSPTree<Vector3D, RegionBSPTree3D.RegionNode3D> 042 implements BoundarySource3D { 043 044 /** Create a new, empty region. */ 045 public RegionBSPTree3D() { 046 this(false); 047 } 048 049 /** Create a new region. If {@code full} is true, then the region will 050 * represent the entire 3D space. Otherwise, it will be empty. 051 * @param full whether or not the region should contain the entire 052 * 3D space or be empty 053 */ 054 public RegionBSPTree3D(final boolean full) { 055 super(full); 056 } 057 058 /** Return a deep copy of this instance. 059 * @return a deep copy of this instance. 060 * @see #copy(org.apache.commons.geometry.core.partitioning.bsp.BSPTree) 061 */ 062 public RegionBSPTree3D copy() { 063 final RegionBSPTree3D result = RegionBSPTree3D.empty(); 064 result.copy(this); 065 066 return result; 067 } 068 069 /** {@inheritDoc} */ 070 @Override 071 public Iterable<PlaneConvexSubset> boundaries() { 072 return createBoundaryIterable(PlaneConvexSubset.class::cast); 073 } 074 075 /** {@inheritDoc} */ 076 @Override 077 public Stream<PlaneConvexSubset> boundaryStream() { 078 return StreamSupport.stream(boundaries().spliterator(), false); 079 } 080 081 /** {@inheritDoc} */ 082 @Override 083 public List<PlaneConvexSubset> getBoundaries() { 084 return createBoundaryList(PlaneConvexSubset.class::cast); 085 } 086 087 /** Return a list of {@link ConvexVolume}s representing the same region 088 * as this instance. One convex volume is returned for each interior leaf 089 * node in the tree. 090 * @return a list of convex volumes representing the same region as this 091 * instance 092 */ 093 public List<ConvexVolume> toConvex() { 094 final List<ConvexVolume> result = new ArrayList<>(); 095 096 toConvexRecursive(getRoot(), ConvexVolume.full(), result); 097 098 return result; 099 } 100 101 /** Recursive method to compute the convex volumes of all inside leaf nodes in the subtree rooted at the given 102 * node. The computed convex volumes are added to the given list. 103 * @param node root of the subtree to compute the convex volumes for 104 * @param nodeVolume the convex volume for the current node; this will be split by the node's cut hyperplane to 105 * form the convex volumes for any child nodes 106 * @param result list containing the results of the computation 107 */ 108 private void toConvexRecursive(final RegionNode3D node, final ConvexVolume nodeVolume, 109 final List<? super ConvexVolume> result) { 110 111 if (node.isLeaf()) { 112 // base case; only add to the result list if the node is inside 113 if (node.isInside()) { 114 result.add(nodeVolume); 115 } 116 } else { 117 // recurse 118 final Split<ConvexVolume> split = nodeVolume.split(node.getCutHyperplane()); 119 120 toConvexRecursive(node.getMinus(), split.getMinus(), result); 121 toConvexRecursive(node.getPlus(), split.getPlus(), result); 122 } 123 } 124 125 /** {@inheritDoc} */ 126 @Override 127 public Split<RegionBSPTree3D> split(final Hyperplane<Vector3D> splitter) { 128 return split(splitter, RegionBSPTree3D.empty(), RegionBSPTree3D.empty()); 129 } 130 131 /** {@inheritDoc} */ 132 @Override 133 public Vector3D project(final Vector3D pt) { 134 // use our custom projector so that we can disambiguate points that are 135 // actually equidistant from the target point 136 final BoundaryProjector3D projector = new BoundaryProjector3D(pt); 137 accept(projector); 138 139 return projector.getProjected(); 140 } 141 142 /** Return the current instance. 143 */ 144 @Override 145 public RegionBSPTree3D toTree() { 146 return this; 147 } 148 149 /** {@inheritDoc} */ 150 @Override 151 public List<LinecastPoint3D> linecast(final LineConvexSubset3D subset) { 152 final LinecastVisitor visitor = new LinecastVisitor(subset, false); 153 accept(visitor); 154 155 return visitor.getResults(); 156 } 157 158 /** {@inheritDoc} */ 159 @Override 160 public LinecastPoint3D linecastFirst(final LineConvexSubset3D subset) { 161 final LinecastVisitor visitor = new LinecastVisitor(subset, true); 162 accept(visitor); 163 164 return visitor.getFirstResult(); 165 } 166 167 /** {@inheritDoc} */ 168 @Override 169 protected RegionSizeProperties<Vector3D> computeRegionSizeProperties() { 170 // handle simple cases 171 if (isFull()) { 172 return new RegionSizeProperties<>(Double.POSITIVE_INFINITY, null); 173 } else if (isEmpty()) { 174 return new RegionSizeProperties<>(0, null); 175 } 176 177 final RegionSizePropertiesVisitor visitor = new RegionSizePropertiesVisitor(); 178 accept(visitor); 179 180 return visitor.getRegionSizeProperties(); 181 } 182 183 /** {@inheritDoc} */ 184 @Override 185 protected RegionNode3D createNode() { 186 return new RegionNode3D(this); 187 } 188 189 /** Return a new instance containing all of 3D space. 190 * @return a new instance containing all of 3D space. 191 */ 192 public static RegionBSPTree3D full() { 193 return new RegionBSPTree3D(true); 194 } 195 196 /** Return a new, empty instance. The represented region is completely empty. 197 * @return a new, empty instance. 198 */ 199 public static RegionBSPTree3D empty() { 200 return new RegionBSPTree3D(false); 201 } 202 203 /** Construct a new tree from the given boundaries. If no boundaries 204 * are present, the returned tree is empty. 205 * @param boundaries boundaries to construct the tree from 206 * @return a new tree instance constructed from the given boundaries 207 * @see #from(Iterable, boolean) 208 */ 209 public static RegionBSPTree3D from(final Iterable<? extends PlaneConvexSubset> boundaries) { 210 return from(boundaries, false); 211 } 212 213 /** Construct a new tree from the given boundaries. If {@code full} is true, then 214 * the initial tree before boundary insertion contains the entire space. Otherwise, 215 * it is empty. 216 * @param boundaries boundaries to construct the tree from 217 * @param full if true, the initial tree will contain the entire space 218 * @return a new tree instance constructed from the given boundaries 219 */ 220 public static RegionBSPTree3D from(final Iterable<? extends PlaneConvexSubset> boundaries, final boolean full) { 221 final RegionBSPTree3D tree = new RegionBSPTree3D(full); 222 tree.insert(boundaries); 223 224 return tree; 225 } 226 227 /** Create a new {@link PartitionedRegionBuilder3D} instance which can be used to build balanced 228 * BSP trees from region boundaries. 229 * @return a new {@link PartitionedRegionBuilder3D} instance 230 */ 231 public static PartitionedRegionBuilder3D partitionedRegionBuilder() { 232 return new PartitionedRegionBuilder3D(); 233 } 234 235 /** BSP tree node for three dimensional Euclidean space. 236 */ 237 public static final class RegionNode3D extends AbstractRegionBSPTree.AbstractRegionNode<Vector3D, RegionNode3D> { 238 /** Simple constructor. 239 * @param tree the owning tree instance 240 */ 241 RegionNode3D(final AbstractBSPTree<Vector3D, RegionNode3D> tree) { 242 super(tree); 243 } 244 245 /** Get the region represented by this node. The returned region contains 246 * the entire area contained in this node, regardless of the attributes of 247 * any child nodes. 248 * @return the region represented by this node 249 */ 250 public ConvexVolume getNodeRegion() { 251 ConvexVolume volume = ConvexVolume.full(); 252 253 RegionNode3D child = this; 254 RegionNode3D parent; 255 256 while ((parent = child.getParent()) != null) { 257 final Split<ConvexVolume> split = volume.split(parent.getCutHyperplane()); 258 259 volume = child.isMinus() ? split.getMinus() : split.getPlus(); 260 261 child = parent; 262 } 263 264 return volume; 265 } 266 267 /** {@inheritDoc} */ 268 @Override 269 protected RegionNode3D getSelf() { 270 return this; 271 } 272 } 273 274 /** Class used to build regions in Euclidean 3D space by inserting boundaries into a BSP 275 * tree containing "partitions", i.e. structural cuts where both sides of the cut have the same region location. 276 * When partitions are chosen that effectively divide the region boundaries at each partition level, the 277 * constructed tree is shallower and more balanced than one constructed from the region boundaries alone, 278 * resulting in improved performance. For example, consider a mesh approximation of a sphere. The region is 279 * convex so each boundary has all of the other boundaries on its minus side; the plus sides are all empty. 280 * When these boundaries are inserted directly into a tree, the tree degenerates into a simple linked list of 281 * nodes with a height directly proportional to the number of boundaries. This means that many operations on the 282 * tree, such as inside/outside testing of points, involve iterating through each and every region boundary. In 283 * contrast, if a partition is first inserted that passes through the sphere center, the first BSP tree node 284 * contains region nodes on its plus <em>and</em> minus sides, cutting the height of the tree in half. Operations 285 * such as inside/outside testing are then able to skip half of the tree nodes with a single test on the 286 * root node, resulting in drastically improved performance. Insertion of additional partitions (using a grid 287 * layout, for example) can produce even shallower trees, although there is a point unique to each boundary set at 288 * which the addition of more partitions begins to decrease instead of increase performance. 289 * 290 * <h2>Usage</h2> 291 * <p>Usage of this class consists of two phases: (1) <em>partition insertion</em> and (2) <em>boundary 292 * insertion</em>. Instances begin in the <em>partition insertion</em> phase. Here, partitions can be inserted 293 * into the empty tree using {@link PartitionedRegionBuilder3D#insertPartition(PlaneConvexSubset) insertPartition} 294 * or similar methods. The {@link org.apache.commons.geometry.core.partitioning.bsp.RegionCutRule#INHERIT INHERIT} 295 * cut rule is used internally to insert the cut so the represented region remains empty even as partitions are 296 * inserted. 297 * </p> 298 * 299 * <p>The instance moves into the <em>boundary insertion</em> phase when the caller inserts the first region 300 * boundary, using {@link PartitionedRegionBuilder3D#insertBoundary(PlaneConvexSubset) insertBoundary} or 301 * similar methods. Attempting to insert a partition after this point results in an {@code IllegalStateException}. 302 * This ensures that partitioning cuts are always located higher up the tree than boundary cuts.</p> 303 * 304 * <p>After all boundaries are inserted, the {@link PartitionedRegionBuilder3D#build() build} method is used 305 * to perform final processing and return the computed tree.</p> 306 */ 307 public static final class PartitionedRegionBuilder3D 308 extends AbstractPartitionedRegionBuilder<Vector3D, RegionNode3D> { 309 310 /** Construct a new builder instance. 311 */ 312 private PartitionedRegionBuilder3D() { 313 super(RegionBSPTree3D.empty()); 314 } 315 316 /** Insert a partition plane. 317 * @param partition partition to insert 318 * @return this instance 319 * @throws IllegalStateException if a boundary has previously been inserted 320 */ 321 public PartitionedRegionBuilder3D insertPartition(final Plane partition) { 322 return insertPartition(partition.span()); 323 } 324 325 /** Insert a plane convex subset as a partition. 326 * @param partition partition to insert 327 * @return this instance 328 * @throws IllegalStateException if a boundary has previously been inserted 329 */ 330 public PartitionedRegionBuilder3D insertPartition(final PlaneConvexSubset partition) { 331 insertPartitionInternal(partition); 332 333 return this; 334 } 335 336 /** Insert a set of three axis aligned planes intersecting at the given point as partitions. 337 * The planes all contain the {@code center} point and have the normals {@code +x}, {@code +y}, 338 * and {@code +z} in that order. If inserted into an empty tree, this will partition the space 339 * into 8 sections. 340 * @param center center point for the partitions; all 3 inserted planes intersect at this point 341 * @param precision precision context used to construct the planes 342 * @return this instance 343 * @throws IllegalStateException if a boundary has previously been inserted 344 */ 345 public PartitionedRegionBuilder3D insertAxisAlignedPartitions(final Vector3D center, 346 final Precision.DoubleEquivalence precision) { 347 348 insertPartition(Planes.fromPointAndNormal(center, Vector3D.Unit.PLUS_X, precision)); 349 insertPartition(Planes.fromPointAndNormal(center, Vector3D.Unit.PLUS_Y, precision)); 350 insertPartition(Planes.fromPointAndNormal(center, Vector3D.Unit.PLUS_Z, precision)); 351 352 return this; 353 } 354 355 /** Insert a 3D grid of partitions. The partitions are constructed recursively: at each level a set of 356 * three axis-aligned partitioning planes are inserted using 357 * {@link #insertAxisAlignedPartitions(Vector3D, Precision.DoubleEquivalence) insertAxisAlignedPartitions}. 358 * The algorithm then recurses using bounding boxes from the min point to the center and from the center 359 * point to the max. Note that this means no partitions are ever inserted directly on the boundaries of 360 * the given bounding box. This is intentional and done to allow this method to be called directly with the 361 * bounding box from a set of boundaries to be inserted without unnecessarily adding partitions that will 362 * never have region boundaries on both sides. 363 * @param bounds bounding box for the grid 364 * @param level recursion level for the grid; each level subdivides each grid cube into 8 sections, making the 365 * total number of grid cubes equal to {@code 8 ^ level} 366 * @param precision precision context used to construct the partition planes 367 * @return this instance 368 * @throws IllegalStateException if a boundary has previously been inserted 369 */ 370 public PartitionedRegionBuilder3D insertAxisAlignedGrid(final Bounds3D bounds, final int level, 371 final Precision.DoubleEquivalence precision) { 372 373 insertAxisAlignedGridRecursive(bounds.getMin(), bounds.getMax(), level, precision); 374 375 return this; 376 } 377 378 /** Recursively insert axis-aligned grid partitions. 379 * @param min min point for the grid cube to partition 380 * @param max max point for the grid cube to partition 381 * @param level current recursion level 382 * @param precision precision context used to construct the partition planes 383 */ 384 private void insertAxisAlignedGridRecursive(final Vector3D min, final Vector3D max, final int level, 385 final Precision.DoubleEquivalence precision) { 386 if (level > 0) { 387 final Vector3D center = min.lerp(max, 0.5); 388 389 insertAxisAlignedPartitions(center, precision); 390 391 final int nextLevel = level - 1; 392 insertAxisAlignedGridRecursive(min, center, nextLevel, precision); 393 insertAxisAlignedGridRecursive(center, max, nextLevel, precision); 394 } 395 } 396 397 /** Insert a region boundary. 398 * @param boundary region boundary to insert 399 * @return this instance 400 */ 401 public PartitionedRegionBuilder3D insertBoundary(final PlaneConvexSubset boundary) { 402 insertBoundaryInternal(boundary); 403 404 return this; 405 } 406 407 /** Insert a collection of region boundaries. 408 * @param boundaries boundaries to insert 409 * @return this instance 410 */ 411 public PartitionedRegionBuilder3D insertBoundaries(final Iterable<? extends PlaneConvexSubset> boundaries) { 412 for (final PlaneConvexSubset boundary : boundaries) { 413 insertBoundaryInternal(boundary); 414 } 415 416 return this; 417 } 418 419 /** Insert all boundaries from the given source. 420 * @param boundarySrc source of boundaries to insert 421 * @return this instance 422 */ 423 public PartitionedRegionBuilder3D insertBoundaries(final BoundarySource3D boundarySrc) { 424 try (Stream<PlaneConvexSubset> stream = boundarySrc.boundaryStream()) { 425 stream.forEach(this::insertBoundaryInternal); 426 } 427 428 return this; 429 } 430 431 /** Build and return the region BSP tree. 432 * @return the region BSP tree 433 */ 434 public RegionBSPTree3D build() { 435 return (RegionBSPTree3D) buildInternal(); 436 } 437 } 438 439 /** Class used to project points onto the 3D region boundary. 440 */ 441 private static final class BoundaryProjector3D extends BoundaryProjector<Vector3D, RegionNode3D> { 442 /** Simple constructor. 443 * @param point the point to project onto the region's boundary 444 */ 445 private BoundaryProjector3D(final Vector3D point) { 446 super(point); 447 } 448 449 /** {@inheritDoc} */ 450 @Override 451 protected Vector3D disambiguateClosestPoint(final Vector3D target, final Vector3D a, final Vector3D b) { 452 // return the point with the smallest coordinate values 453 final int cmp = Vector3D.COORDINATE_ASCENDING_ORDER.compare(a, b); 454 return cmp < 0 ? a : b; 455 } 456 } 457 458 /** Visitor for computing geometric properties for 3D BSP tree instances. 459 * The volume of the region is computed using the equation 460 * <code>V = (1/3)*Σ<sub>F</sub>[(C<sub>F</sub>⋅N<sub>F</sub>)*area(F)]</code>, 461 * where <code>F</code> represents each face in the region, <code>C<sub>F</sub></code> 462 * represents the centroid of the face, and <code>N<sub>F</sub></code> represents the 463 * normal of the face. (More details can be found in the article 464 * <a href="https://en.wikipedia.org/wiki/Polyhedron#Volume">here</a>.) 465 * This essentially splits up the region into pyramids with a 2D face forming 466 * the base of each pyramid. The centroid is computed in a similar way. The centroid 467 * of each pyramid is calculated using the fact that it is located 3/4 of the way along the 468 * line from the apex to the base. The region centroid then becomes the volume-weighted 469 * average of these pyramid centers. 470 * @see <a href="https://en.wikipedia.org/wiki/Polyhedron#Volume">Polyhedron#Volume</a> 471 */ 472 private static final class RegionSizePropertiesVisitor implements BSPTreeVisitor<Vector3D, RegionNode3D> { 473 474 /** Accumulator for boundary volume contributions. */ 475 private double volumeSum; 476 477 /** Centroid contribution x coordinate accumulator. */ 478 private double sumX; 479 480 /** Centroid contribution y coordinate accumulator. */ 481 private double sumY; 482 483 /** Centroid contribution z coordinate accumulator. */ 484 private double sumZ; 485 486 /** {@inheritDoc} */ 487 @Override 488 public Result visit(final RegionNode3D node) { 489 if (node.isInternal()) { 490 final RegionCutBoundary<Vector3D> boundary = node.getCutBoundary(); 491 492 for (final HyperplaneConvexSubset<Vector3D> outsideFacing : boundary.getOutsideFacing()) { 493 addBoundaryContribution(outsideFacing, false); 494 } 495 496 for (final HyperplaneConvexSubset<Vector3D> insideFacing : boundary.getInsideFacing()) { 497 addBoundaryContribution(insideFacing, true); 498 } 499 } 500 501 return Result.CONTINUE; 502 } 503 504 /** Return the computed size properties for the visited region. 505 * @return the computed size properties for the visited region. 506 */ 507 public RegionSizeProperties<Vector3D> getRegionSizeProperties() { 508 double size = Double.POSITIVE_INFINITY; 509 Vector3D centroid = null; 510 511 // we only have a finite size if the volume sum is finite and positive 512 // (negative indicates a finite outside surrounded by an infinite inside) 513 if (Double.isFinite(volumeSum) && volumeSum > 0.0) { 514 // apply the 1/3 pyramid volume scaling factor 515 size = volumeSum / 3.0; 516 517 // Since the volume we used when adding together the boundary contributions 518 // was 3x the actual pyramid size, we'll multiply by 1/4 here instead 519 // of 3/4 to adjust for the actual centroid position in each pyramid. 520 final double centroidScale = 1.0 / (4 * size); 521 centroid = Vector3D.of( 522 sumX * centroidScale, 523 sumY * centroidScale, 524 sumZ * centroidScale); 525 } 526 527 return new RegionSizeProperties<>(size, centroid); 528 } 529 530 /** Add the contribution of the given node cut boundary. If {@code reverse} is true, 531 * the volume of the contribution is reversed before being added to the total. 532 * @param boundary node cut boundary 533 * @param reverse if true, the boundary contribution is reversed before being added to the total. 534 */ 535 private void addBoundaryContribution(final HyperplaneSubset<Vector3D> boundary, final boolean reverse) { 536 final PlaneSubset boundarySubset = (PlaneSubset) boundary; 537 538 final Plane boundaryPlane = boundarySubset.getPlane(); 539 final double boundaryArea = boundarySubset.getSize(); 540 final Vector3D boundaryCentroid = boundarySubset.getCentroid(); 541 542 if (Double.isInfinite(boundaryArea)) { 543 volumeSum = Double.POSITIVE_INFINITY; 544 } else if (boundaryCentroid != null) { 545 // the volume here is actually 3x the actual pyramid volume; we'll apply 546 // the final scaling all at once at the end 547 double scaledVolume = boundaryArea * boundaryCentroid.dot(boundaryPlane.getNormal()); 548 if (reverse) { 549 scaledVolume = -scaledVolume; 550 } 551 552 volumeSum += scaledVolume; 553 554 sumX += scaledVolume * boundaryCentroid.getX(); 555 sumY += scaledVolume * boundaryCentroid.getY(); 556 sumZ += scaledVolume * boundaryCentroid.getZ(); 557 } 558 } 559 } 560 561 /** BSP tree visitor that performs a linecast operation against the boundaries of the visited tree. 562 */ 563 private static final class LinecastVisitor implements BSPTreeVisitor<Vector3D, RegionNode3D> { 564 565 /** The line subset to intersect with the boundaries of the BSP tree. */ 566 private final LineConvexSubset3D linecastSubset; 567 568 /** If true, the visitor will stop visiting the tree once the first linecast 569 * point is determined. 570 */ 571 private final boolean firstOnly; 572 573 /** The minimum abscissa found during the search. */ 574 private double minAbscissa = Double.POSITIVE_INFINITY; 575 576 /** List of results from the linecast operation. */ 577 private final List<LinecastPoint3D> results = new ArrayList<>(); 578 579 /** Create a new instance with the given intersecting line convex subset. 580 * @param linecastSubset line subset to intersect with the BSP tree region boundary 581 * @param firstOnly if true, the visitor will stop visiting the tree once the first 582 * linecast point is determined 583 */ 584 LinecastVisitor(final LineConvexSubset3D linecastSubset, final boolean firstOnly) { 585 this.linecastSubset = linecastSubset; 586 this.firstOnly = firstOnly; 587 } 588 589 /** Get the first {@link org.apache.commons.geometry.euclidean.twod.LinecastPoint2D} 590 * resulting from the linecast operation. 591 * @return the first linecast result point 592 */ 593 public LinecastPoint3D getFirstResult() { 594 final List<LinecastPoint3D> sortedResults = getResults(); 595 596 return sortedResults.isEmpty() ? 597 null : 598 sortedResults.get(0); 599 } 600 601 /** Get a list containing the results of the linecast operation. The list is 602 * sorted and filtered. 603 * @return list of sorted and filtered results from the linecast operation 604 */ 605 public List<LinecastPoint3D> getResults() { 606 LinecastPoint3D.sortAndFilter(results); 607 608 return results; 609 } 610 611 /** {@inheritDoc} */ 612 @Override 613 public Order visitOrder(final RegionNode3D internalNode) { 614 final Plane cut = (Plane) internalNode.getCutHyperplane(); 615 final Line3D line = linecastSubset.getLine(); 616 617 final boolean plusIsNear = line.getDirection().dot(cut.getNormal()) < 0; 618 619 return plusIsNear ? 620 Order.PLUS_NODE_MINUS : 621 Order.MINUS_NODE_PLUS; 622 } 623 624 /** {@inheritDoc} */ 625 @Override 626 public Result visit(final RegionNode3D node) { 627 if (node.isInternal()) { 628 // check if the line subset intersects the node cut hyperplane 629 final Line3D line = linecastSubset.getLine(); 630 final Vector3D pt = ((Plane) node.getCutHyperplane()).intersection(line); 631 632 if (pt != null) { 633 if (firstOnly && !results.isEmpty() && 634 line.getPrecision().compare(minAbscissa, line.abscissa(pt)) < 0) { 635 // we have results and we are now sure that no other intersection points will be 636 // found that are closer or at the same position on the intersecting line. 637 return Result.TERMINATE; 638 } else if (linecastSubset.contains(pt)) { 639 // we've potentially found a new linecast point; add it to the list of potential 640 // results 641 final LinecastPoint3D potentialResult = computeLinecastPoint(pt, node); 642 if (potentialResult != null) { 643 results.add(potentialResult); 644 645 // update the min abscissa 646 minAbscissa = Math.min(minAbscissa, potentialResult.getAbscissa()); 647 } 648 } 649 } 650 } 651 652 return Result.CONTINUE; 653 } 654 655 /** Compute the linecast point for the given intersection point and tree node, returning null 656 * if the point does not actually lie on the region boundary. 657 * @param pt intersection point 658 * @param node node containing the cut that the linecast line intersected with 659 * @return a new linecast point instance or null if the intersection point does not lie 660 * on the region boundary 661 */ 662 private LinecastPoint3D computeLinecastPoint(final Vector3D pt, final RegionNode3D node) { 663 final Plane cut = (Plane) node.getCutHyperplane(); 664 final RegionCutBoundary<Vector3D> boundary = node.getCutBoundary(); 665 666 boolean onBoundary = false; 667 boolean negateNormal = false; 668 669 if (boundary.containsInsideFacing(pt)) { 670 // on inside-facing boundary 671 onBoundary = true; 672 negateNormal = true; 673 } else if (boundary.containsOutsideFacing(pt)) { 674 // on outside-facing boundary 675 onBoundary = true; 676 } 677 678 if (onBoundary) { 679 Vector3D normal = cut.getNormal(); 680 if (negateNormal) { 681 normal = normal.negate(); 682 } 683 684 return new LinecastPoint3D(pt, normal, linecastSubset.getLine()); 685 } 686 687 return null; 688 } 689 } 690}