AbstractConvexPolygon3D.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.Iterator;
  20. import java.util.List;

  21. import org.apache.commons.geometry.core.RegionLocation;
  22. import org.apache.commons.geometry.core.partitioning.Hyperplane;
  23. import org.apache.commons.geometry.core.partitioning.HyperplaneLocation;
  24. import org.apache.commons.geometry.core.partitioning.Split;
  25. import org.apache.commons.geometry.euclidean.internal.Vectors;
  26. import org.apache.commons.geometry.euclidean.threed.line.Lines3D;
  27. import org.apache.commons.geometry.euclidean.twod.ConvexArea;
  28. import org.apache.commons.geometry.euclidean.twod.Vector2D;
  29. import org.apache.commons.numbers.core.Precision;

  30. /** Abstract base class for {@link ConvexPolygon3D} implementations.
  31.  */
  32. abstract class AbstractConvexPolygon3D extends AbstractPlaneSubset implements ConvexPolygon3D {

  33.     /** Plane containing the convex polygon. */
  34.     private final Plane plane;

  35.     /** Simple constructor.
  36.      * @param plane the plane containing the convex polygon
  37.      */
  38.     AbstractConvexPolygon3D(final Plane plane) {
  39.         this.plane = plane;
  40.     }

  41.     /** {@inheritDoc} */
  42.     @Override
  43.     public Plane getPlane() {
  44.         return plane;
  45.     }

  46.     /** {@inheritDoc}
  47.      *
  48.      *  <p>This method always returns {@code false}.</p>
  49.      */
  50.     @Override
  51.     public boolean isFull() {
  52.         return false;
  53.     }

  54.     /** {@inheritDoc}
  55.      *
  56.      *  <p>This method always returns {@code false}.</p>
  57.      */
  58.     @Override
  59.     public boolean isEmpty() {
  60.         return false;
  61.     }

  62.     /** {@inheritDoc} */
  63.     @Override
  64.     public double getSize() {
  65.         // see http://geomalgorithms.com/a01-_area.html#3D-Planar-Polygons
  66.         final List<Vector3D> vertices = getVertices();

  67.         double crossSumX = 0.0;
  68.         double crossSumY = 0.0;
  69.         double crossSumZ = 0.0;

  70.         Vector3D prevPt = vertices.get(vertices.size() - 1);
  71.         Vector3D cross;
  72.         for (final Vector3D curPt : vertices) {
  73.             cross = prevPt.cross(curPt);

  74.             crossSumX += cross.getX();
  75.             crossSumY += cross.getY();
  76.             crossSumZ += cross.getZ();

  77.             prevPt = curPt;
  78.         }

  79.         return 0.5 * plane.getNormal().dot(Vector3D.of(crossSumX, crossSumY, crossSumZ));
  80.     }

  81.     /** {@inheritDoc} */
  82.     @Override
  83.     public Vector3D getCentroid() {
  84.         final List<Vector3D> vertices = getVertices();

  85.         double areaSum = 0.0;
  86.         double scaledCentroidSumX = 0.0;
  87.         double scaledCentroidSumY = 0.0;
  88.         double scaledCentroidSumZ = 0.0;

  89.         final Iterator<Vector3D> it = vertices.iterator();

  90.         final Vector3D startPt = it.next();

  91.         Vector3D prevPt = it.next();
  92.         Vector3D curPt;

  93.         Vector3D prevVec = startPt.vectorTo(prevPt);
  94.         Vector3D curVec;

  95.         double triArea;
  96.         Vector3D triCentroid;
  97.         while (it.hasNext()) {
  98.             curPt = it.next();
  99.             curVec = startPt.vectorTo(curPt);

  100.             triArea = 0.5 * prevVec.cross(curVec).norm();
  101.             triCentroid = Vector3D.centroid(startPt, prevPt, curPt);

  102.             areaSum += triArea;

  103.             scaledCentroidSumX += triArea * triCentroid.getX();
  104.             scaledCentroidSumY += triArea * triCentroid.getY();
  105.             scaledCentroidSumZ += triArea * triCentroid.getZ();

  106.             prevPt = curPt;
  107.             prevVec = curVec;
  108.         }

  109.         if (areaSum > 0) {
  110.             final double scale = 1 / areaSum;
  111.             return Vector3D.of(
  112.                         scale * scaledCentroidSumX,
  113.                         scale * scaledCentroidSumY,
  114.                         scale * scaledCentroidSumZ
  115.                     );
  116.         }

  117.         // zero area, which means that the points are all linear; return the point midway between the
  118.         // min and max points
  119.         final Vector3D min = Vector3D.min(vertices);
  120.         final Vector3D max = Vector3D.max(vertices);

  121.         return min.lerp(max, 0.5);
  122.     }

  123.     /** {@inheritDoc} */
  124.     @Override
  125.     public Bounds3D getBounds() {
  126.         return Bounds3D.from(getVertices());
  127.     }

  128.     /** {@inheritDoc} */
  129.     @Override
  130.     public RegionLocation classify(final Vector3D pt) {
  131.         if (plane.contains(pt)) {
  132.             final List<Vector3D> vertices = getVertices();
  133.             final Precision.DoubleEquivalence precision = plane.getPrecision();

  134.             final Vector3D normal = plane.getNormal();
  135.             Vector3D edgeVec;
  136.             Vector3D edgePlusVec;
  137.             Vector3D testVec;

  138.             Vector3D offsetVec;
  139.             double offsetSign;
  140.             double offset;
  141.             int cmp;

  142.             boolean onBoundary = false;

  143.             Vector3D startVertex = vertices.get(vertices.size() - 1);
  144.             for (final Vector3D nextVertex : vertices) {

  145.                 edgeVec = startVertex.vectorTo(nextVertex);
  146.                 edgePlusVec = edgeVec.cross(normal);

  147.                 testVec = startVertex.vectorTo(pt);

  148.                 offsetVec = testVec.reject(edgeVec);
  149.                 offsetSign = Math.signum(offsetVec.dot(edgePlusVec));
  150.                 offset = offsetSign * offsetVec.norm();

  151.                 cmp = precision.compare(offset, 0.0);
  152.                 if (cmp > 0) {
  153.                     // the point is on the plus side (outside) of a boundary
  154.                     return RegionLocation.OUTSIDE;
  155.                 } else if (cmp == 0) {
  156.                     onBoundary = true;
  157.                 }

  158.                 startVertex = nextVertex;
  159.             }

  160.             if (onBoundary) {
  161.                 // the point is not on the outside of any boundaries and is directly on at least one
  162.                 return RegionLocation.BOUNDARY;
  163.             }

  164.             // the point is on the inside of all boundaries
  165.             return RegionLocation.INSIDE;
  166.         }

  167.         // the point is not on the plane
  168.         return RegionLocation.OUTSIDE;
  169.     }

  170.     /** {@inheritDoc} */
  171.     @Override
  172.     public Vector3D closest(final Vector3D pt) {
  173.         final Vector3D normal = plane.getNormal();
  174.         final Precision.DoubleEquivalence precision = plane.getPrecision();

  175.         final List<Vector3D> vertices = getVertices();

  176.         final Vector3D projPt = plane.project(pt);

  177.         Vector3D edgeVec;
  178.         Vector3D edgePlusVec;
  179.         Vector3D testVec;

  180.         Vector3D offsetVec;
  181.         double offsetSign;
  182.         double offset;
  183.         int cmp;

  184.         Vector3D boundaryVec;
  185.         double boundaryPointT;
  186.         Vector3D boundaryPoint;
  187.         double boundaryPointDistSq;

  188.         double closestBoundaryPointDistSq = Double.POSITIVE_INFINITY;
  189.         Vector3D closestBoundaryPoint = null;

  190.         Vector3D startVertex = vertices.get(vertices.size() - 1);
  191.         for (final Vector3D nextVertex : vertices) {

  192.             edgeVec = startVertex.vectorTo(nextVertex);
  193.             edgePlusVec = edgeVec.cross(normal);

  194.             testVec = startVertex.vectorTo(projPt);

  195.             offsetVec = testVec.reject(edgeVec);
  196.             offsetSign = Math.signum(offsetVec.dot(edgePlusVec));
  197.             offset = offsetSign * offsetVec.norm();

  198.             cmp = precision.compare(offset, 0.0);
  199.             if (cmp >= 0) {
  200.                 // the point is on directly on the boundary or on its plus side; project the point onto the
  201.                 // boundary, taking care to restrict the point to the actual extent of the boundary,
  202.                 // and select the point with the shortest distance
  203.                 boundaryVec = testVec.subtract(offsetVec);
  204.                 boundaryPointT =
  205.                         Math.signum(boundaryVec.dot(edgeVec)) * (boundaryVec.norm() / Vectors.checkedNorm(edgeVec));
  206.                 boundaryPointT = Math.max(0, Math.min(1, boundaryPointT));

  207.                 boundaryPoint = startVertex.lerp(nextVertex, boundaryPointT);

  208.                 boundaryPointDistSq = boundaryPoint.distanceSq(projPt);
  209.                 if (boundaryPointDistSq < closestBoundaryPointDistSq) {
  210.                     closestBoundaryPointDistSq = boundaryPointDistSq;
  211.                     closestBoundaryPoint = boundaryPoint;
  212.                 }
  213.             }

  214.             startVertex = nextVertex;
  215.         }

  216.         if (closestBoundaryPoint != null) {
  217.             // the point is on the outside of the polygon; return the closest point on the boundary
  218.             return closestBoundaryPoint;
  219.         }

  220.         // the projected point is on the inside of all boundaries and therefore on the inside of the subset
  221.         return projPt;
  222.     }

  223.     /** {@inheritDoc} */
  224.     @Override
  225.     public PlaneConvexSubset.Embedded getEmbedded() {
  226.         final EmbeddingPlane embeddingPlane = plane.getEmbedding();
  227.         final List<Vector2D> subspaceVertices = embeddingPlane.toSubspace(getVertices());
  228.         final ConvexArea area = ConvexArea.convexPolygonFromVertices(subspaceVertices,
  229.                 embeddingPlane.getPrecision());

  230.         return new EmbeddedAreaPlaneConvexSubset(embeddingPlane, area);
  231.     }

  232.     /** {@inheritDoc} */
  233.     @Override
  234.     public Split<PlaneConvexSubset> split(final Hyperplane<Vector3D> splitter) {
  235.         final Plane splitterPlane = (Plane) splitter;
  236.         final List<Vector3D> vertices = getVertices();

  237.         final int size = vertices.size();

  238.         int minusPlusTransitionIdx = -1;
  239.         Vector3D minusPlusInsertVertex = null;

  240.         int plusMinusTransitionIdx = -1;
  241.         Vector3D plusMinusInsertVertex = null;

  242.         int transitionCount = 0;

  243.         Vector3D curVertex;
  244.         HyperplaneLocation curLoc;

  245.         int lastSideIdx = -1;
  246.         Vector3D lastSideVertex = null;
  247.         HyperplaneLocation lastSideLoc = null;

  248.         int lastBoundaryIdx = -1;

  249.         for (int i = 0; i <= size || transitionCount == 1; ++i) {

  250.             curVertex = vertices.get(i % size);
  251.             curLoc = splitter.classify(curVertex);

  252.             if (lastSideLoc == HyperplaneLocation.MINUS && curLoc == HyperplaneLocation.PLUS) {
  253.                 // transitioned from minus side to plus side
  254.                 minusPlusTransitionIdx = Math.max(lastSideIdx, lastBoundaryIdx);
  255.                 ++transitionCount;

  256.                 if (lastBoundaryIdx < 0) {
  257.                     // no shared boundary point; compute a new vertex
  258.                     minusPlusInsertVertex = splitterPlane.intersection(
  259.                             Lines3D.fromPoints(lastSideVertex, curVertex, splitterPlane.getPrecision()));
  260.                 }
  261.             } else if (lastSideLoc == HyperplaneLocation.PLUS && curLoc == HyperplaneLocation.MINUS) {
  262.                 // transitioned from plus side to minus side
  263.                 plusMinusTransitionIdx = Math.max(lastSideIdx, lastBoundaryIdx);
  264.                 ++transitionCount;

  265.                 if (lastBoundaryIdx < 0) {
  266.                     // no shared boundary point; compute a new vertex
  267.                     plusMinusInsertVertex = splitterPlane.intersection(
  268.                             Lines3D.fromPoints(lastSideVertex, curVertex, splitterPlane.getPrecision()));
  269.                 }
  270.             }

  271.             if (curLoc == HyperplaneLocation.ON) {
  272.                 lastBoundaryIdx = i;
  273.             } else {
  274.                 lastBoundaryIdx = -1;

  275.                 lastSideIdx = i;
  276.                 lastSideVertex = curVertex;
  277.                 lastSideLoc = curLoc;
  278.             }
  279.         }

  280.         if (minusPlusTransitionIdx > -1 && plusMinusTransitionIdx > -1) {
  281.             // we've split; compute the vertex list for each side
  282.             final List<Vector3D> minusVertices =  buildPolygonSplitVertexList(
  283.                     plusMinusTransitionIdx, plusMinusInsertVertex,
  284.                     minusPlusTransitionIdx, minusPlusInsertVertex, vertices);
  285.             final List<Vector3D> plusVertices = buildPolygonSplitVertexList(
  286.                     minusPlusTransitionIdx, minusPlusInsertVertex,
  287.                     plusMinusTransitionIdx, plusMinusInsertVertex, vertices);

  288.             // delegate back to the Planes factory methods to determine the concrete types
  289.             // for each side of the split
  290.             return new Split<>(
  291.                     Planes.fromConvexPlanarVertices(plane, minusVertices),
  292.                     Planes.fromConvexPlanarVertices(plane, plusVertices));

  293.         } else if (lastSideLoc == HyperplaneLocation.PLUS) {
  294.             // we lie entirely on the plus side of the splitter
  295.             return new Split<>(null, this);
  296.         } else if (lastSideLoc == HyperplaneLocation.MINUS) {
  297.             // we lie entirely on the minus side of the splitter
  298.             return new Split<>(this, null);
  299.         }

  300.         // we lie entirely on the splitter
  301.         return new Split<>(null, null);
  302.     }

  303.     /** Internal method for building a vertex list for one side of a split result. The method is
  304.      * designed to make the fewest allocations possible.
  305.      * @param enterIdx the index of the vertex from {@code vertices} immediately before the polygon transitioned
  306.      *      to being fully entered into this side of the split result. If no point from {@code vertices} lay
  307.      *      directly on the splitting plane while entering this side and a new vertex had to be computed for the
  308.      *      split result, then this index will be the last vertex on the opposite side of the split. If a vertex
  309.      *      did lie directly on the splitting plane, then this index will point to that vertex.
  310.      * @param newEnterPt the newly-computed point to be added as the first vertex in the split result; may
  311.      *      be null if no such point exists
  312.      * @param exitIdx the index of the vertex from {@code vertices} immediately before the polygon transitioned
  313.      *      to being fully exited from this side of the split result. If no point from {@code vertices} lay
  314.      *      directly on the splitting plane while exiting this side and a new vertex had to be computed for the
  315.      *      split result, then this index will be the last vertex on the this side of the split. If a vertex did
  316.      *      lie directly on the splitting plane, then this index will point to that vertex.
  317.      * @param newExitPt the newly-computed point to be added as the last vertex in the split result; may
  318.      *      be null if no such point exists
  319.      * @param vertices the original list of vertices that this split result originated from; this list is
  320.      *      not modified by this operation
  321.      * @return the list of vertices for the split result
  322.      */
  323.     private List<Vector3D> buildPolygonSplitVertexList(final int enterIdx, final Vector3D newEnterPt,
  324.             final int exitIdx, final Vector3D newExitPt, final List<? extends Vector3D> vertices) {

  325.         final int size = vertices.size();

  326.         final boolean hasNewEnterPt = newEnterPt != null;
  327.         final boolean hasNewExitPt = newExitPt != null;

  328.         final int startIdx = (hasNewEnterPt ? enterIdx + 1 : enterIdx) % size;
  329.         final int endIdx = exitIdx % size;

  330.         final boolean hasWrappedIndices = endIdx < startIdx;

  331.         final int resultSize = (hasWrappedIndices ? endIdx + size : endIdx) - startIdx + 1;
  332.         final List<Vector3D> result = new ArrayList<>(resultSize);

  333.         if (hasNewEnterPt) {
  334.             result.add(newEnterPt);
  335.         }

  336.         if (hasWrappedIndices) {
  337.             result.addAll(vertices.subList(startIdx, size));
  338.             result.addAll(vertices.subList(0, endIdx + 1));
  339.         } else {
  340.             result.addAll(vertices.subList(startIdx, endIdx + 1));
  341.         }

  342.         if (hasNewExitPt) {
  343.             result.add(newExitPt);
  344.         }

  345.         return result;
  346.     }

  347.     /** {@inheritDoc} */
  348.     @Override
  349.     public String toString() {
  350.         final StringBuilder sb = new StringBuilder();
  351.         sb.append(getClass().getSimpleName())
  352.             .append("[normal= ")
  353.             .append(getPlane().getNormal())
  354.             .append(", vertices= ")
  355.             .append(getVertices())
  356.             .append(']');

  357.         return sb.toString();
  358.     }
  359. }