View Javadoc
1   // ******************************************************************************
2   //
3   // Title:       Force Field X.
4   // Description: Force Field X - Software for Molecular Biophysics.
5   // Copyright:   Copyright (c) Michael J. Schnieders 2001-2025.
6   //
7   // This file is part of Force Field X.
8   //
9   // Force Field X is free software; you can redistribute it and/or modify it
10  // under the terms of the GNU General Public License version 3 as published by
11  // the Free Software Foundation.
12  //
13  // Force Field X is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16  // details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // Force Field X; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA 02111-1307 USA
21  //
22  // Linking this library statically or dynamically with other modules is making a
23  // combined work based on this library. Thus, the terms and conditions of the
24  // GNU General Public License cover the whole combination.
25  //
26  // As a special exception, the copyright holders of this library give you
27  // permission to link this library with independent modules to produce an
28  // executable, regardless of the license terms of these independent modules, and
29  // to copy and distribute the resulting executable under terms of your choice,
30  // provided that you also meet, for each linked independent module, the terms
31  // and conditions of the license of that module. An independent module is a
32  // module which is not derived from or based on this library. If you modify this
33  // library, you may extend this exception to your version of the library, but
34  // you are not obligated to do so. If you do not wish to do so, delete this
35  // exception statement from your version.
36  //
37  // ******************************************************************************
38  package ffx.numerics.quickhull;
39  
40  import java.io.PrintStream;
41  import java.util.Arrays;
42  import java.util.Iterator;
43  import java.util.Vector;
44  import java.util.logging.Level;
45  import java.util.logging.Logger;
46  
47  /**
48   * Computes the convex hull of a set of three dimensional points.
49   * <p>
50   * The algorithm is a three dimensional implementation of Quickhull, as
51   * described in Barber, Dobkin, and Huhdanpaa, <a
52   * href=http://citeseer.ist.psu.edu/barber96quickhull.html> ``The Quickhull
53   * Algorithm for Convex Hulls''</a> (ACM Transactions on Mathematical Software,
54   * Vol. 22, No. 4, December 1996), and has a complexity of O(n log(n)) with
55   * respect to the number of points. A well-known C implementation of Quickhull
56   * that works for arbitrary dimensions is provided by <a
57   * href=http://www.qhull.org>qhull</a>.
58   * <p>
59   * A hull is constructed by providing a set of points to either a constructor or
60   * a {@link #build(Point3d[]) build} method. After the hull is built, its
61   * vertices and faces can be retrieved using {@link #getVertices() getVertices}
62   * and {@link #getFaces() getFaces}. A typical usage might look like this:
63   *
64   * <pre>
65   * // x y z coordinates of 6 points
66   * Point3d[] points = new Point3d[]{
67   *     new Point3d(0.0, 0.0, 0.0),
68   *     new Point3d(1.0, 0.5, 0.0),
69   *     new Point3d(2.0, 0.0, 0.0),
70   *     new Point3d(0.5, 0.5, 0.5),
71   *     new Point3d(0.0, 0.0, 2.0),
72   *     new Point3d(0.1, 0.2, 0.3),
73   *     new Point3d(0.0, 2.0, 0.0),
74   * };
75   *
76   * QuickHull3D hull = new QuickHull3D();
77   * hull.build(points);
78   *
79   * System.out.println(&quot;Vertices:&quot;);
80   * Point3d[] vertices = hull.getVertices();
81   * for (int i = 0; i &lt; vertices.length; i++) {
82   *     Point3d pnt = vertices[i];
83   *     System.out.println(pnt.x + &quot; &quot; + pnt.y + &quot; &quot; + pnt.z);
84   * }
85   *
86   * System.out.println(&quot;Faces:&quot;);
87   * int[][] faceIndices = hull.getFaces();
88   * for (int i = 0; i &lt; faceIndices.length; i++) {
89   *     for (int k = 0; k &lt; faceIndices[i].length; k++) {
90   *         System.out.print(faceIndices[i][k] + &quot; &quot;);
91   *     }
92   *     System.out.println(&quot;&quot;);
93   * }
94   * </pre>
95   * <p>
96   * As a convenience, there are also {@link #build(double[]) build} and
97   * {@link #getVertices(double[]) getVertex} methods which pass point information
98   * using an array of doubles.
99   * <p>
100  * <b>Robustness</b>
101  * <p>
102  * Because this algorithm uses floating
103  * point arithmetic, it is potentially vulnerable to errors arising from
104  * numerical imprecision. We address this problem in the same way as <a>
105  * href=http://www.qhull.org>qhull</a>, by merging faces whose edges are not
106  * clearly convex. A face is convex if its edges are convex, and an edge is
107  * convex if the centroid of each adjacent plane is clearly <i>below</i> the
108  * plane of the other face. The centroid is considered below a plane if its
109  * distance to the plane is less than the negative of a
110  * {@link #getDistanceTolerance() distance tolerance}. This tolerance represents
111  * the smallest distance that can be reliably computed within the available
112  * numeric precision. It is normally computed automatically from the point data,
113  * although an application may {@link #setExplicitDistanceTolerance set this
114  * tolerance explicitly}.
115  * <p>
116  * Numerical problems are more likely to arise in situations where data points
117  * lie on or within the faces or edges of the convex hull. We have tested
118  * QuickHull3D for such situations by computing the convex hull of a random
119  * point set, then adding additional randomly chosen points which lie very close
120  * to the hull vertices and edges, and computing the convex hull again. The hull
121  * is deemed correct if {@link #check check} returns <code>true</code>. These
122  * tests have been successful for a large number of trials and so we are
123  * confident that QuickHull3D is reasonably robust.
124  * <p>
125  * <b>Merged Faces</b>
126  * <p>The merging of faces means that the faces returned by
127  * QuickHull3D may be convex polygons instead of triangles. If triangles are
128  * desired, the application may {@link #triangulate triangulate} the faces, but
129  * it should be noted that this may result in triangles which are very small or
130  * thin and hence difficult to perform reliable convexity tests on. In other
131  * words, triangulating a merged face is likely to restore the numerical
132  * problems which the merging process removed. Hence is it possible that, after
133  * triangulation, {@link #check check} will fail (the same behavior is observed
134  * with triangulated output from <a href=http://www.qhull.org>qhull</a>).
135  * <p>
136  * <b>Degenerate Input</b>
137  * <p>
138  * It is assumed that the input points are
139  * non-degenerate in that they are not coincident, colinear, or colplanar, and
140  * thus the convex hull has a non-zero volume. If the input points are detected
141  * to be degenerate within the {@link #getDistanceTolerance() distance
142  * tolerance}, an IllegalArgumentException will be thrown.
143  *
144  * @author John E. Lloyd, Fall 2004
145  * @author Michael J. Schnieders
146  * @since 1.0
147  */
148 public class QuickHull3D {
149 
150   /**
151    * Logger to log to.
152    */
153   private static final Logger logger = Logger.getLogger(QuickHull3D.class.getName());
154 
155   /**
156    * Specifies that (on output) vertex indices for a face should be listed in
157    * clockwise order.
158    */
159   public static final int CLOCKWISE = 0x1;
160 
161   /**
162    * Specifies that (on output) the vertex indices for a face should be
163    * numbered starting from 1.
164    */
165   public static final int INDEXED_FROM_ONE = 0x2;
166 
167   /**
168    * Specifies that (on output) the vertex indices for a face should be
169    * numbered starting from 0.
170    */
171   public static final int INDEXED_FROM_ZERO = 0x4;
172 
173   /**
174    * Specifies that (on output) the vertex indices for a face should be
175    * numbered with respect to the original input points.
176    */
177   public static final int POINT_RELATIVE = 0x8;
178 
179   /**
180    * Specifies that the distance tolerance should be computed automatically
181    * from the input point data.
182    */
183   public static final double AUTOMATIC_TOLERANCE = -1;
184 
185   protected int findIndex = -1;
186 
187   // estimated size of the point set
188   protected double charLength;
189 
190   protected Vertex[] pointBuffer = new Vertex[0];
191 
192   protected int[] vertexPointIndices = new int[0];
193 
194   private Face[] discardedFaces = new Face[3];
195 
196   private Vertex[] maxVtxs = new Vertex[3];
197 
198   private Vertex[] minVtxs = new Vertex[3];
199 
200   protected Vector<Face> faces = new Vector<>(16);
201 
202   protected Vector<HalfEdge> horizon = new Vector<>(16);
203 
204   private FaceList newFaces = new FaceList();
205 
206   private VertexList unclaimed = new VertexList();
207 
208   private VertexList claimed = new VertexList();
209 
210   protected int numVertices;
211 
212   protected int numFaces;
213 
214   protected int numPoints;
215 
216   protected double explicitTolerance = AUTOMATIC_TOLERANCE;
217 
218   protected double tolerance;
219 
220   /**
221    * Precision of a double.
222    */
223   private static final double DOUBLE_PREC = 2.2204460492503131e-16;
224 
225   /**
226    * Returns the distance tolerance that was used for the most recently
227    * computed hull. The distance tolerance is used to determine when faces are
228    * unambiguously convex with respect to each other, and when points are
229    * unambiguously above or below a face plane, in the presence of <a
230    * href=#distTol>numerical imprecision</a>. Normally, this tolerance is
231    * computed automatically for each set of input points, but it can be set
232    * explicitly by the application.
233    *
234    * @return distance tolerance
235    * @see QuickHull3D#setExplicitDistanceTolerance
236    */
237   public double getDistanceTolerance() {
238     return tolerance;
239   }
240 
241   /**
242    * Sets an explicit distance tolerance for convexity tests. If
243    * {@link #AUTOMATIC_TOLERANCE AUTOMATIC_TOLERANCE} is specified (the
244    * default), then the tolerance will be computed automatically from the
245    * point data.
246    *
247    * @param tol explicit tolerance
248    * @see #getDistanceTolerance
249    */
250   public void setExplicitDistanceTolerance(double tol) {
251     explicitTolerance = tol;
252   }
253 
254   /**
255    * Returns the explicit distance tolerance.
256    *
257    * @return explicit tolerance
258    * @see #setExplicitDistanceTolerance
259    */
260   public double getExplicitDistanceTolerance() {
261     return explicitTolerance;
262   }
263 
264   private void addPointToFace(Vertex vtx, Face face) {
265     vtx.face = face;
266 
267     if (face.outside == null) {
268       claimed.add(vtx);
269     } else {
270       claimed.insertBefore(vtx, face.outside);
271     }
272     face.outside = vtx;
273   }
274 
275   private void removePointFromFace(Vertex vtx, Face face) {
276     if (vtx == face.outside) {
277       if (vtx.next != null && vtx.next.face == face) {
278         face.outside = vtx.next;
279       } else {
280         face.outside = null;
281       }
282     }
283     claimed.delete(vtx);
284   }
285 
286   private Vertex removeAllPointsFromFace(Face face) {
287     if (face.outside != null) {
288       Vertex end = face.outside;
289       while (end.next != null && end.next.face == face) {
290         end = end.next;
291       }
292       claimed.delete(face.outside, end);
293       end.next = null;
294       return face.outside;
295     } else {
296       return null;
297     }
298   }
299 
300   /**
301    * Creates an empty convex hull object.
302    */
303   public QuickHull3D() {
304   }
305 
306   /**
307    * Creates a convex hull object and initializes it to the convex hull of a
308    * set of points whose coordinates are given by an array of doubles.
309    *
310    * @param coords x, y, and z coordinates of each input point. The length of
311    *               this array will be three times the the number of input points.
312    * @throws IllegalArgumentException the number of input points is less than four, or the points
313    *                                  appear to be coincident, colinear, or coplanar.
314    */
315   public QuickHull3D(double[] coords) throws IllegalArgumentException {
316     build(coords, coords.length / 3);
317   }
318 
319   /**
320    * Creates a convex hull object and initializes it to the convex hull of a
321    * set of points.
322    *
323    * @param points input points.
324    * @throws IllegalArgumentException the number of input points is less than four, or the points
325    *                                  appear to be coincident, colinear, or coplanar.
326    */
327   public QuickHull3D(Point3d[] points) throws IllegalArgumentException {
328     build(points, points.length);
329   }
330 
331   /**
332    * Finds the half-edge within the current hull whose tail/head match the given vertices.
333    * Used during setHull to establish opposite half-edge links between faces.
334    *
335    * @param tail the tail vertex of the desired half-edge
336    * @param head the head vertex of the desired half-edge
337    * @return the matching HalfEdge, or null if none is found
338    */
339   private HalfEdge findHalfEdge(Vertex tail, Vertex head) {
340     // brute force ... OK, since setHull is not used much
341     for (Face face : faces) {
342       HalfEdge he = face.findEdge(tail, head);
343       if (he != null) {
344         return he;
345       }
346     }
347     return null;
348   }
349 
350   /**
351    * Initializes the hull from precomputed face indices and point coordinates.
352    * This is primarily used for testing or reconstructing a hull state.
353    *
354    * @param coords      flat array of x,y,z coordinates (length >= 3*nump)
355    * @param nump        number of points in coords
356    * @param faceIndices face vertex indices (each row lists vertices of a face in CCW order)
357    * @param numf        number of faces to add
358    */
359   protected void setHull(double[] coords, int nump, int[][] faceIndices, int numf) {
360     initBuffers(nump);
361     setPoints(coords, nump);
362     computeMaxAndMin();
363     for (int i = 0; i < numf; i++) {
364       Face face = Face.create(pointBuffer, faceIndices[i]);
365       HalfEdge he = face.he0;
366       do {
367         HalfEdge heOpp = findHalfEdge(he.head(), he.tail());
368         if (heOpp != null) {
369           he.setOpposite(heOpp);
370         }
371         he = he.next;
372       } while (he != face.he0);
373       faces.add(face);
374     }
375   }
376 
377   /**
378    * print all points to the print stream (very point a line)
379    *
380    * @param ps the print stream to write to
381    */
382   public void printPoints(PrintStream ps) {
383     for (int i = 0; i < numPoints; i++) {
384       Point3d pnt = pointBuffer[i].pnt;
385       ps.println(pnt.x + ", " + pnt.y + ", " + pnt.z + ",");
386     }
387   }
388 
389   /**
390    * Constructs the convex hull of a set of points whose coordinates are given
391    * by an array of doubles.
392    *
393    * @param coords x, y, and z coordinates of each input point. The length of
394    *               this array will be three times the number of input points.
395    * @throws IllegalArgumentException the number of input points is less than four, or the points
396    *                                  appear to be coincident, colinear, or coplanar.
397    */
398   public void build(double[] coords) throws IllegalArgumentException {
399     build(coords, coords.length / 3);
400   }
401 
402   /**
403    * Constructs the convex hull of a set of points whose coordinates are given
404    * by an array of doubles.
405    *
406    * @param coords x, y, and z coordinates of each input point. The length of
407    *               this array must be at least three times <code>nump</code>.
408    * @param nump   number of input points
409    * @throws IllegalArgumentException the number of input points is less than four or greater than
410    *                                  1/3 the length of <code>coords</code>, or the points appear
411    *                                  to be coincident, colinear, or coplanar.
412    */
413   public void build(double[] coords, int nump) throws IllegalArgumentException {
414     if (nump < 4) {
415       throw new IllegalArgumentException("Less than four input points specified");
416     }
417     if (coords.length / 3 < nump) {
418       throw new IllegalArgumentException("Coordinate array too small for specified number of points");
419     }
420     initBuffers(nump);
421     setPoints(coords, nump);
422     buildHull();
423   }
424 
425   /**
426    * Constructs the convex hull of a set of points.
427    *
428    * @param points input points
429    * @throws IllegalArgumentException the number of input points is less than four, or the points
430    *                                  appear to be coincident, colinear, or coplanar.
431    */
432   public void build(Point3d[] points) throws IllegalArgumentException {
433     build(points, points.length);
434   }
435 
436   /**
437    * Constructs the convex hull of a set of points.
438    *
439    * @param points input points
440    * @param nump   number of input points
441    * @throws IllegalArgumentException the number of input points is less than four or greater then
442    *                                  the length of <code>points</code>, or the points appear to be
443    *                                  coincident, colinear, or coplanar.
444    */
445   public void build(Point3d[] points, int nump) throws IllegalArgumentException {
446     if (nump < 4) {
447       throw new IllegalArgumentException("Less than four input points specified");
448     }
449     if (points.length < nump) {
450       throw new IllegalArgumentException("Point array too small for specified number of points");
451     }
452     initBuffers(nump);
453     setPoints(points, nump);
454     buildHull();
455   }
456 
457   /**
458    * Triangulates any non-triangular hull faces. In some cases, due to
459    * precision issues, the resulting triangles may be very thin or small, and
460    * hence appear to be non-convex (this same limitation is present in <a
461    * href=http://www.qhull.org>qhull</a>).
462    */
463   public void triangulate() {
464     double minArea = 1000 * charLength * DOUBLE_PREC;
465     newFaces.clear();
466     for (Face face : faces) {
467       if (face.mark == Face.VISIBLE) {
468         face.triangulate(newFaces, minArea);
469         // splitFace (face);
470       }
471     }
472     for (Face face = newFaces.first(); face != null; face = face.next) {
473       faces.add(face);
474     }
475   }
476 
477   /**
478    * (Re)initializes internal buffers and lists for a hull build of nump points.
479    * Ensures pointBuffer capacity, clears face/claim lists, and sets counters.
480    *
481    * @param nump number of input points to prepare for
482    */
483   protected void initBuffers(int nump) {
484     if (pointBuffer.length < nump) {
485       Vertex[] newBuffer = new Vertex[nump];
486       vertexPointIndices = new int[nump];
487       System.arraycopy(pointBuffer, 0, newBuffer, 0, pointBuffer.length);
488       for (int i = pointBuffer.length; i < nump; i++) {
489         newBuffer[i] = new Vertex();
490       }
491       pointBuffer = newBuffer;
492     }
493     faces.clear();
494     claimed.clear();
495     numFaces = 0;
496     numPoints = nump;
497   }
498 
499   /**
500    * Populates the internal vertex buffer from a flat coordinate array.
501    *
502    * @param coords flat array of x,y,z coordinates (length >= 3*nump)
503    * @param nump   number of points to load
504    */
505   protected void setPoints(double[] coords, int nump) {
506     for (int i = 0; i < nump; i++) {
507       Vertex vtx = pointBuffer[i];
508       vtx.pnt.set(coords[i * 3 + 0], coords[i * 3 + 1], coords[i * 3 + 2]);
509       vtx.index = i;
510     }
511   }
512 
513   /**
514    * Populates the internal vertex buffer from an array of Point3d.
515    *
516    * @param pnts array of Point3d
517    * @param nump number of points to load
518    */
519   protected void setPoints(Point3d[] pnts, int nump) {
520     for (int i = 0; i < nump; i++) {
521       Vertex vtx = pointBuffer[i];
522       vtx.pnt.set(pnts[i]);
523       vtx.index = i;
524     }
525   }
526 
527   /**
528    * Computes per-axis min/max vertices and sets characteristic length and tolerance.
529    * Used to seed the initial simplex and numerical thresholds.
530    */
531   protected void computeMaxAndMin() {
532     Vector3d max = new Vector3d();
533     Vector3d min = new Vector3d();
534 
535     for (int i = 0; i < 3; i++) {
536       maxVtxs[i] = minVtxs[i] = pointBuffer[0];
537     }
538     max.set(pointBuffer[0].pnt);
539     min.set(pointBuffer[0].pnt);
540 
541     for (int i = 1; i < numPoints; i++) {
542       Point3d pnt = pointBuffer[i].pnt;
543       if (pnt.x > max.x) {
544         max.x = pnt.x;
545         maxVtxs[0] = pointBuffer[i];
546       } else if (pnt.x < min.x) {
547         min.x = pnt.x;
548         minVtxs[0] = pointBuffer[i];
549       }
550       if (pnt.y > max.y) {
551         max.y = pnt.y;
552         maxVtxs[1] = pointBuffer[i];
553       } else if (pnt.y < min.y) {
554         min.y = pnt.y;
555         minVtxs[1] = pointBuffer[i];
556       }
557       if (pnt.z > max.z) {
558         max.z = pnt.z;
559         maxVtxs[2] = pointBuffer[i];
560       } else if (pnt.z < min.z) {
561         min.z = pnt.z;
562         minVtxs[2] = pointBuffer[i];
563       }
564     }
565 
566     // this epsilon formula comes from QuickHull, and I'm
567     // not about to quibble.
568     charLength = Math.max(max.x - min.x, max.y - min.y);
569     charLength = Math.max(max.z - min.z, charLength);
570     if (explicitTolerance == AUTOMATIC_TOLERANCE) {
571       tolerance =
572           3 * DOUBLE_PREC * (Math.max(Math.abs(max.x), Math.abs(min.x)) + Math.max(Math.abs(max.y), Math.abs(min.y)) + Math.max(Math.abs(max.z), Math.abs(min.z)));
573     } else {
574       tolerance = explicitTolerance;
575     }
576   }
577 
578   /**
579    * Creates the initial tetrahedral simplex from extremal points, throwing if points are
580    * coincident/colinear/coplanar within the current tolerance.
581    *
582    * @throws IllegalArgumentException if a non-degenerate simplex cannot be formed
583    */
584   protected void createInitialSimplex() throws IllegalArgumentException {
585     double max = 0;
586     int imax = 0;
587 
588     for (int i = 0; i < 3; i++) {
589       double diff = maxVtxs[i].pnt.get(i) - minVtxs[i].pnt.get(i);
590       if (diff > max) {
591         max = diff;
592         imax = i;
593       }
594     }
595 
596     if (max <= tolerance) {
597       throw new IllegalArgumentException("Input points appear to be coincident");
598     }
599     Vertex[] vtx = new Vertex[4];
600     // set first two vertices to be those with the greatest
601     // one dimensional separation
602 
603     vtx[0] = maxVtxs[imax];
604     vtx[1] = minVtxs[imax];
605 
606     // set third vertex to be the vertex farthest from
607     // the line between vtx0 and vtx1
608     Vector3d u01 = new Vector3d();
609     Vector3d diff02 = new Vector3d();
610     Vector3d nrml = new Vector3d();
611     Vector3d xprod = new Vector3d();
612     double maxSqr = 0;
613     u01.sub(vtx[1].pnt, vtx[0].pnt);
614     u01.normalize();
615     for (int i = 0; i < numPoints; i++) {
616       diff02.sub(pointBuffer[i].pnt, vtx[0].pnt);
617       xprod.cross(u01, diff02);
618       double lenSqr = xprod.normSquared();
619       if (lenSqr > maxSqr && pointBuffer[i] != vtx[0] && // paranoid
620           pointBuffer[i] != vtx[1]) {
621         maxSqr = lenSqr;
622         vtx[2] = pointBuffer[i];
623         nrml.set(xprod);
624       }
625     }
626     if (Math.sqrt(maxSqr) <= 100 * tolerance) {
627       throw new IllegalArgumentException("Input points appear to be colinear");
628     }
629     nrml.normalize();
630 
631     double maxDist = 0;
632     double d0 = vtx[2].pnt.dot(nrml);
633     for (int i = 0; i < numPoints; i++) {
634       double dist = Math.abs(pointBuffer[i].pnt.dot(nrml) - d0);
635       if (dist > maxDist && pointBuffer[i] != vtx[0] && // paranoid
636           pointBuffer[i] != vtx[1] && pointBuffer[i] != vtx[2]) {
637         maxDist = dist;
638         vtx[3] = pointBuffer[i];
639       }
640     }
641     if (Math.abs(maxDist) <= 100 * tolerance) {
642       throw new IllegalArgumentException("Input points appear to be coplanar");
643     }
644 
645     if (logger.isLoggable(Level.FINE)) {
646       logger.fine("initial vertices:");
647       logger.fine(vtx[0].index + ": " + vtx[0].pnt);
648       logger.fine(vtx[1].index + ": " + vtx[1].pnt);
649       logger.fine(vtx[2].index + ": " + vtx[2].pnt);
650       logger.fine(vtx[3].index + ": " + vtx[3].pnt);
651     }
652 
653     Face[] tris = new Face[4];
654 
655     if (vtx[3].pnt.dot(nrml) - d0 < 0) {
656       tris[0] = Face.createTriangle(vtx[0], vtx[1], vtx[2]);
657       tris[1] = Face.createTriangle(vtx[3], vtx[1], vtx[0]);
658       tris[2] = Face.createTriangle(vtx[3], vtx[2], vtx[1]);
659       tris[3] = Face.createTriangle(vtx[3], vtx[0], vtx[2]);
660 
661       for (int i = 0; i < 3; i++) {
662         int k = (i + 1) % 3;
663         tris[i + 1].getEdge(1).setOpposite(tris[k + 1].getEdge(0));
664         tris[i + 1].getEdge(2).setOpposite(tris[0].getEdge(k));
665       }
666     } else {
667       tris[0] = Face.createTriangle(vtx[0], vtx[2], vtx[1]);
668       tris[1] = Face.createTriangle(vtx[3], vtx[0], vtx[1]);
669       tris[2] = Face.createTriangle(vtx[3], vtx[1], vtx[2]);
670       tris[3] = Face.createTriangle(vtx[3], vtx[2], vtx[0]);
671 
672       for (int i = 0; i < 3; i++) {
673         int k = (i + 1) % 3;
674         tris[i + 1].getEdge(0).setOpposite(tris[k + 1].getEdge(1));
675         tris[i + 1].getEdge(2).setOpposite(tris[0].getEdge((3 - i) % 3));
676       }
677     }
678 
679     faces.addAll(Arrays.asList(tris).subList(0, 4));
680 
681     for (int i = 0; i < numPoints; i++) {
682       Vertex v = pointBuffer[i];
683 
684       if (v == vtx[0] || v == vtx[1] || v == vtx[2] || v == vtx[3]) {
685         continue;
686       }
687 
688       maxDist = tolerance;
689       Face maxFace = null;
690       for (int k = 0; k < 4; k++) {
691         double dist = tris[k].distanceToPlane(v.pnt);
692         if (dist > maxDist) {
693           maxFace = tris[k];
694           maxDist = dist;
695         }
696       }
697       if (maxFace != null) {
698         addPointToFace(v, maxFace);
699       }
700     }
701   }
702 
703   /**
704    * Returns the number of vertices in this hull.
705    *
706    * @return number of vertices
707    */
708   public int getNumVertices() {
709     return numVertices;
710   }
711 
712   /**
713    * Returns the vertex points in this hull.
714    *
715    * @return array of vertex points
716    * @see QuickHull3D#getVertices(double[])
717    * @see QuickHull3D#getFaces()
718    */
719   public Point3d[] getVertices() {
720     Point3d[] vtxs = new Point3d[numVertices];
721     for (int i = 0; i < numVertices; i++) {
722       vtxs[i] = pointBuffer[vertexPointIndices[i]].pnt;
723     }
724     return vtxs;
725   }
726 
727   /**
728    * Returns the coordinates of the vertex points of this hull.
729    *
730    * @param coords returns the x, y, z coordinates of each vertex. This length of
731    *               this array must be at least three times the number of
732    *               vertices.
733    * @return the number of vertices
734    * @see QuickHull3D#getVertices()
735    * @see QuickHull3D#getFaces()
736    */
737   public int getVertices(double[] coords) {
738     for (int i = 0; i < numVertices; i++) {
739       Point3d pnt = pointBuffer[vertexPointIndices[i]].pnt;
740       coords[i * 3 + 0] = pnt.x;
741       coords[i * 3 + 1] = pnt.y;
742       coords[i * 3 + 2] = pnt.z;
743     }
744     return numVertices;
745   }
746 
747   /**
748    * Returns an array specifing the index of each hull vertex with respect to
749    * the original input points.
750    *
751    * @return vertex indices with respect to the original points
752    */
753   public int[] getVertexPointIndices() {
754     int[] indices = new int[numVertices];
755     System.arraycopy(vertexPointIndices, 0, indices, 0, numVertices);
756     return indices;
757   }
758 
759   /**
760    * Returns the number of faces in this hull.
761    *
762    * @return number of faces
763    */
764   public int getNumFaces() {
765     return faces.size();
766   }
767 
768   /**
769    * Returns the faces associated with this hull.
770    * <p>
771    * Each face is represented by an integer array which gives the indices of
772    * the vertices. These indices are numbered relative to the hull vertices,
773    * are zero-based, and are arranged counter-clockwise. More control over the
774    * index format can be obtained using {@link #getFaces(int)
775    * getFaces(indexFlags)}.
776    *
777    * @return array of integer arrays, giving the vertex indices for each face.
778    * @see QuickHull3D#getVertices()
779    * @see QuickHull3D#getFaces(int)
780    */
781   public int[][] getFaces() {
782     return getFaces(0);
783   }
784 
785   /**
786    * Returns the faces associated with this hull.
787    * <p>
788    * Each face is represented by an integer array which gives the indices of
789    * the vertices. By default, these indices are numbered with respect to the
790    * hull vertices (as opposed to the input points), are zero-based, and are
791    * arranged counter-clockwise. However, this can be changed by setting
792    * {@link #POINT_RELATIVE POINT_RELATIVE}, {@link #INDEXED_FROM_ONE
793    * INDEXED_FROM_ONE}, or {@link #CLOCKWISE CLOCKWISE} in the indexFlags
794    * parameter.
795    *
796    * @param indexFlags specifies index characteristics (0 results in the default)
797    * @return array of integer arrays, giving the vertex indices for each face.
798    * @see QuickHull3D#getVertices()
799    */
800   public int[][] getFaces(int indexFlags) {
801     int[][] allFaces = new int[faces.size()][];
802     int k = 0;
803     for (Face face : faces) {
804       allFaces[k] = new int[face.numVertices()];
805       getFaceIndices(allFaces[k], face, indexFlags);
806       k++;
807     }
808     return allFaces;
809   }
810 
811   /**
812    * Prints the vertices and faces of this hull to the stream ps.
813    * <p>
814    * This is done using the Alias Wavefront .obj file format, with the
815    * vertices printed first (each preceding by the letter <code>v</code>),
816    * followed by the vertex indices for each face (each preceded by the letter
817    * <code>f</code>).
818    * <p>
819    * The face indices are numbered with respect to the hull vertices (as
820    * opposed to the input points), with a lowest index of 1, and are arranged
821    * counter-clockwise. More control over the index format can be obtained
822    * using {@link #print(PrintStream, int) print(ps,indexFlags)}.
823    *
824    * @param ps stream used for printing
825    * @see QuickHull3D#print(PrintStream, int)
826    * @see QuickHull3D#getVertices()
827    * @see QuickHull3D#getFaces()
828    */
829   public void print(PrintStream ps) {
830     print(ps, 0);
831   }
832 
833   /**
834    * Prints the vertices and faces of this hull to the stream ps.
835    * <p>
836    * This is done using the Alias Wavefront .obj file format, with the
837    * vertices printed first (each preceding by the letter <code>v</code>),
838    * followed by the vertex indices for each face (each preceded by the letter
839    * <code>f</code>).
840    * <p>
841    * By default, the face indices are numbered with respect to the hull
842    * vertices (as opposed to the input points), with a lowest index of 1, and
843    * are arranged counter-clockwise. However, this can be changed by setting
844    * {@link #POINT_RELATIVE POINT_RELATIVE}, {@link #INDEXED_FROM_ONE
845    * INDEXED_FROM_ZERO}, or {@link #CLOCKWISE CLOCKWISE} in the indexFlags
846    * parameter.
847    *
848    * @param ps         stream used for printing
849    * @param indexFlags specifies index characteristics (0 results in the default).
850    * @see QuickHull3D#getVertices()
851    * @see QuickHull3D#getFaces()
852    */
853   public void print(PrintStream ps, int indexFlags) {
854     if ((indexFlags & INDEXED_FROM_ZERO) == 0) {
855       indexFlags |= INDEXED_FROM_ONE;
856     }
857     for (int i = 0; i < numVertices; i++) {
858       Point3d pnt = pointBuffer[vertexPointIndices[i]].pnt;
859       ps.println("v " + pnt.x + " " + pnt.y + " " + pnt.z);
860     }
861     for (Face face : faces) {
862       int[] indices = new int[face.numVertices()];
863       getFaceIndices(indices, face, indexFlags);
864 
865       ps.print("f");
866       for (int index : indices) {
867         ps.print(" " + index);
868       }
869       ps.println();
870     }
871   }
872 
873   private void getFaceIndices(int[] indices, Face face, int flags) {
874     boolean ccw = (flags & CLOCKWISE) == 0;
875     boolean indexedFromOne = (flags & INDEXED_FROM_ONE) != 0;
876     boolean pointRelative = (flags & POINT_RELATIVE) != 0;
877 
878     HalfEdge hedge = face.he0;
879     int k = 0;
880     do {
881       int idx = hedge.head().index;
882       if (pointRelative) {
883         idx = vertexPointIndices[idx];
884       }
885       if (indexedFromOne) {
886         idx++;
887       }
888       indices[k++] = idx;
889       hedge = (ccw ? hedge.next : hedge.prev);
890     } while (hedge != face.he0);
891   }
892 
893   /**
894    * Assigns previously unclaimed vertices to the newly created faces most distant above them.
895    *
896    * @param newFaces list of newly created faces during the current expansion
897    */
898   protected void resolveUnclaimedPoints(FaceList newFaces) {
899     Vertex vtxNext = unclaimed.first();
900     for (Vertex vtx = vtxNext; vtx != null; vtx = vtxNext) {
901       vtxNext = vtx.next;
902 
903       double maxDist = tolerance;
904       Face maxFace = null;
905       for (Face newFace = newFaces.first(); newFace != null; newFace = newFace.next) {
906         if (newFace.mark == Face.VISIBLE) {
907           double dist = newFace.distanceToPlane(vtx.pnt);
908           if (dist > maxDist) {
909             maxDist = dist;
910             maxFace = newFace;
911           }
912           if (maxDist > 1000 * tolerance) {
913             break;
914           }
915         }
916       }
917       if (maxFace != null) {
918         addPointToFace(vtx, maxFace);
919         if (logger.isLoggable(Level.FINE) && vtx.index == findIndex) {
920           logger.fine(findIndex + " CLAIMED BY " + maxFace.getVertexString());
921         }
922       } else {
923         if (logger.isLoggable(Level.FINE) && vtx.index == findIndex) {
924           logger.fine(findIndex + " DISCARDED");
925         }
926       }
927     }
928   }
929 
930   /**
931    * Removes all outside vertices from a face and either discards them as unclaimed
932    * or reassigns them to an absorbing face if they lie above it.
933    *
934    * @param face          face whose outside vertices are being removed
935    * @param absorbingFace face to receive reassigned vertices; may be null
936    */
937   protected void deleteFacePoints(Face face, Face absorbingFace) {
938     Vertex faceVtxs = removeAllPointsFromFace(face);
939     if (faceVtxs != null) {
940       if (absorbingFace == null) {
941         unclaimed.addAll(faceVtxs);
942       } else {
943         Vertex vtxNext = faceVtxs;
944         for (Vertex vtx = vtxNext; vtx != null; vtx = vtxNext) {
945           vtxNext = vtx.next;
946           double dist = absorbingFace.distanceToPlane(vtx.pnt);
947           if (dist > tolerance) {
948             addPointToFace(vtx, absorbingFace);
949           } else {
950             unclaimed.add(vtx);
951           }
952         }
953       }
954     }
955   }
956 
957   private static final int NONCONVEX_WRT_LARGER_FACE = 1;
958 
959   private static final int NONCONVEX = 2;
960 
961   /**
962    * Computes the signed distance from the centroid of the opposite face across this edge
963    * to the plane of this edge's face. Positive indicates non-convexity.
964    *
965    * @param he the half-edge whose adjacent faces are considered
966    * @return signed distance of opposite face centroid to this face plane
967    */
968   protected double oppFaceDistance(HalfEdge he) {
969     return he.face.distanceToPlane(he.opposite.face.getCentroid());
970   }
971 
972   /**
973    * Attempts to merge a face with one of its adjacent faces if they are sufficiently
974    * coplanar or non-convex per the specified mergeType.
975    *
976    * @param face      the face under consideration
977    * @param mergeType NONCONVEX or NONCONVEX_WRT_LARGER_FACE
978    * @return true if a merge occurred; false otherwise
979    */
980   private boolean doAdjacentMerge(Face face, int mergeType) {
981     HalfEdge hedge = face.he0;
982 
983     boolean convex = true;
984     do {
985       Face oppFace = hedge.oppositeFace();
986       boolean merge = false;
987 
988       if (mergeType == NONCONVEX) { // then merge faces if they are
989         // definitively non-convex
990         if (oppFaceDistance(hedge) > -tolerance || oppFaceDistance(hedge.opposite) > -tolerance) {
991           merge = true;
992         }
993       } else {
994         // mergeType == NONCONVEX_WRT_LARGER_FACE
995         // merge faces if they are parallel or non-convex
996         // wrt to the larger face; otherwise, just mark
997         // the face non-convex for the second pass.
998         if (face.area > oppFace.area) {
999           if (oppFaceDistance(hedge) > -tolerance) {
1000             merge = true;
1001           } else if (oppFaceDistance(hedge.opposite) > -tolerance) {
1002             convex = false;
1003           }
1004         } else {
1005           if (oppFaceDistance(hedge.opposite) > -tolerance) {
1006             merge = true;
1007           } else if (oppFaceDistance(hedge) > -tolerance) {
1008             convex = false;
1009           }
1010         }
1011       }
1012 
1013       if (merge) {
1014         if (logger.isLoggable(Level.FINE)) {
1015           logger.fine("  merging " + face.getVertexString() + "  and  " + oppFace.getVertexString());
1016         }
1017 
1018         int numd = face.mergeAdjacentFace(hedge, discardedFaces);
1019         for (int i = 0; i < numd; i++) {
1020           deleteFacePoints(discardedFaces[i], face);
1021         }
1022         if (logger.isLoggable(Level.FINE)) {
1023           logger.fine("  result: " + face.getVertexString());
1024         }
1025         return true;
1026       }
1027       hedge = hedge.next;
1028     } while (hedge != face.he0);
1029     if (!convex) {
1030       face.mark = Face.NON_CONVEX;
1031     }
1032     return false;
1033   }
1034 
1035   /**
1036    * Recursively computes the horizon (boundary) edges visible from a given eye point,
1037    * marking visited faces deleted and collecting the bordering edges.
1038    *
1039    * @param eyePnt  the point from which visibility is tested
1040    * @param edge0   starting edge within the face (may be null to start at edge 0)
1041    * @param face    the current face being visited
1042    * @param horizon output collection of horizon half-edges
1043    */
1044   protected void calculateHorizon(Point3d eyePnt, HalfEdge edge0, Face face, Vector<HalfEdge> horizon) {
1045     // oldFaces.add (face);
1046     deleteFacePoints(face, null);
1047     face.mark = Face.DELETED;
1048     if (logger.isLoggable(Level.FINE)) {
1049       logger.fine("  visiting face " + face.getVertexString());
1050     }
1051     HalfEdge edge;
1052     if (edge0 == null) {
1053       edge0 = face.getEdge(0);
1054       edge = edge0;
1055     } else {
1056       edge = edge0.getNext();
1057     }
1058     do {
1059       Face oppFace = edge.oppositeFace();
1060       if (oppFace.mark == Face.VISIBLE) {
1061         if (oppFace.distanceToPlane(eyePnt) > tolerance) {
1062           calculateHorizon(eyePnt, edge.getOpposite(), oppFace, horizon);
1063         } else {
1064           horizon.add(edge);
1065           if (logger.isLoggable(Level.FINE)) {
1066             logger.fine("  adding horizon edge " + edge.getVertexString());
1067           }
1068         }
1069       }
1070       edge = edge.getNext();
1071     } while (edge != edge0);
1072   }
1073 
1074   /**
1075    * Creates a new triangular face adjoining an existing horizon edge using the eye vertex,
1076    * links it into the hull, and returns the half-edge from eyeVtx to he.tail().
1077    *
1078    * @param eyeVtx the vertex being added to the hull
1079    * @param he     a horizon half-edge to adjoin
1080    * @return the half-edge associated with the new face for chaining
1081    */
1082   private HalfEdge addAdjoiningFace(Vertex eyeVtx, HalfEdge he) {
1083     Face face = Face.createTriangle(eyeVtx, he.tail(), he.head());
1084     faces.add(face);
1085     face.getEdge(-1).setOpposite(he.getOpposite());
1086     return face.getEdge(0);
1087   }
1088 
1089   /**
1090    * Builds and links the ring of new faces around the horizon using the eye vertex,
1091    * recording them in the provided newFaces list.
1092    *
1093    * @param newFaces list that will collect the created faces
1094    * @param eyeVtx   the vertex being added to the hull
1095    * @param horizon  ordered list of horizon half-edges
1096    */
1097   protected void addNewFaces(FaceList newFaces, Vertex eyeVtx, Vector<HalfEdge> horizon) {
1098     newFaces.clear();
1099 
1100     HalfEdge hedgeSidePrev = null;
1101     HalfEdge hedgeSideBegin = null;
1102 
1103     for (HalfEdge horizonHe : horizon) {
1104       HalfEdge hedgeSide = addAdjoiningFace(eyeVtx, horizonHe);
1105       if (logger.isLoggable(Level.FINE)) {
1106         logger.fine("new face: " + hedgeSide.face.getVertexString());
1107       }
1108       if (hedgeSidePrev != null) {
1109         hedgeSide.next.setOpposite(hedgeSidePrev);
1110       } else {
1111         hedgeSideBegin = hedgeSide;
1112       }
1113       newFaces.add(hedgeSide.getFace());
1114       hedgeSidePrev = hedgeSide;
1115     }
1116     hedgeSideBegin.next.setOpposite(hedgeSidePrev);
1117   }
1118 
1119   /**
1120    * Selects the next vertex to add: the farthest point above some face that currently
1121    * has outside points claimed.
1122    *
1123    * @return the next Vertex to add to the hull, or null if none remain
1124    */
1125   protected Vertex nextPointToAdd() {
1126     if (!claimed.isEmpty()) {
1127       Face eyeFace = claimed.first().face;
1128       Vertex eyeVtx = null;
1129       double maxDist = 0;
1130       for (Vertex vtx = eyeFace.outside; vtx != null && vtx.face == eyeFace; vtx = vtx.next) {
1131         double dist = eyeFace.distanceToPlane(vtx.pnt);
1132         if (dist > maxDist) {
1133           maxDist = dist;
1134           eyeVtx = vtx;
1135         }
1136       }
1137       return eyeVtx;
1138     } else {
1139       return null;
1140     }
1141   }
1142 
1143   /**
1144    * Incorporates the specified eye vertex into the hull: computes the horizon, creates
1145    * new faces, merges non-convex adjacencies, and resolves/reassigns outside points.
1146    *
1147    * @param eyeVtx the vertex to add to the hull
1148    */
1149   protected void addPointToHull(Vertex eyeVtx) {
1150     horizon.clear();
1151     unclaimed.clear();
1152 
1153     if (logger.isLoggable(Level.FINE)) {
1154       logger.fine("Adding point: " + eyeVtx.index);
1155       logger.fine(" which is " + eyeVtx.face.distanceToPlane(eyeVtx.pnt) + " above face " + eyeVtx.face.getVertexString());
1156     }
1157     removePointFromFace(eyeVtx, eyeVtx.face);
1158     calculateHorizon(eyeVtx.pnt, null, eyeVtx.face, horizon);
1159     newFaces.clear();
1160     addNewFaces(newFaces, eyeVtx, horizon);
1161 
1162     // first merge pass ... merge faces which are non-convex
1163     // as determined by the larger face
1164 
1165     for (Face face = newFaces.first(); face != null; face = face.next) {
1166       if (face.mark == Face.VISIBLE) {
1167         while (doAdjacentMerge(face, NONCONVEX_WRT_LARGER_FACE)) {
1168           // Do nothing.
1169         }
1170       }
1171     }
1172     // second merge pass ... merge faces which are non-convex
1173     // wrt either face
1174     for (Face face = newFaces.first(); face != null; face = face.next) {
1175       if (face.mark == Face.NON_CONVEX) {
1176         face.mark = Face.VISIBLE;
1177         while (doAdjacentMerge(face, NONCONVEX)) {
1178           // Do nothing.
1179         }
1180       }
1181     }
1182     resolveUnclaimedPoints(newFaces);
1183   }
1184 
1185   /**
1186    * Builds the full convex hull by repeatedly selecting and adding extreme points
1187    * until no outside points remain; then reindexes faces and vertices.
1188    */
1189   protected void buildHull() {
1190     int cnt = 0;
1191     Vertex eyeVtx;
1192 
1193     computeMaxAndMin();
1194     createInitialSimplex();
1195     while ((eyeVtx = nextPointToAdd()) != null) {
1196       addPointToHull(eyeVtx);
1197       cnt++;
1198       logger.fine("iteration " + cnt + " done");
1199     }
1200     reindexFacesAndVertices();
1201     logger.fine("hull done");
1202   }
1203 
1204   /**
1205    * Marks all vertices around a face's boundary with the provided integer mark value.
1206    *
1207    * @param face the face whose boundary vertices will be marked
1208    * @param mark the mark value to assign
1209    */
1210   private void markFaceVertices(Face face, int mark) {
1211     HalfEdge he0 = face.getFirstEdge();
1212     HalfEdge he = he0;
1213     do {
1214       he.head().index = mark;
1215       he = he.next;
1216     } while (he != he0);
1217   }
1218 
1219   /**
1220    * Removes deleted faces, marks active vertices, and assigns new contiguous indices
1221    * to both faces and vertices for compact output.
1222    */
1223   protected void reindexFacesAndVertices() {
1224     for (int i = 0; i < numPoints; i++) {
1225       pointBuffer[i].index = -1;
1226     }
1227     // remove inactive faces and mark active vertices
1228     numFaces = 0;
1229     for (Iterator<Face> it = faces.iterator(); it.hasNext(); ) {
1230       Face face = it.next();
1231       if (face.mark != Face.VISIBLE) {
1232         it.remove();
1233       } else {
1234         markFaceVertices(face, 0);
1235         numFaces++;
1236       }
1237     }
1238     // reindex vertices
1239     numVertices = 0;
1240     for (int i = 0; i < numPoints; i++) {
1241       Vertex vtx = pointBuffer[i];
1242       if (vtx.index == 0) {
1243         vertexPointIndices[numVertices] = i;
1244         vtx.index = numVertices++;
1245       }
1246     }
1247   }
1248 
1249   /**
1250    * Verifies that a face is locally convex by checking distances between opposite face
1251    * centroids and face planes, and ensuring no redundant vertices exist.
1252    *
1253    * @param face the face to validate
1254    * @param tol  distance tolerance for convexity checks
1255    * @param ps   optional PrintStream for diagnostics (null to suppress)
1256    * @return true if the face passes local convexity checks
1257    */
1258   protected boolean checkFaceConvexity(Face face, double tol, PrintStream ps) {
1259     double dist;
1260     HalfEdge he = face.he0;
1261     do {
1262       face.checkConsistency();
1263       // make sure edge is convex
1264       dist = oppFaceDistance(he);
1265       if (dist > tol) {
1266         if (ps != null) {
1267           ps.println("Edge " + he.getVertexString() + " non-convex by " + dist);
1268         }
1269         return false;
1270       }
1271       dist = oppFaceDistance(he.opposite);
1272       if (dist > tol) {
1273         if (ps != null) {
1274           ps.println("Opposite edge " + he.opposite.getVertexString() + " non-convex by " + dist);
1275         }
1276         return false;
1277       }
1278       if (he.next.oppositeFace() == he.oppositeFace()) {
1279         if (ps != null) {
1280           ps.println("Redundant vertex " + he.head().index + " in face " + face.getVertexString());
1281         }
1282         return false;
1283       }
1284       he = he.next;
1285     } while (he != face.he0);
1286     return true;
1287   }
1288 
1289   /**
1290    * Performs convexity validation across all visible faces using a specified tolerance.
1291    *
1292    * @param tol distance tolerance for convexity checks
1293    * @param ps  optional PrintStream for diagnostics (null to suppress)
1294    * @return true if all visible faces pass convexity checks
1295    */
1296   protected boolean checkFaces(double tol, PrintStream ps) {
1297     // check edge convexity
1298     boolean convex = true;
1299     for (Face face : faces) {
1300       if (face.mark == Face.VISIBLE && !checkFaceConvexity(face, tol, ps)) {
1301         convex = false;
1302       }
1303     }
1304     return convex;
1305   }
1306 
1307   /**
1308    * Checks the correctness of the hull using the distance tolerance returned
1309    * by {@link QuickHull3D#getDistanceTolerance getDistanceTolerance}; see
1310    * {@link QuickHull3D#check(PrintStream, double) check(PrintStream,double)}
1311    * for details.
1312    *
1313    * @param ps print stream for diagnostic messages; may be set to
1314    *           <code>null</code> if no messages are desired.
1315    * @return true if the hull is valid
1316    * @see QuickHull3D#check(PrintStream, double)
1317    */
1318   public boolean check(PrintStream ps) {
1319     return check(ps, getDistanceTolerance());
1320   }
1321 
1322   /**
1323    * Checks the correctness of the hull. This is done by making sure that no
1324    * faces are non-convex and that no points are outside any face. These tests
1325    * are performed using the distance tolerance <i>tol</i>. Faces are
1326    * considered non-convex if any edge is non-convex, and an edge is
1327    * non-convex if the centroid of either adjoining face is more than
1328    * <i>tol</i> above the plane of the other face. Similarly, a point is
1329    * considered outside a face if its distance to that face's plane is more
1330    * than 10 times <i>tol</i>.
1331    * <p>
1332    * If the hull has been {@link #triangulate triangulated}, then this routine
1333    * may fail if some of the resulting triangles are very small or thin.
1334    *
1335    * @param ps  print stream for diagnostic messages; may be set to
1336    *            <code>null</code> if no messages are desired.
1337    * @param tol distance tolerance
1338    * @return true if the hull is valid
1339    * @see QuickHull3D#check(PrintStream)
1340    */
1341   public boolean check(PrintStream ps, double tol) {
1342     // check to make sure all edges are fully connected
1343     // and that the edges are convex
1344     double dist;
1345     double pointTol = 10 * tol;
1346 
1347     if (!checkFaces(tolerance, ps)) {
1348       return false;
1349     }
1350 
1351     // check point inclusion
1352 
1353     for (int i = 0; i < numPoints; i++) {
1354       Point3d pnt = pointBuffer[i].pnt;
1355       for (Face face : faces) {
1356         if (face.mark == Face.VISIBLE) {
1357           dist = face.distanceToPlane(pnt);
1358           if (dist > pointTol) {
1359             if (ps != null) {
1360               ps.println("Point " + i + " " + dist + " above face " + face.getVertexString());
1361             }
1362             return false;
1363           }
1364         }
1365       }
1366     }
1367     return true;
1368   }
1369 }