View Javadoc
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  
19  import java.text.MessageFormat;
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.Collection;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.function.BiFunction;
26  
27  import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
28  import org.apache.commons.geometry.core.partitioning.Split;
29  import org.apache.commons.geometry.core.partitioning.SplitLocation;
30  import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
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.twod.ConvexArea;
34  import org.apache.commons.geometry.euclidean.twod.Line;
35  import org.apache.commons.geometry.euclidean.twod.LineConvexSubset;
36  import org.apache.commons.geometry.euclidean.twod.Lines;
37  import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
38  import org.apache.commons.geometry.euclidean.twod.Vector2D;
39  import org.apache.commons.geometry.euclidean.twod.path.LinePath;
40  
41  /** Class containing factory methods for constructing {@link Plane} and {@link PlaneSubset} instances.
42   */
43  public final class Planes {
44  
45      /** Utility class; no instantiation. */
46      private Planes() {
47      }
48  
49      /** Build a plane from a point and two (on plane) vectors.
50       * @param p the provided point (on plane)
51       * @param u u vector (on plane)
52       * @param v v vector (on plane)
53       * @param precision precision context used to compare floating point values
54       * @return a new plane
55       * @throws IllegalArgumentException if the norm of the given values is zero, NaN, or infinite.
56       */
57      public static EmbeddingPlane fromPointAndPlaneVectors(final Vector3D p, final Vector3D u, final Vector3D v,
58              final DoublePrecisionContext precision) {
59          final Vector3D.Unit uNorm = u.normalize();
60          final Vector3D.Unit vNorm = uNorm.orthogonal(v);
61          final Vector3D.Unit wNorm = uNorm.cross(vNorm).normalize();
62          final double originOffset = -p.dot(wNorm);
63  
64          return new EmbeddingPlane(uNorm, vNorm, wNorm, originOffset, precision);
65      }
66  
67      /** Build a plane from a normal.
68       * Chooses origin as point on plane.
69       * @param normal normal direction to the plane
70       * @param precision precision context used to compare floating point values
71       * @return a new plane
72       * @throws IllegalArgumentException if the norm of the given values is zero, NaN, or infinite.
73       */
74      public static Plane fromNormal(final Vector3D normal, final DoublePrecisionContext precision) {
75          return fromPointAndNormal(Vector3D.ZERO, normal, precision);
76      }
77  
78      /** Build a plane from a point and a normal.
79       *
80       * @param p point belonging to the plane
81       * @param normal normal direction to the plane
82       * @param precision precision context used to compare floating point values
83       * @return a new plane
84       * @throws IllegalArgumentException if the norm of the given values is zero, NaN, or infinite.
85       */
86      public static Plane fromPointAndNormal(final Vector3D p, final Vector3D normal,
87              final DoublePrecisionContext precision) {
88          final Vector3D.Unit unitNormal = normal.normalize();
89          final double originOffset = -p.dot(unitNormal);
90  
91          return new Plane(unitNormal, originOffset, precision);
92      }
93  
94      /** Build a plane from three points.
95       * <p>
96       * The plane is oriented in the direction of {@code (p2-p1) ^ (p3-p1)}
97       * </p>
98       *
99       * @param p1 first point belonging to the plane
100      * @param p2 second point belonging to the plane
101      * @param p3 third point belonging to the plane
102      * @param precision precision context used to compare floating point values
103      * @return a new plane
104      * @throws IllegalArgumentException if the points do not define a unique plane
105      */
106     public static Plane fromPoints(final Vector3D p1, final Vector3D p2, final Vector3D p3,
107             final DoublePrecisionContext precision) {
108         return fromPoints(Arrays.asList(p1, p2, p3), precision);
109     }
110 
111     /** Construct a plane from a collection of points lying on the plane. The plane orientation is
112      * determined by the overall orientation of the point sequence. For example, if the points wind
113      * around the z-axis in a counter-clockwise direction, then the plane normal will point up the
114      * +z axis. If the points wind in the opposite direction, then the plane normal will point down
115      * the -z axis. The {@code u} vector for the plane is set to the first non-zero vector between
116      * points in the sequence (ie, the first direction in the path).
117      *
118      * @param pts collection of sequenced points lying on the plane
119      * @param precision precision context used to compare floating point values
120      * @return a new plane containing the given points
121      * @throws IllegalArgumentException if the given collection does not contain at least 3 points or the
122      *      points do not define a unique plane
123      */
124     public static Plane fromPoints(final Collection<Vector3D> pts, final DoublePrecisionContext precision) {
125         return new PlaneBuilder(pts, precision).build();
126     }
127 
128     /** Create a new plane subset from a plane and an embedded convex subspace area.
129      * @param plane embedding plane for the area
130      * @param area area embedded in the plane
131      * @return a new convex sub plane instance
132      */
133     public static PlaneConvexSubset subsetFromConvexArea(final EmbeddingPlane plane, final ConvexArea area) {
134         if (area.isFinite()) {
135             // prefer a vertex-based representation for finite areas
136             final List<Vector3D> vertices = plane.toSpace(area.getVertices());
137             return fromConvexPlanarVertices(plane, vertices);
138         }
139 
140         return new EmbeddedAreaPlaneConvexSubset(plane, area);
141     }
142 
143     /** Create a new convex polygon from the given sequence of vertices. The vertices must define a unique
144      * plane, meaning that at least 3 unique vertices must be given. The given sequence is assumed to be closed,
145      * ie that an edge exists between the last vertex and the first.
146      * @param pts collection of points defining the convex polygon
147      * @param precision precision context used to compare floating point values
148      * @return a new convex polygon defined by the given sequence of vertices
149      * @throws IllegalArgumentException if fewer than 3 vertices are given or the vertices do not define a
150      *       unique plane
151      * @see #fromPoints(Collection, DoublePrecisionContext)
152      */
153     public static ConvexPolygon3D convexPolygonFromVertices(final Collection<Vector3D> pts,
154             final DoublePrecisionContext precision) {
155         final List<Vector3D> vertices = new ArrayList<>(pts.size());
156         final Plane plane = new PlaneBuilder(pts, precision).buildForConvexPolygon(vertices);
157 
158         // make sure that the first point is not repeated at the end
159         final Vector3D firstPt = vertices.get(0);
160         final Vector3D lastPt = vertices.get(vertices.size() - 1);
161         if (firstPt.eq(lastPt, precision)) {
162             vertices.remove(vertices.size() - 1);
163         }
164 
165         if (vertices.size() == 3) {
166             return new SimpleTriangle3D(plane, vertices.get(0), vertices.get(1), vertices.get(2));
167         }
168         return new VertexListConvexPolygon3D(plane, vertices);
169     }
170 
171     /** Construct a triangle from three vertices. The triangle plane is oriented such that the points
172      * are arranged in a counter-clockwise order when looking down the plane normal.
173      * @param p1 first vertex
174      * @param p2 second vertex
175      * @param p3 third vertex
176      * @param precision precision context used for floating point comparisons
177      * @return a triangle constructed from the three vertices
178      * @throws IllegalArgumentException if the points do not define a unique plane
179      */
180     public static Triangle3D triangleFromVertices(final Vector3D p1, final Vector3D p2, final Vector3D p3,
181             final DoublePrecisionContext precision) {
182         final Plane plane = fromPoints(p1, p2, p3, precision);
183         return new SimpleTriangle3D(plane, p1, p2, p3);
184     }
185 
186     /** Construct a list of {@link Triangle3D} instances from a set of vertices and arrays of face indices.
187      * For example, the following code constructs a list of triangles forming a square pyramid.
188      * <pre>
189      * DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-10);
190      *
191      * Vector3D[] vertices = {
192      *      Vector3D.ZERO,
193      *      Vector3D.of(1, 0, 0),
194      *      Vector3D.of(1, 1, 0),
195      *      Vector3D.of(0, 1, 0),
196      *      Vector3D.of(0.5, 0.5, 4)
197      * };
198      *
199      * int[][] faceIndices = {
200      *      {0, 2, 1},
201      *      {0, 3, 2},
202      *      {0, 1, 4},
203      *      {1, 2, 4},
204      *      {2, 3, 4},
205      *      {3, 0, 4}
206      * };
207      *
208      * List&lt;Triangle3D&gt; triangles = Planes.indexedTriangles(vertices, faceIndices, TEST_PRECISION);
209      * </pre>
210      * @param vertices vertices available for use in triangle construction
211      * @param faceIndices array of indices for each triangular face; each entry in the array is an array of
212      *      3 index values into {@code vertices}, defining the 3 vertices that will be used to construct the
213      *      triangle
214      * @param precision precision context used for floating point comparisons
215      * @return a list of triangles constructed from the set of vertices and face indices
216      * @throws IllegalArgumentException if any face index array does not contain exactly 3 elements or a set
217      *      of 3 vertices do not define a plane
218      * @throws IndexOutOfBoundsException if any index into {@code vertices} is out of bounds
219      */
220     public static List<Triangle3D> indexedTriangles(final Vector3D[] vertices, final int[][] faceIndices,
221             final DoublePrecisionContext precision) {
222         return indexedTriangles(Arrays.asList(vertices), faceIndices, precision);
223     }
224 
225     /** Construct a list of {@link Triangle3D} instances from a set of vertices and arrays of face indices.
226      * @param vertices vertices available for use in triangle construction
227      * @param faceIndices array of indices for each triangular face; each entry in the array is an array of
228      *      3 index values into {@code vertices}, defining the 3 vertices that will be used to construct the
229      *      triangle
230      * @param precision precision context used for floating point comparisons
231      * @return a list of triangles constructed from the set of vertices and face indices
232      * @throws IllegalArgumentException if any face index array does not contain exactly 3 elements or a set
233      *      of 3 vertices do not define a plane
234      * @throws IndexOutOfBoundsException if any index into {@code vertices} is out of bounds
235      * @see #indexedTriangles(Vector3D[], int[][], DoublePrecisionContext)
236      */
237     public static List<Triangle3D> indexedTriangles(final List<Vector3D> vertices, final int[][] faceIndices,
238             final DoublePrecisionContext precision) {
239 
240         final int numFaces = faceIndices.length;
241         final List<Triangle3D> triangles = new ArrayList<>(numFaces);
242 
243         int[] face;
244         for (int i = 0; i < numFaces; ++i) {
245             face = faceIndices[i];
246             if (face.length != 3) {
247                 throw new IllegalArgumentException(MessageFormat.format(
248                         "Invalid number of vertex indices for face at index {0}: expected 3 but found {1}",
249                         i, face.length));
250             }
251 
252             triangles.add(triangleFromVertices(
253                         vertices.get(face[0]),
254                         vertices.get(face[1]),
255                         vertices.get(face[2]),
256                         precision
257                     ));
258         }
259 
260         return triangles;
261     }
262 
263     /** Construct a list of {@link ConvexPolygon3D} instances from a set of vertices and arrays of face indices. Each
264      * face must contain at least 3 vertices but the number of vertices per face does not need to be constant.
265      * For example, the following code constructs a list of convex polygons forming a square pyramid.
266      * Note that the first face (the pyramid base) uses a different number of vertices than the other faces.
267      * <pre>
268      * DoublePrecisionContext precision = new EpsilonDoublePrecisionContext(1e-10);
269      *
270      * Vector3D[] vertices = {
271      *      Vector3D.ZERO,
272      *      Vector3D.of(1, 0, 0),
273      *      Vector3D.of(1, 1, 0),
274      *      Vector3D.of(0, 1, 0),
275      *      Vector3D.of(0.5, 0.5, 4)
276      * };
277      *
278      * int[][] faceIndices = {
279      *      {0, 3, 2, 1}, // square base
280      *      {0, 1, 4},
281      *      {1, 2, 4},
282      *      {2, 3, 4},
283      *      {3, 0, 4}
284      * };
285      *
286      * List&lt;ConvexPolygon3D&gt; polygons = Planes.indexedConvexPolygons(vertices, faceIndices, precision);
287      * </pre>
288      * @param vertices vertices available for use in convex polygon construction
289      * @param faceIndices array of indices for each triangular face; each entry in the array is an array of
290      *      at least 3 index values into {@code vertices}, defining the vertices that will be used to construct the
291      *      convex polygon
292      * @param precision precision context used for floating point comparisons
293      * @return a list of convex polygons constructed from the set of vertices and face indices
294      * @throws IllegalArgumentException if any face index array does not contain at least 3 elements or a set
295      *      of vertices do not define a planar convex polygon
296      * @throws IndexOutOfBoundsException if any index into {@code vertices} is out of bounds
297      */
298     public static List<ConvexPolygon3D> indexedConvexPolygons(final Vector3D[] vertices, final int[][] faceIndices,
299             final DoublePrecisionContext precision) {
300         return indexedConvexPolygons(Arrays.asList(vertices), faceIndices, precision);
301     }
302 
303     /** Construct a list of {@link ConvexPolygon3D} instances from a set of vertices and arrays of face indices. Each
304      * face must contain at least 3 vertices but the number of vertices per face does not need to be constant.
305      * @param vertices vertices available for use in convex polygon construction
306      * @param faceIndices array of indices for each triangular face; each entry in the array is an array of
307      *      at least 3 index values into {@code vertices}, defining the vertices that will be used to construct the
308      *      convex polygon
309      * @param precision precision context used for floating point comparisons
310      * @return a list of convex polygons constructed from the set of vertices and face indices
311      * @throws IllegalArgumentException if any face index array does not contain at least 3 elements or a set
312      *      of vertices do not define a planar convex polygon
313      * @throws IndexOutOfBoundsException if any index into {@code vertices} is out of bounds
314      * @see #indexedConvexPolygons(Vector3D[], int[][], DoublePrecisionContext)
315      */
316     public static List<ConvexPolygon3D> indexedConvexPolygons(final List<Vector3D> vertices, final int[][] faceIndices,
317             final DoublePrecisionContext precision) {
318         final int numFaces = faceIndices.length;
319         final List<ConvexPolygon3D> polygons = new ArrayList<>(numFaces);
320         final List<Vector3D> faceVertices = new ArrayList<>();
321 
322         int[] face;
323         for (int i = 0; i < numFaces; ++i) {
324             face = faceIndices[i];
325             if (face.length < 3) {
326                 throw new IllegalArgumentException(MessageFormat.format(
327                         "Invalid number of vertex indices for face at index {0}: required at least 3 but found {1}",
328                         i, face.length));
329             }
330 
331             for (final int vertexIndex : face) {
332                 faceVertices.add(vertices.get(vertexIndex));
333             }
334 
335             polygons.add(convexPolygonFromVertices(
336                         faceVertices,
337                         precision
338                     ));
339 
340             faceVertices.clear();
341         }
342 
343         return polygons;
344     }
345 
346     /** Get the boundaries of a 3D region created by extruding a polygon defined by a list of vertices. The ends
347      * ("top" and "bottom") of the extruded 3D region are flat while the sides follow the boundaries of the original
348      * 2D region.
349      * @param vertices vertices forming the 2D polygon to extrude
350      * @param plane plane to extrude the 2D polygon from
351      * @param extrusionVector vector to extrude the polygon vertices through
352      * @param precision precision context used to construct the 3D region boundaries
353      * @return the boundaries of the extruded 3D region
354      * @throws IllegalStateException if {@code vertices} contains only a single unique vertex
355      * @throws IllegalArgumentException if regions of non-zero size cannot be produced with the
356      *      given plane and extrusion vector. This occurs when the extrusion vector has zero length
357      *      or is orthogonal to the plane normal
358      * @see LinePath#fromVertexLoop(Collection, DoublePrecisionContext)
359      * @see #extrude(LinePath, EmbeddingPlane, Vector3D, DoublePrecisionContext)
360      */
361     public static List<PlaneConvexSubset> extrudeVertexLoop(final List<Vector2D> vertices,
362             final EmbeddingPlane plane, final Vector3D extrusionVector, final DoublePrecisionContext precision) {
363         final LinePath path = LinePath.fromVertexLoop(vertices, precision);
364         return extrude(path, plane, extrusionVector, precision);
365     }
366 
367     /** Get the boundaries of the 3D region created by extruding a 2D line path. The ends ("top" and "bottom") of
368      * the extruded 3D region are flat while the sides follow the boundaries of the original 2D region. The path is
369      * converted to a BSP tree before extrusion.
370      * @param path path to extrude
371      * @param plane plane to extrude the path from
372      * @param extrusionVector vector to extrude the polygon points through
373      * @param precision precision precision context used to construct the 3D region boundaries
374      * @return the boundaries of the extruded 3D region
375      * @throws IllegalArgumentException if regions of non-zero size cannot be produced with the
376      *      given plane and extrusion vector. This occurs when the extrusion vector has zero length
377      *      or is orthogonal to the plane normal
378      * @see #extrude(RegionBSPTree2D, EmbeddingPlane, Vector3D, DoublePrecisionContext)
379      */
380     public static List<PlaneConvexSubset> extrude(final LinePath path, final EmbeddingPlane plane,
381             final Vector3D extrusionVector, final DoublePrecisionContext precision) {
382         return extrude(path.toTree(), plane, extrusionVector, precision);
383     }
384 
385     /** Get the boundaries of the 3D region created by extruding a 2D region. The ends ("top" and "bottom") of
386      * the extruded 3D region are flat while the sides follow the boundaries of the original 2D region.
387      * @param region region to extrude
388      * @param plane plane to extrude the region from
389      * @param extrusionVector vector to extrude the region points through
390      * @param precision precision precision context used to construct the 3D region boundaries
391      * @return the boundaries of the extruded 3D region
392      * @throws IllegalArgumentException if regions of non-zero size cannot be produced with the
393      *      given plane and extrusion vector. This occurs when the extrusion vector has zero length
394      *      or is orthogonal to the plane normal
395      */
396     public static List<PlaneConvexSubset> extrude(final RegionBSPTree2D region, final EmbeddingPlane plane,
397             final Vector3D extrusionVector, final DoublePrecisionContext precision) {
398         return new PlaneRegionExtruder(plane, extrusionVector, precision).extrude(region);
399     }
400 
401     /** Get the unique intersection of the plane subset with the given line. Null is
402      * returned if no unique intersection point exists (ie, the line and plane are
403      * parallel or coincident) or the line does not intersect the plane subset.
404      * @param planeSubset plane subset to intersect with
405      * @param line line to intersect with this plane subset
406      * @return the unique intersection point between the line and this plane subset
407      *      or null if no such point exists.
408      */
409     static Vector3D intersection(final PlaneSubset planeSubset, final Line3D line) {
410         final Vector3D pt = planeSubset.getPlane().intersection(line);
411         return (pt != null && planeSubset.contains(pt)) ? pt : null;
412     }
413 
414     /** Get the unique intersection of the plane subset with the given line subset. Null
415      * is returned if the underlying line and plane do not have a unique intersection
416      * point (ie, they are parallel or coincident) or the intersection point is unique
417      * but is not contained in both the line subset and plane subset.
418      * @param planeSubset plane subset to intersect with
419      * @param lineSubset line subset to intersect with
420      * @return the unique intersection point between this plane subset and the argument or
421      *      null if no such point exists.
422      */
423     static Vector3D intersection(final PlaneSubset planeSubset, final LineConvexSubset3D lineSubset) {
424         final Vector3D pt = intersection(planeSubset, lineSubset.getLine());
425         return (pt != null && lineSubset.contains(pt)) ? pt : null;
426     }
427 
428     /** Validate that the actual plane contains the same points as the expected plane, throwing an exception if not.
429      * The subspace orientations of embedding planes are not considered.
430      * @param expected the expected plane
431      * @param actual the actual plane
432      * @throws IllegalArgumentException if the actual plane is not equivalent to the expected plane
433      */
434     static void validatePlanesEquivalent(final Plane expected, final Plane actual) {
435         if (!expected.eq(actual, expected.getPrecision())) {
436             throw new IllegalArgumentException("Arguments do not represent the same plane. Expected " +
437                     expected + " but was " + actual + ".");
438         }
439     }
440 
441     /** Generic split method that uses performs the split using the subspace region of the plane subset.
442      * @param splitter splitting hyperplane
443      * @param subset the plane subset being split
444      * @param factory function used to create new plane subset instances
445      * @param <T> Plane subset implementation type
446      * @return the result of the split operation
447      */
448     static <T extends PlaneSubset> Split<T> subspaceSplit(final Plane splitter, final T subset,
449             final BiFunction<EmbeddingPlane, HyperplaneBoundedRegion<Vector2D>, T> factory) {
450 
451         final EmbeddingPlane thisPlane = subset.getPlane().getEmbedding();
452 
453         final Line3D intersection = thisPlane.intersection(splitter);
454         if (intersection == null) {
455             return getNonIntersectingSplitResult(splitter, subset);
456         } else {
457             final EmbeddingPlane embeddingPlane = subset.getPlane().getEmbedding();
458 
459             // the lines intersect; split the subregion
460             final Vector3D intersectionOrigin = intersection.getOrigin();
461             final Vector2D subspaceP1 = embeddingPlane.toSubspace(intersectionOrigin);
462             final Vector2D subspaceP2 = embeddingPlane.toSubspace(intersectionOrigin.add(intersection.getDirection()));
463 
464             final Line subspaceSplitter = Lines.fromPoints(subspaceP1, subspaceP2, thisPlane.getPrecision());
465 
466             final Split<? extends HyperplaneBoundedRegion<Vector2D>> split =
467                     subset.getEmbedded().getSubspaceRegion().split(subspaceSplitter);
468             final SplitLocation subspaceSplitLoc = split.getLocation();
469 
470             if (SplitLocation.MINUS == subspaceSplitLoc) {
471                 return new Split<>(subset, null);
472             } else if (SplitLocation.PLUS == subspaceSplitLoc) {
473                 return new Split<>(null, subset);
474             }
475 
476             final T minus = (split.getMinus() != null) ? factory.apply(thisPlane, split.getMinus()) : null;
477             final T plus = (split.getPlus() != null) ? factory.apply(thisPlane, split.getPlus()) : null;
478 
479             return new Split<>(minus, plus);
480         }
481     }
482 
483     /** Get a split result for cases where the splitting plane and the plane containing the subset being split
484      * do not intersect. Callers are responsible for ensuring that the planes involved do not actually intersect.
485      * @param <T> Plane subset implementation type
486      * @param splitter plane performing the splitting
487      * @param subset subset being split
488      * @return the split result for the non-intersecting split
489      */
490     private static <T extends PlaneSubset> Split<T> getNonIntersectingSplitResult(
491             final Plane splitter, final T subset) {
492         final Plane plane = subset.getPlane();
493 
494         final double offset = splitter.offset(plane);
495         final int comp = plane.getPrecision().compare(offset, 0.0);
496 
497         if (comp < 0) {
498             return new Split<>(subset, null);
499         } else if (comp > 0) {
500             return new Split<>(null, subset);
501         } else {
502             return new Split<>(null, null);
503         }
504     }
505 
506     /** Construct a convex polygon 3D from a plane and a list of vertices lying in the plane. Callers are
507      * responsible for ensuring that the vertices lie in the plane and define a convex polygon.
508      * @param plane the plane containing the convex polygon
509      * @param vertices vertices defining the closed, convex polygon. The must must contain at least 3 unique
510      *      vertices and should not include the start vertex at the end of the list.
511      * @return a new convex polygon instance
512      * @throws IllegalArgumentException if the size of {@code vertices} if less than 3
513      */
514     static ConvexPolygon3D fromConvexPlanarVertices(final Plane plane, final List<Vector3D> vertices) {
515         final int size = vertices.size();
516 
517         if (size == 3) {
518             return new SimpleTriangle3D(plane, vertices.get(0), vertices.get(1), vertices.get(2));
519         }
520 
521         return new VertexListConvexPolygon3D(plane, vertices);
522     }
523 
524     /** Convert a convex polygon defined by a plane and list of points into a triangle fan.
525      * @param plane plane containing the convex polygon
526      * @param vertices vertices defining the convex polygon
527      * @return a triangle fan representing the same area as the convex polygon
528      * @throws IllegalArgumentException if fewer than 3 vertices are given
529      */
530     static List<Triangle3D> convexPolygonToTriangleFan(final Plane plane, final List<Vector3D> vertices) {
531         final int size = vertices.size();
532         if (size < 3) {
533             throw new IllegalArgumentException("Cannot create triangle fan: 3 or more vertices are required " +
534                     "but found only " + vertices.size());
535         }
536 
537         final List<Triangle3D> triangles = new ArrayList<>(size - 2);
538 
539         final int fanIdx = findBestTriangleFanIndex(vertices);
540         int vertexIdx = (fanIdx + 1) % size;
541 
542         final Vector3D fanBase = vertices.get(fanIdx);
543         Vector3D vertexA = vertices.get(vertexIdx);
544         Vector3D vertexB;
545 
546         vertexIdx = (vertexIdx + 1) % size;
547         while (vertexIdx != fanIdx) {
548             vertexB = vertices.get(vertexIdx);
549 
550             // add directly as a triangle instance to avoid computation of the plane again
551             triangles.add(new SimpleTriangle3D(plane, fanBase, vertexA, vertexB));
552 
553             vertexA = vertexB;
554             vertexIdx = (vertexIdx + 1) % size;
555         }
556 
557         return triangles;
558     }
559 
560     /** Find the index of the best vertex to use as the base for a triangle fan split of the convex polygon
561      * defined by the given vertices. The best vertex is the one that forms the largest interior angle in the
562      * polygon since a split at that point will help prevent the creation of very thin triangles.
563      * @param vertices vertices defining the convex polygon; must not be empty
564      * @return the index of the best vertex to use as the base for a triangle fan split of the convex polygon
565      */
566     private static int findBestTriangleFanIndex(final List<Vector3D> vertices) {
567         final Iterator<Vector3D> it = vertices.iterator();
568 
569         Vector3D curPt = it.next();
570         Vector3D nextPt;
571 
572         final Vector3D lastVec = vertices.get(vertices.size() - 1).directionTo(curPt);
573         Vector3D incomingVec = lastVec;
574         Vector3D outgoingVec;
575 
576         int bestIdx = 0;
577         double bestDot = -1.0;
578 
579         int idx = 0;
580         double dot;
581         while (it.hasNext()) {
582             nextPt = it.next();
583             outgoingVec = curPt.directionTo(nextPt);
584 
585             dot = incomingVec.dot(outgoingVec);
586             if (dot > bestDot) {
587                 bestIdx = idx;
588                 bestDot = dot;
589             }
590 
591             curPt = nextPt;
592             incomingVec = outgoingVec;
593 
594             ++idx;
595         }
596 
597         // handle the last vertex on its own
598         dot = incomingVec.dot(lastVec);
599         if (dot > bestDot) {
600             bestIdx = idx;
601         }
602 
603         return bestIdx;
604     }
605 
606     /** Internal helper class used to construct planes from sequences of points. Instances can be also be
607      * configured to collect lists of unique points found during plane construction and validate that the
608      * defined region is convex.
609      */
610     private static final class PlaneBuilder {
611 
612         /** The point sequence to build a plane for. */
613         private final Collection<Vector3D> pts;
614 
615         /** Precision context used for floating point comparisons. */
616         private final DoublePrecisionContext precision;
617 
618         /** The start point from the point sequence. */
619         private Vector3D startPt;
620 
621         /** The previous point from the point sequence. */
622         private Vector3D prevPt;
623 
624         /** The previous vector from the point sequence, preceding from the {@code startPt} to {@code prevPt}. */
625         private Vector3D prevVector;
626 
627         /** The computed {@code normal} vector for the plane. */
628         private Vector3D.Unit normal;
629 
630         /** The x component of the sum of all cross products from adjacent vectors in the point sequence. */
631         private double crossSumX;
632 
633         /** The y component of the sum of all cross products from adjacent vectors in the point sequence. */
634         private double crossSumY;
635 
636         /** The z component of the sum of all cross products from adjacent vectors in the point sequence. */
637         private double crossSumZ;
638 
639         /** If true, an exception will be thrown if the point sequence is discovered to be non-convex. */
640         private boolean requireConvex = false;
641 
642         /** List that unique vertices discovered in the input sequence will be added to. */
643         private List<Vector3D> uniqueVertexOutput;
644 
645         /** Construct a new build instance for the given point sequence and precision context.
646          * @param pts point sequence
647          * @param precision precision context used to perform floating point comparisons
648          */
649         PlaneBuilder(final Collection<Vector3D> pts, final DoublePrecisionContext precision) {
650             this.pts = pts;
651             this.precision = precision;
652         }
653 
654         /** Build a plane from the configured point sequence.
655          * @return a plane built from the configured point sequence
656          * @throws IllegalArgumentException if the points do not define a plane
657          */
658         Plane build() {
659             if (pts.size() < 3) {
660                 throw nonPlanar();
661             }
662 
663             pts.forEach(this::processPoint);
664 
665             return createPlane();
666         }
667 
668         /** Build a plane from the configured point sequence, validating that the points form a convex region
669          * and adding all discovered unique points to the given list.
670          * @param vertexOutput list that unique points discovered in the point sequence will be added to
671          * @return a plane created from the configured point sequence
672          * @throws IllegalArgumentException if the points do not define a plane or the {@code requireConvex}
673          *      flag is true and the points do not define a convex area
674          */
675         Plane buildForConvexPolygon(final List<Vector3D> vertexOutput) {
676             this.requireConvex = true;
677             this.uniqueVertexOutput = vertexOutput;
678 
679             return build();
680         }
681 
682         /** Process a point from the point sequence.
683          * @param pt
684          * @throws IllegalArgumentException if the points do not define a plane or the {@code requireConvex}
685          *      flag is true and the points do not define a convex area
686          */
687         private void processPoint(final Vector3D pt) {
688             if (prevPt == null) {
689                 startPt = pt;
690                 prevPt = pt;
691 
692                 if (uniqueVertexOutput != null) {
693                     uniqueVertexOutput.add(pt);
694                 }
695 
696             } else if (!prevPt.eq(pt, precision)) { // skip duplicate points
697                 final Vector3D vec = startPt.vectorTo(pt);
698 
699                 if (prevVector != null) {
700                     processCrossProduct(prevVector.cross(vec));
701                 }
702 
703                 if (uniqueVertexOutput != null) {
704                     uniqueVertexOutput.add(pt);
705                 }
706 
707                 prevPt = pt;
708                 prevVector = vec;
709             }
710         }
711 
712         /** Process the computed cross product of two vectors from the input point sequence. The vectors
713          * start at the first point in the sequence and point to adjacent points later in the sequence.
714          * @param cross the cross product of two vectors from the input point sequence
715          * @throws IllegalArgumentException if the points do not define a plane or the {@code requireConvex}
716          *      flag is true and the points do not define a convex area
717          */
718         private void processCrossProduct(final Vector3D cross) {
719             crossSumX += cross.getX();
720             crossSumY += cross.getY();
721             crossSumZ += cross.getZ();
722 
723             final double crossNorm = cross.norm();
724 
725             if (!precision.eqZero(crossNorm)) {
726                 // the cross product has non-zero magnitude
727                 if (normal == null) {
728                     // save the first non-zero cross product as our normal
729                     normal = cross.normalize();
730                 } else {
731                     final double crossDot = normal.dot(cross) / crossNorm;
732 
733                     // check non-planar before non-convex since the former is a more general type
734                     // of issue
735                     if (!precision.eq(1.0, Math.abs(crossDot))) {
736                         throw nonPlanar();
737                     } else if (requireConvex && crossDot < 0) {
738                         throw nonConvex();
739                     }
740                 }
741             }
742         }
743 
744         /** Construct the plane instance using the value gathered during point processing.
745          * @return the created plane instance
746          * @throws IllegalArgumentException if the point do not define a plane
747          */
748         private Plane createPlane() {
749             if (normal == null) {
750                 throw nonPlanar();
751             }
752 
753             // flip the normal if needed to match the overall orientation of the points
754             if (normal.dot(Vector3D.of(crossSumX, crossSumY, crossSumZ)) < 0) {
755                 normal = normal.negate();
756             }
757 
758             // construct the plane
759             final double originOffset = -startPt.dot(normal);
760 
761             return new Plane(normal, originOffset, precision);
762         }
763 
764         /** Return an exception with a message stating that the points given to this builder do not
765          * define a plane.
766          * @return an exception stating that the points do not define a plane
767          */
768         private IllegalArgumentException nonPlanar() {
769             return new IllegalArgumentException("Points do not define a plane: " + pts);
770         }
771 
772         /** Return an exception with a message stating that the points given to this builder do not
773          * define a convex region.
774          * @return an exception stating that the points do not define a plane
775          */
776         private IllegalArgumentException nonConvex() {
777             return new IllegalArgumentException("Points do not define a convex region: " + pts);
778         }
779     }
780 
781     /** Class designed to create 3D regions by taking a 2D region and extruding from a base plane
782      * through an extrusion vector. The ends ("top" and "bottom") of the extruded 3D region are flat
783      * while the sides follow the boundaries of the original 2D region.
784      */
785     private static final class PlaneRegionExtruder {
786         /** Base plane to extrude from. */
787         private final EmbeddingPlane basePlane;
788 
789         /** Vector to extrude along; the extruded plane is translated from the base plane by this amount. */
790         private final Vector3D extrusionVector;
791 
792         /** True if the extrusion vector points to the plus side of the base plane. */
793         private final boolean extrudingOnPlusSide;
794 
795         /** Precision context used to create boundaries. */
796         private final DoublePrecisionContext precision;
797 
798         /** Construct a new instance that performs extrusions from {@code basePlane} along {@code extrusionVector}.
799          * @param basePlane base plane to extrude from
800          * @param extrusionVector vector to extrude along
801          * @param precision precision context used to construct boundaries
802          * @throws IllegalArgumentException if the given extrusion vector and plane produce regions
803          *      of zero size
804          */
805         PlaneRegionExtruder(final EmbeddingPlane basePlane, final Vector3D extrusionVector,
806                 final DoublePrecisionContext precision) {
807 
808             this.basePlane = basePlane;
809 
810             // Extruded plane; this forms the end of the 3D region opposite the base plane.
811             EmbeddingPlane extrudedPlane = basePlane.translate(extrusionVector);
812 
813             if (basePlane.contains(extrudedPlane)) {
814                 throw new IllegalArgumentException(
815                         "Extrusion vector produces regions of zero size: extrusionVector= " +
816                                 extrusionVector + ",  plane= " + basePlane);
817             }
818 
819             this.extrusionVector = extrusionVector;
820             this.extrudingOnPlusSide = basePlane.getNormal().dot(extrusionVector) > 0;
821 
822             this.precision = precision;
823         }
824 
825         /** Extrude the given 2D BSP tree using the configured base plane and extrusion vector.
826          * @param subspaceRegion region to extrude
827          * @return the boundaries of the extruded region
828          */
829         public List<PlaneConvexSubset> extrude(final RegionBSPTree2D subspaceRegion) {
830             final List<PlaneConvexSubset> extrudedBoundaries = new ArrayList<>();
831 
832             // add the boundaries
833             addEnds(subspaceRegion, extrudedBoundaries);
834             addSides(subspaceRegion, extrudedBoundaries);
835 
836             return extrudedBoundaries;
837         }
838 
839         /** Add the end ("top" and "bottom") of the extruded subspace region to the result list.
840          * @param subspaceRegion subspace region being extruded.
841          * @param result list to add the boundary results to
842          */
843         private void addEnds(final RegionBSPTree2D subspaceRegion, final List<PlaneConvexSubset> result) {
844             // add the base boundaries
845             final List<ConvexArea> baseAreas = subspaceRegion.toConvex();
846 
847             final List<PlaneConvexSubset> baseList = new ArrayList<>(baseAreas.size());
848             final List<PlaneConvexSubset> extrudedList = new ArrayList<>(baseAreas.size());
849 
850             final AffineTransformMatrix3D extrudeTransform = AffineTransformMatrix3D.createTranslation(extrusionVector);
851 
852             PlaneConvexSubset base;
853             for (final ConvexArea area : baseAreas) {
854                 base = subsetFromConvexArea(basePlane, area);
855                 if (extrudingOnPlusSide) {
856                     base = base.reverse();
857                 }
858 
859                 baseList.add(base);
860                 extrudedList.add(base.transform(extrudeTransform).reverse());
861             }
862 
863             result.addAll(baseList);
864             result.addAll(extrudedList);
865         }
866 
867         /** Add the side boundaries of the extruded region to the result list.
868          * @param subspaceRegion subspace region being extruded.
869          * @param result list to add the boundary results to
870          */
871         private void addSides(final RegionBSPTree2D subspaceRegion, final List<PlaneConvexSubset> result) {
872             Vector2D subStartPt;
873             Vector2D subEndPt;
874 
875             PlaneConvexSubset boundary;
876             for (final LinePath path : subspaceRegion.getBoundaryPaths()) {
877                 for (final LineConvexSubset lineSubset : path.getElements()) {
878                     subStartPt = lineSubset.getStartPoint();
879                     subEndPt = lineSubset.getEndPoint();
880 
881                     boundary = (subStartPt != null && subEndPt != null) ?
882                             extrudeSideFinite(basePlane.toSpace(subStartPt), basePlane.toSpace(subEndPt)) :
883                             extrudeSideInfinite(lineSubset);
884 
885                     result.add(boundary);
886                 }
887             }
888         }
889 
890         /** Extrude a single, finite boundary forming one of the sides of the extruded region.
891          * @param startPt start point of the boundary
892          * @param endPt end point of the boundary
893          * @return the extruded region side boundary
894          */
895         private ConvexPolygon3D extrudeSideFinite(final Vector3D startPt, final Vector3D endPt) {
896             final Vector3D extrudedStartPt = startPt.add(extrusionVector);
897             final Vector3D extrudedEndPt = endPt.add(extrusionVector);
898 
899             final List<Vector3D> vertices = extrudingOnPlusSide ?
900                     Arrays.asList(startPt, endPt, extrudedEndPt, extrudedStartPt) :
901                     Arrays.asList(startPt, extrudedStartPt, extrudedEndPt, endPt);
902 
903             return convexPolygonFromVertices(vertices, precision);
904         }
905 
906         /** Extrude a single, infinite boundary forming one of the sides of the extruded region.
907          * @param lineSubset line subset to extrude
908          * @return the extruded region side boundary
909          */
910         private PlaneConvexSubset extrudeSideInfinite(final LineConvexSubset lineSubset) {
911             final Vector2D subLinePt = lineSubset.getLine().getOrigin();
912             final Vector2D subLineDir = lineSubset.getLine().getDirection();
913 
914             final Vector3D linePt = basePlane.toSpace(subLinePt);
915             final Vector3D lineDir = linePt.vectorTo(basePlane.toSpace(subLinePt.add(subLineDir)));
916 
917             final EmbeddingPlane sidePlane;
918             if (extrudingOnPlusSide) {
919                 sidePlane = fromPointAndPlaneVectors(linePt, lineDir, extrusionVector, precision);
920             } else {
921                 sidePlane = fromPointAndPlaneVectors(linePt, extrusionVector, lineDir, precision);
922             }
923 
924             final Vector2D sideLineOrigin = sidePlane.toSubspace(linePt);
925             final Vector2D sideLineDir = sideLineOrigin.vectorTo(sidePlane.toSubspace(linePt.add(lineDir)));
926 
927             final Vector2D extrudedSideLineOrigin = sidePlane.toSubspace(linePt.add(extrusionVector));
928 
929             final Vector2D sideExtrusionDir = sidePlane.toSubspace(sidePlane.getOrigin().add(extrusionVector))
930                     .normalize();
931 
932             // construct a list of lines forming the bounds of the extruded subspace region
933             final List<Line> lines = new ArrayList<>();
934 
935             // add the top and bottom lines (original and extruded)
936             if (extrudingOnPlusSide) {
937                 lines.add(Lines.fromPointAndDirection(sideLineOrigin, sideLineDir, precision));
938                 lines.add(Lines.fromPointAndDirection(extrudedSideLineOrigin, sideLineDir.negate(), precision));
939             } else {
940                 lines.add(Lines.fromPointAndDirection(sideLineOrigin, sideLineDir.negate(), precision));
941                 lines.add(Lines.fromPointAndDirection(extrudedSideLineOrigin, sideLineDir, precision));
942             }
943 
944             // if we have a point on the original line, then connect the two
945             final Vector2D startPt = lineSubset.getStartPoint();
946             final Vector2D endPt = lineSubset.getEndPoint();
947             if (startPt != null) {
948                 lines.add(Lines.fromPointAndDirection(
949                         sidePlane.toSubspace(basePlane.toSpace(startPt)),
950                         extrudingOnPlusSide ? sideExtrusionDir.negate() : sideExtrusionDir,
951                         precision));
952             } else if (endPt != null) {
953                 lines.add(Lines.fromPointAndDirection(
954                         sidePlane.toSubspace(basePlane.toSpace(endPt)),
955                         extrudingOnPlusSide ? sideExtrusionDir : sideExtrusionDir.negate(),
956                         precision));
957             }
958 
959             return subsetFromConvexArea(sidePlane, ConvexArea.fromBounds(lines));
960         }
961     }
962 }