This is an automated email from the ASF dual-hosted git repository.

mattjuntunen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git


The following commit(s) were added to refs/heads/master by this push:
     new 2f4e86b7 GEOMETRY-110: add ConvexHull3D using Quickhull algorithm
2f4e86b7 is described below

commit 2f4e86b792693d4c2c0e5fb2c4f198541e1fecb6
Author: Andreas Goß <75903169+agos...@users.noreply.github.com>
AuthorDate: Sun Apr 28 02:14:00 2024 +0200

    GEOMETRY-110: add ConvexHull3D using Quickhull algorithm
    
    * GEOMETRY-110
    
    Add ConvexHull3D class and tests.
    
    * GEOMETRY-110
    
    Return only an unmodifiable collection of facets.
    
    * GEOMETRY-110
    
    Test for a large number of points.
    
    * Checkstyle Fix.
    
    * GEOMETRY-110
    
    Points are added to candidates again instead of a new point set.
    
    * GEOMETRY-110
    
    Only candidates points are distributed.
    
    * GEOMETRY-110
    
    Add Simplex type
    
    * GEOMETRY-110
    
    Create simplex while appending and cache it for later.
    
    * GEOMETRY-110
    
    Build simplex incrementially while appending points.
    
    * GEOMETRY-110
    
    Return facets as list and filter out duplicates in vertices.
    
    * GEOMETRY-110
    
    Returned collections have to be unmodifiable in public methods.
    
    * GEOMETRY-110
    
    Rename method and calculate maximum in a loop.
    
    * GEOMETRY-110
    
    Declare variable outside of loop.
    
    * GEOMETRY-110
    
    Rename methods.
    
    * GEOMETRY-110
    
    Replace bounds by array.
    
    * GEOMETRY-110
    
    Add check for appending a collection of points
    
    * [GEOMETRY-110]
    
    Delete hasOusidePoints and add exclusion for facets to spotbugs.
    
    * [GEOMETRY-110]
    
    Move isInside method into Facet class and reverse and rename it to isOutside
    
    * [GEOMETRY-110]
    
    Replace vertexToFacetMap with edgeMap
    
    * [Geometry-110]
    
    Change loop for creating edges.
    
    * [GEOMETRY-110]
    
    Facets are all located through edge operations. The edges are oriented in 
such a way, that neighbors share an edge in inverse orientation.
    
    * [GEOMETRY-110]
    
    Delete unnecessary debug code.
    
    * [GEOMETRY-110]
    
    Edges are oriented and lookup is done via an edgeMap. Each oriented edge is 
unique in association with a facet. Each facet stores the maximum offset and 
the associated point of outside points.
    
    * [GEOMETRY-110]
    
    - Rename Edge fields.
    - Improve testing and assert all outputs of the algorithm.
    
    * [GEOMETRY-110]
    
    Check isDegenerate for all non-degenerate hulls.
    
    * [GEOMETRY-110]
    
    Even though the defined region is the same the number of facets can vary 
depending on the execution order of the algorithm.
    
    ---------
    
    Co-authored-by: agoß <agoß@Goß>
    Co-authored-by: Andreas Goss <ag...@itemis.com>
---
 .../euclidean/threed/hull/ConvexHull3D.java        | 728 +++++++++++++++++++++
 .../euclidean/threed/hull/package-info.java        |  25 +
 .../euclidean/threed/hull/ConvexHull3DTest.java    | 276 ++++++++
 .../resources/spotbugs/spotbugs-exclude-filter.xml |   5 +
 4 files changed, 1034 insertions(+)

diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java
new file mode 100644
index 00000000..aec0a5ee
--- /dev/null
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java
@@ -0,0 +1,728 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.geometry.euclidean.threed.hull;
+
+import org.apache.commons.geometry.core.ConvexHull;
+import org.apache.commons.geometry.core.collection.PointSet;
+import org.apache.commons.geometry.euclidean.EuclideanCollections;
+import org.apache.commons.geometry.euclidean.threed.Bounds3D;
+import org.apache.commons.geometry.euclidean.threed.ConvexPolygon3D;
+import org.apache.commons.geometry.euclidean.threed.ConvexVolume;
+import org.apache.commons.geometry.euclidean.threed.Plane;
+import org.apache.commons.geometry.euclidean.threed.Planes;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.euclidean.threed.line.Line3D;
+import org.apache.commons.geometry.euclidean.threed.line.Lines3D;
+import org.apache.commons.numbers.core.Precision.DoubleEquivalence;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * This class represents a convex hull in three-dimensional Euclidean space.
+ */
+public class ConvexHull3D implements ConvexHull<Vector3D> {
+
+    /**
+     * The vertices of the convex hull.
+     */
+    private final List<Vector3D> vertices;
+
+    /**
+     * The region defined by the hull.
+     */
+    private final ConvexVolume region;
+
+    /**
+     * A collection of all facets that form the convex volume of the hull.
+     */
+    private final List<ConvexPolygon3D> facets;
+
+    /**
+     * Flag for when the hull is degenerate.
+     */
+    private final boolean isDegenerate;
+
+    /**
+     * Simple constructor no validation performed. This constructor is called 
if the
+     * hull is well-formed and non-degenerative.
+     *
+     * @param facets the facets of the hull.
+     */
+    ConvexHull3D(Collection<? extends ConvexPolygon3D> facets) {
+        vertices = Collections.unmodifiableList(new 
ArrayList<>(facets.stream().flatMap(f -> f.getVertices().stream())
+                .collect(Collectors.toSet())));
+        region = ConvexVolume.fromBounds(() -> 
facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableList(new ArrayList<>(facets));
+        this.isDegenerate = false;
+    }
+
+    /**
+     * Simple constructor no validation performed. No Region is formed as it is
+     * assumed that the hull is degenerate.
+     *
+     * @param points       the given vertices of the hull.
+     * @param isDegenerate boolean flag
+     */
+    ConvexHull3D(Collection<Vector3D> points, boolean isDegenerate) {
+        vertices = Collections.unmodifiableList(new ArrayList<>(points));
+        region = null;
+        this.facets = Collections.emptyList();
+        this.isDegenerate = isDegenerate;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public List<Vector3D> getVertices() {
+        return vertices;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public ConvexVolume getRegion() {
+        return region;
+    }
+
+    /**
+     * Return a collection of all two-dimensional faces (called facets) of the
+     * convex hull.
+     *
+     * @return a collection of all two-dimensional faces.
+     */
+    public List<ConvexPolygon3D> getFacets() {
+        return facets;
+    }
+
+    /**
+     * Return {@code true} if the hull is degenerate.
+     *
+     * @return the isDegenerate
+     */
+    public boolean isDegenerate() {
+        return isDegenerate;
+    }
+
+    /**
+     * Implementation of quick-hull algorithm by Barber, Dobkin and Huhdanpaa. 
The
+     * algorithm constructs the convex hull for a given finite set of points.
+     * Empirically, the number of points processed by quickhull is 
proportional to
+     * the number of vertices in the output. The algorithm runs on an input of 
size
+     * n with r processed points in time O(n log r). We define a point of the 
given
+     * set to be extreme, if and only if the point is part of the final hull. 
The
+     * algorithm runs in multiple stages:
+     * <ol>
+     * <li>First we construct a simplex with extreme properties from the given 
point
+     * set to maximize the possibility of choosing extreme points as initial 
simplex
+     * vertices.</li>
+     * <li>We partition all the remaining points into outside sets. Each 
polygon
+     * face of the simplex defines a positive and negative half-space. A point 
can
+     * be assigned to the outside set of the polygon if it is an element of the
+     * positive half space.</li>
+     * <li>For each polygon-face (facet) with a non empty outside set we 
choose a
+     * point with maximal distance to the given facet.</li>
+     * <li>We determine all the visible facets from the given outside point 
and find
+     * a path around the horizon.</li>
+     * <li>We construct a new cone of polygons from the edges of the horizon 
to the
+     * outside point. All visible facets are removed and the points in the 
outside
+     * sets of the visible facets are redistributed.</li>
+     * <li>We repeat step 3-5 until each outside set is empty.</li>
+     * </ol>
+     */
+    public static class Builder {
+
+        /**
+         * Set of possible candidates.
+         */
+        private final PointSet<Vector3D> candidates;
+
+        /**
+         * Precision context used to compare floating point numbers.
+         */
+        private final DoubleEquivalence precision;
+        /**
+         * Map containing all edges as keys and the associated facets as 
values.
+         */
+        private final Map<Edge, Facet> edgeMap;
+        /**
+         * Simplex for testing new points and starting the algorithm.
+         */
+        private Simplex simplex;
+        /**
+         * The minX, maxX, minY, maxY, minZ, maxZ points.
+         */
+        private Bounds3D box;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            simplex = new Simplex(Collections.emptySet());
+            edgeMap = new HashMap<>();
+        }
+
+        /**
+         * Associates the given point with an outside set if possible.
+         *
+         * @param p      the given point.
+         * @param facets the facets to check against.
+         */
+        private static void distributePoint(Vector3D p, Iterable<Facet> 
facets) {
+            for (Facet facet : facets) {
+                if (facet.addPoint(p)) {
+                    return;
+                }
+            }
+        }
+
+        /**
+         * Appends to the point to the set of possible candidates.
+         *
+         * @param point the given point.
+         * @return this instance.
+         */
+        public Builder append(Vector3D point) {
+            boolean recomputeSimplex = false;
+            if (box == null) {
+                box = Bounds3D.from(point);
+                recomputeSimplex = true;
+            } else if (!box.contains(point)) {
+                box = Bounds3D.from(box.getMin(), box.getMax(), point);
+                recomputeSimplex = true;
+            }
+            candidates.add(point);
+            if (recomputeSimplex) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p 
-> candidates.addAll(p.getVertices()));
+                simplex = createSimplex(candidates);
+            }
+            distributePoints(simplex.getFacets());
+            return this;
+        }
+
+        /**
+         * Appends the given collection of points to the set of possible 
candidates.
+         *
+         * @param points the given collection of points.
+         * @return this instance.
+         */
+        public Builder append(Collection<Vector3D> points) {
+            boolean recomputeSimplex = false;
+            if (box == null) {
+                box = Bounds3D.from(points);
+                recomputeSimplex = true;
+            } else if (points.stream().anyMatch(p -> !box.contains(p))) {
+                box = Bounds3D.builder().add(box).addAll(points).build();
+                recomputeSimplex = true;
+            }
+
+            candidates.addAll(points);
+            if (recomputeSimplex) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p 
-> candidates.addAll(p.getVertices()));
+                simplex = createSimplex(candidates);
+            }
+            distributePoints(simplex.getFacets());
+            return this;
+        }
+
+        /**
+         * Builds a convex hull containing all appended points.
+         *
+         * @return a convex hull containing all appended points.
+         */
+        public ConvexHull3D build() {
+            if (simplex == null) {
+                return new ConvexHull3D(candidates, true);
+            }
+
+            // The simplex is degenerate.
+            if (simplex.isDegenerate()) {
+                return new ConvexHull3D(candidates, true);
+            }
+
+
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            Facet conflictFacet = getConflictFacet();
+            while (conflictFacet != null) {
+                Vector3D conflictPoint = conflictFacet.getOutsidePoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                visibleFacets.add(conflictFacet);
+                Set<Edge> horizon = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets, 
horizon);
+                Vector3D referencePoint = 
conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, 
referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+                conflictFacet = getConflictFacet();
+            }
+            Collection<ConvexPolygon3D> hull = edgeMap.values().stream()
+                    .map(Facet::getPolygon).collect(Collectors.toSet());
+            return new ConvexHull3D(hull);
+        }
+
+        /**
+         * Constructs a new cone with conflict point and the given edges. The 
reference
+         * point is used for orientation in such a way, that the reference 
point lies in
+         * the negative half-space of all newly constructed facets.
+         *
+         * @param conflictPoint  the given conflict point.
+         * @param horizon        the given set of edges.
+         * @param referencePoint a reference point for orientation.
+         * @return a set of newly constructed facets.
+         */
+        private Set<Facet> constructCone(Vector3D conflictPoint, Set<Edge> 
horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Edge edge : horizon) {
+                ConvexPolygon3D newPolygon = 
Planes.convexPolygonFromVertices(Arrays.asList(edge.getStart(),
+                        edge.getEnd(), conflictPoint), precision);
+                newFacets.add(new Facet(newPolygon, referencePoint, 
precision));
+            }
+            return newFacets;
+        }
+
+        /**
+         * Create an initial simplex for the given point set. If no non-zero 
simplex can
+         * be formed the point set is degenerate and an empty Collection is 
returned.
+         * Each vertex of the simplex must be inside the given point set.
+         *
+         * @param points the given point set.
+         * @return an initial simplex.
+         */
+        private Simplex createSimplex(Collection<Vector3D> points) {
+
+            // First vertex of the simplex
+            Vector3D vertex1 = 
points.stream().min(Vector3D.COORDINATE_ASCENDING_ORDER).get();
+
+            // Find a point with maximal distance to the second.
+            Vector3D vertex2 = 
points.stream().max(Comparator.comparingDouble(vertex1::distance)).get();
+
+            // The point is degenerate if all points are equivalent.
+            if (vertex1.eq(vertex2, precision)) {
+                return new Simplex(Collections.emptyList());
+            }
+
+            // First and second vertex form a line.
+            Line3D line = Lines3D.fromPoints(vertex1, vertex2, precision);
+
+            // Find a point with maximal distance from the line.
+            Vector3D vertex3 = 
points.stream().max(Comparator.comparingDouble(line::distance)).get();
+
+            // The point set is degenerate because all points are collinear.
+            if (line.contains(vertex3)) {
+                return new Simplex(Collections.emptyList());
+            }
+
+            // Form a triangle with the first three vertices.
+            ConvexPolygon3D facet1 = Planes.triangleFromVertices(vertex1, 
vertex2, vertex3, precision);
+
+            // Find a point with maximal distance to the plane formed by the 
triangle.
+            Plane plane = facet1.getPlane();
+            Vector3D vertex4 = 
points.stream().max(Comparator.comparingDouble(d -> 
Math.abs(plane.offset(d)))).get();
+
+            // The point set is degenerate, because all points are coplanar.
+            if (plane.contains(vertex4)) {
+                return new Simplex(Collections.emptyList());
+            }
+
+            // Construct the other three facets.
+            ConvexPolygon3D facet2 = 
Planes.convexPolygonFromVertices(Arrays.asList(vertex1, vertex2, vertex4),
+                    precision);
+            ConvexPolygon3D facet3 = 
Planes.convexPolygonFromVertices(Arrays.asList(vertex1, vertex3, vertex4),
+                    precision);
+            ConvexPolygon3D facet4 = 
Planes.convexPolygonFromVertices(Arrays.asList(vertex2, vertex3, vertex4),
+                    precision);
+
+            List<Facet> facets = new ArrayList<>();
+
+            // Choose the right orientation for all facets.
+            facets.add(new Facet(facet1, vertex4, precision));
+            facets.add(new Facet(facet2, vertex3, precision));
+            facets.add(new Facet(facet3, vertex2, precision));
+            facets.add(new Facet(facet4, vertex1, precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Adds the facet for the quickhull algorithm.
+         *
+         * @param facet the given facet.
+         */
+        private void addFacet(Facet facet) {
+            for (Edge e : facet.getEdges()) {
+                edgeMap.put(e, facet);
+            }
+        }
+
+        /**
+         * Associates each point of the candidates set with an outside set of 
the given
+         * facets. Afterward the candidates set is cleared.
+         *
+         * @param facets the facets to check against.
+         */
+        private void distributePoints(Collection<Facet> facets) {
+            if (!facets.isEmpty()) {
+                candidates.forEach(p -> distributePoint(p, facets));
+                candidates.clear();
+            }
+        }
+
+        /**
+         * Returns any facet, which is currently in conflict e.g. has a 
non-empty outside
+         * set.
+         *
+         * @return any facet, which is currently in conflict e.g. has a 
non-empty outside
+         * set.
+         */
+        private Facet getConflictFacet() {
+            return 
edgeMap.values().stream().filter(Facet::hasOutsidePoints).findFirst()
+                    .orElse(null);
+        }
+
+        /**
+         * Adds all visible facets to the provided set.
+         *
+         * @param facet         the given conflictFacet.
+         * @param outsidePoint  the given outside point.
+         * @param visibleFacets visible facets are collected in this set.
+         * @param horizon       horizon edges.
+         */
+        private void getVisibleFacets(Facet facet, Vector3D outsidePoint, 
Set<Facet> visibleFacets, Set<Edge> horizon) {
+            for (Edge e : facet.getEdges()) {
+                Facet neighbor = edgeMap.get(e.getInverse());
+                if (precision.gt(neighbor.offset(outsidePoint), 0.0)) {
+                    if (visibleFacets.add(neighbor)) {
+                        getVisibleFacets(neighbor, outsidePoint, 
visibleFacets, horizon);
+                    }
+                } else {
+                    horizon.add(e);
+                }
+            }
+        }
+
+        /**
+         * Removes the facets from vertexToFacets map and returns a set of all
+         * associated outside points. All outside set associated with the 
visible facets
+         * are added to the possible candidates again.
+         *
+         * @param visibleFacets a set of facets.
+         */
+        private void removeFacets(Set<Facet> visibleFacets) {
+            visibleFacets.forEach(f -> candidates.addAll(f.getOutsideSet()));
+            if (!edgeMap.isEmpty()) {
+                removeFacetsFromVertexMap(visibleFacets);
+            }
+        }
+
+        /**
+         * Removes the given facets from the vertexToFacetMap.
+         *
+         * @param visibleFacets the facets to be removed.
+         */
+        private void removeFacetsFromVertexMap(Set<Facet> visibleFacets) {
+            // Remove facets from edgeMap
+            for (Facet facet : visibleFacets) {
+                for (Edge e : facet.getEdges()) {
+                    edgeMap.remove(e);
+                }
+            }
+        }
+
+    }
+
+    /**
+     * A facet is a convex polygon with an associated outside set.
+     */
+    static class Facet {
+
+        /**
+         * The polygon of the facet.
+         */
+        private final ConvexPolygon3D polygon;
+
+        /**
+         * The edges of the facet.
+         */
+        private final List<Edge> edges;
+
+        /**
+         * The outside set of the facet.
+         */
+        private final Set<Vector3D> outsideSet;
+
+        /**
+         * Precision context used to compare floating point numbers.
+         */
+        private final DoubleEquivalence precision;
+
+        /**
+         * Store the offset with the biggest distance to the plane.
+         */
+        private double maximumOffset;
+
+        /**
+         * Store the point with the biggest distance.
+         */
+        private Vector3D maximumPoint;
+
+        /**
+         * Constructs a new facet with the given polygon and an associated 
empty
+         * outside set in such a way, that the reference point is in the 
negative half-space of the associated oriented
+         * polygon.
+         *
+         * @param polygon        the given polygon.
+         * @param referencePoint reference point for construction.
+         * @param precision      context used to compare floating point 
numbers.
+         */
+        Facet(ConvexPolygon3D polygon, Vector3D referencePoint, 
DoubleEquivalence precision) {
+            this.polygon = 
precision.lte(polygon.getPlane().offset(referencePoint), 0) ? polygon : 
polygon.reverse();
+            outsideSet = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            List<Edge> edgesCol = new ArrayList<>();
+            List<Vector3D> vertices = this.polygon.getVertices();
+            maximumOffset = 0.0;
+
+            for (int i = 0; i < vertices.size(); i++) {
+                Vector3D start = vertices.get(i);
+                Vector3D end = vertices.get(i + 1 == vertices.size() ? 0 : i + 
1);
+                edgesCol.add(new Edge(start, end));
+            }
+            edges = Collections.unmodifiableList(edgesCol);
+        }
+
+        /**
+         * Return {@code true} if the facet is in conflict e.g. the outside 
set is
+         * non-empty.
+         *
+         * @return {@code true} if the facet is in conflict e.g. the outside 
set is
+         * non-empty.
+         */
+        boolean hasOutsidePoints() {
+            return !outsideSet.isEmpty();
+        }
+
+        /**
+         * Returns the associated polygon.
+         *
+         * @return the associated polygon.
+         */
+        public ConvexPolygon3D getPolygon() {
+            return polygon;
+        }
+
+        /**
+         * Returns an unmodifiable view of the associated outside set.
+         *
+         * @return an unmodifiable view of the associated outside set.
+         */
+        public Set<Vector3D> getOutsideSet() {
+            return outsideSet;
+        }
+
+        /**
+         * Returns a list of all edges.
+         *
+         * @return a list of all edges.
+         */
+        public List<Edge> getEdges() {
+            return edges;
+        }
+
+        /**
+         * Returns {@code true} if the point resides in the positive 
half-space defined
+         * by the associated hyperplane of the polygon and {@code false} 
otherwise. If
+         * {@code true} the point is added to the associated outside set.
+         *
+         * @param p the given point.
+         * @return {@code true} if the point is added to the outside set, 
{@code false}
+         * otherwise.
+         */
+        public boolean addPoint(Vector3D p) {
+            double offset = offset(p);
+            if (precision.gt(offset, 0.0)) {
+                outsideSet.add(p);
+                if (precision.gt(offset, maximumOffset)) {
+                    maximumOffset = offset;
+                    maximumPoint = p;
+                }
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * Returns the offset of the given point to the plane defined by this 
instance.
+         *
+         * @param point a reference point.
+         * @return the offset of the given point to the plane defined by this 
instance.
+         */
+        public double offset(Vector3D point) {
+            return polygon.getPlane().offset(point);
+        }
+
+        /**
+         * Returns the outside point with the greatest offset distance to the 
hyperplane
+         * defined by the associated polygon.
+         *
+         * @return the outside point with the greatest offset distance to the 
hyperplane
+         * defined by the associated polygon.
+         */
+        public Vector3D getOutsidePoint() {
+            return maximumPoint;
+        }
+    }
+
+    /**
+     * This class represents an edge consisting of two vertices. The order of 
the vertices is not relevant so two edges
+     * are equivalent if the edges are equivalent irrespectively of the order.
+     */
+    static class Edge {
+
+        /**
+         * The first vertex.
+         */
+        private final Vector3D start;
+
+        /**
+         * The second vertex.
+         */
+        private final Vector3D end;
+
+        /**
+         * Simple Constructor.
+         *
+         * @param start the start of the edge.
+         * @param end   the end of the edge.
+         */
+        Edge(Vector3D start, Vector3D end) {
+            this.start = start;
+            this.end = end;
+        }
+
+        /**
+         * Getter for the start vertex.
+         *
+         * @return the start vertex.
+         */
+        public Vector3D getStart() {
+            return start;
+        }
+
+        /**
+         * Getter for the end vertex.
+         *
+         * @return the end vertex.
+         */
+        public Vector3D getEnd() {
+            return end;
+        }
+
+        /**
+         * Returns the inverse of this given edge.
+         *
+         * @return the inverse of this given edge.
+         */
+        public Edge getInverse() {
+            return new Edge(end, start);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            Edge edge = (Edge) o;
+            return Objects.equals(start, edge.start) && Objects.equals(end, 
edge.end);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public int hashCode() {
+            return Objects.hash(start, end);
+        }
+    }
+
+    /**
+     * This class represents a simple simplex with four facets.
+     */
+    private static class Simplex {
+
+
+        /**
+         * The facets of the simplex.
+         */
+        private final Set<Facet> facets;
+
+        /**
+         * Constructs a new simplex with the given facets.
+         *
+         * @param facets the given facets.
+         */
+        Simplex(Collection<Facet> facets) {
+            this.facets = new HashSet<>(facets);
+        }
+
+        /**
+         * Returns {@code true} if the collection of facets is empty.
+         *
+         * @return {@code true} if the collection of facets is empty.
+         */
+        public boolean isDegenerate() {
+            return facets.isEmpty();
+        }
+
+        /**
+         * Returns the facets of the simplex as set.
+         *
+         * @return the facets of the simplex as set.
+         */
+        public Set<Facet> getFacets() {
+            return facets;
+        }
+    }
+}
diff --git 
a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/package-info.java
 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/package-info.java
new file mode 100644
index 00000000..05c972b9
--- /dev/null
+++ 
b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ *
+ * <p>
+ * This package provides algorithms to generate the convex hull
+ * for a set of points in an three-dimensional Euclidean space.
+ * </p>
+ *
+ */
+package org.apache.commons.geometry.euclidean.threed.hull;
diff --git 
a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3DTest.java
 
b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3DTest.java
new file mode 100644
index 00000000..015669d2
--- /dev/null
+++ 
b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3DTest.java
@@ -0,0 +1,276 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.geometry.euclidean.threed.hull;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.commons.geometry.euclidean.EuclideanCollections;
+import org.apache.commons.geometry.euclidean.threed.ConvexPolygon3D;
+import org.apache.commons.geometry.euclidean.threed.ConvexVolume;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.numbers.core.Precision;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class ConvexHull3DTest {
+
+    private static final double TEST_EPS = 1e-10;
+
+    private static final Precision.DoubleEquivalence TEST_PRECISION = 
Precision.doubleEquivalenceOfEpsilon(TEST_EPS);
+
+    private ConvexHull3D.Builder builder;
+
+    private UniformRandomProvider random;
+
+    @BeforeEach
+    public void setUp() {
+        builder = new ConvexHull3D.Builder(TEST_PRECISION);
+        random = RandomSource.XO_SHI_RO_256_PP.create(10);
+    }
+
+    /**
+     * A hull with less than four points is degenerate.
+     */
+    @Test
+    void lessThanFourPoints() {
+        List<Vector3D> vertices = Arrays.asList(Vector3D.of(0, 0, 0), 
Vector3D.of(1, 0, 0), Vector3D.of(0, 1, 0));
+        builder.append(vertices);
+        ConvexHull3D hull = builder.build();
+        checkDegenerateHull(hull, vertices);
+    }
+
+    /**
+     * A Hull with less than four points is degenerate.
+     */
+    @Test
+    void samePoints() {
+        List<Vector3D> vertices = Arrays.asList(Vector3D.ZERO, Vector3D.ZERO, 
Vector3D.ZERO, Vector3D.ZERO);
+        builder.append(vertices);
+        ConvexHull3D hull = builder.build();
+        checkDegenerateHull(hull, vertices);
+    }
+
+    @Test
+    void collinearPoints() {
+        List<Vector3D> vertices = Arrays.asList(Vector3D.ZERO, Vector3D.of(1, 
0, 0), Vector3D.of(2, 0, 0),
+                Vector3D.of(3, 0, 0));
+        builder.append(vertices);
+        ConvexHull3D hull = builder.build();
+        checkDegenerateHull(hull, vertices);
+    }
+
+    @Test
+    void coplanarPoints() {
+        List<Vector3D> vertices = Arrays.asList(Vector3D.ZERO, Vector3D.of(1, 
0, 0), Vector3D.of(0, 1, 0),
+                Vector3D.of(3, 0, 0));
+        builder.append(vertices);
+        ConvexHull3D hull = builder.build();
+        checkDegenerateHull(hull, vertices);
+    }
+
+    @Test
+    void simplex() {
+        List<Vector3D> vertices = Arrays.asList(Vector3D.ZERO, Vector3D.of(1, 
0, 0), Vector3D.of(0, 1, 0),
+                Vector3D.of(0, 0, 1));
+        builder.append(vertices);
+        ConvexHull3D hull = builder.build();
+        checkHull(hull, vertices);
+        assertTrue(TEST_PRECISION.eq(1.0 / 6.0, hull.getRegion().getSize()));
+        assertEquals(4, hull.getFacets().size());
+    }
+
+    @Test
+    void simplexPlusPoint() {
+        List<Vector3D> vertices = Arrays.asList(Vector3D.ZERO, Vector3D.of(1, 
0, 0), Vector3D.of(0, 1, 0),
+                Vector3D.of(0, 0, 1), Vector3D.of(1, 1, 1));
+        builder.append(vertices);
+        ConvexHull3D hull = builder.build();
+        checkHull(hull, vertices);
+        assertTrue(TEST_PRECISION.eq(1.0 / 2.0, hull.getRegion().getSize()));
+        assertEquals(6, hull.getFacets().size());
+    }
+
+    @Test
+    void unitCube() {
+        List<Vector3D> vertices = Arrays.asList(Vector3D.ZERO, Vector3D.of(1, 
0, 0), Vector3D.of(0, 1, 0),
+                Vector3D.of(0, 0, 1), Vector3D.of(1, 1, 0), Vector3D.of(1, 0, 
1), Vector3D.of(0, 1, 1),
+                Vector3D.of(1, 1, 1));
+        builder.append(vertices);
+        ConvexHull3D hull = builder.build();
+        checkHull(hull, vertices);
+        assertTrue(TEST_PRECISION.eq(1.0, hull.getRegion().getSize()));
+        assertEquals(12, hull.getFacets().size());
+    }
+
+    @Test
+    void unitCubeSequentially() {
+        List<Vector3D> vertices = Arrays.asList(Vector3D.ZERO, Vector3D.of(1, 
0, 0), Vector3D.of(0, 1, 0),
+                Vector3D.of(0, 0, 1), Vector3D.of(1, 1, 0), Vector3D.of(1, 0, 
1), Vector3D.of(0, 1, 1),
+                Vector3D.of(1, 1, 1));
+        vertices.forEach(builder::append);
+        ConvexHull3D hull = builder.build();
+        checkHull(hull, vertices);
+        assertTrue(TEST_PRECISION.eq(1.0, hull.getRegion().getSize()));
+        assertEquals(12, hull.getFacets().size());
+    }
+
+    @Test
+    void multiplePoints() {
+        List<Vector3D> vertices = Arrays.asList(Vector3D.ZERO, Vector3D.of(1, 
0, 0), Vector3D.of(0, 1, 0),
+                Vector3D.of(0, 0, 1), Vector3D.of(1, 1, 0), Vector3D.of(1, 0, 
1), Vector3D.of(0, 1, 1),
+                Vector3D.of(1, 1, 1), Vector3D.of(10, 20, 30), 
Vector3D.of(-0.5, 0, 5));
+        builder.append(vertices);
+        ConvexHull3D hull = builder.build();
+        checkHull(hull, vertices);
+        assertTrue(TEST_PRECISION.eq(42.58333333333329, 
hull.getRegion().getSize()));
+    }
+
+    /**
+     * Create 1000 points on a unit sphere. Then every point of the set must 
be a
+     * vertex of the hull.
+     */
+    @Test
+    void randomUnitPoints() {
+        // All points in the set must be on the hull. This is a worst case 
scenario.
+        Set<Vector3D> set = createRandomPoints(1000, true);
+        builder.append(set);
+        ConvexHull3D hull = builder.build();
+        ConvexVolume region = hull.getRegion();
+        assertNotNull(region);
+        List<Vector3D> vertices = hull.getVertices();
+        for (Vector3D p : set) {
+            assertTrue(vertices.contains(p));
+        }
+        checkHull(hull, vertices);
+        assertEquals(1000, hull.getVertices().size());
+    }
+
+    @Test
+    void randomPoints() {
+        Set<Vector3D> set = createRandomPoints(100000, false);
+        builder.append(set);
+        ConvexHull3D hull = builder.build();
+        checkHull(hull, set);
+    }
+
+    @Test
+    void randomPointsInTwoSets() {
+        Set<Vector3D> set1 = createRandomPoints(50000, false);
+        Set<Vector3D> set2 = createRandomPoints(50000, false);
+        builder.append(set1);
+        builder.append(set2);
+        ConvexHull3D hull = builder.build();
+        checkHull(hull, set1);
+        checkHull(hull, set2);
+    }
+
+    @Test
+    void randomPointsSequentially() {
+        // Points are added sequentially
+        List<Vector3D> list = new ArrayList<>(createRandomPoints(100, false));
+        list.forEach(builder::append);
+        ConvexHull3D hull = builder.build();
+        checkHull(hull, list);
+    }
+
+    /**
+     * Create a specified number of random points on the unit sphere.
+     *
+     * @param number    the given number.
+     * @param normalize normalize the output points.
+     * @return a specified number of random points on the unit sphere.
+     */
+    private Set<Vector3D> createRandomPoints(int number, boolean normalize) {
+        Set<Vector3D> set = EuclideanCollections.pointSet3D(TEST_PRECISION);
+        for (int i = 0; i < number; i++) {
+            if (normalize) {
+                set.add(Vector3D.Unit.from(random.nextDouble(), 
random.nextDouble(), random.nextDouble()));
+            } else {
+                set.add(Vector3D.of(random.nextDouble(), random.nextDouble(), 
random.nextDouble()));
+            }
+        }
+        return set;
+    }
+
+    /**
+     * Check if the hull contains all the points in the given collection and 
checks if the volume is finite and
+     * non-zero.
+     */
+    private void checkHull(ConvexHull3D hull, Collection<Vector3D> points) {
+        ConvexVolume region = hull.getRegion();
+        assertNotNull(region);
+        assertTrue(region.isFinite());
+        assertFalse(region.isEmpty());
+        assertFalse(hull.isDegenerate());
+        for (Vector3D p : points) {
+            assertTrue(region.contains(p));
+        }
+        checkFacets(hull);
+    }
+
+    private void checkFacets(ConvexHull3D hull) {
+        List<ConvexPolygon3D> polygons = hull.getFacets();
+        assertFalse(polygons.isEmpty());
+
+        // Build an edge map to check if every facet has a neighbor and all 
edges share two facets.
+        Vector3D centroid = hull.getRegion().getCentroid();
+        Set<ConvexHull3D.Facet> facets = polygons.stream().map(p -> new 
ConvexHull3D.Facet(p, centroid, TEST_PRECISION))
+                .collect(Collectors.toSet());
+
+        //Populate edgeMap.
+        Map<ConvexHull3D.Edge, ConvexHull3D.Facet> edgeMap = new HashMap<>();
+        for (ConvexHull3D.Facet f : facets) {
+            for (ConvexHull3D.Edge e : f.getEdges()) {
+                edgeMap.put(e, f);
+            }
+        }
+
+        //Check if all edges are shared by two facets.
+        for (ConvexHull3D.Facet f : facets) {
+            for (ConvexHull3D.Edge e : f.getEdges()) {
+                assertTrue(edgeMap.containsKey(e.getInverse()));
+            }
+        }
+    }
+
+    private void checkDegenerateHull(ConvexHull3D hull, Collection<Vector3D> 
points) {
+        assertTrue(hull.isDegenerate());
+        assertNull(hull.getRegion());
+        assertTrue(hull.getFacets().isEmpty());
+        List<Vector3D> vertices = hull.getVertices();
+        for (Vector3D p : points) {
+            assertTrue(vertices.contains(p));
+        }
+    }
+
+}
diff --git a/src/main/resources/spotbugs/spotbugs-exclude-filter.xml 
b/src/main/resources/spotbugs/spotbugs-exclude-filter.xml
index 3c953c8e..a6415a84 100644
--- a/src/main/resources/spotbugs/spotbugs-exclude-filter.xml
+++ b/src/main/resources/spotbugs/spotbugs-exclude-filter.xml
@@ -95,5 +95,10 @@
     <Field name="entrySetInstance" />
     <BugPattern name="EI_EXPOSE_REP"/>
   </Match>
+  <Match>
+    <Class 
name="org.apache.commons.geometry.euclidean.threed.hull.ConvexHull3D" />
+    <Field name="facets" />
+    <BugPattern name="EI_EXPOSE_REP"/>
+  </Match>
 
 </FindBugsFilter>


Reply via email to