// Assumes that the vertices are located in the same plane // https://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf // Return null if triangulation does not succeed // can potential also delete edges (if edges of length 0 is found) public List <Face> Triangulate(bool step = false, StringWriter debug = null) { #if HMDebug if (debug != null) { debug.WriteLine("Triangulate face with debug"); } #endif Debug.Assert(!IsDestroyed()); List <Face> res = new List <Face>(); res.Add(this); var face = this; bool isVerticesLinear = IsVerticesLinear(); #if HMDebug if (debug != null) { debug.WriteLine("isVerticesLinear " + isVerticesLinear); } #endif if (isVerticesLinear) { return(TriangulateFaceOnLine()); } var halfedges = Circulate(); var normal = GetNormal(); int iterations = halfedges.Count * 2; while (halfedges.Count > 3 && iterations > 0) { iterations--; Halfedge zeroEdge = null; Halfedge degenerateEdge = null; Halfedge sharpCorner = null; double minimumAngle = 999; // find ear to clip for (int i = 0; i < halfedges.Count; i++) { var he1 = halfedges[i]; var he2 = he1.next; var dir1 = he1.GetDirection(); var dir2 = he2.GetDirection(); Vector3D crossDir = Vector3D.Cross(dir1, dir2); var angle = Vector3D.Angle(dir1, dir2); if (dir1.sqrMagnitude < face.hmesh.zeroMagnitudeTresholdSqr) { zeroEdge = he1; break; } bool isParallel = Vector3D.IsParallelDist(dir1, dir2, hmesh.zeroMagnitudeTresholdSqr); bool isSameDir = Vector3D.Dot(dir1, dir2) > 0; bool isNormalSameDir = Vector3D.Dot(crossDir, normal) > 0; if (isParallel && isSameDir == false) { degenerateEdge = he1; break; } bool intersection = false; if (isNormalSameDir && !isParallel) { for (int j = i + 2; j < i + halfedges.Count; j++) { var heJ = halfedges[j % halfedges.Count]; if (PointInTriangle(heJ.prev.vert.positionD, he1.prev.vert.positionD, he1.vert.positionD, he2.vert.positionD) || PointInTriangle(heJ.vert.positionD, he1.prev.vert.positionD, he1.vert.positionD, he2.vert.positionD)) { intersection = true; break; // break intersection test } } // clip ear if (intersection == false) { if (angle < minimumAngle) { minimumAngle = angle; sharpCorner = he1; } } } } if (zeroEdge != null) { #if HMDebug if (debug != null) { debug.WriteLine("zeroEdge " + zeroEdge); } #endif zeroEdge.Collapse(); if (face.IsDestroyed()) { Debug.LogError("Fix"); return(new List <Face>()); // Fix } halfedges = face.Circulate(); } else if (degenerateEdge != null) { // the main idea, is to cut away the degenerate edge (and collapse the degenerate face) // // // Example (4 vertices) // || // || // |\ // | \ // ---- #if HMDebug if (debug != null) { debug.WriteLine("degenerateEdge " + degenerateEdge); } #endif var he1 = degenerateEdge; var he2 = he1.next; // degenerate edges detected Halfedge next = he2.next; double thisDist = he1.GetDirection().magnitude; double nextDist = he2.GetDirection().magnitude; Vertex nextVert = he2.vert; Vertex prevVert = he1.prev.vert; if (Math.Abs(nextDist - thisDist) <= face.hmesh.zeroMagnitudeTreshold) { // cut from nextVert to prevVert } else if (nextDist > thisDist) { nextVert = he2.Split(); nextVert.positionD = prevVert.positionD; } else if (thisDist > nextDist) { prevVert = he1.Split(); prevVert.positionD = nextVert.positionD; } var cutFace = face.Cut(prevVert, nextVert); var sharedEdge = nextVert.GetSharedEdge(prevVert); sharedEdge.Collapse(); if (!cutFace.IsDestroyed()) { #if HMDebug Debug.LogWarning("Cut face survived \n" + cutFace.ExportLocalNeighbourhoodToObj()); #endif return(new List <Face>()); // Fix } halfedges = face.Circulate(); } else if (sharpCorner != null) { #if HMDebug if (debug != null) { debug.WriteLine("sharpCorner "); } #endif var he1 = sharpCorner; var he2 = he1.next; var newFace = face.Cut(he1.prev.vert, he2.vert); newFace.label = label; res.Add(newFace); if (newFace.Circulate().Count > 3) { face = newFace; } halfedges = face.Circulate(); } if (step) { return(res); } } for (int i = res.Count - 1; i >= 0; i--) { if (res[i] == null || res[i].IsDestroyed()) { res.RemoveAt(i); } } return(res); }