You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@commons.apache.org by "agoss94 (via GitHub)" <gi...@apache.org> on 2023/10/31 15:23:56 UTC

[PR] GEOMETRY-110 [commons-geometry]

agoss94 opened a new pull request, #225:
URL: https://github.com/apache/commons-geometry/pull/225

   Implementation of a quickhull algorithm with Builder pattern. 


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1421771473


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private Bounds3D box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * 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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            while (hasOutsidePoints()) {
+                Facet conflictFacet = getConflictFacet();

Review Comment:
   I have changed this.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "darkma773r (via GitHub)" <gi...@apache.org>.
darkma773r commented on PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#issuecomment-1866875898

   @agoss94, I just wanted to let you know that I see the work you're doing on this and I will try to get some time soon to look closer at your new set of changes. I appreciate all your hard work!


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1411734726


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private Bounds3D box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * 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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            while (hasOutsidePoints()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getOutsidePoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {

Review Comment:
   Yes, the only reason it isn't right now is because of line 345f. where it is used before creating the facet. I could move this in the Facet class, but this would mean that we have to create a facet first and then determine if we need to reverse the orientation, which would potentially mean that we have to create two objects. 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "darkma773r (via GitHub)" <gi...@apache.org>.
darkma773r commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1398625096


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(facets);

Review Comment:
   There are already a number of exclusions for this bug in `src/main/resources/spotbugs/spotbugs-exclude-filter.xml`. The bug does not apply in this case so we can add an exception.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#issuecomment-1792003699

   Please review the code and let me know if there are any issues.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "darkma773r (via GitHub)" <gi...@apache.org>.
darkma773r commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1382586386


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));

Review Comment:
   This will produce duplicates in the `vertices` list. We should make the vertex list only contain unique points.



##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            box = new Vector3D[6];
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * Appends to the point to the set of possible candidates.
+         *
+         * @param point the given point.
+         * @return this instance.
+         */
+        public Builder append(Vector3D point) {
+            if (box[0] == null) {
+                box[0] = box[1] = box[2] = box[3] = box[4] = box[5] = point;
+                candidates.add(point);
+                return this;
+            }
+            boolean hasBeenModified = false;
+            if (box[0].getX() > point.getX()) {
+                box[0] = point;
+                hasBeenModified = true;
+            }
+            if (box[1].getX() < point.getX()) {
+                box[1] = point;
+                hasBeenModified = true;
+            }
+            if (box[2].getY() > point.getY()) {
+                box[2] = point;
+                hasBeenModified = true;
+            }
+            if (box[3].getY() < point.getY()) {
+                box[3] = point;
+                hasBeenModified = true;
+            }
+            if (box[4].getZ() > point.getZ()) {
+                box[4] = point;
+                hasBeenModified = true;
+            }
+            if (box[5].getZ() < point.getZ()) {
+                box[5] = point;
+                hasBeenModified = true;
+            }
+            candidates.add(point);
+            if (hasBeenModified) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+                simplex = createSimplex(candidates);
+            }
+            distributePoints(simplex.facets());
+            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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            simplex = createSimplex(candidates);
+            distributePoints(simplex.facets());
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.facets().forEach(this::addFacet);
+            distributePoints(simplex.facets());
+            while (isInconflict()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getConflictPoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {
+            return precision.lte(facet.getPlane().offset(point), 0);
+        }
+
+        /**
+         * Returns {@code true} if any of the facets is in conflict.
+         *
+         * @return {@code true} if any of the facets is in conflict.
+         */
+        private boolean isInconflict() {
+            return vertexToFacetMap.values().stream().flatMap(Collection::stream).anyMatch(Facet::isInConflict);
+        }
+
+        /**
+         * Adds the facet for the quickhull algorithm.
+         *
+         * @param facet the given facet.
+         */
+        private void addFacet(Facet facet) {
+            for (Vector3D p : facet.getPolygon().getVertices()) {
+                if (vertexToFacetMap.containsKey(p)) {
+                    Set<Facet> set = vertexToFacetMap.get(p);
+                    set.add(facet);
+                } else {
+                    Set<Facet> set = new HashSet<>(3);
+                    set.add(facet);
+                    vertexToFacetMap.put(p, set);
+                }
+            }
+        }
+
+        /**
+         * Associates each point of the candidates set with an outside set of the given
+         * facets. Afterwards 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();
+            }
+        }
+
+        /**
+         * 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;
+                }
+            }
+        }
+
+        /**
+         * 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 vertexToFacetMap.values().stream().flatMap(Collection::stream).filter(Facet::isInConflict)
+                    .findFirst().get();
+        }
+
+        /**
+         * Adds all visible facets to the provided set.
+         *
+         * @param facet the given conflictFacet.
+         * @param conflictPoint the given conflict point.
+         * @param collector     visible facets are collected in this set.
+         */
+        private void getVisibleFacets(Facet facet, Vector3D conflictPoint, Set<Facet> collector) {
+            if (collector.contains(facet)) {
+                return;
+            }
+
+            // Check the facet and all neighbors.
+            if (!Builder.isInside(facet.getPolygon(), conflictPoint, precision)) {
+                collector.add(facet);
+                findNeighbors(facet).stream().forEach(f -> getVisibleFacets(f, conflictPoint, collector));
+            }
+        }
+
+        /**
+         * Returns a set of all neighbors for the given facet.
+         *
+         * @param facet the given facet.
+         * @return a set of all neighbors.
+         */
+        private Set<Facet> findNeighbors(Facet facet) {
+            List<Vector3D> vertices = facet.getPolygon().getVertices();
+            Set<Facet> neighbors = new HashSet<>();
+            for (int i = 0; i < vertices.size(); i++) {
+                for (int j = i + 1; j < vertices.size(); j++) {
+                    neighbors.addAll(getFacets(vertices.get(i), vertices.get(j)));
+                }
+            }
+            neighbors.remove(facet);
+            return neighbors;
+        }
+
+        /**
+         * Gets all the facets, which have the given point as vertex or {@code null}.
+         *
+         * @param vertex the given point.
+         * @return a set containing all facets with the given vertex.
+         */
+        private Set<Facet> getFacets(Vector3D vertex) {
+            Set<Facet> set = vertexToFacetMap.get(vertex);
+            return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Returns a set of all facets that have the given points as vertices.
+         *
+         * @param first  the first vertex.
+         * @param second the second vertex.
+         * @return a set of all facets that have the given points as vertices.
+         */
+        private Set<Facet> getFacets(Vector3D first, Vector3D second) {
+            Set<Facet> set = new HashSet<>(getFacets(first));
+            set.retainAll(getFacets(second));
+            return Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Finds the horizon of the given set of facets as a set of arrays.
+         *
+         * @param visibleFacets the given set of facets.
+         * @return a set of arrays with size 2.
+         */
+        private Set<Vector3D[]> getHorizon(Set<Facet> visibleFacets) {
+            Set<Vector3D[]> edges = new HashSet<>();
+            for (Facet facet : visibleFacets) {
+                for (Facet neighbor : findNeighbors(facet)) {
+                    if (!visibleFacets.contains(neighbor)) {
+                        edges.add(findEdge(facet, neighbor));
+                    }
+                }
+            }
+            return edges;
+        }
+
+        /**
+         * Finds the two vertices that form the edge between the facet and neighbor
+         * facet..
+         *
+         * @param facet    the given facet.
+         * @param neighbor the neighboring facet.
+         * @return the edge between the two polygons as array.
+         */
+        private Vector3D[] findEdge(Facet facet, Facet neighbor) {
+            List<Vector3D> vertices = new ArrayList<>(facet.getPolygon().getVertices());
+            vertices.retainAll(neighbor.getPolygon().getVertices());
+            // Only two vertices can remain.
+            Vector3D[] edge = {vertices.get(0), vertices.get(1)};
+            return edge;
+        }
+
+        /**
+         * 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.outsideSet()));
+            if (!vertexToFacetMap.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 vertxToFacetMap
+            for (Facet facet : visibleFacets) {
+                for (Vector3D vertex : facet.getPolygon().getVertices()) {
+                    Set<Facet> facets = vertexToFacetMap.get(vertex);
+                    facets.remove(facet);
+                    if (facets.isEmpty()) {
+                        vertexToFacetMap.remove(vertex);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * A facet is a convex polygon with an associated outside set.
+     */
+    private static class Facet {
+
+        /** The polygon of the facet. */
+        private final ConvexPolygon3D polygon;
+
+        /** The outside set of the facet. */
+        private final Set<Vector3D> outsideSet;
+
+        /** Precision context used to compare floating point numbers. */
+        private final DoubleEquivalence precision;
+
+        /**
+         * Constructs a new facet with a the given polygon and an associated empty
+         * outside set.
+         *
+         * @param polygon   the given polygon.
+         * @param precision context used to compare floating point numbers.
+         */
+        Facet(ConvexPolygon3D polygon, DoubleEquivalence precision) {
+            this.polygon = polygon;
+            outsideSet = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+        }
+
+        /**
+         * 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 isInConflict() {
+            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> outsideSet() {
+            return Collections.unmodifiableSet(outsideSet);
+        }
+
+        /**
+         * 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) {
+            return !Builder.isInside(polygon, p, precision) ? outsideSet.add(p) : false;
+        }
+
+        /**
+         * 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 getConflictPoint() {
+            Plane plane = polygon.getPlane();
+            return outsideSet.stream().max((u, v) -> Double.compare(plane.offset(u), plane.offset(v))).get();

Review Comment:
   It might be best to use a standard loop here. As it is, we're computing the plane offset of the current maximum for each comparison.



##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(facets));

Review Comment:
   Is a set necessary here or can we just use a list? We can require that the caller ensure that the facets are unique.



##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;

Review Comment:
   We should be able to replace this with `Bounds3D`. Ex:
   ```java
   private Bounds3D box;
   
   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;
       }
   
       // ...
   }
   ```



##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(facets);

Review Comment:
   Is this necessary? `facets` is already unmodifiable.



##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            box = new Vector3D[6];
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * Appends to the point to the set of possible candidates.
+         *
+         * @param point the given point.
+         * @return this instance.
+         */
+        public Builder append(Vector3D point) {
+            if (box[0] == null) {
+                box[0] = box[1] = box[2] = box[3] = box[4] = box[5] = point;
+                candidates.add(point);
+                return this;
+            }
+            boolean hasBeenModified = false;
+            if (box[0].getX() > point.getX()) {
+                box[0] = point;
+                hasBeenModified = true;
+            }
+            if (box[1].getX() < point.getX()) {
+                box[1] = point;
+                hasBeenModified = true;
+            }
+            if (box[2].getY() > point.getY()) {
+                box[2] = point;
+                hasBeenModified = true;
+            }
+            if (box[3].getY() < point.getY()) {
+                box[3] = point;
+                hasBeenModified = true;
+            }
+            if (box[4].getZ() > point.getZ()) {
+                box[4] = point;
+                hasBeenModified = true;
+            }
+            if (box[5].getZ() < point.getZ()) {
+                box[5] = point;
+                hasBeenModified = true;
+            }
+            candidates.add(point);
+            if (hasBeenModified) {

Review Comment:
   It seems like we have a discrepancy between `append(Vector3D)` and `append(Collection)`. The single point overload uses `box` to detect changes in the min/max values and only rebuilds the simplex if modified whereas the collection overload rebuilds on each call. Why do we have the difference?



##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            box = new Vector3D[6];
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * Appends to the point to the set of possible candidates.
+         *
+         * @param point the given point.
+         * @return this instance.
+         */
+        public Builder append(Vector3D point) {
+            if (box[0] == null) {
+                box[0] = box[1] = box[2] = box[3] = box[4] = box[5] = point;
+                candidates.add(point);
+                return this;
+            }
+            boolean hasBeenModified = false;
+            if (box[0].getX() > point.getX()) {
+                box[0] = point;
+                hasBeenModified = true;
+            }
+            if (box[1].getX() < point.getX()) {
+                box[1] = point;
+                hasBeenModified = true;
+            }
+            if (box[2].getY() > point.getY()) {
+                box[2] = point;
+                hasBeenModified = true;
+            }
+            if (box[3].getY() < point.getY()) {
+                box[3] = point;
+                hasBeenModified = true;
+            }
+            if (box[4].getZ() > point.getZ()) {
+                box[4] = point;
+                hasBeenModified = true;
+            }
+            if (box[5].getZ() < point.getZ()) {
+                box[5] = point;
+                hasBeenModified = true;
+            }
+            candidates.add(point);
+            if (hasBeenModified) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+                simplex = createSimplex(candidates);
+            }
+            distributePoints(simplex.facets());
+            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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            simplex = createSimplex(candidates);
+            distributePoints(simplex.facets());
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.facets().forEach(this::addFacet);
+            distributePoints(simplex.facets());
+            while (isInconflict()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getConflictPoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {
+            return precision.lte(facet.getPlane().offset(point), 0);
+        }
+
+        /**
+         * Returns {@code true} if any of the facets is in conflict.
+         *
+         * @return {@code true} if any of the facets is in conflict.
+         */
+        private boolean isInconflict() {
+            return vertexToFacetMap.values().stream().flatMap(Collection::stream).anyMatch(Facet::isInConflict);
+        }
+
+        /**
+         * Adds the facet for the quickhull algorithm.
+         *
+         * @param facet the given facet.
+         */
+        private void addFacet(Facet facet) {
+            for (Vector3D p : facet.getPolygon().getVertices()) {
+                if (vertexToFacetMap.containsKey(p)) {
+                    Set<Facet> set = vertexToFacetMap.get(p);
+                    set.add(facet);
+                } else {
+                    Set<Facet> set = new HashSet<>(3);
+                    set.add(facet);
+                    vertexToFacetMap.put(p, set);
+                }
+            }
+        }
+
+        /**
+         * Associates each point of the candidates set with an outside set of the given
+         * facets. Afterwards 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();
+            }
+        }
+
+        /**
+         * 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;
+                }
+            }
+        }
+
+        /**
+         * 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 vertexToFacetMap.values().stream().flatMap(Collection::stream).filter(Facet::isInConflict)
+                    .findFirst().get();
+        }
+
+        /**
+         * Adds all visible facets to the provided set.
+         *
+         * @param facet the given conflictFacet.
+         * @param conflictPoint the given conflict point.
+         * @param collector     visible facets are collected in this set.
+         */
+        private void getVisibleFacets(Facet facet, Vector3D conflictPoint, Set<Facet> collector) {
+            if (collector.contains(facet)) {
+                return;
+            }
+
+            // Check the facet and all neighbors.
+            if (!Builder.isInside(facet.getPolygon(), conflictPoint, precision)) {
+                collector.add(facet);
+                findNeighbors(facet).stream().forEach(f -> getVisibleFacets(f, conflictPoint, collector));
+            }
+        }
+
+        /**
+         * Returns a set of all neighbors for the given facet.
+         *
+         * @param facet the given facet.
+         * @return a set of all neighbors.
+         */
+        private Set<Facet> findNeighbors(Facet facet) {
+            List<Vector3D> vertices = facet.getPolygon().getVertices();
+            Set<Facet> neighbors = new HashSet<>();
+            for (int i = 0; i < vertices.size(); i++) {
+                for (int j = i + 1; j < vertices.size(); j++) {
+                    neighbors.addAll(getFacets(vertices.get(i), vertices.get(j)));
+                }
+            }
+            neighbors.remove(facet);
+            return neighbors;
+        }
+
+        /**
+         * Gets all the facets, which have the given point as vertex or {@code null}.
+         *
+         * @param vertex the given point.
+         * @return a set containing all facets with the given vertex.
+         */
+        private Set<Facet> getFacets(Vector3D vertex) {
+            Set<Facet> set = vertexToFacetMap.get(vertex);
+            return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Returns a set of all facets that have the given points as vertices.
+         *
+         * @param first  the first vertex.
+         * @param second the second vertex.
+         * @return a set of all facets that have the given points as vertices.
+         */
+        private Set<Facet> getFacets(Vector3D first, Vector3D second) {
+            Set<Facet> set = new HashSet<>(getFacets(first));
+            set.retainAll(getFacets(second));
+            return Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Finds the horizon of the given set of facets as a set of arrays.
+         *
+         * @param visibleFacets the given set of facets.
+         * @return a set of arrays with size 2.
+         */
+        private Set<Vector3D[]> getHorizon(Set<Facet> visibleFacets) {
+            Set<Vector3D[]> edges = new HashSet<>();
+            for (Facet facet : visibleFacets) {
+                for (Facet neighbor : findNeighbors(facet)) {
+                    if (!visibleFacets.contains(neighbor)) {
+                        edges.add(findEdge(facet, neighbor));
+                    }
+                }
+            }
+            return edges;
+        }
+
+        /**
+         * Finds the two vertices that form the edge between the facet and neighbor
+         * facet..
+         *
+         * @param facet    the given facet.
+         * @param neighbor the neighboring facet.
+         * @return the edge between the two polygons as array.
+         */
+        private Vector3D[] findEdge(Facet facet, Facet neighbor) {
+            List<Vector3D> vertices = new ArrayList<>(facet.getPolygon().getVertices());
+            vertices.retainAll(neighbor.getPolygon().getVertices());
+            // Only two vertices can remain.
+            Vector3D[] edge = {vertices.get(0), vertices.get(1)};
+            return edge;
+        }
+
+        /**
+         * 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.outsideSet()));
+            if (!vertexToFacetMap.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 vertxToFacetMap
+            for (Facet facet : visibleFacets) {
+                for (Vector3D vertex : facet.getPolygon().getVertices()) {
+                    Set<Facet> facets = vertexToFacetMap.get(vertex);
+                    facets.remove(facet);
+                    if (facets.isEmpty()) {
+                        vertexToFacetMap.remove(vertex);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * A facet is a convex polygon with an associated outside set.
+     */
+    private static class Facet {
+
+        /** The polygon of the facet. */
+        private final ConvexPolygon3D polygon;
+
+        /** The outside set of the facet. */
+        private final Set<Vector3D> outsideSet;
+
+        /** Precision context used to compare floating point numbers. */
+        private final DoubleEquivalence precision;
+
+        /**
+         * Constructs a new facet with a the given polygon and an associated empty
+         * outside set.
+         *
+         * @param polygon   the given polygon.
+         * @param precision context used to compare floating point numbers.
+         */
+        Facet(ConvexPolygon3D polygon, DoubleEquivalence precision) {
+            this.polygon = polygon;
+            outsideSet = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+        }
+
+        /**
+         * 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 isInConflict() {
+            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> outsideSet() {
+            return Collections.unmodifiableSet(outsideSet);

Review Comment:
   This is purely an internal class so we can forego the defensive wrapper here.



##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            box = new Vector3D[6];
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * Appends to the point to the set of possible candidates.
+         *
+         * @param point the given point.
+         * @return this instance.
+         */
+        public Builder append(Vector3D point) {
+            if (box[0] == null) {
+                box[0] = box[1] = box[2] = box[3] = box[4] = box[5] = point;
+                candidates.add(point);
+                return this;
+            }
+            boolean hasBeenModified = false;
+            if (box[0].getX() > point.getX()) {
+                box[0] = point;
+                hasBeenModified = true;
+            }
+            if (box[1].getX() < point.getX()) {
+                box[1] = point;
+                hasBeenModified = true;
+            }
+            if (box[2].getY() > point.getY()) {
+                box[2] = point;
+                hasBeenModified = true;
+            }
+            if (box[3].getY() < point.getY()) {
+                box[3] = point;
+                hasBeenModified = true;
+            }
+            if (box[4].getZ() > point.getZ()) {
+                box[4] = point;
+                hasBeenModified = true;
+            }
+            if (box[5].getZ() < point.getZ()) {
+                box[5] = point;
+                hasBeenModified = true;
+            }
+            candidates.add(point);
+            if (hasBeenModified) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+                simplex = createSimplex(candidates);
+            }
+            distributePoints(simplex.facets());
+            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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            simplex = createSimplex(candidates);
+            distributePoints(simplex.facets());
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.facets().forEach(this::addFacet);
+            distributePoints(simplex.facets());
+            while (isInconflict()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getConflictPoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {
+            return precision.lte(facet.getPlane().offset(point), 0);
+        }
+
+        /**
+         * Returns {@code true} if any of the facets is in conflict.
+         *
+         * @return {@code true} if any of the facets is in conflict.
+         */
+        private boolean isInconflict() {
+            return vertexToFacetMap.values().stream().flatMap(Collection::stream).anyMatch(Facet::isInConflict);
+        }
+
+        /**
+         * Adds the facet for the quickhull algorithm.
+         *
+         * @param facet the given facet.
+         */
+        private void addFacet(Facet facet) {
+            for (Vector3D p : facet.getPolygon().getVertices()) {
+                if (vertexToFacetMap.containsKey(p)) {
+                    Set<Facet> set = vertexToFacetMap.get(p);
+                    set.add(facet);
+                } else {
+                    Set<Facet> set = new HashSet<>(3);
+                    set.add(facet);
+                    vertexToFacetMap.put(p, set);
+                }
+            }
+        }
+
+        /**
+         * Associates each point of the candidates set with an outside set of the given
+         * facets. Afterwards 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();
+            }
+        }
+
+        /**
+         * 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;
+                }
+            }
+        }
+
+        /**
+         * 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 vertexToFacetMap.values().stream().flatMap(Collection::stream).filter(Facet::isInConflict)
+                    .findFirst().get();
+        }
+
+        /**
+         * Adds all visible facets to the provided set.
+         *
+         * @param facet the given conflictFacet.
+         * @param conflictPoint the given conflict point.
+         * @param collector     visible facets are collected in this set.
+         */
+        private void getVisibleFacets(Facet facet, Vector3D conflictPoint, Set<Facet> collector) {
+            if (collector.contains(facet)) {
+                return;
+            }
+
+            // Check the facet and all neighbors.
+            if (!Builder.isInside(facet.getPolygon(), conflictPoint, precision)) {
+                collector.add(facet);
+                findNeighbors(facet).stream().forEach(f -> getVisibleFacets(f, conflictPoint, collector));
+            }
+        }
+
+        /**
+         * Returns a set of all neighbors for the given facet.
+         *
+         * @param facet the given facet.
+         * @return a set of all neighbors.
+         */
+        private Set<Facet> findNeighbors(Facet facet) {
+            List<Vector3D> vertices = facet.getPolygon().getVertices();
+            Set<Facet> neighbors = new HashSet<>();
+            for (int i = 0; i < vertices.size(); i++) {
+                for (int j = i + 1; j < vertices.size(); j++) {
+                    neighbors.addAll(getFacets(vertices.get(i), vertices.get(j)));
+                }
+            }
+            neighbors.remove(facet);
+            return neighbors;
+        }
+
+        /**
+         * Gets all the facets, which have the given point as vertex or {@code null}.
+         *
+         * @param vertex the given point.
+         * @return a set containing all facets with the given vertex.
+         */
+        private Set<Facet> getFacets(Vector3D vertex) {
+            Set<Facet> set = vertexToFacetMap.get(vertex);
+            return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Returns a set of all facets that have the given points as vertices.
+         *
+         * @param first  the first vertex.
+         * @param second the second vertex.
+         * @return a set of all facets that have the given points as vertices.
+         */
+        private Set<Facet> getFacets(Vector3D first, Vector3D second) {
+            Set<Facet> set = new HashSet<>(getFacets(first));
+            set.retainAll(getFacets(second));
+            return Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Finds the horizon of the given set of facets as a set of arrays.
+         *
+         * @param visibleFacets the given set of facets.
+         * @return a set of arrays with size 2.
+         */
+        private Set<Vector3D[]> getHorizon(Set<Facet> visibleFacets) {
+            Set<Vector3D[]> edges = new HashSet<>();
+            for (Facet facet : visibleFacets) {
+                for (Facet neighbor : findNeighbors(facet)) {
+                    if (!visibleFacets.contains(neighbor)) {
+                        edges.add(findEdge(facet, neighbor));
+                    }
+                }
+            }
+            return edges;
+        }
+
+        /**
+         * Finds the two vertices that form the edge between the facet and neighbor
+         * facet..
+         *
+         * @param facet    the given facet.
+         * @param neighbor the neighboring facet.
+         * @return the edge between the two polygons as array.
+         */
+        private Vector3D[] findEdge(Facet facet, Facet neighbor) {
+            List<Vector3D> vertices = new ArrayList<>(facet.getPolygon().getVertices());
+            vertices.retainAll(neighbor.getPolygon().getVertices());
+            // Only two vertices can remain.
+            Vector3D[] edge = {vertices.get(0), vertices.get(1)};
+            return edge;
+        }
+
+        /**
+         * 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.outsideSet()));
+            if (!vertexToFacetMap.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 vertxToFacetMap
+            for (Facet facet : visibleFacets) {
+                for (Vector3D vertex : facet.getPolygon().getVertices()) {
+                    Set<Facet> facets = vertexToFacetMap.get(vertex);
+                    facets.remove(facet);
+                    if (facets.isEmpty()) {
+                        vertexToFacetMap.remove(vertex);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * A facet is a convex polygon with an associated outside set.
+     */
+    private static class Facet {
+
+        /** The polygon of the facet. */
+        private final ConvexPolygon3D polygon;
+
+        /** The outside set of the facet. */
+        private final Set<Vector3D> outsideSet;
+
+        /** Precision context used to compare floating point numbers. */
+        private final DoubleEquivalence precision;
+
+        /**
+         * Constructs a new facet with a the given polygon and an associated empty
+         * outside set.
+         *
+         * @param polygon   the given polygon.
+         * @param precision context used to compare floating point numbers.
+         */
+        Facet(ConvexPolygon3D polygon, DoubleEquivalence precision) {
+            this.polygon = polygon;
+            outsideSet = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+        }
+
+        /**
+         * 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 isInConflict() {

Review Comment:
   Could we rename this to something like `hasOutsidePoints`? I would find that easier to understand.



##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            box = new Vector3D[6];
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * Appends to the point to the set of possible candidates.
+         *
+         * @param point the given point.
+         * @return this instance.
+         */
+        public Builder append(Vector3D point) {
+            if (box[0] == null) {
+                box[0] = box[1] = box[2] = box[3] = box[4] = box[5] = point;
+                candidates.add(point);
+                return this;
+            }
+            boolean hasBeenModified = false;
+            if (box[0].getX() > point.getX()) {
+                box[0] = point;
+                hasBeenModified = true;
+            }
+            if (box[1].getX() < point.getX()) {
+                box[1] = point;
+                hasBeenModified = true;
+            }
+            if (box[2].getY() > point.getY()) {
+                box[2] = point;
+                hasBeenModified = true;
+            }
+            if (box[3].getY() < point.getY()) {
+                box[3] = point;
+                hasBeenModified = true;
+            }
+            if (box[4].getZ() > point.getZ()) {
+                box[4] = point;
+                hasBeenModified = true;
+            }
+            if (box[5].getZ() < point.getZ()) {
+                box[5] = point;
+                hasBeenModified = true;
+            }
+            candidates.add(point);
+            if (hasBeenModified) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+                simplex = createSimplex(candidates);
+            }
+            distributePoints(simplex.facets());
+            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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            simplex = createSimplex(candidates);
+            distributePoints(simplex.facets());
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.facets().forEach(this::addFacet);
+            distributePoints(simplex.facets());
+            while (isInconflict()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getConflictPoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {
+            return precision.lte(facet.getPlane().offset(point), 0);
+        }
+
+        /**
+         * Returns {@code true} if any of the facets is in conflict.
+         *
+         * @return {@code true} if any of the facets is in conflict.
+         */
+        private boolean isInconflict() {
+            return vertexToFacetMap.values().stream().flatMap(Collection::stream).anyMatch(Facet::isInConflict);
+        }
+
+        /**
+         * Adds the facet for the quickhull algorithm.
+         *
+         * @param facet the given facet.
+         */
+        private void addFacet(Facet facet) {
+            for (Vector3D p : facet.getPolygon().getVertices()) {
+                if (vertexToFacetMap.containsKey(p)) {
+                    Set<Facet> set = vertexToFacetMap.get(p);
+                    set.add(facet);
+                } else {
+                    Set<Facet> set = new HashSet<>(3);
+                    set.add(facet);
+                    vertexToFacetMap.put(p, set);
+                }
+            }
+        }
+
+        /**
+         * Associates each point of the candidates set with an outside set of the given
+         * facets. Afterwards 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();
+            }
+        }
+
+        /**
+         * 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;
+                }
+            }
+        }
+
+        /**
+         * 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 vertexToFacetMap.values().stream().flatMap(Collection::stream).filter(Facet::isInConflict)
+                    .findFirst().get();
+        }
+
+        /**
+         * Adds all visible facets to the provided set.
+         *
+         * @param facet the given conflictFacet.
+         * @param conflictPoint the given conflict point.
+         * @param collector     visible facets are collected in this set.
+         */
+        private void getVisibleFacets(Facet facet, Vector3D conflictPoint, Set<Facet> collector) {
+            if (collector.contains(facet)) {
+                return;
+            }
+
+            // Check the facet and all neighbors.
+            if (!Builder.isInside(facet.getPolygon(), conflictPoint, precision)) {
+                collector.add(facet);
+                findNeighbors(facet).stream().forEach(f -> getVisibleFacets(f, conflictPoint, collector));
+            }
+        }
+
+        /**
+         * Returns a set of all neighbors for the given facet.
+         *
+         * @param facet the given facet.
+         * @return a set of all neighbors.
+         */
+        private Set<Facet> findNeighbors(Facet facet) {
+            List<Vector3D> vertices = facet.getPolygon().getVertices();
+            Set<Facet> neighbors = new HashSet<>();
+            for (int i = 0; i < vertices.size(); i++) {
+                for (int j = i + 1; j < vertices.size(); j++) {
+                    neighbors.addAll(getFacets(vertices.get(i), vertices.get(j)));
+                }
+            }
+            neighbors.remove(facet);
+            return neighbors;
+        }
+
+        /**
+         * Gets all the facets, which have the given point as vertex or {@code null}.
+         *
+         * @param vertex the given point.
+         * @return a set containing all facets with the given vertex.
+         */
+        private Set<Facet> getFacets(Vector3D vertex) {
+            Set<Facet> set = vertexToFacetMap.get(vertex);
+            return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Returns a set of all facets that have the given points as vertices.
+         *
+         * @param first  the first vertex.
+         * @param second the second vertex.
+         * @return a set of all facets that have the given points as vertices.
+         */
+        private Set<Facet> getFacets(Vector3D first, Vector3D second) {
+            Set<Facet> set = new HashSet<>(getFacets(first));
+            set.retainAll(getFacets(second));
+            return Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Finds the horizon of the given set of facets as a set of arrays.
+         *
+         * @param visibleFacets the given set of facets.
+         * @return a set of arrays with size 2.
+         */
+        private Set<Vector3D[]> getHorizon(Set<Facet> visibleFacets) {
+            Set<Vector3D[]> edges = new HashSet<>();
+            for (Facet facet : visibleFacets) {
+                for (Facet neighbor : findNeighbors(facet)) {
+                    if (!visibleFacets.contains(neighbor)) {
+                        edges.add(findEdge(facet, neighbor));
+                    }
+                }
+            }
+            return edges;
+        }
+
+        /**
+         * Finds the two vertices that form the edge between the facet and neighbor
+         * facet..
+         *
+         * @param facet    the given facet.
+         * @param neighbor the neighboring facet.
+         * @return the edge between the two polygons as array.
+         */
+        private Vector3D[] findEdge(Facet facet, Facet neighbor) {
+            List<Vector3D> vertices = new ArrayList<>(facet.getPolygon().getVertices());
+            vertices.retainAll(neighbor.getPolygon().getVertices());
+            // Only two vertices can remain.
+            Vector3D[] edge = {vertices.get(0), vertices.get(1)};
+            return edge;
+        }
+
+        /**
+         * 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.outsideSet()));
+            if (!vertexToFacetMap.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 vertxToFacetMap
+            for (Facet facet : visibleFacets) {
+                for (Vector3D vertex : facet.getPolygon().getVertices()) {
+                    Set<Facet> facets = vertexToFacetMap.get(vertex);
+                    facets.remove(facet);
+                    if (facets.isEmpty()) {
+                        vertexToFacetMap.remove(vertex);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * A facet is a convex polygon with an associated outside set.
+     */
+    private static class Facet {
+
+        /** The polygon of the facet. */
+        private final ConvexPolygon3D polygon;
+
+        /** The outside set of the facet. */
+        private final Set<Vector3D> outsideSet;
+
+        /** Precision context used to compare floating point numbers. */
+        private final DoubleEquivalence precision;
+
+        /**
+         * Constructs a new facet with a the given polygon and an associated empty
+         * outside set.
+         *
+         * @param polygon   the given polygon.
+         * @param precision context used to compare floating point numbers.
+         */
+        Facet(ConvexPolygon3D polygon, DoubleEquivalence precision) {
+            this.polygon = polygon;
+            outsideSet = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+        }
+
+        /**
+         * 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 isInConflict() {
+            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> outsideSet() {
+            return Collections.unmodifiableSet(outsideSet);
+        }
+
+        /**
+         * 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) {
+            return !Builder.isInside(polygon, p, precision) ? outsideSet.add(p) : false;
+        }
+
+        /**
+         * 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 getConflictPoint() {
+            Plane plane = polygon.getPlane();
+            return outsideSet.stream().max((u, v) -> Double.compare(plane.offset(u), plane.offset(v))).get();
+        }
+    }
+
+    /**
+     * 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> facets() {
+            return Collections.unmodifiableSet(facets);

Review Comment:
   Same as on `Facet`: we can skip the defensive wrapper on internal classes.



##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            box = new Vector3D[6];
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * Appends to the point to the set of possible candidates.
+         *
+         * @param point the given point.
+         * @return this instance.
+         */
+        public Builder append(Vector3D point) {
+            if (box[0] == null) {
+                box[0] = box[1] = box[2] = box[3] = box[4] = box[5] = point;
+                candidates.add(point);
+                return this;
+            }
+            boolean hasBeenModified = false;
+            if (box[0].getX() > point.getX()) {
+                box[0] = point;
+                hasBeenModified = true;
+            }
+            if (box[1].getX() < point.getX()) {
+                box[1] = point;
+                hasBeenModified = true;
+            }
+            if (box[2].getY() > point.getY()) {
+                box[2] = point;
+                hasBeenModified = true;
+            }
+            if (box[3].getY() < point.getY()) {
+                box[3] = point;
+                hasBeenModified = true;
+            }
+            if (box[4].getZ() > point.getZ()) {
+                box[4] = point;
+                hasBeenModified = true;
+            }
+            if (box[5].getZ() < point.getZ()) {
+                box[5] = point;
+                hasBeenModified = true;
+            }
+            candidates.add(point);
+            if (hasBeenModified) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+                simplex = createSimplex(candidates);
+            }
+            distributePoints(simplex.facets());
+            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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            simplex = createSimplex(candidates);
+            distributePoints(simplex.facets());
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.facets().forEach(this::addFacet);
+            distributePoints(simplex.facets());
+            while (isInconflict()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getConflictPoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {
+            return precision.lte(facet.getPlane().offset(point), 0);
+        }
+
+        /**
+         * Returns {@code true} if any of the facets is in conflict.
+         *
+         * @return {@code true} if any of the facets is in conflict.
+         */
+        private boolean isInconflict() {
+            return vertexToFacetMap.values().stream().flatMap(Collection::stream).anyMatch(Facet::isInConflict);
+        }
+
+        /**
+         * Adds the facet for the quickhull algorithm.
+         *
+         * @param facet the given facet.
+         */
+        private void addFacet(Facet facet) {
+            for (Vector3D p : facet.getPolygon().getVertices()) {
+                if (vertexToFacetMap.containsKey(p)) {
+                    Set<Facet> set = vertexToFacetMap.get(p);
+                    set.add(facet);
+                } else {
+                    Set<Facet> set = new HashSet<>(3);
+                    set.add(facet);
+                    vertexToFacetMap.put(p, set);
+                }
+            }
+        }
+
+        /**
+         * Associates each point of the candidates set with an outside set of the given
+         * facets. Afterwards 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();
+            }
+        }
+
+        /**
+         * 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;
+                }
+            }
+        }
+
+        /**
+         * 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 vertexToFacetMap.values().stream().flatMap(Collection::stream).filter(Facet::isInConflict)
+                    .findFirst().get();
+        }
+
+        /**
+         * Adds all visible facets to the provided set.
+         *
+         * @param facet the given conflictFacet.
+         * @param conflictPoint the given conflict point.
+         * @param collector     visible facets are collected in this set.
+         */
+        private void getVisibleFacets(Facet facet, Vector3D conflictPoint, Set<Facet> collector) {
+            if (collector.contains(facet)) {
+                return;
+            }
+
+            // Check the facet and all neighbors.
+            if (!Builder.isInside(facet.getPolygon(), conflictPoint, precision)) {
+                collector.add(facet);
+                findNeighbors(facet).stream().forEach(f -> getVisibleFacets(f, conflictPoint, collector));
+            }
+        }
+
+        /**
+         * Returns a set of all neighbors for the given facet.
+         *
+         * @param facet the given facet.
+         * @return a set of all neighbors.
+         */
+        private Set<Facet> findNeighbors(Facet facet) {
+            List<Vector3D> vertices = facet.getPolygon().getVertices();
+            Set<Facet> neighbors = new HashSet<>();
+            for (int i = 0; i < vertices.size(); i++) {
+                for (int j = i + 1; j < vertices.size(); j++) {
+                    neighbors.addAll(getFacets(vertices.get(i), vertices.get(j)));
+                }
+            }
+            neighbors.remove(facet);
+            return neighbors;
+        }
+
+        /**
+         * Gets all the facets, which have the given point as vertex or {@code null}.
+         *
+         * @param vertex the given point.
+         * @return a set containing all facets with the given vertex.
+         */
+        private Set<Facet> getFacets(Vector3D vertex) {
+            Set<Facet> set = vertexToFacetMap.get(vertex);
+            return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Returns a set of all facets that have the given points as vertices.
+         *
+         * @param first  the first vertex.
+         * @param second the second vertex.
+         * @return a set of all facets that have the given points as vertices.
+         */
+        private Set<Facet> getFacets(Vector3D first, Vector3D second) {
+            Set<Facet> set = new HashSet<>(getFacets(first));
+            set.retainAll(getFacets(second));
+            return Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Finds the horizon of the given set of facets as a set of arrays.
+         *
+         * @param visibleFacets the given set of facets.
+         * @return a set of arrays with size 2.
+         */
+        private Set<Vector3D[]> getHorizon(Set<Facet> visibleFacets) {
+            Set<Vector3D[]> edges = new HashSet<>();
+            for (Facet facet : visibleFacets) {
+                for (Facet neighbor : findNeighbors(facet)) {
+                    if (!visibleFacets.contains(neighbor)) {
+                        edges.add(findEdge(facet, neighbor));
+                    }
+                }
+            }
+            return edges;
+        }
+
+        /**
+         * Finds the two vertices that form the edge between the facet and neighbor
+         * facet..
+         *
+         * @param facet    the given facet.
+         * @param neighbor the neighboring facet.
+         * @return the edge between the two polygons as array.
+         */
+        private Vector3D[] findEdge(Facet facet, Facet neighbor) {
+            List<Vector3D> vertices = new ArrayList<>(facet.getPolygon().getVertices());
+            vertices.retainAll(neighbor.getPolygon().getVertices());
+            // Only two vertices can remain.
+            Vector3D[] edge = {vertices.get(0), vertices.get(1)};
+            return edge;
+        }
+
+        /**
+         * 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.outsideSet()));
+            if (!vertexToFacetMap.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 vertxToFacetMap
+            for (Facet facet : visibleFacets) {
+                for (Vector3D vertex : facet.getPolygon().getVertices()) {
+                    Set<Facet> facets = vertexToFacetMap.get(vertex);
+                    facets.remove(facet);
+                    if (facets.isEmpty()) {
+                        vertexToFacetMap.remove(vertex);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * A facet is a convex polygon with an associated outside set.
+     */
+    private static class Facet {
+
+        /** The polygon of the facet. */
+        private final ConvexPolygon3D polygon;
+
+        /** The outside set of the facet. */
+        private final Set<Vector3D> outsideSet;
+
+        /** Precision context used to compare floating point numbers. */
+        private final DoubleEquivalence precision;
+
+        /**
+         * Constructs a new facet with a the given polygon and an associated empty
+         * outside set.
+         *
+         * @param polygon   the given polygon.
+         * @param precision context used to compare floating point numbers.
+         */
+        Facet(ConvexPolygon3D polygon, DoubleEquivalence precision) {
+            this.polygon = polygon;
+            outsideSet = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+        }
+
+        /**
+         * 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 isInConflict() {
+            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> outsideSet() {
+            return Collections.unmodifiableSet(outsideSet);

Review Comment:
   Also, it seems like the name should be `getOutsideSet()` or `getOutputPoints` for consistency.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1411657541


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private Bounds3D box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * 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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            while (hasOutsidePoints()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getOutsidePoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::stream)
+                    .map(Facet::getPolygon).collect(Collectors.toSet());

Review Comment:
   That schould not be the case since we collect to a set in the end and all polygons are the same since they are handed as a parameter in the constructor.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1398704684


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            box = new Vector3D[6];
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * Appends to the point to the set of possible candidates.
+         *
+         * @param point the given point.
+         * @return this instance.
+         */
+        public Builder append(Vector3D point) {
+            if (box[0] == null) {
+                box[0] = box[1] = box[2] = box[3] = box[4] = box[5] = point;
+                candidates.add(point);
+                return this;
+            }
+            boolean hasBeenModified = false;
+            if (box[0].getX() > point.getX()) {
+                box[0] = point;
+                hasBeenModified = true;
+            }
+            if (box[1].getX() < point.getX()) {
+                box[1] = point;
+                hasBeenModified = true;
+            }
+            if (box[2].getY() > point.getY()) {
+                box[2] = point;
+                hasBeenModified = true;
+            }
+            if (box[3].getY() < point.getY()) {
+                box[3] = point;
+                hasBeenModified = true;
+            }
+            if (box[4].getZ() > point.getZ()) {
+                box[4] = point;
+                hasBeenModified = true;
+            }
+            if (box[5].getZ() < point.getZ()) {
+                box[5] = point;
+                hasBeenModified = true;
+            }
+            candidates.add(point);
+            if (hasBeenModified) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+                simplex = createSimplex(candidates);
+            }
+            distributePoints(simplex.facets());
+            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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            simplex = createSimplex(candidates);
+            distributePoints(simplex.facets());
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.facets().forEach(this::addFacet);
+            distributePoints(simplex.facets());
+            while (isInconflict()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getConflictPoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {
+            return precision.lte(facet.getPlane().offset(point), 0);
+        }
+
+        /**
+         * Returns {@code true} if any of the facets is in conflict.
+         *
+         * @return {@code true} if any of the facets is in conflict.
+         */
+        private boolean isInconflict() {
+            return vertexToFacetMap.values().stream().flatMap(Collection::stream).anyMatch(Facet::isInConflict);
+        }
+
+        /**
+         * Adds the facet for the quickhull algorithm.
+         *
+         * @param facet the given facet.
+         */
+        private void addFacet(Facet facet) {
+            for (Vector3D p : facet.getPolygon().getVertices()) {
+                if (vertexToFacetMap.containsKey(p)) {
+                    Set<Facet> set = vertexToFacetMap.get(p);
+                    set.add(facet);
+                } else {
+                    Set<Facet> set = new HashSet<>(3);
+                    set.add(facet);
+                    vertexToFacetMap.put(p, set);
+                }
+            }
+        }
+
+        /**
+         * Associates each point of the candidates set with an outside set of the given
+         * facets. Afterwards 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();
+            }
+        }
+
+        /**
+         * 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;
+                }
+            }
+        }
+
+        /**
+         * 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 vertexToFacetMap.values().stream().flatMap(Collection::stream).filter(Facet::isInConflict)
+                    .findFirst().get();
+        }
+
+        /**
+         * Adds all visible facets to the provided set.
+         *
+         * @param facet the given conflictFacet.
+         * @param conflictPoint the given conflict point.
+         * @param collector     visible facets are collected in this set.
+         */
+        private void getVisibleFacets(Facet facet, Vector3D conflictPoint, Set<Facet> collector) {
+            if (collector.contains(facet)) {
+                return;
+            }
+
+            // Check the facet and all neighbors.
+            if (!Builder.isInside(facet.getPolygon(), conflictPoint, precision)) {
+                collector.add(facet);
+                findNeighbors(facet).stream().forEach(f -> getVisibleFacets(f, conflictPoint, collector));
+            }
+        }
+
+        /**
+         * Returns a set of all neighbors for the given facet.
+         *
+         * @param facet the given facet.
+         * @return a set of all neighbors.
+         */
+        private Set<Facet> findNeighbors(Facet facet) {
+            List<Vector3D> vertices = facet.getPolygon().getVertices();
+            Set<Facet> neighbors = new HashSet<>();
+            for (int i = 0; i < vertices.size(); i++) {
+                for (int j = i + 1; j < vertices.size(); j++) {
+                    neighbors.addAll(getFacets(vertices.get(i), vertices.get(j)));
+                }
+            }
+            neighbors.remove(facet);
+            return neighbors;
+        }
+
+        /**
+         * Gets all the facets, which have the given point as vertex or {@code null}.
+         *
+         * @param vertex the given point.
+         * @return a set containing all facets with the given vertex.
+         */
+        private Set<Facet> getFacets(Vector3D vertex) {
+            Set<Facet> set = vertexToFacetMap.get(vertex);
+            return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Returns a set of all facets that have the given points as vertices.
+         *
+         * @param first  the first vertex.
+         * @param second the second vertex.
+         * @return a set of all facets that have the given points as vertices.
+         */
+        private Set<Facet> getFacets(Vector3D first, Vector3D second) {
+            Set<Facet> set = new HashSet<>(getFacets(first));
+            set.retainAll(getFacets(second));
+            return Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Finds the horizon of the given set of facets as a set of arrays.
+         *
+         * @param visibleFacets the given set of facets.
+         * @return a set of arrays with size 2.
+         */
+        private Set<Vector3D[]> getHorizon(Set<Facet> visibleFacets) {
+            Set<Vector3D[]> edges = new HashSet<>();
+            for (Facet facet : visibleFacets) {
+                for (Facet neighbor : findNeighbors(facet)) {
+                    if (!visibleFacets.contains(neighbor)) {
+                        edges.add(findEdge(facet, neighbor));
+                    }
+                }
+            }
+            return edges;
+        }
+
+        /**
+         * Finds the two vertices that form the edge between the facet and neighbor
+         * facet..
+         *
+         * @param facet    the given facet.
+         * @param neighbor the neighboring facet.
+         * @return the edge between the two polygons as array.
+         */
+        private Vector3D[] findEdge(Facet facet, Facet neighbor) {
+            List<Vector3D> vertices = new ArrayList<>(facet.getPolygon().getVertices());
+            vertices.retainAll(neighbor.getPolygon().getVertices());
+            // Only two vertices can remain.
+            Vector3D[] edge = {vertices.get(0), vertices.get(1)};
+            return edge;
+        }
+
+        /**
+         * 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.outsideSet()));
+            if (!vertexToFacetMap.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 vertxToFacetMap
+            for (Facet facet : visibleFacets) {
+                for (Vector3D vertex : facet.getPolygon().getVertices()) {
+                    Set<Facet> facets = vertexToFacetMap.get(vertex);
+                    facets.remove(facet);
+                    if (facets.isEmpty()) {
+                        vertexToFacetMap.remove(vertex);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * A facet is a convex polygon with an associated outside set.
+     */
+    private static class Facet {
+
+        /** The polygon of the facet. */
+        private final ConvexPolygon3D polygon;
+
+        /** The outside set of the facet. */
+        private final Set<Vector3D> outsideSet;
+
+        /** Precision context used to compare floating point numbers. */
+        private final DoubleEquivalence precision;
+
+        /**
+         * Constructs a new facet with a the given polygon and an associated empty
+         * outside set.
+         *
+         * @param polygon   the given polygon.
+         * @param precision context used to compare floating point numbers.
+         */
+        Facet(ConvexPolygon3D polygon, DoubleEquivalence precision) {
+            this.polygon = polygon;
+            outsideSet = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+        }
+
+        /**
+         * 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 isInConflict() {
+            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> outsideSet() {
+            return Collections.unmodifiableSet(outsideSet);
+        }
+
+        /**
+         * 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) {
+            return !Builder.isInside(polygon, p, precision) ? outsideSet.add(p) : false;
+        }
+
+        /**
+         * 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 getConflictPoint() {
+            Plane plane = polygon.getPlane();
+            return outsideSet.stream().max((u, v) -> Double.compare(plane.offset(u), plane.offset(v))).get();
+        }
+    }
+
+    /**
+     * 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> facets() {
+            return Collections.unmodifiableSet(facets);

Review Comment:
   Done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1411728680


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;

Review Comment:
   I think I have been under a false assumption. Initially, I wanted to build a convex structure with all the maximum and minimum points, which would have been a double pyramid. I had this still in mind. But still, if I understand the box class correctly we only track one of the maximim and minimum coordinates of all points provided, so the box should be at least a bit bigger.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1389284896


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(facets);

Review Comment:
   It a habit of mine to make stuff unmodifiable as often as possible. I think i overdid it here in some places.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#issuecomment-2070577697

   What is the current state of this PR? Would it be possible to merge what I have done for the time being and to think about Facet merging in a follow up ticket? 


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "darkma773r (via GitHub)" <gi...@apache.org>.
darkma773r commented on PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#issuecomment-1818176294

   @agoss94, I took a closer look at the algorithm code and added some more comments. Let me know what you think!


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1389284291


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(facets));

Review Comment:
   Yes, currently duplicates are already filtered out right at the end of the build method, so the facets should be unique here.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1389338141


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(facets);

Review Comment:
   It also seems like there is bug check which will not allow me to return only the facets here even it is already unmodifiable.  
   `Medium: org.apache.commons.geometry.euclidean.threed.hull.ConvexHull3D.getFacets() may expose internal representation by returning ConvexHull3D.facets [org.apache.commons.geometry.euclidean.threed.hull.ConvexHull3D] At ConvexHull3D.java:[line 107] EI_EXPOSE_REP`
   
   So I could make the facets set in the constructor modifiable, but is seems like the wrapper has to stay here. 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1411734726


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private Bounds3D box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * 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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            while (hasOutsidePoints()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getOutsidePoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {

Review Comment:
   Yes, the only reason it isn't right now is because of line 345f. where it is used before creating the facet. I could move this in the Facet class, but this would mean that we have to create a facet first and then determine if we need to reverse the orientation, which would potentially mean that we have to create two objects. 
   ```
   // Choose the right orientation for all facets.
               facets.add(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
                   new Facet(facet1.reverse(), precision));
               facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
                   new Facet(facet2.reverse(), precision));
               facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
                   new Facet(facet3.reverse(), precision));
               facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
                   new Facet(facet4.reverse(), precision));
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#issuecomment-1868505809

   Thanks @darkma773r I think I am mostly done with all remarks. There is one other thing we should discuss. The original paper [Quickhull](https://dl.acm.org/doi/pdf/10.1145/235815.235821) and this source here [Implementing Quickhull] (http://media.steampowered.com/apps/valve/2014/DirkGregorius_ImplementingQuickHull.pdf) mention facet merging as an error handling strategy. The qhull documentation has similar strategies [Qhull Imprecision](http://www.qhull.org/html/qh-impre.htm).
   
   Currently we joggle the input in some place, but leave all vertices fixed. Also we do not merge any facets as can be seen in the unit cube test. I should at least mention, that most sources assume facet merging to be a superior error handling strategy compared to joggling input. The strategy itself is relatively straight forward to understand although it is not immediately obvious how this would fit into our current implementation. 


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "darkma773r (via GitHub)" <gi...@apache.org>.
darkma773r commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1411519166


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,694 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(facets);

Review Comment:
   We can make this unmodifiable in the constructor. This will likely require another exception in spotbugs.



##########
commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3DTest.java:
##########
@@ -0,0 +1,283 @@
+/*
+ * 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.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.List;
+import java.util.Set;
+
+import org.apache.commons.geometry.euclidean.EuclideanCollections;
+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();
+        assertNotNull(hull);
+        assertNull(hull.getRegion());
+        assertTrue(hull.getFacets().isEmpty());
+        assertTrue(vertices.equals(hull.getVertices()));
+    }
+
+    /**
+     * 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();
+        assertNotNull(hull);
+        assertNull(hull.getRegion());
+        assertTrue(hull.getFacets().isEmpty());
+        List<Vector3D> hullVertices = hull.getVertices();
+        assertEquals(1, hullVertices.size());
+        assertTrue(hullVertices.contains(Vector3D.ZERO));
+    }
+
+    @Test
+    void colinearPoints() {
+        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();
+        assertNotNull(hull);
+        assertNull(hull.getRegion());
+        assertTrue(hull.getFacets().isEmpty());
+        assertTrue(vertices.equals(hull.getVertices()));
+    }
+
+    @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();
+        assertNotNull(hull);
+        assertNull(hull.getRegion());
+        assertTrue(hull.getFacets().isEmpty());
+        assertTrue(vertices.equals(hull.getVertices()));
+    }
+
+    @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();
+        assertNotNull(hull.getRegion());
+        assertTrue(hull.getRegion().contains(Vector3D.ZERO));
+        assertTrue(hull.getRegion().contains(Vector3D.of(1, 0, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 1, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 0, 1)));
+        // The size of the simplex is finite and non zero.
+        assertTrue(TEST_PRECISION.eq(1.0 / 6.0, hull.getRegion().getSize()));
+    }
+
+    @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();
+        assertNotNull(hull.getRegion());
+        assertTrue(hull.getRegion().contains(Vector3D.ZERO));
+        assertTrue(hull.getRegion().contains(Vector3D.of(1, 0, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 1, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 0, 1)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(1, 1, 1)));
+        assertTrue(TEST_PRECISION.eq(1.0 / 2.0, hull.getRegion().getSize()));
+
+        // Check if all facets are connected with neighbors.
+    }
+
+    @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();
+        assertNotNull(hull.getRegion());
+        assertTrue(hull.getRegion().contains(Vector3D.ZERO));
+        assertTrue(hull.getRegion().contains(Vector3D.of(1, 0, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 1, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 0, 1)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(1, 1, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(1, 0, 1)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 1, 1)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(1, 1, 1)));
+        assertTrue(TEST_PRECISION.eq(1.0, hull.getRegion().getSize()));

Review Comment:
   We need to assert all of the algorithm outputs in each one of these tests. This includes the
   - vertices
   - region
   - facets
   - degenerate status



##########
commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3DTest.java:
##########
@@ -0,0 +1,283 @@
+/*
+ * 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.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.List;
+import java.util.Set;
+
+import org.apache.commons.geometry.euclidean.EuclideanCollections;
+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();
+        assertNotNull(hull);
+        assertNull(hull.getRegion());
+        assertTrue(hull.getFacets().isEmpty());
+        assertTrue(vertices.equals(hull.getVertices()));
+    }
+
+    /**
+     * 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();
+        assertNotNull(hull);
+        assertNull(hull.getRegion());
+        assertTrue(hull.getFacets().isEmpty());
+        List<Vector3D> hullVertices = hull.getVertices();
+        assertEquals(1, hullVertices.size());
+        assertTrue(hullVertices.contains(Vector3D.ZERO));
+    }
+
+    @Test
+    void colinearPoints() {
+        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();
+        assertNotNull(hull);
+        assertNull(hull.getRegion());
+        assertTrue(hull.getFacets().isEmpty());
+        assertTrue(vertices.equals(hull.getVertices()));
+    }
+
+    @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();
+        assertNotNull(hull);
+        assertNull(hull.getRegion());
+        assertTrue(hull.getFacets().isEmpty());
+        assertTrue(vertices.equals(hull.getVertices()));
+    }
+
+    @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();
+        assertNotNull(hull.getRegion());
+        assertTrue(hull.getRegion().contains(Vector3D.ZERO));
+        assertTrue(hull.getRegion().contains(Vector3D.of(1, 0, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 1, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 0, 1)));
+        // The size of the simplex is finite and non zero.
+        assertTrue(TEST_PRECISION.eq(1.0 / 6.0, hull.getRegion().getSize()));
+    }
+
+    @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();
+        assertNotNull(hull.getRegion());
+        assertTrue(hull.getRegion().contains(Vector3D.ZERO));
+        assertTrue(hull.getRegion().contains(Vector3D.of(1, 0, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 1, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 0, 1)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(1, 1, 1)));
+        assertTrue(TEST_PRECISION.eq(1.0 / 2.0, hull.getRegion().getSize()));
+
+        // Check if all facets are connected with neighbors.

Review Comment:
   Does this remain to be done?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1429910862


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private Bounds3D box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * 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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            while (hasOutsidePoints()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getOutsidePoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {
+            return precision.lte(facet.getPlane().offset(point), 0);
+        }
+
+        /**
+         * Returns {@code true} if any of the facets has outside points.
+         *
+         * @return {@code true} if any of the facets has outside points.
+         */
+        private boolean hasOutsidePoints() {
+            return vertexToFacetMap.values().stream().flatMap(Collection::stream).anyMatch(Facet::hasOutsidePoints);
+        }
+
+        /**
+         * Adds the facet for the quickhull algorithm.
+         *
+         * @param facet the given facet.
+         */
+        private void addFacet(Facet facet) {
+            for (Vector3D p : facet.getPolygon().getVertices()) {
+                if (vertexToFacetMap.containsKey(p)) {
+                    Set<Facet> set = vertexToFacetMap.get(p);
+                    set.add(facet);
+                } else {
+                    Set<Facet> set = new HashSet<>(3);
+                    set.add(facet);
+                    vertexToFacetMap.put(p, set);
+                }
+            }
+        }
+
+        /**
+         * Associates each point of the candidates set with an outside set of the given
+         * facets. Afterwards 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();
+            }
+        }
+
+        /**
+         * 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;
+                }
+            }
+        }
+
+        /**
+         * 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 vertexToFacetMap.values().stream().flatMap(Collection::stream).filter(Facet::hasOutsidePoints)
+                    .findFirst().get();
+        }
+
+        /**
+         * Adds all visible facets to the provided set.
+         *
+         * @param facet the given conflictFacet.
+         * @param conflictPoint the given conflict point.
+         * @param collector     visible facets are collected in this set.
+         */
+        private void getVisibleFacets(Facet facet, Vector3D conflictPoint, Set<Facet> collector) {
+            if (collector.contains(facet)) {
+                return;
+            }
+
+            // Check the facet and all neighbors.
+            if (!Builder.isInside(facet.getPolygon(), conflictPoint, precision)) {
+                collector.add(facet);
+                findNeighbors(facet).stream().forEach(f -> getVisibleFacets(f, conflictPoint, collector));
+            }
+        }
+
+        /**
+         * Returns a set of all neighbors for the given facet.
+         *
+         * @param facet the given facet.
+         * @return a set of all neighbors.
+         */
+        private Set<Facet> findNeighbors(Facet facet) {

Review Comment:
   Or I could use the orientation as you suggested and get rid of Set in the Map as well. The more I think about it the more I like this solution.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#issuecomment-1825699425

   I think I took a look at every comment so far and made alterations. Are there more remarks on your side? 


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1398127457


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            box = new Vector3D[6];
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * Appends to the point to the set of possible candidates.
+         *
+         * @param point the given point.
+         * @return this instance.
+         */
+        public Builder append(Vector3D point) {
+            if (box[0] == null) {
+                box[0] = box[1] = box[2] = box[3] = box[4] = box[5] = point;
+                candidates.add(point);
+                return this;
+            }
+            boolean hasBeenModified = false;
+            if (box[0].getX() > point.getX()) {
+                box[0] = point;
+                hasBeenModified = true;
+            }
+            if (box[1].getX() < point.getX()) {
+                box[1] = point;
+                hasBeenModified = true;
+            }
+            if (box[2].getY() > point.getY()) {
+                box[2] = point;
+                hasBeenModified = true;
+            }
+            if (box[3].getY() < point.getY()) {
+                box[3] = point;
+                hasBeenModified = true;
+            }
+            if (box[4].getZ() > point.getZ()) {
+                box[4] = point;
+                hasBeenModified = true;
+            }
+            if (box[5].getZ() < point.getZ()) {
+                box[5] = point;
+                hasBeenModified = true;
+            }
+            candidates.add(point);
+            if (hasBeenModified) {

Review Comment:
   To be honest I would keep this different behavior and and document it better. On my machine test were significantly lower with the same beahvior. With the same behavior the test suite takes 6.3s and without it is only 4.9s
   
   <img width="161" alt="image" src="https://github.com/apache/commons-geometry/assets/75903169/3bbda548-739d-4815-aff5-290b60fa034e">
   
   <img width="169" alt="image" src="https://github.com/apache/commons-geometry/assets/75903169/ceae4e3e-ee9c-45be-8030-421922b6933e">
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1398708776


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            box = new Vector3D[6];
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * Appends to the point to the set of possible candidates.
+         *
+         * @param point the given point.
+         * @return this instance.
+         */
+        public Builder append(Vector3D point) {
+            if (box[0] == null) {
+                box[0] = box[1] = box[2] = box[3] = box[4] = box[5] = point;
+                candidates.add(point);
+                return this;
+            }
+            boolean hasBeenModified = false;
+            if (box[0].getX() > point.getX()) {
+                box[0] = point;
+                hasBeenModified = true;
+            }
+            if (box[1].getX() < point.getX()) {
+                box[1] = point;
+                hasBeenModified = true;
+            }
+            if (box[2].getY() > point.getY()) {
+                box[2] = point;
+                hasBeenModified = true;
+            }
+            if (box[3].getY() < point.getY()) {
+                box[3] = point;
+                hasBeenModified = true;
+            }
+            if (box[4].getZ() > point.getZ()) {
+                box[4] = point;
+                hasBeenModified = true;
+            }
+            if (box[5].getZ() < point.getZ()) {
+                box[5] = point;
+                hasBeenModified = true;
+            }
+            candidates.add(point);
+            if (hasBeenModified) {

Review Comment:
   Maybe this is an indication to go for another test while adding points, which more closely resembles the original algorithm. We could collect a certain number of points and then perform the algorithm. After that, we could reset the counter, test the newly added point against the existing structure, and perform the algorithm again after the counter passes the threshold.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1434449271


##########
commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3DTest.java:
##########
@@ -0,0 +1,283 @@
+/*
+ * 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.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.List;
+import java.util.Set;
+
+import org.apache.commons.geometry.euclidean.EuclideanCollections;
+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();
+        assertNotNull(hull);
+        assertNull(hull.getRegion());
+        assertTrue(hull.getFacets().isEmpty());
+        assertTrue(vertices.equals(hull.getVertices()));
+    }
+
+    /**
+     * 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();
+        assertNotNull(hull);
+        assertNull(hull.getRegion());
+        assertTrue(hull.getFacets().isEmpty());
+        List<Vector3D> hullVertices = hull.getVertices();
+        assertEquals(1, hullVertices.size());
+        assertTrue(hullVertices.contains(Vector3D.ZERO));
+    }
+
+    @Test
+    void colinearPoints() {
+        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();
+        assertNotNull(hull);
+        assertNull(hull.getRegion());
+        assertTrue(hull.getFacets().isEmpty());
+        assertTrue(vertices.equals(hull.getVertices()));
+    }
+
+    @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();
+        assertNotNull(hull);
+        assertNull(hull.getRegion());
+        assertTrue(hull.getFacets().isEmpty());
+        assertTrue(vertices.equals(hull.getVertices()));
+    }
+
+    @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();
+        assertNotNull(hull.getRegion());
+        assertTrue(hull.getRegion().contains(Vector3D.ZERO));
+        assertTrue(hull.getRegion().contains(Vector3D.of(1, 0, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 1, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 0, 1)));
+        // The size of the simplex is finite and non zero.
+        assertTrue(TEST_PRECISION.eq(1.0 / 6.0, hull.getRegion().getSize()));
+    }
+
+    @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();
+        assertNotNull(hull.getRegion());
+        assertTrue(hull.getRegion().contains(Vector3D.ZERO));
+        assertTrue(hull.getRegion().contains(Vector3D.of(1, 0, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 1, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 0, 1)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(1, 1, 1)));
+        assertTrue(TEST_PRECISION.eq(1.0 / 2.0, hull.getRegion().getSize()));
+
+        // Check if all facets are connected with neighbors.
+    }
+
+    @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();
+        assertNotNull(hull.getRegion());
+        assertTrue(hull.getRegion().contains(Vector3D.ZERO));
+        assertTrue(hull.getRegion().contains(Vector3D.of(1, 0, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 1, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 0, 1)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(1, 1, 0)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(1, 0, 1)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(0, 1, 1)));
+        assertTrue(hull.getRegion().contains(Vector3D.of(1, 1, 1)));
+        assertTrue(TEST_PRECISION.eq(1.0, hull.getRegion().getSize()));

Review Comment:
   I improved the testing to assert all the outputs.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1432455642


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private Bounds3D box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * 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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            while (hasOutsidePoints()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getOutsidePoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {
+            return precision.lte(facet.getPlane().offset(point), 0);
+        }
+
+        /**
+         * Returns {@code true} if any of the facets has outside points.
+         *
+         * @return {@code true} if any of the facets has outside points.
+         */
+        private boolean hasOutsidePoints() {
+            return vertexToFacetMap.values().stream().flatMap(Collection::stream).anyMatch(Facet::hasOutsidePoints);
+        }
+
+        /**
+         * Adds the facet for the quickhull algorithm.
+         *
+         * @param facet the given facet.
+         */
+        private void addFacet(Facet facet) {
+            for (Vector3D p : facet.getPolygon().getVertices()) {
+                if (vertexToFacetMap.containsKey(p)) {
+                    Set<Facet> set = vertexToFacetMap.get(p);
+                    set.add(facet);
+                } else {
+                    Set<Facet> set = new HashSet<>(3);
+                    set.add(facet);
+                    vertexToFacetMap.put(p, set);
+                }
+            }
+        }
+
+        /**
+         * Associates each point of the candidates set with an outside set of the given
+         * facets. Afterwards 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();
+            }
+        }
+
+        /**
+         * 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;
+                }
+            }
+        }
+
+        /**
+         * 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 vertexToFacetMap.values().stream().flatMap(Collection::stream).filter(Facet::hasOutsidePoints)
+                    .findFirst().get();
+        }
+
+        /**
+         * Adds all visible facets to the provided set.
+         *
+         * @param facet the given conflictFacet.
+         * @param conflictPoint the given conflict point.
+         * @param collector     visible facets are collected in this set.
+         */
+        private void getVisibleFacets(Facet facet, Vector3D conflictPoint, Set<Facet> collector) {
+            if (collector.contains(facet)) {
+                return;
+            }
+
+            // Check the facet and all neighbors.
+            if (!Builder.isInside(facet.getPolygon(), conflictPoint, precision)) {
+                collector.add(facet);
+                findNeighbors(facet).stream().forEach(f -> getVisibleFacets(f, conflictPoint, collector));
+            }
+        }
+
+        /**
+         * Returns a set of all neighbors for the given facet.
+         *
+         * @param facet the given facet.
+         * @return a set of all neighbors.
+         */
+        private Set<Facet> findNeighbors(Facet facet) {

Review Comment:
   I changed the lookup as you suggested. Also I changed `getVisibleFacets` in such a way that it calculates the visible facets and the horizon simultaneously.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "darkma773r (via GitHub)" <gi...@apache.org>.
darkma773r commented on PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#issuecomment-2072383397

   @agoss94, apologies. This completely fell off my radar. I'll need to review this code again to remind myself where we were on this and then we can push this forward. I plan on looking at it this weekend.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1421771828


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private Bounds3D box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * 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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            while (hasOutsidePoints()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getOutsidePoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {
+            return precision.lte(facet.getPlane().offset(point), 0);
+        }
+
+        /**
+         * Returns {@code true} if any of the facets has outside points.
+         *
+         * @return {@code true} if any of the facets has outside points.
+         */
+        private boolean hasOutsidePoints() {
+            return vertexToFacetMap.values().stream().flatMap(Collection::stream).anyMatch(Facet::hasOutsidePoints);
+        }
+
+        /**
+         * Adds the facet for the quickhull algorithm.
+         *
+         * @param facet the given facet.
+         */
+        private void addFacet(Facet facet) {
+            for (Vector3D p : facet.getPolygon().getVertices()) {
+                if (vertexToFacetMap.containsKey(p)) {
+                    Set<Facet> set = vertexToFacetMap.get(p);
+                    set.add(facet);
+                } else {
+                    Set<Facet> set = new HashSet<>(3);
+                    set.add(facet);
+                    vertexToFacetMap.put(p, set);
+                }
+            }
+        }
+
+        /**
+         * Associates each point of the candidates set with an outside set of the given
+         * facets. Afterwards 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();
+            }
+        }
+
+        /**
+         * 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;
+                }
+            }
+        }
+
+        /**
+         * 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 vertexToFacetMap.values().stream().flatMap(Collection::stream).filter(Facet::hasOutsidePoints)
+                    .findFirst().get();
+        }
+
+        /**
+         * Adds all visible facets to the provided set.
+         *
+         * @param facet the given conflictFacet.
+         * @param conflictPoint the given conflict point.
+         * @param collector     visible facets are collected in this set.
+         */
+        private void getVisibleFacets(Facet facet, Vector3D conflictPoint, Set<Facet> collector) {
+            if (collector.contains(facet)) {
+                return;
+            }
+
+            // Check the facet and all neighbors.
+            if (!Builder.isInside(facet.getPolygon(), conflictPoint, precision)) {
+                collector.add(facet);
+                findNeighbors(facet).stream().forEach(f -> getVisibleFacets(f, conflictPoint, collector));
+            }
+        }
+
+        /**
+         * Returns a set of all neighbors for the given facet.
+         *
+         * @param facet the given facet.
+         * @return a set of all neighbors.
+         */
+        private Set<Facet> findNeighbors(Facet facet) {

Review Comment:
   I implemented everything with an `edgeMap`. Instead of `getInverse` I overwrote `hashCode` and `equals` to be orientation agnostic.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1421772046


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private Bounds3D box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * 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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            while (hasOutsidePoints()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getOutsidePoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {

Review Comment:
   I changed the constructor of Facet to handle this logic and moved the method there. Since it was then always negated I also inverted the calculation and renamed the method to `isOutside`



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1389338141


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(facets);

Review Comment:
   It also seems like there is bug check which will not allow me to return only the facets here even it is already unmodifiable.  Medium: org.apache.commons.geometry.euclidean.threed.hull.ConvexHull3D.getFacets() may expose internal representation by returning ConvexHull3D.facets [org.apache.commons.geometry.euclidean.threed.hull.ConvexHull3D] At ConvexHull3D.java:[line 107] EI_EXPOSE_REP
   
   So I could make the facets set in the constructor modifiable, but is seems like the wrapper has to stay here. 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1398704561


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            box = new Vector3D[6];
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * Appends to the point to the set of possible candidates.
+         *
+         * @param point the given point.
+         * @return this instance.
+         */
+        public Builder append(Vector3D point) {
+            if (box[0] == null) {
+                box[0] = box[1] = box[2] = box[3] = box[4] = box[5] = point;
+                candidates.add(point);
+                return this;
+            }
+            boolean hasBeenModified = false;
+            if (box[0].getX() > point.getX()) {
+                box[0] = point;
+                hasBeenModified = true;
+            }
+            if (box[1].getX() < point.getX()) {
+                box[1] = point;
+                hasBeenModified = true;
+            }
+            if (box[2].getY() > point.getY()) {
+                box[2] = point;
+                hasBeenModified = true;
+            }
+            if (box[3].getY() < point.getY()) {
+                box[3] = point;
+                hasBeenModified = true;
+            }
+            if (box[4].getZ() > point.getZ()) {
+                box[4] = point;
+                hasBeenModified = true;
+            }
+            if (box[5].getZ() < point.getZ()) {
+                box[5] = point;
+                hasBeenModified = true;
+            }
+            candidates.add(point);
+            if (hasBeenModified) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+                simplex = createSimplex(candidates);
+            }
+            distributePoints(simplex.facets());
+            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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            simplex = createSimplex(candidates);
+            distributePoints(simplex.facets());
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.facets().forEach(this::addFacet);
+            distributePoints(simplex.facets());
+            while (isInconflict()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getConflictPoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {
+            return precision.lte(facet.getPlane().offset(point), 0);
+        }
+
+        /**
+         * Returns {@code true} if any of the facets is in conflict.
+         *
+         * @return {@code true} if any of the facets is in conflict.
+         */
+        private boolean isInconflict() {
+            return vertexToFacetMap.values().stream().flatMap(Collection::stream).anyMatch(Facet::isInConflict);
+        }
+
+        /**
+         * Adds the facet for the quickhull algorithm.
+         *
+         * @param facet the given facet.
+         */
+        private void addFacet(Facet facet) {
+            for (Vector3D p : facet.getPolygon().getVertices()) {
+                if (vertexToFacetMap.containsKey(p)) {
+                    Set<Facet> set = vertexToFacetMap.get(p);
+                    set.add(facet);
+                } else {
+                    Set<Facet> set = new HashSet<>(3);
+                    set.add(facet);
+                    vertexToFacetMap.put(p, set);
+                }
+            }
+        }
+
+        /**
+         * Associates each point of the candidates set with an outside set of the given
+         * facets. Afterwards 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();
+            }
+        }
+
+        /**
+         * 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;
+                }
+            }
+        }
+
+        /**
+         * 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 vertexToFacetMap.values().stream().flatMap(Collection::stream).filter(Facet::isInConflict)
+                    .findFirst().get();
+        }
+
+        /**
+         * Adds all visible facets to the provided set.
+         *
+         * @param facet the given conflictFacet.
+         * @param conflictPoint the given conflict point.
+         * @param collector     visible facets are collected in this set.
+         */
+        private void getVisibleFacets(Facet facet, Vector3D conflictPoint, Set<Facet> collector) {
+            if (collector.contains(facet)) {
+                return;
+            }
+
+            // Check the facet and all neighbors.
+            if (!Builder.isInside(facet.getPolygon(), conflictPoint, precision)) {
+                collector.add(facet);
+                findNeighbors(facet).stream().forEach(f -> getVisibleFacets(f, conflictPoint, collector));
+            }
+        }
+
+        /**
+         * Returns a set of all neighbors for the given facet.
+         *
+         * @param facet the given facet.
+         * @return a set of all neighbors.
+         */
+        private Set<Facet> findNeighbors(Facet facet) {
+            List<Vector3D> vertices = facet.getPolygon().getVertices();
+            Set<Facet> neighbors = new HashSet<>();
+            for (int i = 0; i < vertices.size(); i++) {
+                for (int j = i + 1; j < vertices.size(); j++) {
+                    neighbors.addAll(getFacets(vertices.get(i), vertices.get(j)));
+                }
+            }
+            neighbors.remove(facet);
+            return neighbors;
+        }
+
+        /**
+         * Gets all the facets, which have the given point as vertex or {@code null}.
+         *
+         * @param vertex the given point.
+         * @return a set containing all facets with the given vertex.
+         */
+        private Set<Facet> getFacets(Vector3D vertex) {
+            Set<Facet> set = vertexToFacetMap.get(vertex);
+            return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Returns a set of all facets that have the given points as vertices.
+         *
+         * @param first  the first vertex.
+         * @param second the second vertex.
+         * @return a set of all facets that have the given points as vertices.
+         */
+        private Set<Facet> getFacets(Vector3D first, Vector3D second) {
+            Set<Facet> set = new HashSet<>(getFacets(first));
+            set.retainAll(getFacets(second));
+            return Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Finds the horizon of the given set of facets as a set of arrays.
+         *
+         * @param visibleFacets the given set of facets.
+         * @return a set of arrays with size 2.
+         */
+        private Set<Vector3D[]> getHorizon(Set<Facet> visibleFacets) {
+            Set<Vector3D[]> edges = new HashSet<>();
+            for (Facet facet : visibleFacets) {
+                for (Facet neighbor : findNeighbors(facet)) {
+                    if (!visibleFacets.contains(neighbor)) {
+                        edges.add(findEdge(facet, neighbor));
+                    }
+                }
+            }
+            return edges;
+        }
+
+        /**
+         * Finds the two vertices that form the edge between the facet and neighbor
+         * facet..
+         *
+         * @param facet    the given facet.
+         * @param neighbor the neighboring facet.
+         * @return the edge between the two polygons as array.
+         */
+        private Vector3D[] findEdge(Facet facet, Facet neighbor) {
+            List<Vector3D> vertices = new ArrayList<>(facet.getPolygon().getVertices());
+            vertices.retainAll(neighbor.getPolygon().getVertices());
+            // Only two vertices can remain.
+            Vector3D[] edge = {vertices.get(0), vertices.get(1)};
+            return edge;
+        }
+
+        /**
+         * 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.outsideSet()));
+            if (!vertexToFacetMap.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 vertxToFacetMap
+            for (Facet facet : visibleFacets) {
+                for (Vector3D vertex : facet.getPolygon().getVertices()) {
+                    Set<Facet> facets = vertexToFacetMap.get(vertex);
+                    facets.remove(facet);
+                    if (facets.isEmpty()) {
+                        vertexToFacetMap.remove(vertex);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * A facet is a convex polygon with an associated outside set.
+     */
+    private static class Facet {
+
+        /** The polygon of the facet. */
+        private final ConvexPolygon3D polygon;
+
+        /** The outside set of the facet. */
+        private final Set<Vector3D> outsideSet;
+
+        /** Precision context used to compare floating point numbers. */
+        private final DoubleEquivalence precision;
+
+        /**
+         * Constructs a new facet with a the given polygon and an associated empty
+         * outside set.
+         *
+         * @param polygon   the given polygon.
+         * @param precision context used to compare floating point numbers.
+         */
+        Facet(ConvexPolygon3D polygon, DoubleEquivalence precision) {
+            this.polygon = polygon;
+            outsideSet = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+        }
+
+        /**
+         * 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 isInConflict() {
+            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> outsideSet() {
+            return Collections.unmodifiableSet(outsideSet);

Review Comment:
   I renamed the method.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "codecov-commenter (via GitHub)" <gi...@apache.org>.
codecov-commenter commented on PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#issuecomment-1793754529

   ## [Codecov](https://app.codecov.io/gh/apache/commons-geometry/pull/225?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
   > Merging [#225](https://app.codecov.io/gh/apache/commons-geometry/pull/225?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) (74fbc17) into [master](https://app.codecov.io/gh/apache/commons-geometry/commit/c73c2ce311a3f6bb71ec39d04b13f4a92d255e06?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) (c73c2ce) will **decrease** coverage by `0.03%`.
   > The diff coverage is `97.52%`.
   
   ```diff
   @@             Coverage Diff              @@
   ##             master     #225      +/-   ##
   ============================================
   - Coverage     98.79%   98.77%   -0.03%     
   - Complexity     3696     3703       +7     
   ============================================
     Files           192      193       +1     
     Lines         10452    10654     +202     
     Branches       1538     1578      +40     
   ============================================
   + Hits          10326    10523     +197     
   - Misses           12       14       +2     
   - Partials        114      117       +3     
   ```
   
   
   | [Files](https://app.codecov.io/gh/apache/commons-geometry/pull/225?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | |
   |---|---|---|
   | [...s/geometry/euclidean/threed/hull/ConvexHull3D.java](https://app.codecov.io/gh/apache/commons-geometry/pull/225?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-Y29tbW9ucy1nZW9tZXRyeS1ldWNsaWRlYW4vc3JjL21haW4vamF2YS9vcmcvYXBhY2hlL2NvbW1vbnMvZ2VvbWV0cnkvZXVjbGlkZWFuL3RocmVlZC9odWxsL0NvbnZleEh1bGwzRC5qYXZh) | `97.52% <97.52%> (ø)` | |
   
   :mega: Codecov offers a browser extension for seamless coverage viewing on GitHub. Try it in [Chrome](https://chrome.google.com/webstore/detail/codecov/gedikamndpbemklijjkncpnolildpbgo) or [Firefox](https://addons.mozilla.org/en-US/firefox/addon/codecov/) today!
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "darkma773r (via GitHub)" <gi...@apache.org>.
darkma773r merged PR #225:
URL: https://github.com/apache/commons-geometry/pull/225


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1398127457


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            box = new Vector3D[6];
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * Appends to the point to the set of possible candidates.
+         *
+         * @param point the given point.
+         * @return this instance.
+         */
+        public Builder append(Vector3D point) {
+            if (box[0] == null) {
+                box[0] = box[1] = box[2] = box[3] = box[4] = box[5] = point;
+                candidates.add(point);
+                return this;
+            }
+            boolean hasBeenModified = false;
+            if (box[0].getX() > point.getX()) {
+                box[0] = point;
+                hasBeenModified = true;
+            }
+            if (box[1].getX() < point.getX()) {
+                box[1] = point;
+                hasBeenModified = true;
+            }
+            if (box[2].getY() > point.getY()) {
+                box[2] = point;
+                hasBeenModified = true;
+            }
+            if (box[3].getY() < point.getY()) {
+                box[3] = point;
+                hasBeenModified = true;
+            }
+            if (box[4].getZ() > point.getZ()) {
+                box[4] = point;
+                hasBeenModified = true;
+            }
+            if (box[5].getZ() < point.getZ()) {
+                box[5] = point;
+                hasBeenModified = true;
+            }
+            candidates.add(point);
+            if (hasBeenModified) {

Review Comment:
   To be honest i would keep this different behavior and and document it better. On my machine test were significantly lower with the same beahvior. With the same behavior the test suite takes 6.3s and without it is only 4.9s
   
   <img width="161" alt="image" src="https://github.com/apache/commons-geometry/assets/75903169/3bbda548-739d-4815-aff5-290b60fa034e">
   
   <img width="169" alt="image" src="https://github.com/apache/commons-geometry/assets/75903169/ceae4e3e-ee9c-45be-8030-421922b6933e">
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1389358540


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            box = new Vector3D[6];
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * Appends to the point to the set of possible candidates.
+         *
+         * @param point the given point.
+         * @return this instance.
+         */
+        public Builder append(Vector3D point) {
+            if (box[0] == null) {
+                box[0] = box[1] = box[2] = box[3] = box[4] = box[5] = point;
+                candidates.add(point);
+                return this;
+            }
+            boolean hasBeenModified = false;
+            if (box[0].getX() > point.getX()) {
+                box[0] = point;
+                hasBeenModified = true;
+            }
+            if (box[1].getX() < point.getX()) {
+                box[1] = point;
+                hasBeenModified = true;
+            }
+            if (box[2].getY() > point.getY()) {
+                box[2] = point;
+                hasBeenModified = true;
+            }
+            if (box[3].getY() < point.getY()) {
+                box[3] = point;
+                hasBeenModified = true;
+            }
+            if (box[4].getZ() > point.getZ()) {
+                box[4] = point;
+                hasBeenModified = true;
+            }
+            if (box[5].getZ() < point.getZ()) {
+                box[5] = point;
+                hasBeenModified = true;
+            }
+            candidates.add(point);
+            if (hasBeenModified) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+                simplex = createSimplex(candidates);
+            }
+            distributePoints(simplex.facets());
+            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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            simplex = createSimplex(candidates);
+            distributePoints(simplex.facets());
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.facets().forEach(this::addFacet);
+            distributePoints(simplex.facets());
+            while (isInconflict()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getConflictPoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {
+            return precision.lte(facet.getPlane().offset(point), 0);
+        }
+
+        /**
+         * Returns {@code true} if any of the facets is in conflict.
+         *
+         * @return {@code true} if any of the facets is in conflict.
+         */
+        private boolean isInconflict() {
+            return vertexToFacetMap.values().stream().flatMap(Collection::stream).anyMatch(Facet::isInConflict);
+        }
+
+        /**
+         * Adds the facet for the quickhull algorithm.
+         *
+         * @param facet the given facet.
+         */
+        private void addFacet(Facet facet) {
+            for (Vector3D p : facet.getPolygon().getVertices()) {
+                if (vertexToFacetMap.containsKey(p)) {
+                    Set<Facet> set = vertexToFacetMap.get(p);
+                    set.add(facet);
+                } else {
+                    Set<Facet> set = new HashSet<>(3);
+                    set.add(facet);
+                    vertexToFacetMap.put(p, set);
+                }
+            }
+        }
+
+        /**
+         * Associates each point of the candidates set with an outside set of the given
+         * facets. Afterwards 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();
+            }
+        }
+
+        /**
+         * 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;
+                }
+            }
+        }
+
+        /**
+         * 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 vertexToFacetMap.values().stream().flatMap(Collection::stream).filter(Facet::isInConflict)
+                    .findFirst().get();
+        }
+
+        /**
+         * Adds all visible facets to the provided set.
+         *
+         * @param facet the given conflictFacet.
+         * @param conflictPoint the given conflict point.
+         * @param collector     visible facets are collected in this set.
+         */
+        private void getVisibleFacets(Facet facet, Vector3D conflictPoint, Set<Facet> collector) {
+            if (collector.contains(facet)) {
+                return;
+            }
+
+            // Check the facet and all neighbors.
+            if (!Builder.isInside(facet.getPolygon(), conflictPoint, precision)) {
+                collector.add(facet);
+                findNeighbors(facet).stream().forEach(f -> getVisibleFacets(f, conflictPoint, collector));
+            }
+        }
+
+        /**
+         * Returns a set of all neighbors for the given facet.
+         *
+         * @param facet the given facet.
+         * @return a set of all neighbors.
+         */
+        private Set<Facet> findNeighbors(Facet facet) {
+            List<Vector3D> vertices = facet.getPolygon().getVertices();
+            Set<Facet> neighbors = new HashSet<>();
+            for (int i = 0; i < vertices.size(); i++) {
+                for (int j = i + 1; j < vertices.size(); j++) {
+                    neighbors.addAll(getFacets(vertices.get(i), vertices.get(j)));
+                }
+            }
+            neighbors.remove(facet);
+            return neighbors;
+        }
+
+        /**
+         * Gets all the facets, which have the given point as vertex or {@code null}.
+         *
+         * @param vertex the given point.
+         * @return a set containing all facets with the given vertex.
+         */
+        private Set<Facet> getFacets(Vector3D vertex) {
+            Set<Facet> set = vertexToFacetMap.get(vertex);
+            return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Returns a set of all facets that have the given points as vertices.
+         *
+         * @param first  the first vertex.
+         * @param second the second vertex.
+         * @return a set of all facets that have the given points as vertices.
+         */
+        private Set<Facet> getFacets(Vector3D first, Vector3D second) {
+            Set<Facet> set = new HashSet<>(getFacets(first));
+            set.retainAll(getFacets(second));
+            return Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Finds the horizon of the given set of facets as a set of arrays.
+         *
+         * @param visibleFacets the given set of facets.
+         * @return a set of arrays with size 2.
+         */
+        private Set<Vector3D[]> getHorizon(Set<Facet> visibleFacets) {
+            Set<Vector3D[]> edges = new HashSet<>();
+            for (Facet facet : visibleFacets) {
+                for (Facet neighbor : findNeighbors(facet)) {
+                    if (!visibleFacets.contains(neighbor)) {
+                        edges.add(findEdge(facet, neighbor));
+                    }
+                }
+            }
+            return edges;
+        }
+
+        /**
+         * Finds the two vertices that form the edge between the facet and neighbor
+         * facet..
+         *
+         * @param facet    the given facet.
+         * @param neighbor the neighboring facet.
+         * @return the edge between the two polygons as array.
+         */
+        private Vector3D[] findEdge(Facet facet, Facet neighbor) {
+            List<Vector3D> vertices = new ArrayList<>(facet.getPolygon().getVertices());
+            vertices.retainAll(neighbor.getPolygon().getVertices());
+            // Only two vertices can remain.
+            Vector3D[] edge = {vertices.get(0), vertices.get(1)};
+            return edge;
+        }
+
+        /**
+         * 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.outsideSet()));
+            if (!vertexToFacetMap.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 vertxToFacetMap
+            for (Facet facet : visibleFacets) {
+                for (Vector3D vertex : facet.getPolygon().getVertices()) {
+                    Set<Facet> facets = vertexToFacetMap.get(vertex);
+                    facets.remove(facet);
+                    if (facets.isEmpty()) {
+                        vertexToFacetMap.remove(vertex);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * A facet is a convex polygon with an associated outside set.
+     */
+    private static class Facet {
+
+        /** The polygon of the facet. */
+        private final ConvexPolygon3D polygon;
+
+        /** The outside set of the facet. */
+        private final Set<Vector3D> outsideSet;
+
+        /** Precision context used to compare floating point numbers. */
+        private final DoubleEquivalence precision;
+
+        /**
+         * Constructs a new facet with a the given polygon and an associated empty
+         * outside set.
+         *
+         * @param polygon   the given polygon.
+         * @param precision context used to compare floating point numbers.
+         */
+        Facet(ConvexPolygon3D polygon, DoubleEquivalence precision) {
+            this.polygon = polygon;
+            outsideSet = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+        }
+
+        /**
+         * 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 isInConflict() {
+            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> outsideSet() {
+            return Collections.unmodifiableSet(outsideSet);
+        }
+
+        /**
+         * 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) {
+            return !Builder.isInside(polygon, p, precision) ? outsideSet.add(p) : false;
+        }
+
+        /**
+         * 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 getConflictPoint() {
+            Plane plane = polygon.getPlane();
+            return outsideSet.stream().max((u, v) -> Double.compare(plane.offset(u), plane.offset(v))).get();

Review Comment:
   I refactored the stream to a normal for loop.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1389268624


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));

Review Comment:
   Yeah, I will correct that. Maybe its best to first collect to a set and then to build a list.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1432459041


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private Bounds3D box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * 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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            while (hasOutsidePoints()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getOutsidePoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {
+            return precision.lte(facet.getPlane().offset(point), 0);
+        }
+
+        /**
+         * Returns {@code true} if any of the facets has outside points.
+         *
+         * @return {@code true} if any of the facets has outside points.
+         */
+        private boolean hasOutsidePoints() {
+            return vertexToFacetMap.values().stream().flatMap(Collection::stream).anyMatch(Facet::hasOutsidePoints);
+        }
+
+        /**
+         * Adds the facet for the quickhull algorithm.
+         *
+         * @param facet the given facet.
+         */
+        private void addFacet(Facet facet) {
+            for (Vector3D p : facet.getPolygon().getVertices()) {
+                if (vertexToFacetMap.containsKey(p)) {
+                    Set<Facet> set = vertexToFacetMap.get(p);
+                    set.add(facet);
+                } else {
+                    Set<Facet> set = new HashSet<>(3);
+                    set.add(facet);
+                    vertexToFacetMap.put(p, set);
+                }
+            }
+        }
+
+        /**
+         * Associates each point of the candidates set with an outside set of the given
+         * facets. Afterwards 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();
+            }
+        }
+
+        /**
+         * 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;
+                }
+            }
+        }
+
+        /**
+         * 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 vertexToFacetMap.values().stream().flatMap(Collection::stream).filter(Facet::hasOutsidePoints)
+                    .findFirst().get();
+        }
+
+        /**
+         * Adds all visible facets to the provided set.
+         *
+         * @param facet the given conflictFacet.
+         * @param conflictPoint the given conflict point.
+         * @param collector     visible facets are collected in this set.
+         */
+        private void getVisibleFacets(Facet facet, Vector3D conflictPoint, Set<Facet> collector) {
+            if (collector.contains(facet)) {
+                return;
+            }
+
+            // Check the facet and all neighbors.
+            if (!Builder.isInside(facet.getPolygon(), conflictPoint, precision)) {
+                collector.add(facet);
+                findNeighbors(facet).stream().forEach(f -> getVisibleFacets(f, conflictPoint, collector));
+            }
+        }
+
+        /**
+         * Returns a set of all neighbors for the given facet.
+         *
+         * @param facet the given facet.
+         * @return a set of all neighbors.
+         */
+        private Set<Facet> findNeighbors(Facet facet) {
+            List<Vector3D> vertices = facet.getPolygon().getVertices();
+            Set<Facet> neighbors = new HashSet<>();
+            for (int i = 0; i < vertices.size(); i++) {
+                for (int j = i + 1; j < vertices.size(); j++) {
+                    neighbors.addAll(getFacets(vertices.get(i), vertices.get(j)));
+                }
+            }
+            neighbors.remove(facet);
+            return neighbors;
+        }
+
+        /**
+         * Gets all the facets, which have the given point as vertex or {@code null}.
+         *
+         * @param vertex the given point.
+         * @return a set containing all facets with the given vertex.
+         */
+        private Set<Facet> getFacets(Vector3D vertex) {
+            Set<Facet> set = vertexToFacetMap.get(vertex);
+            return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Returns a set of all facets that have the given points as vertices.
+         *
+         * @param first  the first vertex.
+         * @param second the second vertex.
+         * @return a set of all facets that have the given points as vertices.
+         */
+        private Set<Facet> getFacets(Vector3D first, Vector3D second) {
+            Set<Facet> set = new HashSet<>(getFacets(first));
+            set.retainAll(getFacets(second));
+            return Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Finds the horizon of the given set of facets as a set of arrays.
+         *
+         * @param visibleFacets the given set of facets.
+         * @return a set of arrays with size 2.
+         */
+        private Set<Vector3D[]> getHorizon(Set<Facet> visibleFacets) {
+            Set<Vector3D[]> edges = new HashSet<>();
+            for (Facet facet : visibleFacets) {
+                for (Facet neighbor : findNeighbors(facet)) {
+                    if (!visibleFacets.contains(neighbor)) {
+                        edges.add(findEdge(facet, neighbor));
+                    }
+                }
+            }
+            return edges;
+        }
+
+        /**
+         * Finds the two vertices that form the edge between the facet and neighbor
+         * facet..
+         *
+         * @param facet    the given facet.
+         * @param neighbor the neighboring facet.
+         * @return the edge between the two polygons as array.
+         */
+        private Vector3D[] findEdge(Facet facet, Facet neighbor) {
+            List<Vector3D> vertices = new ArrayList<>(facet.getPolygon().getVertices());
+            vertices.retainAll(neighbor.getPolygon().getVertices());
+            // Only two vertices can remain.
+            Vector3D[] edge = {vertices.get(0), vertices.get(1)};
+            return edge;
+        }
+
+        /**
+         * 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 (!vertexToFacetMap.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 vertxToFacetMap
+            for (Facet facet : visibleFacets) {
+                for (Vector3D vertex : facet.getPolygon().getVertices()) {
+                    Set<Facet> facets = vertexToFacetMap.get(vertex);
+                    facets.remove(facet);
+                    if (facets.isEmpty()) {
+                        vertexToFacetMap.remove(vertex);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * A facet is a convex polygon with an associated outside set.
+     */
+    private static class Facet {
+
+        /** The polygon of the facet. */
+        private final ConvexPolygon3D polygon;
+
+        /** The outside set of the facet. */
+        private final Set<Vector3D> outsideSet;
+
+        /** Precision context used to compare floating point numbers. */
+        private final DoubleEquivalence precision;
+
+        /**
+         * Constructs a new facet with a the given polygon and an associated empty
+         * outside set.
+         *
+         * @param polygon   the given polygon.
+         * @param precision context used to compare floating point numbers.
+         */
+        Facet(ConvexPolygon3D polygon, DoubleEquivalence precision) {
+            this.polygon = polygon;
+            outsideSet = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+        }
+
+        /**
+         * 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 {@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) {
+            return !Builder.isInside(polygon, p, precision) ? outsideSet.add(p) : false;

Review Comment:
   I changed `isOutside` to `offset` in `Facet` and cache the offset and point with maximal distance.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "darkma773r (via GitHub)" <gi...@apache.org>.
darkma773r commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1411516706


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;

Review Comment:
   Can you explain this geometric difference a bit more?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1411657541


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private Bounds3D box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * 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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            while (hasOutsidePoints()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getOutsidePoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::stream)
+                    .map(Facet::getPolygon).collect(Collectors.toSet());

Review Comment:
   That schould not be the case since we collect to a set in the end and all polygons and facets are the same and schould be filtered out by the set.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1411657541


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private Bounds3D box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * 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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            while (hasOutsidePoints()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getOutsidePoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::stream)
+                    .map(Facet::getPolygon).collect(Collectors.toSet());

Review Comment:
   That schould not be the case since we collect to a set in the end and all polygons and facets are the same and should be filtered out by the set.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1389357864


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            box = new Vector3D[6];
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * Appends to the point to the set of possible candidates.
+         *
+         * @param point the given point.
+         * @return this instance.
+         */
+        public Builder append(Vector3D point) {
+            if (box[0] == null) {
+                box[0] = box[1] = box[2] = box[3] = box[4] = box[5] = point;
+                candidates.add(point);
+                return this;
+            }
+            boolean hasBeenModified = false;
+            if (box[0].getX() > point.getX()) {
+                box[0] = point;
+                hasBeenModified = true;
+            }
+            if (box[1].getX() < point.getX()) {
+                box[1] = point;
+                hasBeenModified = true;
+            }
+            if (box[2].getY() > point.getY()) {
+                box[2] = point;
+                hasBeenModified = true;
+            }
+            if (box[3].getY() < point.getY()) {
+                box[3] = point;
+                hasBeenModified = true;
+            }
+            if (box[4].getZ() > point.getZ()) {
+                box[4] = point;
+                hasBeenModified = true;
+            }
+            if (box[5].getZ() < point.getZ()) {
+                box[5] = point;
+                hasBeenModified = true;
+            }
+            candidates.add(point);
+            if (hasBeenModified) {

Review Comment:
   I thought you would most likely go for this option only if you have most if not all points available beforehand and would most likely reconstruct the simplex anyway. But you are right it would be more consequential to add the same check here as well.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1398708946


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            box = new Vector3D[6];
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * Appends to the point to the set of possible candidates.
+         *
+         * @param point the given point.
+         * @return this instance.
+         */
+        public Builder append(Vector3D point) {
+            if (box[0] == null) {
+                box[0] = box[1] = box[2] = box[3] = box[4] = box[5] = point;
+                candidates.add(point);
+                return this;
+            }
+            boolean hasBeenModified = false;
+            if (box[0].getX() > point.getX()) {
+                box[0] = point;
+                hasBeenModified = true;
+            }
+            if (box[1].getX() < point.getX()) {
+                box[1] = point;
+                hasBeenModified = true;
+            }
+            if (box[2].getY() > point.getY()) {
+                box[2] = point;
+                hasBeenModified = true;
+            }
+            if (box[3].getY() < point.getY()) {
+                box[3] = point;
+                hasBeenModified = true;
+            }
+            if (box[4].getZ() > point.getZ()) {
+                box[4] = point;
+                hasBeenModified = true;
+            }
+            if (box[5].getZ() < point.getZ()) {
+                box[5] = point;
+                hasBeenModified = true;
+            }
+            candidates.add(point);
+            if (hasBeenModified) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+                simplex = createSimplex(candidates);
+            }
+            distributePoints(simplex.facets());
+            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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.facets());
+                simplex.facets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            simplex = createSimplex(candidates);
+            distributePoints(simplex.facets());
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.facets().forEach(this::addFacet);
+            distributePoints(simplex.facets());
+            while (isInconflict()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getConflictPoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {
+            return precision.lte(facet.getPlane().offset(point), 0);
+        }
+
+        /**
+         * Returns {@code true} if any of the facets is in conflict.
+         *
+         * @return {@code true} if any of the facets is in conflict.
+         */
+        private boolean isInconflict() {
+            return vertexToFacetMap.values().stream().flatMap(Collection::stream).anyMatch(Facet::isInConflict);
+        }
+
+        /**
+         * Adds the facet for the quickhull algorithm.
+         *
+         * @param facet the given facet.
+         */
+        private void addFacet(Facet facet) {
+            for (Vector3D p : facet.getPolygon().getVertices()) {
+                if (vertexToFacetMap.containsKey(p)) {
+                    Set<Facet> set = vertexToFacetMap.get(p);
+                    set.add(facet);
+                } else {
+                    Set<Facet> set = new HashSet<>(3);
+                    set.add(facet);
+                    vertexToFacetMap.put(p, set);
+                }
+            }
+        }
+
+        /**
+         * Associates each point of the candidates set with an outside set of the given
+         * facets. Afterwards 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();
+            }
+        }
+
+        /**
+         * 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;
+                }
+            }
+        }
+
+        /**
+         * 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 vertexToFacetMap.values().stream().flatMap(Collection::stream).filter(Facet::isInConflict)
+                    .findFirst().get();
+        }
+
+        /**
+         * Adds all visible facets to the provided set.
+         *
+         * @param facet the given conflictFacet.
+         * @param conflictPoint the given conflict point.
+         * @param collector     visible facets are collected in this set.
+         */
+        private void getVisibleFacets(Facet facet, Vector3D conflictPoint, Set<Facet> collector) {
+            if (collector.contains(facet)) {
+                return;
+            }
+
+            // Check the facet and all neighbors.
+            if (!Builder.isInside(facet.getPolygon(), conflictPoint, precision)) {
+                collector.add(facet);
+                findNeighbors(facet).stream().forEach(f -> getVisibleFacets(f, conflictPoint, collector));
+            }
+        }
+
+        /**
+         * Returns a set of all neighbors for the given facet.
+         *
+         * @param facet the given facet.
+         * @return a set of all neighbors.
+         */
+        private Set<Facet> findNeighbors(Facet facet) {
+            List<Vector3D> vertices = facet.getPolygon().getVertices();
+            Set<Facet> neighbors = new HashSet<>();
+            for (int i = 0; i < vertices.size(); i++) {
+                for (int j = i + 1; j < vertices.size(); j++) {
+                    neighbors.addAll(getFacets(vertices.get(i), vertices.get(j)));
+                }
+            }
+            neighbors.remove(facet);
+            return neighbors;
+        }
+
+        /**
+         * Gets all the facets, which have the given point as vertex or {@code null}.
+         *
+         * @param vertex the given point.
+         * @return a set containing all facets with the given vertex.
+         */
+        private Set<Facet> getFacets(Vector3D vertex) {
+            Set<Facet> set = vertexToFacetMap.get(vertex);
+            return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Returns a set of all facets that have the given points as vertices.
+         *
+         * @param first  the first vertex.
+         * @param second the second vertex.
+         * @return a set of all facets that have the given points as vertices.
+         */
+        private Set<Facet> getFacets(Vector3D first, Vector3D second) {
+            Set<Facet> set = new HashSet<>(getFacets(first));
+            set.retainAll(getFacets(second));
+            return Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Finds the horizon of the given set of facets as a set of arrays.
+         *
+         * @param visibleFacets the given set of facets.
+         * @return a set of arrays with size 2.
+         */
+        private Set<Vector3D[]> getHorizon(Set<Facet> visibleFacets) {
+            Set<Vector3D[]> edges = new HashSet<>();
+            for (Facet facet : visibleFacets) {
+                for (Facet neighbor : findNeighbors(facet)) {
+                    if (!visibleFacets.contains(neighbor)) {
+                        edges.add(findEdge(facet, neighbor));
+                    }
+                }
+            }
+            return edges;
+        }
+
+        /**
+         * Finds the two vertices that form the edge between the facet and neighbor
+         * facet..
+         *
+         * @param facet    the given facet.
+         * @param neighbor the neighboring facet.
+         * @return the edge between the two polygons as array.
+         */
+        private Vector3D[] findEdge(Facet facet, Facet neighbor) {
+            List<Vector3D> vertices = new ArrayList<>(facet.getPolygon().getVertices());
+            vertices.retainAll(neighbor.getPolygon().getVertices());
+            // Only two vertices can remain.
+            Vector3D[] edge = {vertices.get(0), vertices.get(1)};
+            return edge;
+        }
+
+        /**
+         * 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.outsideSet()));
+            if (!vertexToFacetMap.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 vertxToFacetMap
+            for (Facet facet : visibleFacets) {
+                for (Vector3D vertex : facet.getPolygon().getVertices()) {
+                    Set<Facet> facets = vertexToFacetMap.get(vertex);
+                    facets.remove(facet);
+                    if (facets.isEmpty()) {
+                        vertexToFacetMap.remove(vertex);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * A facet is a convex polygon with an associated outside set.
+     */
+    private static class Facet {
+
+        /** The polygon of the facet. */
+        private final ConvexPolygon3D polygon;
+
+        /** The outside set of the facet. */
+        private final Set<Vector3D> outsideSet;
+
+        /** Precision context used to compare floating point numbers. */
+        private final DoubleEquivalence precision;
+
+        /**
+         * Constructs a new facet with a the given polygon and an associated empty
+         * outside set.
+         *
+         * @param polygon   the given polygon.
+         * @param precision context used to compare floating point numbers.
+         */
+        Facet(ConvexPolygon3D polygon, DoubleEquivalence precision) {
+            this.polygon = polygon;
+            outsideSet = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+        }
+
+        /**
+         * 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 isInConflict() {

Review Comment:
   Done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1398704316


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;

Review Comment:
   I used bounds although this will slightly altered the geometric structure in which we look for changes. The initial structure was more like a double pyramid.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1404368676


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,697 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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.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;
+
+/**
+ * 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 Collection<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(facets.stream().flatMap(f -> f.getVertices().stream()).collect(Collectors.toList()));
+        region = ConvexVolume.fromBounds(() -> facets.stream().map(ConvexPolygon3D::getPlane).iterator());
+        this.facets = Collections.unmodifiableSet(new HashSet<>(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.emptySet();
+        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 Collection<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableCollection(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private final Vector3D[] box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            box = new Vector3D[6];
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * Appends to the point to the set of possible candidates.
+         *
+         * @param point the given point.
+         * @return this instance.
+         */
+        public Builder append(Vector3D point) {
+            if (box[0] == null) {
+                box[0] = box[1] = box[2] = box[3] = box[4] = box[5] = point;
+                candidates.add(point);
+                return this;
+            }
+            boolean hasBeenModified = false;
+            if (box[0].getX() > point.getX()) {
+                box[0] = point;
+                hasBeenModified = true;
+            }
+            if (box[1].getX() < point.getX()) {
+                box[1] = point;
+                hasBeenModified = true;
+            }
+            if (box[2].getY() > point.getY()) {
+                box[2] = point;
+                hasBeenModified = true;
+            }
+            if (box[3].getY() < point.getY()) {
+                box[3] = point;
+                hasBeenModified = true;
+            }
+            if (box[4].getZ() > point.getZ()) {
+                box[4] = point;
+                hasBeenModified = true;
+            }
+            if (box[5].getZ() < point.getZ()) {
+                box[5] = point;
+                hasBeenModified = true;
+            }
+            candidates.add(point);
+            if (hasBeenModified) {

Review Comment:
   I added a similar check as in the append(Vector3D) case. After playing around with a few approaches this seemed to be the most performant.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "darkma773r (via GitHub)" <gi...@apache.org>.
darkma773r commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1398626667


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private Bounds3D box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * 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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            while (hasOutsidePoints()) {
+                Facet conflictFacet = getConflictFacet();

Review Comment:
   `hasOutsidePoints()` and `getConflictFacet()` are essentially doing the same thing: iterating through all of the facets and checking if one of them has outside points. The only difference is that `hasOutsidePoints()` returns a boolean and `getConflictFacet()` returns the actual facet. We can remove `hasOutsidePoints()` if we make `getConflictFacet()` return null if no matching facet was found.



##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private Bounds3D box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * 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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            while (hasOutsidePoints()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getOutsidePoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {
+            return precision.lte(facet.getPlane().offset(point), 0);
+        }
+
+        /**
+         * Returns {@code true} if any of the facets has outside points.
+         *
+         * @return {@code true} if any of the facets has outside points.
+         */
+        private boolean hasOutsidePoints() {
+            return vertexToFacetMap.values().stream().flatMap(Collection::stream).anyMatch(Facet::hasOutsidePoints);
+        }
+
+        /**
+         * Adds the facet for the quickhull algorithm.
+         *
+         * @param facet the given facet.
+         */
+        private void addFacet(Facet facet) {
+            for (Vector3D p : facet.getPolygon().getVertices()) {
+                if (vertexToFacetMap.containsKey(p)) {
+                    Set<Facet> set = vertexToFacetMap.get(p);
+                    set.add(facet);
+                } else {
+                    Set<Facet> set = new HashSet<>(3);
+                    set.add(facet);
+                    vertexToFacetMap.put(p, set);
+                }
+            }
+        }
+
+        /**
+         * Associates each point of the candidates set with an outside set of the given
+         * facets. Afterwards 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();
+            }
+        }
+
+        /**
+         * 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;
+                }
+            }
+        }
+
+        /**
+         * 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 vertexToFacetMap.values().stream().flatMap(Collection::stream).filter(Facet::hasOutsidePoints)
+                    .findFirst().get();
+        }
+
+        /**
+         * Adds all visible facets to the provided set.
+         *
+         * @param facet the given conflictFacet.
+         * @param conflictPoint the given conflict point.
+         * @param collector     visible facets are collected in this set.
+         */
+        private void getVisibleFacets(Facet facet, Vector3D conflictPoint, Set<Facet> collector) {
+            if (collector.contains(facet)) {
+                return;
+            }
+
+            // Check the facet and all neighbors.
+            if (!Builder.isInside(facet.getPolygon(), conflictPoint, precision)) {
+                collector.add(facet);
+                findNeighbors(facet).stream().forEach(f -> getVisibleFacets(f, conflictPoint, collector));
+            }
+        }
+
+        /**
+         * Returns a set of all neighbors for the given facet.
+         *
+         * @param facet the given facet.
+         * @return a set of all neighbors.
+         */
+        private Set<Facet> findNeighbors(Facet facet) {

Review Comment:
   Locating neighbors and determining the visible horizon are both operations that use polygon edges. What if we made an `Edge` data structure and performed our lookups using that. Ex:
   ```java
   class Edge {
       private final Vector3D start;
       private final Vector3D end;
   
       Edge(final Vector3D start, final Vector3D end) {
          this.start = start;
          this.end = end;
       }
   
       Edge getInverse() {
           return new Edge(end, start);
       }
   
      // equals, hashCode, etc.
   }
   
   // ...
   
   private final Map<Edge, Facet> edgeMap;
   
   private Set<Facet> findNeighbors(Facet facet) {
       for (Edge edge : facet.getEdges()) {
           Facet neighbor = edgeMap.get(edge.getInverse());
           // ... do something with the neighbor
       }
   }
   ```



##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private Bounds3D box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * 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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            while (hasOutsidePoints()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getOutsidePoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::stream)
+                    .map(Facet::getPolygon).collect(Collectors.toSet());

Review Comment:
   Each facet appears multiple times in `vertexToFacetMap` so `hull` here contains duplicate facets. We need our unit tests to check for this.



##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private Bounds3D box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * 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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            while (hasOutsidePoints()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getOutsidePoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {
+            return precision.lte(facet.getPlane().offset(point), 0);
+        }
+
+        /**
+         * Returns {@code true} if any of the facets has outside points.
+         *
+         * @return {@code true} if any of the facets has outside points.
+         */
+        private boolean hasOutsidePoints() {
+            return vertexToFacetMap.values().stream().flatMap(Collection::stream).anyMatch(Facet::hasOutsidePoints);
+        }
+
+        /**
+         * Adds the facet for the quickhull algorithm.
+         *
+         * @param facet the given facet.
+         */
+        private void addFacet(Facet facet) {
+            for (Vector3D p : facet.getPolygon().getVertices()) {
+                if (vertexToFacetMap.containsKey(p)) {
+                    Set<Facet> set = vertexToFacetMap.get(p);
+                    set.add(facet);
+                } else {
+                    Set<Facet> set = new HashSet<>(3);
+                    set.add(facet);
+                    vertexToFacetMap.put(p, set);
+                }
+            }
+        }
+
+        /**
+         * Associates each point of the candidates set with an outside set of the given
+         * facets. Afterwards 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();
+            }
+        }
+
+        /**
+         * 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;
+                }
+            }
+        }
+
+        /**
+         * 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 vertexToFacetMap.values().stream().flatMap(Collection::stream).filter(Facet::hasOutsidePoints)
+                    .findFirst().get();
+        }
+
+        /**
+         * Adds all visible facets to the provided set.
+         *
+         * @param facet the given conflictFacet.
+         * @param conflictPoint the given conflict point.
+         * @param collector     visible facets are collected in this set.
+         */
+        private void getVisibleFacets(Facet facet, Vector3D conflictPoint, Set<Facet> collector) {
+            if (collector.contains(facet)) {
+                return;
+            }
+
+            // Check the facet and all neighbors.
+            if (!Builder.isInside(facet.getPolygon(), conflictPoint, precision)) {
+                collector.add(facet);
+                findNeighbors(facet).stream().forEach(f -> getVisibleFacets(f, conflictPoint, collector));
+            }
+        }
+
+        /**
+         * Returns a set of all neighbors for the given facet.
+         *
+         * @param facet the given facet.
+         * @return a set of all neighbors.
+         */
+        private Set<Facet> findNeighbors(Facet facet) {
+            List<Vector3D> vertices = facet.getPolygon().getVertices();
+            Set<Facet> neighbors = new HashSet<>();
+            for (int i = 0; i < vertices.size(); i++) {
+                for (int j = i + 1; j < vertices.size(); j++) {
+                    neighbors.addAll(getFacets(vertices.get(i), vertices.get(j)));
+                }
+            }
+            neighbors.remove(facet);
+            return neighbors;
+        }
+
+        /**
+         * Gets all the facets, which have the given point as vertex or {@code null}.
+         *
+         * @param vertex the given point.
+         * @return a set containing all facets with the given vertex.
+         */
+        private Set<Facet> getFacets(Vector3D vertex) {
+            Set<Facet> set = vertexToFacetMap.get(vertex);
+            return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Returns a set of all facets that have the given points as vertices.
+         *
+         * @param first  the first vertex.
+         * @param second the second vertex.
+         * @return a set of all facets that have the given points as vertices.
+         */
+        private Set<Facet> getFacets(Vector3D first, Vector3D second) {
+            Set<Facet> set = new HashSet<>(getFacets(first));
+            set.retainAll(getFacets(second));
+            return Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Finds the horizon of the given set of facets as a set of arrays.
+         *
+         * @param visibleFacets the given set of facets.
+         * @return a set of arrays with size 2.
+         */
+        private Set<Vector3D[]> getHorizon(Set<Facet> visibleFacets) {
+            Set<Vector3D[]> edges = new HashSet<>();
+            for (Facet facet : visibleFacets) {
+                for (Facet neighbor : findNeighbors(facet)) {
+                    if (!visibleFacets.contains(neighbor)) {
+                        edges.add(findEdge(facet, neighbor));
+                    }
+                }
+            }
+            return edges;
+        }
+
+        /**
+         * Finds the two vertices that form the edge between the facet and neighbor
+         * facet..
+         *
+         * @param facet    the given facet.
+         * @param neighbor the neighboring facet.
+         * @return the edge between the two polygons as array.
+         */
+        private Vector3D[] findEdge(Facet facet, Facet neighbor) {
+            List<Vector3D> vertices = new ArrayList<>(facet.getPolygon().getVertices());
+            vertices.retainAll(neighbor.getPolygon().getVertices());
+            // Only two vertices can remain.
+            Vector3D[] edge = {vertices.get(0), vertices.get(1)};
+            return edge;
+        }
+
+        /**
+         * 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 (!vertexToFacetMap.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 vertxToFacetMap
+            for (Facet facet : visibleFacets) {
+                for (Vector3D vertex : facet.getPolygon().getVertices()) {
+                    Set<Facet> facets = vertexToFacetMap.get(vertex);
+                    facets.remove(facet);
+                    if (facets.isEmpty()) {
+                        vertexToFacetMap.remove(vertex);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * A facet is a convex polygon with an associated outside set.
+     */
+    private static class Facet {
+
+        /** The polygon of the facet. */
+        private final ConvexPolygon3D polygon;
+
+        /** The outside set of the facet. */
+        private final Set<Vector3D> outsideSet;
+
+        /** Precision context used to compare floating point numbers. */
+        private final DoubleEquivalence precision;
+
+        /**
+         * Constructs a new facet with a the given polygon and an associated empty
+         * outside set.
+         *
+         * @param polygon   the given polygon.
+         * @param precision context used to compare floating point numbers.
+         */
+        Facet(ConvexPolygon3D polygon, DoubleEquivalence precision) {
+            this.polygon = polygon;
+            outsideSet = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+        }
+
+        /**
+         * 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 {@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) {
+            return !Builder.isInside(polygon, p, precision) ? outsideSet.add(p) : false;

Review Comment:
   We compute the offset from the plane as points are added. We could store the point with the maximum offset from the plane while we're doing that and save having to recompute everything later in `getOutsidePoint`.



##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private Bounds3D box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * 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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            while (hasOutsidePoints()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getOutsidePoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {

Review Comment:
   This feels like it should be a method of `Facet`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "darkma773r (via GitHub)" <gi...@apache.org>.
darkma773r commented on PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#issuecomment-1835289653

   Sorry, @agoss94. I added some more comments a while back but I guess I didn't hit "submit" on my review so they didn't get posted. I've submitted them now.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] GEOMETRY-110 [commons-geometry]

Posted by "agoss94 (via GitHub)" <gi...@apache.org>.
agoss94 commented on code in PR #225:
URL: https://github.com/apache/commons-geometry/pull/225#discussion_r1432456940


##########
commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/hull/ConvexHull3D.java:
##########
@@ -0,0 +1,685 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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;
+
+/**
+ * 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 = 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<? extends ConvexPolygon3D> getFacets() {
+        return Collections.unmodifiableList(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;
+
+        /** Simplex for testing new points and starting the algorithm. */
+        private Simplex simplex;
+
+        /** The minX, maxX, minY, maxY, minZ, maxZ points. */
+        private Bounds3D box;
+
+        /**
+         * A map which contains all the vertices of the current hull as keys and the
+         * associated facets as values.
+         */
+        private Map<Vector3D, Set<Facet>> vertexToFacetMap;
+
+        /**
+         * Constructor for a builder with the given precision.
+         *
+         * @param precision the given precision.
+         */
+        public Builder(DoubleEquivalence precision) {
+            candidates = EuclideanCollections.pointSet3D(precision);
+            this.precision = precision;
+            vertexToFacetMap = EuclideanCollections.pointMap3D(precision);
+            simplex = new Simplex(Collections.emptySet());
+        }
+
+        /**
+         * 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) {
+            if (simplex != null) {
+                // Remove all outside Points and add all vertices again.
+                removeFacets(simplex.getFacets());
+                simplex.getFacets().stream().map(Facet::getPolygon).forEach(p -> candidates.addAll(p.getVertices()));
+            }
+            candidates.addAll(points);
+            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);
+            }
+
+            vertexToFacetMap = new HashMap<>();
+            simplex.getFacets().forEach(this::addFacet);
+            distributePoints(simplex.getFacets());
+            while (hasOutsidePoints()) {
+                Facet conflictFacet = getConflictFacet();
+                Vector3D conflictPoint = conflictFacet.getOutsidePoint();
+                Set<Facet> visibleFacets = new HashSet<>();
+                getVisibleFacets(conflictFacet, conflictPoint, visibleFacets);
+                Set<Vector3D[]> horizon = getHorizon(visibleFacets);
+                Vector3D referencePoint = conflictFacet.getPolygon().getCentroid();
+                Set<Facet> cone = constructCone(conflictPoint, horizon, referencePoint);
+                removeFacets(visibleFacets);
+                cone.forEach(this::addFacet);
+                distributePoints(cone);
+            }
+            Collection<ConvexPolygon3D> hull = vertexToFacetMap.values().stream().flatMap(Collection::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<Vector3D[]> horizon, Vector3D referencePoint) {
+            Set<Facet> newFacets = new HashSet<>();
+            for (Vector3D[] edge : horizon) {
+                ConvexPolygon3D newFacet = Planes
+                        .convexPolygonFromVertices(Arrays.asList(edge[0], edge[1], conflictPoint), precision);
+                if (!isInside(newFacet, referencePoint, precision)) {
+                    newFacet = newFacet.reverse();
+                }
+                newFacets.add(new Facet(newFacet, 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((u, v) -> Double.compare(vertex1.distance(u), vertex1.distance(v)))
+                    .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((u, v) -> Double.compare(line.distance(u), line.distance(v))).get();
+
+            // The point set is degenerate because all points are colinear.
+            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((u, v) -> Double.compare(Math.abs(plane.offset(u)), Math.abs(plane.offset(v)))).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(isInside(facet1, vertex4, precision) ? new Facet(facet1, precision) :
+                new Facet(facet1.reverse(), precision));
+            facets.add(isInside(facet2, vertex3, precision) ? new Facet(facet2, precision) :
+                new Facet(facet2.reverse(), precision));
+            facets.add(isInside(facet3, vertex2, precision) ? new Facet(facet3, precision) :
+                new Facet(facet3.reverse(), precision));
+            facets.add(isInside(facet4, vertex1, precision) ? new Facet(facet4, precision) :
+                new Facet(facet4.reverse(), precision));
+
+            return new Simplex(facets);
+        }
+
+        /**
+         * Returns {@code true} if the given point resides inside the negative
+         * half-space of the oriented facet. Points which are coplanar are also assumed
+         * to be inside. Mathematically a point is inside if the calculated oriented
+         * offset, of the point to the hyperplane is less than or equal to zero.
+         *
+         * @param facet     the given facet.
+         * @param point     a reference point.
+         * @param precision the given precision.
+         * @return {@code true} if the given point resides inside the negative
+         *         half-space.
+         */
+        private static boolean isInside(ConvexPolygon3D facet, Vector3D point, DoubleEquivalence precision) {

Review Comment:
   The method is no longer present. Due to other changes it is no an offset method in the Facet class.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@commons.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org