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  /**
41   * Basic triangular face used to form the hull.
42   * <p>
43   * The information stored for each face consists of a planar normal, a planar
44   * offset, and a doubly-linked list of three <a href=HalfEdge>HalfEdges</a>
45   * which surround the face in a counter-clockwise direction.
46   *
47   * @author John E. Lloyd, Fall 2004
48   * @author Michael J. Schnieders
49   * @since 1.0
50   */
51  public class Face {
52  
53    protected static final int DELETED = 3;
54  
55    protected static final int NON_CONVEX = 2;
56  
57    protected static final int VISIBLE = 1;
58  
59    protected double area;
60  
61    protected HalfEdge he0;
62  
63    protected int mark;
64  
65    protected Face next;
66  
67    protected int numVerts;
68  
69    protected Vertex outside;
70  
71    protected double planeOffset;
72  
73    private final Point3d centroid;
74  
75    private final Vector3d normal;
76  
77    public Face() {
78      normal = new Vector3d();
79      centroid = new Point3d();
80      mark = VISIBLE;
81    }
82  
83    /**
84     * Creates a Face by linking the specified indices of a vertex array into a closed
85     * counter-clockwise half-edge loop and computing its normal and centroid.
86     *
87     * @param vtxArray array of vertices
88     * @param indices  indices into vtxArray specifying the face vertices in CCW order
89     * @return the newly created Face
90     */
91    public static Face create(Vertex[] vtxArray, int[] indices) {
92      Face face = new Face();
93      HalfEdge hePrev = null;
94      for (int index : indices) {
95        HalfEdge he = new HalfEdge(vtxArray[index], face);
96        if (hePrev != null) {
97          he.setPrev(hePrev);
98          hePrev.setNext(he);
99        } else {
100         face.he0 = he;
101       }
102       hePrev = he;
103     }
104     face.he0.setPrev(hePrev);
105     hePrev.setNext(face.he0);
106 
107     // compute the normal and offset
108     face.computeNormalAndCentroid();
109     return face;
110   }
111 
112   /**
113    * Convenience method to create a triangular Face from three vertices.
114    *
115    * @param v0 first vertex
116    * @param v1 second vertex
117    * @param v2 third vertex
118    * @return the newly created triangular Face
119    */
120   public static Face createTriangle(Vertex v0, Vertex v1, Vertex v2) {
121     return createTriangle(v0, v1, v2, 0);
122   }
123 
124   /**
125    * Constructs a triangular Face from vertices v0, v1, and v2 and computes its
126    * normal and centroid. If the computed area is below minArea, the normal is
127    * adjusted for robustness against near-collinearity.
128    *
129    * @param v0      first vertex
130    * @param v1      second vertex
131    * @param v2      third vertex
132    * @param minArea minimum area threshold used to stabilize the normal
133    * @return the newly created triangular Face
134    */
135   public static Face createTriangle(Vertex v0, Vertex v1, Vertex v2, double minArea) {
136     Face face = new Face();
137     HalfEdge he0 = new HalfEdge(v0, face);
138     HalfEdge he1 = new HalfEdge(v1, face);
139     HalfEdge he2 = new HalfEdge(v2, face);
140 
141     he0.prev = he2;
142     he0.next = he1;
143     he1.prev = he0;
144     he1.next = he2;
145     he2.prev = he1;
146     he2.next = he0;
147 
148     face.he0 = he0;
149 
150     // compute the normal and offset
151     face.computeNormalAndCentroid(minArea);
152     return face;
153   }
154 
155   /**
156    * Computes the centroid (arithmetic mean of vertices) of this face into the
157    * provided point.
158    *
159    * @param centroid output parameter to receive the centroid
160    */
161   public void computeCentroid(Point3d centroid) {
162     centroid.setZero();
163     HalfEdge he = he0;
164     do {
165       centroid.add(he.head().pnt);
166       he = he.next;
167     } while (he != he0);
168     centroid.scale(1 / (double) numVerts);
169   }
170 
171   /**
172    * Computes a unit-length normal vector for this face using the vertex winding
173    * and writes it into the provided vector. Also updates the face area and number
174    * of vertices encountered.
175    *
176    * @param normal output parameter to receive the unit normal
177    */
178   public void computeNormal(Vector3d normal) {
179     HalfEdge he1 = he0.next;
180     HalfEdge he2 = he1.next;
181 
182     Point3d p0 = he0.head().pnt;
183     Point3d p2 = he1.head().pnt;
184 
185     double d2x = p2.x - p0.x;
186     double d2y = p2.y - p0.y;
187     double d2z = p2.z - p0.z;
188 
189     normal.setZero();
190 
191     numVerts = 2;
192 
193     while (he2 != he0) {
194       double d1x = d2x;
195       double d1y = d2y;
196       double d1z = d2z;
197 
198       p2 = he2.head().pnt;
199       d2x = p2.x - p0.x;
200       d2y = p2.y - p0.y;
201       d2z = p2.z - p0.z;
202 
203       normal.x += d1y * d2z - d1z * d2y;
204       normal.y += d1z * d2x - d1x * d2z;
205       normal.z += d1x * d2y - d1y * d2x;
206 
207       he1 = he2;
208       he2 = he2.next;
209       numVerts++;
210     }
211     area = normal.norm();
212     normal.scale(1 / area);
213   }
214 
215   /**
216    * Computes a unit-length normal for this face, and if the preliminary area is
217    * below the specified minArea, adjusts the normal to be more orthogonal to the
218    * longest edge to improve robustness.
219    *
220    * @param normal  output parameter to receive the unit normal
221    * @param minArea minimum area threshold used to stabilize the normal computation
222    */
223   public void computeNormal(Vector3d normal, double minArea) {
224     computeNormal(normal);
225 
226     if (area < minArea) {
227       // make the normal more robust by removing
228       // components parallel to the longest edge
229 
230       HalfEdge hedgeMax = null;
231       double lenSqrMax = 0;
232       HalfEdge hedge = he0;
233       do {
234         double lenSqr = hedge.lengthSquared();
235         if (lenSqr > lenSqrMax) {
236           hedgeMax = hedge;
237           lenSqrMax = lenSqr;
238         }
239         hedge = hedge.next;
240       } while (hedge != he0);
241 
242       Point3d p2 = hedgeMax.head().pnt;
243       Point3d p1 = hedgeMax.tail().pnt;
244       double lenMax = Math.sqrt(lenSqrMax);
245       double ux = (p2.x - p1.x) / lenMax;
246       double uy = (p2.y - p1.y) / lenMax;
247       double uz = (p2.z - p1.z) / lenMax;
248       double dot = normal.x * ux + normal.y * uy + normal.z * uz;
249       normal.x -= dot * ux;
250       normal.y -= dot * uy;
251       normal.z -= dot * uz;
252 
253       normal.normalize();
254     }
255   }
256 
257   /**
258    * Computes the distance from a point p to the plane of this face.
259    *
260    * @param p the point
261    * @return distance from the point to the plane
262    */
263   public double distanceToPlane(Point3d p) {
264     return normal.x * p.x + normal.y * p.y + normal.z * p.z - planeOffset;
265   }
266 
267   /**
268    * Finds the half-edge within this face which has tail <code>vt</code> and
269    * head <code>vh</code>.
270    *
271    * @param vt tail point
272    * @param vh head point
273    * @return the half-edge, or null if none is found.
274    */
275   public HalfEdge findEdge(Vertex vt, Vertex vh) {
276     HalfEdge he = he0;
277     do {
278       if (he.head() == vh && he.tail() == vt) {
279         return he;
280       }
281       he = he.next;
282     } while (he != he0);
283     return null;
284   }
285 
286   /**
287    * Returns the centroid previously computed for this face.
288    *
289    * @return the centroid point
290    */
291   public Point3d getCentroid() {
292     return centroid;
293   }
294 
295   /**
296    * Gets the i-th half-edge associated with the face.
297    *
298    * @param i the half-edge index, in the range 0-2.
299    * @return the half-edge
300    */
301   public HalfEdge getEdge(int i) {
302     HalfEdge he = he0;
303     while (i > 0) {
304       he = he.next;
305       i--;
306     }
307     while (i < 0) {
308       he = he.prev;
309       i++;
310     }
311     return he;
312   }
313 
314   /**
315    * Returns the first half-edge of this face (the start of the circular list).
316    *
317    * @return the first HalfEdge in the face
318    */
319   public HalfEdge getFirstEdge() {
320     return he0;
321   }
322 
323   /**
324    * Returns the normal of the plane associated with this face.
325    *
326    * @return the planar normal
327    */
328   public Vector3d getNormal() {
329     return normal;
330   }
331 
332   /**
333    * Writes the vertex indices of this face into the provided array in CCW order.
334    * The array must be large enough to hold numVertices() entries.
335    *
336    * @param idxs output array receiving the vertex indices
337    */
338   public void getVertexIndices(int[] idxs) {
339     HalfEdge he = he0;
340     int i = 0;
341     do {
342       idxs[i++] = he.head().index;
343       he = he.next;
344     } while (he != he0);
345   }
346 
347   /**
348    * Returns a space-separated string of the vertex indices defining this face
349    * in counter-clockwise order.
350    *
351    * @return string listing the vertex indices in CCW order
352    */
353   public String getVertexString() {
354     StringBuilder s = null;
355     HalfEdge he = he0;
356     do {
357       if (s == null) {
358         s = new StringBuilder("" + he.head().index);
359       } else {
360         s.append(" ").append(he.head().index);
361       }
362       he = he.next;
363     } while (he != he0);
364     return s.toString();
365   }
366 
367   /**
368    * Merges this face with the adjacent face across the specified half-edge if possible.
369    * Updates connectivity, recomputes normals/centroids, and records any faces that
370    * become redundant in the provided discarded array.
371    *
372    * @param hedgeAdj the half-edge along which to merge with the adjacent face
373    * @param discarded an output array to collect faces that are deleted by the merge
374    * @return the number of faces recorded in discarded (0, 1, or 2)
375    */
376   public int mergeAdjacentFace(HalfEdge hedgeAdj, Face[] discarded) {
377     Face oppFace = hedgeAdj.oppositeFace();
378     int numDiscarded = 0;
379 
380     discarded[numDiscarded++] = oppFace;
381     oppFace.mark = DELETED;
382 
383     HalfEdge hedgeOpp = hedgeAdj.getOpposite();
384 
385     HalfEdge hedgeAdjPrev = hedgeAdj.prev;
386     HalfEdge hedgeAdjNext = hedgeAdj.next;
387     HalfEdge hedgeOppPrev = hedgeOpp.prev;
388     HalfEdge hedgeOppNext = hedgeOpp.next;
389 
390     while (hedgeAdjPrev.oppositeFace() == oppFace) {
391       hedgeAdjPrev = hedgeAdjPrev.prev;
392       hedgeOppNext = hedgeOppNext.next;
393     }
394 
395     while (hedgeAdjNext.oppositeFace() == oppFace) {
396       hedgeOppPrev = hedgeOppPrev.prev;
397       hedgeAdjNext = hedgeAdjNext.next;
398     }
399 
400     HalfEdge hedge;
401 
402     for (hedge = hedgeOppNext; hedge != hedgeOppPrev.next; hedge = hedge.next) {
403       hedge.face = this;
404     }
405 
406     if (hedgeAdj == he0) {
407       he0 = hedgeAdjNext;
408     }
409 
410     // handle the half edges at the head
411     Face discardedFace;
412 
413     discardedFace = connectHalfEdges(hedgeOppPrev, hedgeAdjNext);
414     if (discardedFace != null) {
415       discarded[numDiscarded++] = discardedFace;
416     }
417 
418     // handle the half edges at the tail
419     discardedFace = connectHalfEdges(hedgeAdjPrev, hedgeOppNext);
420     if (discardedFace != null) {
421       discarded[numDiscarded++] = discardedFace;
422     }
423 
424     computeNormalAndCentroid();
425     checkConsistency();
426 
427     return numDiscarded;
428   }
429 
430   /**
431    * Returns the number of vertices (half-edges) bounding this face.
432    *
433    * @return the vertex count of this face
434    */
435   public int numVertices() {
436     return numVerts;
437   }
438 
439   /**
440    * Triangulates this (convex polygonal) face into a fan of triangles sharing the first
441    * vertex. Newly created faces are added to newFaces, and appropriate opposite links are
442    * established. Normals/centroids are updated using the given minArea for stability.
443    *
444    * @param newFaces list that collects newly created triangular faces
445    * @param minArea  minimum area threshold used when computing normals
446    */
447   public void triangulate(FaceList newFaces, double minArea) {
448     HalfEdge hedge;
449 
450     if (numVertices() < 4) {
451       return;
452     }
453 
454     Vertex v0 = he0.head();
455 
456     hedge = he0.next;
457     HalfEdge oppPrev = hedge.opposite;
458     Face face0 = null;
459 
460     for (hedge = hedge.next; hedge != he0.prev; hedge = hedge.next) {
461       Face face = createTriangle(v0, hedge.prev.head(), hedge.head(), minArea);
462       face.he0.next.setOpposite(oppPrev);
463       face.he0.prev.setOpposite(hedge.opposite);
464       oppPrev = face.he0;
465       newFaces.add(face);
466       if (face0 == null) {
467         face0 = face;
468       }
469     }
470     hedge = new HalfEdge(he0.prev.prev.head(), this);
471     hedge.setOpposite(oppPrev);
472 
473     hedge.prev = he0;
474     hedge.prev.next = hedge;
475 
476     hedge.next = he0.prev;
477     hedge.next.prev = hedge;
478 
479     computeNormalAndCentroid(minArea);
480     checkConsistency();
481 
482     for (Face face = face0; face != null; face = face.next) {
483       face.checkConsistency();
484     }
485 
486   }
487 
488   /**
489    * Computes the squared area of the triangle defined by hedge0 (tail->head)
490    * and the point at the head of hedge1. Useful for robust comparisons without
491    * taking a square root.
492    *
493    * @param hedge0 the reference half-edge whose tail and head form two triangle vertices
494    * @param hedge1 a half-edge providing the third vertex via its head
495    * @return the squared area of the resulting triangle
496    */
497   public double areaSquared(HalfEdge hedge0, HalfEdge hedge1) {
498     Point3d p0 = hedge0.tail().pnt;
499     Point3d p1 = hedge0.head().pnt;
500     Point3d p2 = hedge1.head().pnt;
501 
502     double dx1 = p1.x - p0.x;
503     double dy1 = p1.y - p0.y;
504     double dz1 = p1.z - p0.z;
505 
506     double dx2 = p2.x - p0.x;
507     double dy2 = p2.y - p0.y;
508     double dz2 = p2.z - p0.z;
509 
510     double x = dy1 * dz2 - dz1 * dy2;
511     double y = dz1 * dx2 - dx1 * dz2;
512     double z = dx1 * dy2 - dy1 * dx2;
513 
514     return x * x + y * y + z * z;
515   }
516 
517   private void computeNormalAndCentroid() {
518     computeNormal(normal);
519     computeCentroid(centroid);
520     planeOffset = normal.dot(centroid);
521     int numv = 0;
522     HalfEdge he = he0;
523     do {
524       numv++;
525       he = he.next;
526     } while (he != he0);
527     if (numv != numVerts) {
528       throw new InternalErrorException("face " + getVertexString() + " numVerts=" + numVerts + " should be " + numv);
529     }
530   }
531 
532   private void computeNormalAndCentroid(double minArea) {
533     computeNormal(normal, minArea);
534     computeCentroid(centroid);
535     planeOffset = normal.dot(centroid);
536   }
537 
538   private Face connectHalfEdges(HalfEdge hedgePrev, HalfEdge hedge) {
539     Face discardedFace = null;
540 
541     if (hedgePrev.oppositeFace() == hedge.oppositeFace()) { // then there is
542       // a redundant
543       // edge that we
544       // can get rid
545       // off
546 
547       Face oppFace = hedge.oppositeFace();
548       HalfEdge hedgeOpp;
549 
550       if (hedgePrev == he0) {
551         he0 = hedge;
552       }
553       if (oppFace.numVertices() == 3) { // then we can get rid of the
554         // opposite face altogether
555         hedgeOpp = hedge.getOpposite().prev.getOpposite();
556 
557         oppFace.mark = DELETED;
558         discardedFace = oppFace;
559       } else {
560         hedgeOpp = hedge.getOpposite().next;
561 
562         if (oppFace.he0 == hedgeOpp.prev) {
563           oppFace.he0 = hedgeOpp;
564         }
565         hedgeOpp.prev = hedgeOpp.prev.prev;
566         hedgeOpp.prev.next = hedgeOpp;
567       }
568       hedge.prev = hedgePrev.prev;
569       hedge.prev.next = hedge;
570 
571       hedge.opposite = hedgeOpp;
572       hedgeOpp.opposite = hedge;
573 
574       // oppFace was modified, so need to recompute
575       oppFace.computeNormalAndCentroid();
576     } else {
577       hedgePrev.next = hedge;
578       hedge.prev = hedgePrev;
579     }
580     return discardedFace;
581   }
582 
583   /**
584    * Performs sanity checks on this face's topology and geometry, verifying
585    * opposite half-edges, face markings, and vertex counts.
586    *
587    * @throws InternalErrorException if inconsistencies are detected
588    */
589   void checkConsistency() {
590     // do a sanity check on the face
591     HalfEdge hedge = he0;
592     double maxd = 0;
593     int numv = 0;
594 
595     if (numVerts < 3) {
596       throw new InternalErrorException("degenerate face: " + getVertexString());
597     }
598     do {
599       HalfEdge hedgeOpp = hedge.getOpposite();
600       if (hedgeOpp == null) {
601         throw new InternalErrorException("face " + getVertexString() + ": " + "unreflected half edge " + hedge.getVertexString());
602       } else if (hedgeOpp.getOpposite() != hedge) {
603         throw new InternalErrorException("face " + getVertexString() + ": " + "opposite half edge " + hedgeOpp.getVertexString() + " has opposite "
604             + hedgeOpp.getOpposite().getVertexString());
605       }
606       if (hedgeOpp.head() != hedge.tail() || hedge.head() != hedgeOpp.tail()) {
607         throw new InternalErrorException("face " + getVertexString() + ": " + "half edge " + hedge.getVertexString() + " reflected by " + hedgeOpp.getVertexString());
608       }
609       Face oppFace = hedgeOpp.face;
610       if (oppFace == null) {
611         throw new InternalErrorException("face " + getVertexString() + ": " + "no face on half edge " + hedgeOpp.getVertexString());
612       } else if (oppFace.mark == DELETED) {
613         throw new InternalErrorException("face " + getVertexString() + ": " + "opposite face " + oppFace.getVertexString() + " not on hull");
614       }
615       double d = Math.abs(distanceToPlane(hedge.head().pnt));
616       if (d > maxd) {
617         maxd = d;
618       }
619       numv++;
620       hedge = hedge.next;
621     } while (hedge != he0);
622 
623     if (numv != numVerts) {
624       throw new InternalErrorException("face " + getVertexString() + " numVerts=" + numVerts + " should be " + numv);
625     }
626 
627   }
628 }