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("Vertices:");
80 * Point3d[] vertices = hull.getVertices();
81 * for (int i = 0; i < vertices.length; i++) {
82 * Point3d pnt = vertices[i];
83 * System.out.println(pnt.x + " " + pnt.y + " " + pnt.z);
84 * }
85 *
86 * System.out.println("Faces:");
87 * int[][] faceIndices = hull.getFaces();
88 * for (int i = 0; i < faceIndices.length; i++) {
89 * for (int k = 0; k < faceIndices[i].length; k++) {
90 * System.out.print(faceIndices[i][k] + " ");
91 * }
92 * System.out.println("");
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 }