using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
namespace MonoGame.Extended.Triangulation
{
///
/// MIT Licensed: https://github.com/nickgravelyn/Triangulator
///
/// A static class exposing methods for triangulating 2D polygons. This is the sole public
/// class in the entire library; all other classes/structures are intended as internal-only
/// objects used only to assist in triangulation.
///
/// This class makes use of the DEBUG conditional and produces quite verbose output when built
/// in Debug mode. This is quite useful for debugging purposes, but can slow the process down
/// quite a bit. For optimal performance, build the library in Release mode.
///
/// The triangulation is also not optimized for garbage sensitive processing. The point of the
/// library is a robust, yet simple, system for triangulating 2D shapes. It is intended to be
/// used as part of your content pipeline or at load-time. It is not something you want to be
/// using each and every frame unless you really don't care about garbage.
///
public static class Triangulator
{
#region Fields
static readonly IndexableCyclicalLinkedList polygonVertices = new IndexableCyclicalLinkedList();
static readonly IndexableCyclicalLinkedList earVertices = new IndexableCyclicalLinkedList();
static readonly CyclicalList convexVertices = new CyclicalList();
static readonly CyclicalList reflexVertices = new CyclicalList();
#endregion
#region Public Methods
#region Triangulate
///
/// Triangulates a 2D polygon produced the indexes required to render the points as a triangle list.
///
/// The polygon vertices in counter-clockwise winding order.
/// The desired output winding order.
/// The resulting vertices that include any reversals of winding order and holes.
/// The resulting indices for rendering the shape as a triangle list.
public static void Triangulate(
Vector2[] inputVertices,
WindingOrder desiredWindingOrder,
out Vector2[] outputVertices,
out int[] indices)
{
Log("\nBeginning triangulation...");
List triangles = new List();
//make sure we have our vertices wound properly
if (DetermineWindingOrder(inputVertices) == WindingOrder.Clockwise)
outputVertices = ReverseWindingOrder(inputVertices);
else
outputVertices = (Vector2[])inputVertices.Clone();
//clear all of the lists
polygonVertices.Clear();
earVertices.Clear();
convexVertices.Clear();
reflexVertices.Clear();
//generate the cyclical list of vertices in the polygon
for (int i = 0; i < outputVertices.Length; i++)
polygonVertices.AddLast(new Vertex(outputVertices[i], i));
//categorize all of the vertices as convex, reflex, and ear
FindConvexAndReflexVertices();
FindEarVertices();
//clip all the ear vertices
while (polygonVertices.Count > 3 && earVertices.Count > 0)
ClipNextEar(triangles);
//if there are still three points, use that for the last triangle
if (polygonVertices.Count == 3)
triangles.Add(new Triangle(
polygonVertices[0].Value,
polygonVertices[1].Value,
polygonVertices[2].Value));
//add all of the triangle indices to the output array
indices = new int[triangles.Count * 3];
//move the if statement out of the loop to prevent all the
//redundant comparisons
if (desiredWindingOrder == WindingOrder.CounterClockwise)
{
for (int i = 0; i < triangles.Count; i++)
{
indices[(i * 3)] = triangles[i].A.Index;
indices[(i * 3) + 1] = triangles[i].B.Index;
indices[(i * 3) + 2] = triangles[i].C.Index;
}
}
else
{
for (int i = 0; i < triangles.Count; i++)
{
indices[(i * 3)] = triangles[i].C.Index;
indices[(i * 3) + 1] = triangles[i].B.Index;
indices[(i * 3) + 2] = triangles[i].A.Index;
}
}
}
#endregion
#region CutHoleInShape
///
/// Cuts a hole into a shape.
///
/// An array of vertices for the primary shape.
/// An array of vertices for the hole to be cut. It is assumed that these vertices lie completely within the shape verts.
/// The new array of vertices that can be passed to Triangulate to properly triangulate the shape with the hole.
public static Vector2[] CutHoleInShape(Vector2[] shapeVerts, Vector2[] holeVerts)
{
Log("\nCutting hole into shape...");
//make sure the shape vertices are wound counter clockwise and the hole vertices clockwise
shapeVerts = EnsureWindingOrder(shapeVerts, WindingOrder.CounterClockwise);
holeVerts = EnsureWindingOrder(holeVerts, WindingOrder.Clockwise);
//clear all of the lists
polygonVertices.Clear();
earVertices.Clear();
convexVertices.Clear();
reflexVertices.Clear();
//generate the cyclical list of vertices in the polygon
for (int i = 0; i < shapeVerts.Length; i++)
polygonVertices.AddLast(new Vertex(shapeVerts[i], i));
CyclicalList holePolygon = new CyclicalList();
for (int i = 0; i < holeVerts.Length; i++)
holePolygon.Add(new Vertex(holeVerts[i], i + polygonVertices.Count));
#if DEBUG
StringBuilder vString = new StringBuilder();
foreach (Vertex v in polygonVertices)
vString.Append(string.Format("{0}, ", v));
Log("Shape Vertices: {0}", vString);
vString = new StringBuilder();
foreach (Vertex v in holePolygon)
vString.Append(string.Format("{0}, ", v));
Log("Hole Vertices: {0}", vString);
#endif
FindConvexAndReflexVertices();
FindEarVertices();
//find the hole vertex with the largest X value
Vertex rightMostHoleVertex = holePolygon[0];
foreach (Vertex v in holePolygon)
if (v.Position.X > rightMostHoleVertex.Position.X)
rightMostHoleVertex = v;
//construct a list of all line segments where at least one vertex
//is to the right of the rightmost hole vertex with one vertex
//above the hole vertex and one below
List segmentsToTest = new List();
for (int i = 0; i < polygonVertices.Count; i++)
{
Vertex a = polygonVertices[i].Value;
Vertex b = polygonVertices[i + 1].Value;
if ((a.Position.X > rightMostHoleVertex.Position.X || b.Position.X > rightMostHoleVertex.Position.X) &&
((a.Position.Y >= rightMostHoleVertex.Position.Y && b.Position.Y <= rightMostHoleVertex.Position.Y) ||
(a.Position.Y <= rightMostHoleVertex.Position.Y && b.Position.Y >= rightMostHoleVertex.Position.Y)))
segmentsToTest.Add(new LineSegment(a, b));
}
//now we try to find the closest intersection point heading to the right from
//our hole vertex.
float? closestPoint = null;
LineSegment closestSegment = new LineSegment();
foreach (LineSegment segment in segmentsToTest)
{
float? intersection = segment.IntersectsWithRay(rightMostHoleVertex.Position, Vector2.UnitX);
if (intersection != null)
{
if (closestPoint == null || closestPoint.Value > intersection.Value)
{
closestPoint = intersection;
closestSegment = segment;
}
}
}
//if closestPoint is null, there were no collisions (likely from improper input data),
//but we'll just return without doing anything else
if (closestPoint == null)
return shapeVerts;
//otherwise we can find our mutually visible vertex to split the polygon
Vector2 I = rightMostHoleVertex.Position + Vector2.UnitX * closestPoint.Value;
Vertex P = (closestSegment.A.Position.X > closestSegment.B.Position.X)
? closestSegment.A
: closestSegment.B;
//construct triangle MIP
Triangle mip = new Triangle(rightMostHoleVertex, new Vertex(I, 1), P);
//see if any of the reflex vertices lie inside of the MIP triangle
List interiorReflexVertices = new List();
foreach (Vertex v in reflexVertices)
if (mip.ContainsPoint(v))
interiorReflexVertices.Add(v);
//if there are any interior reflex vertices, find the one that, when connected
//to our rightMostHoleVertex, forms the line closest to Vector2.UnitX
if (interiorReflexVertices.Count > 0)
{
float closestDot = -1f;
foreach (Vertex v in interiorReflexVertices)
{
//compute the dot product of the vector against the UnitX
Vector2 d = Vector2.Normalize(v.Position - rightMostHoleVertex.Position);
float dot = Vector2.Dot(Vector2.UnitX, d);
//if this line is the closest we've found
if (dot > closestDot)
{
//save the value and save the vertex as P
closestDot = dot;
P = v;
}
}
}
//now we just form our output array by injecting the hole vertices into place
//we know we have to inject the hole into the main array after point P going from
//rightMostHoleVertex around and then back to P.
int mIndex = holePolygon.IndexOf(rightMostHoleVertex);
int injectPoint = polygonVertices.IndexOf(P);
Log("Inserting hole at injection point {0} starting at hole vertex {1}.",
P,
rightMostHoleVertex);
for (int i = mIndex; i <= mIndex + holePolygon.Count; i++)
{
Log("Inserting vertex {0} after vertex {1}.", holePolygon[i], polygonVertices[injectPoint].Value);
polygonVertices.AddAfter(polygonVertices[injectPoint++], holePolygon[i]);
}
polygonVertices.AddAfter(polygonVertices[injectPoint], P);
#if DEBUG
vString = new StringBuilder();
foreach (Vertex v in polygonVertices)
vString.Append(string.Format("{0}, ", v));
Log("New Shape Vertices: {0}\n", vString);
#endif
//finally we write out the new polygon vertices and return them out
Vector2[] newShapeVerts = new Vector2[polygonVertices.Count];
for (int i = 0; i < polygonVertices.Count; i++)
newShapeVerts[i] = polygonVertices[i].Value.Position;
return newShapeVerts;
}
#endregion
#region EnsureWindingOrder
///
/// Ensures that a set of vertices are wound in a particular order, reversing them if necessary.
///
/// The vertices of the polygon.
/// The desired winding order.
/// A new set of vertices if the winding order didn't match; otherwise the original set.
public static Vector2[] EnsureWindingOrder(Vector2[] vertices, WindingOrder windingOrder)
{
Log("\nEnsuring winding order of {0}...", windingOrder);
if (DetermineWindingOrder(vertices) != windingOrder)
{
Log("Reversing vertices...");
return ReverseWindingOrder(vertices);
}
Log("No reversal needed.");
return vertices;
}
#endregion
#region ReverseWindingOrder
///
/// Reverses the winding order for a set of vertices.
///
/// The vertices of the polygon.
/// The new vertices for the polygon with the opposite winding order.
public static Vector2[] ReverseWindingOrder(Vector2[] vertices)
{
Log("\nReversing winding order...");
Vector2[] newVerts = new Vector2[vertices.Length];
#if DEBUG
StringBuilder vString = new StringBuilder();
foreach (Vector2 v in vertices)
vString.Append(string.Format("{0}, ", v));
Log("Original Vertices: {0}", vString);
#endif
newVerts[0] = vertices[0];
for (int i = 1; i < newVerts.Length; i++)
newVerts[i] = vertices[vertices.Length - i];
#if DEBUG
vString = new StringBuilder();
foreach (Vector2 v in newVerts)
vString.Append(string.Format("{0}, ", v));
Log("New Vertices After Reversal: {0}\n", vString);
#endif
return newVerts;
}
#endregion
#region DetermineWindingOrder
///
/// Determines the winding order of a polygon given a set of vertices.
///
/// The vertices of the polygon.
/// The calculated winding order of the polygon.
public static WindingOrder DetermineWindingOrder(Vector2[] vertices)
{
int clockWiseCount = 0;
int counterClockWiseCount = 0;
Vector2 p1 = vertices[0];
for (int i = 1; i < vertices.Length; i++)
{
Vector2 p2 = vertices[i];
Vector2 p3 = vertices[(i + 1) % vertices.Length];
Vector2 e1 = p1 - p2;
Vector2 e2 = p3 - p2;
if (e1.X * e2.Y - e1.Y * e2.X >= 0)
clockWiseCount++;
else
counterClockWiseCount++;
p1 = p2;
}
return (clockWiseCount > counterClockWiseCount)
? WindingOrder.Clockwise
: WindingOrder.CounterClockwise;
}
#endregion
#endregion
#region Private Methods
#region ClipNextEar
private static void ClipNextEar(ICollection triangles)
{
//find the triangle
Vertex ear = earVertices[0].Value;
Vertex prev = polygonVertices[polygonVertices.IndexOf(ear) - 1].Value;
Vertex next = polygonVertices[polygonVertices.IndexOf(ear) + 1].Value;
triangles.Add(new Triangle(ear, next, prev));
//remove the ear from the shape
earVertices.RemoveAt(0);
polygonVertices.RemoveAt(polygonVertices.IndexOf(ear));
Log("\nRemoved Ear: {0}", ear);
//validate the neighboring vertices
ValidateAdjacentVertex(prev);
ValidateAdjacentVertex(next);
//write out the states of each of the lists
#if DEBUG
StringBuilder rString = new StringBuilder();
foreach (Vertex v in reflexVertices)
rString.Append(string.Format("{0}, ", v.Index));
Log("Reflex Vertices: {0}", rString);
StringBuilder cString = new StringBuilder();
foreach (Vertex v in convexVertices)
cString.Append(string.Format("{0}, ", v.Index));
Log("Convex Vertices: {0}", cString);
StringBuilder eString = new StringBuilder();
foreach (Vertex v in earVertices)
eString.Append(string.Format("{0}, ", v.Index));
Log("Ear Vertices: {0}", eString);
#endif
}
#endregion
#region ValidateAdjacentVertex
private static void ValidateAdjacentVertex(Vertex vertex)
{
Log("Validating: {0}...", vertex);
if (reflexVertices.Contains(vertex))
{
if (IsConvex(vertex))
{
reflexVertices.Remove(vertex);
convexVertices.Add(vertex);
Log("Vertex: {0} now convex", vertex);
}
else
{
Log("Vertex: {0} still reflex", vertex);
}
}
if (convexVertices.Contains(vertex))
{
bool wasEar = earVertices.Contains(vertex);
bool isEar = IsEar(vertex);
if (wasEar && !isEar)
{
earVertices.Remove(vertex);
Log("Vertex: {0} no longer ear", vertex);
}
else if (!wasEar && isEar)
{
earVertices.AddFirst(vertex);
Log("Vertex: {0} now ear", vertex);
}
else
{
Log("Vertex: {0} still ear", vertex);
}
}
}
#endregion
#region FindConvexAndReflexVertices
private static void FindConvexAndReflexVertices()
{
for (int i = 0; i < polygonVertices.Count; i++)
{
Vertex v = polygonVertices[i].Value;
if (IsConvex(v))
{
convexVertices.Add(v);
Log("Convex: {0}", v);
}
else
{
reflexVertices.Add(v);
Log("Reflex: {0}", v);
}
}
}
#endregion
#region FindEarVertices
private static void FindEarVertices()
{
for (int i = 0; i < convexVertices.Count; i++)
{
Vertex c = convexVertices[i];
if (IsEar(c))
{
earVertices.AddLast(c);
Log("Ear: {0}", c);
}
}
}
#endregion
#region IsEar
private static bool IsEar(Vertex c)
{
Vertex p = polygonVertices[polygonVertices.IndexOf(c) - 1].Value;
Vertex n = polygonVertices[polygonVertices.IndexOf(c) + 1].Value;
Log("Testing vertex {0} as ear with triangle {1}, {0}, {2}...", c, p, n);
foreach (Vertex t in reflexVertices)
{
if (t.Equals(p) || t.Equals(c) || t.Equals(n))
continue;
if (Triangle.ContainsPoint(p, c, n, t))
{
Log("\tTriangle contains vertex {0}...", t);
return false;
}
}
return true;
}
#endregion
#region IsConvex
private static bool IsConvex(Vertex c)
{
Vertex p = polygonVertices[polygonVertices.IndexOf(c) - 1].Value;
Vertex n = polygonVertices[polygonVertices.IndexOf(c) + 1].Value;
Vector2 d1 = Vector2.Normalize(c.Position - p.Position);
Vector2 d2 = Vector2.Normalize(n.Position - c.Position);
Vector2 n2 = new Vector2(-d2.Y, d2.X);
return (Vector2.Dot(d1, n2) <= 0f);
}
#endregion
#region IsReflex
private static bool IsReflex(Vertex c)
{
return !IsConvex(c);
}
#endregion
#region Log
[Conditional("DEBUG")]
private static void Log(string format, params object[] parameters)
{
//System.Console.WriteLine(format, parameters);
}
#endregion
#endregion
}
///
/// Specifies a desired winding order for the shape vertices.
///
public enum WindingOrder
{
Clockwise,
CounterClockwise
}
}