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.text.MessageFormat;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Iterator;
024import java.util.List;
025import java.util.function.BiFunction;
026
027import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
028import org.apache.commons.geometry.core.partitioning.Split;
029import org.apache.commons.geometry.core.partitioning.SplitLocation;
030import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
031import org.apache.commons.geometry.euclidean.threed.line.Line3D;
032import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
033import org.apache.commons.geometry.euclidean.twod.ConvexArea;
034import org.apache.commons.geometry.euclidean.twod.Line;
035import org.apache.commons.geometry.euclidean.twod.LineConvexSubset;
036import org.apache.commons.geometry.euclidean.twod.Lines;
037import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
038import org.apache.commons.geometry.euclidean.twod.Vector2D;
039import org.apache.commons.geometry.euclidean.twod.path.LinePath;
040
041/** Class containing factory methods for constructing {@link Plane} and {@link PlaneSubset} instances.
042 */
043public final class Planes {
044
045    /** Utility class; no instantiation. */
046    private Planes() {
047    }
048
049    /** Build a plane from a point and two (on plane) vectors.
050     * @param p the provided point (on plane)
051     * @param u u vector (on plane)
052     * @param v v vector (on plane)
053     * @param precision precision context used to compare floating point values
054     * @return a new plane
055     * @throws IllegalArgumentException if the norm of the given values is zero, NaN, or infinite.
056     */
057    public static EmbeddingPlane fromPointAndPlaneVectors(final Vector3D p, final Vector3D u, final Vector3D v,
058            final DoublePrecisionContext precision) {
059        final Vector3D.Unit uNorm = u.normalize();
060        final Vector3D.Unit vNorm = uNorm.orthogonal(v);
061        final Vector3D.Unit wNorm = uNorm.cross(vNorm).normalize();
062        final double originOffset = -p.dot(wNorm);
063
064        return new EmbeddingPlane(uNorm, vNorm, wNorm, originOffset, precision);
065    }
066
067    /** Build a plane from a normal.
068     * Chooses origin as point on plane.
069     * @param normal normal direction to the plane
070     * @param precision precision context used to compare floating point values
071     * @return a new plane
072     * @throws IllegalArgumentException if the norm of the given values is zero, NaN, or infinite.
073     */
074    public static Plane fromNormal(final Vector3D normal, final DoublePrecisionContext precision) {
075        return fromPointAndNormal(Vector3D.ZERO, normal, precision);
076    }
077
078    /** Build a plane from a point and a normal.
079     *
080     * @param p point belonging to the plane
081     * @param normal normal direction to the plane
082     * @param precision precision context used to compare floating point values
083     * @return a new plane
084     * @throws IllegalArgumentException if the norm of the given values is zero, NaN, or infinite.
085     */
086    public static Plane fromPointAndNormal(final Vector3D p, final Vector3D normal,
087            final DoublePrecisionContext precision) {
088        final Vector3D.Unit unitNormal = normal.normalize();
089        final double originOffset = -p.dot(unitNormal);
090
091        return new Plane(unitNormal, originOffset, precision);
092    }
093
094    /** Build a plane from three points.
095     * <p>
096     * The plane is oriented in the direction of {@code (p2-p1) ^ (p3-p1)}
097     * </p>
098     *
099     * @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}