/*\r
- * Copyright 2010 Mario Zechner (contact@badlogicgames.com), Nathan Sweet (admin@esotericsoftware.com)\r
+ * Copyright 2010 Mario Zechner (contact@badlogicgames.com), Nathan Sweet (admin@esotericsoftware.com), Nicolas Gramlich\r
* \r
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the\r
* License. You may obtain a copy of the License at\r
import java.util.ArrayList;\r
import java.util.Collections;\r
import java.util.List;\r
+import java.util.ArrayList;\r
+import java.util.Collections;\r
+import java.util.List;\r
+\r
+import com.badlogic.gdx.math.Vector2;\r
\r
/**\r
- * A simple implementation of the ear cutting algorithm to triangulate simple polygons without holes. For more information see\r
+ * A simple implementation of the ear cutting algorithm to triangulate simple\r
+ * polygons without holes. For more information:\r
* http://cgm.cs.mcgill.ca/~godfried/teaching/cg-projects/97/Ian/algorithm2.html\r
- * @author badlogicgames@gmail.com\r
+ * http://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf\r
* \r
+ * @author badlogicgames@gmail.com\r
+ * @author Nicolas Gramlich (Improved performance. Collinear edges are now supported.)\r
*/\r
public final class EarCutTriangulator {\r
- /**\r
- * Triangulates the list of points and returns an array of {@link Vector3} triples that each form a single triangle.\r
- * \r
- * @param polygon The polygon to triangulate\r
- * @return The list of triangle vertices.\r
- */\r
- public List<Vector2> triangulate (List<Vector2> polygon) {\r
- List<Vector2> triangles = new ArrayList<Vector2>();\r
- List<Vector2> tmp = new ArrayList<Vector2>(polygon.size());\r
- tmp.addAll(polygon);\r
- polygon = tmp;\r
-\r
- if (polygon.size() == 3) {\r
- triangles.addAll(polygon);\r
- return triangles;\r
- }\r
\r
- while (polygon.size() >= 3) {\r
- int ptType[] = classifyPoints(polygon);\r
+ private static final int CONCAVE = 1;\r
+ private static final int CONVEX = -1;\r
\r
- for (int i = 0; i < polygon.size(); i++) {\r
- float x1 = polygon.get(i == 0 ? polygon.size() - 1 : i - 1).x;\r
- float y1 = polygon.get(i == 0 ? polygon.size() - 1 : i - 1).y;\r
- float x2 = polygon.get(i).x;\r
- float y2 = polygon.get(i).y;\r
- float x3 = polygon.get(i == polygon.size() - 1 ? 0 : i + 1).x;\r
- float y3 = polygon.get(i == polygon.size() - 1 ? 0 : i + 1).y;\r
+ private int concaveVertexCount;\r
\r
- if (ear(polygon, ptType, x1, y1, x2, y2, x3, y3)) {\r
+ public List<Vector2> computeTriangles(final List<Vector2> pVertices) {\r
+ // TODO Check if LinkedList performs better\r
+ final ArrayList<Vector2> triangles = new ArrayList<Vector2>();\r
+ final ArrayList<Vector2> vertices = new ArrayList<Vector2>(pVertices.size());\r
+ vertices.addAll(pVertices);\r
\r
- cutEar(polygon, triangles, i);\r
- updatePolygon(polygon, i);\r
+ if(vertices.size() == 3) {\r
+ triangles.addAll(vertices);\r
+ return triangles;\r
+ }\r
+\r
+ while(vertices.size() >= 3) {\r
+ // TODO Usually(Always?) only the Types of the vertices next to the ear change! --> Improve\r
+ final int vertexTypes[] = this.classifyVertices(vertices);\r
+\r
+ final int vertexCount = vertices.size();\r
+ for(int index = 0; index < vertexCount; index++) {\r
+ if(this.isEarTip(vertices, index, vertexTypes)) {\r
+ this.cutEarTip(vertices, index, triangles);\r
break;\r
}\r
}\r
}\r
\r
-// if( polygon.size() == 3 )\r
-// {\r
-// triangles.add( polygon.get(0) );\r
-// triangles.add( polygon.get(1) );\r
-// triangles.add( polygon.get(2) );\r
-// }\r
-\r
return triangles;\r
}\r
+ \r
+ private static boolean areVerticesClockwise(final ArrayList<Vector2> pVertices) {\r
+ final int vertexCount = pVertices.size();\r
\r
- /*\r
- * polygonClockwise: Returns true if user inputted polygon in clockwise order, false if counterclockwise. The Law of Cosines is\r
- * used to determine the angle.\r
- */\r
- public boolean polygonClockwise (List<Vector2> polygon) {\r
float area = 0;\r
- for (int i = 0; i < polygon.size(); i++) {\r
- Vector2 p1 = polygon.get(i);\r
- Vector2 p2 = polygon.get(i == polygon.size() - 1 ? 0 : i + 1);\r
+ for(int i = 0; i < vertexCount; i++) {\r
+ final Vector2 p1 = pVertices.get(i);\r
+ final Vector2 p2 = pVertices.get(EarCutTriangulator.computeNextIndex(pVertices, i));\r
area += p1.x * p2.y - p2.x * p1.y;\r
}\r
\r
- if (area < 0)\r
+ if(area < 0) {\r
return true;\r
- else\r
+ } else {\r
return false;\r
-\r
-// float aa, bb, cc, b, c, theta;\r
-// float convex_turn;\r
-// float convex_sum = 0;\r
-//\r
-// for (int i = 0; i < polygon.size() - 2; i++) {\r
-// aa = ((polygon.get(i+2).getX() - polygon.get(i).getX()) * (polygon.get(i+2).getX() - polygon.get(i).getX())) +\r
-// ((-polygon.get(i+2).getY() + polygon.get(i).getY()) * (-polygon.get(i+2).getY() + polygon.get(i).getY()));\r
-//\r
-// bb = ((polygon.get(i+1).getX() - polygon.get(i).getX()) * (polygon.get(i+1).getX() - polygon.get(i).getX()) +\r
-// ((-polygon.get(i+1).getY() + polygon.get(i).getY()) * (-polygon.get(i+1).getY() + polygon.get(i).getY())));\r
-//\r
-// cc = ((polygon.get(i+2).getX() - polygon.get(i+1).getX()) *\r
-// (polygon.get(i+2).getX() - polygon.get(i+1).getX())) +\r
-// ((-polygon.get(i+2).getY() + polygon.get(i+1).getY()) *\r
-// (-polygon.get(i+2).getY() + polygon.get(i+1).getY()));\r
-//\r
-// b = (float)Math.sqrt(bb);\r
-// c = (float)Math.sqrt(cc);\r
-// theta = (float)Math.acos((bb + cc - aa) / (2 * b * c));\r
-//\r
-// if (convex(polygon.get(i).getX(), polygon.get(i).getY(),\r
-// polygon.get(i+1).getX(), polygon.get(i+1).getY(),\r
-// polygon.get(i+2).getX(), polygon.get(i+2).getY())) {\r
-// convex_turn = (float)Math.PI - theta;\r
-// convex_sum += convex_turn;\r
-// }\r
-// else {\r
-// convex_sum -= Math.PI - theta;\r
-// }\r
-// }\r
-// aa = ((polygon.get(1).getX() - polygon.get(polygon.size()-1).getX()) *\r
-// (polygon.get(1).getX() - polygon.get(polygon.size()-1).getX())) +\r
-// ((-polygon.get(1).getY() + polygon.get(polygon.size()-1).getY()) *\r
-// (-polygon.get(1).getY() + polygon.get(polygon.size()-1).getY()));\r
-//\r
-// bb = ((polygon.get(0).getX() - polygon.get(polygon.size()-1).getX()) *\r
-// (polygon.get(0).getX() - polygon.get(polygon.size()-1).getX())) +\r
-// ((-polygon.get(0).getY() + polygon.get(polygon.size()-1).getY()) *\r
-// (-polygon.get(0).getY() + polygon.get(polygon.size()-1).getY()));\r
-//\r
-// cc = ((polygon.get(1).getX() - polygon.get(0).getX()) *\r
-// (polygon.get(1).getX() - polygon.get(0).getX())) +\r
-// ((-polygon.get(1).getY() + polygon.get(0).getY()) *\r
-// (-polygon.get(1).getY() + polygon.get(0).getY()));\r
-//\r
-// b = (float)Math.sqrt(bb);\r
-// c = (float)Math.sqrt(cc);\r
-// theta = (float)Math.acos((bb + cc - aa) / (2 * b * c));\r
-//\r
-// if (convex(polygon.get(polygon.size()-1).getX(), polygon.get(polygon.size()-1).getY(),\r
-// polygon.get(0).getX(), polygon.get(0).getY(),\r
-// polygon.get(1).getX(), polygon.get(1).getY())) {\r
-// convex_turn = (float)Math.PI - theta;\r
-// convex_sum += convex_turn;\r
-// }\r
-// else {\r
-// convex_sum -= Math.PI - theta;\r
-// }\r
-//\r
-// if (convex_sum >= (2 * 3.14159))\r
-// return true;\r
-// else\r
-// return false;\r
+ }\r
}\r
\r
- /*\r
- * classifyPoints: Classifies points as "convex" or "concave". Convex points are represented as a "1" in the ptType array;\r
- * concave points are represented as a "-1" in the array.\r
+ /**\r
+ * @param pVertices\r
+ * @return An array of length <code>pVertices.size()</code> filled with either {@link EarCutTriangulator#CONCAVE} or\r
+ * {@link EarCutTriangulator#CONVEX}.\r
*/\r
- int concaveCount = 0;\r
-\r
- int[] classifyPoints (List<Vector2> polygon) {\r
- int[] ptType = new int[polygon.size()];\r
- concaveCount = 0;\r
-\r
- /*\r
- * Before cutting any ears, we must determine if the polygon was inputted in clockwise order or not, since the algorithm for\r
- * cutting ears assumes that the polygon's points are in clockwise order. If the points are in counterclockwise order, they\r
- * are simply reversed in the array.\r
- */\r
- if (!polygonClockwise(polygon)) {\r
- Collections.reverse(polygon);\r
+ private int[] classifyVertices(final ArrayList<Vector2> pVertices) {\r
+ final int vertexCount = pVertices.size();\r
+\r
+ final int[] vertexTypes = new int[vertexCount];\r
+ this.concaveVertexCount = 0;\r
+\r
+ /* Ensure vertices are in clockwise order. */\r
+ if(!EarCutTriangulator.areVerticesClockwise(pVertices)) {\r
+ Collections.reverse(pVertices);\r
}\r
\r
- for (int i = 0; i < polygon.size(); i++) {\r
- if (i == 0) {\r
- if (convex(polygon.get(polygon.size() - 1).x, polygon.get(polygon.size() - 1).y, polygon.get(i).x, polygon.get(i).y,\r
- polygon.get(i + 1).x, polygon.get(i + 1).y)) {\r
- ptType[i] = 1; /* point is convex */\r
- } else {\r
- ptType[i] = -1; /* point is concave */\r
- concaveCount++;\r
- }\r
- } else if (i == polygon.size() - 1) {\r
- if (convex(polygon.get(i - 1).x, polygon.get(i - 1).y, polygon.get(i).x, polygon.get(i).y, polygon.get(0).x,\r
- polygon.get(0).y)) {\r
- ptType[i] = 1; /* point is convex */\r
- } else {\r
- ptType[i] = -1; /* point is concave */\r
- concaveCount++;\r
- }\r
- } else { /* i > 0 */\r
- if (convex(polygon.get(i - 1).x, polygon.get(i - 1).y, polygon.get(i).x, polygon.get(i).y, polygon.get(i + 1).x,\r
- polygon.get(i + 1).y)) {\r
- ptType[i] = 1; /* point is convex */\r
- } else {\r
- ptType[i] = -1; /* point is concave */\r
- concaveCount++;\r
- }\r
+ for(int index = 0; index < vertexCount; index++) {\r
+ final int previousIndex = EarCutTriangulator.computePreviousIndex(pVertices, index);\r
+ final int nextIndex = EarCutTriangulator.computeNextIndex(pVertices, index);\r
+\r
+ final Vector2 previousVertex = pVertices.get(previousIndex);\r
+ final Vector2 currentVertex = pVertices.get(index);\r
+ final Vector2 nextVertex = pVertices.get(nextIndex);\r
+\r
+ if(EarCutTriangulator.isTriangleConvex(previousVertex.x, previousVertex.y, currentVertex.x, currentVertex.y, nextVertex.x, nextVertex.y)) {\r
+ vertexTypes[index] = CONVEX;\r
+ } else {\r
+ vertexTypes[index] = CONCAVE;\r
+ this.concaveVertexCount++;\r
}\r
}\r
\r
- return ptType;\r
+ return vertexTypes;\r
}\r
\r
- /*\r
- * convex: returns true if point (x2, y2) is convex\r
- */\r
- boolean convex (float x1, float y1, float x2, float y2, float x3, float y3) {\r
- if (area(x1, y1, x2, y2, x3, y3) < 0)\r
+ private static boolean isTriangleConvex(final float pX1, final float pY1, final float pX2, final float pY2, final float pX3, final float pY3) {\r
+ if(EarCutTriangulator.computeSpannedAreaSign(pX1, pY1, pX2, pY2, pX3, pY3) < 0) {\r
return false;\r
- else\r
+ } else {\r
return true;\r
+ }\r
}\r
\r
- /*\r
- * area: determines area of triangle formed by three points\r
- */\r
- float area (float x1, float y1, float x2, float y2, float x3, float y3) {\r
- float areaSum = 0;\r
-\r
- areaSum += x1 * (y3 - y2);\r
- areaSum += x2 * (y1 - y3);\r
- areaSum += x3 * (y2 - y1);\r
+ private static int computeSpannedAreaSign(final float pX1, final float pY1, final float pX2, final float pY2, final float pX3, final float pY3) {\r
+ float area = 0;\r
\r
- /*\r
- * for actual area, we need to multiple areaSum * 0.5, but we are only interested in the sign of the area (+/-)\r
- */\r
+ area += pX1 * (pY3 - pY2);\r
+ area += pX2 * (pY1 - pY3);\r
+ area += pX3 * (pY2 - pY1);\r
\r
- return areaSum;\r
+ return (int)Math.signum(area);\r
}\r
\r
- /*\r
- * triangleContainsPoints: returns true if the triangle formed by three points contains another point\r
+ /**\r
+ * @return <code>true</code> when the Triangles contains one or more vertices, <code>false</code> otherwise.\r
*/\r
- boolean triangleContainsPoint (List<Vector2> polygon, int[] ptType, float x1, float y1, float x2, float y2, float x3, float y3) {\r
+ private static boolean isAnyVertexInTriangle(final ArrayList<Vector2> pVertices, final int[] pVertexTypes, final float pX1, final float pY1, final float pX2, final float pY2, final float pX3, final float pY3) {\r
int i = 0;\r
- float area1, area2, area3;\r
- boolean noPointInTriangle = true;\r
-\r
- while ((i < polygon.size() - 1) && (noPointInTriangle)) {\r
- if ((ptType[i] == -1) /* point is concave */\r
- && (((polygon.get(i).x != x1) && (polygon.get(i).y != y1)) || ((polygon.get(i).x != x2) && (polygon.get(i).y != y2)) || ((polygon\r
- .get(i).x != x3) && (polygon.get(i).y != y3)))) {\r
\r
- area1 = area(x1, y1, x2, y2, polygon.get(i).x, polygon.get(i).y);\r
- area2 = area(x2, y2, x3, y3, polygon.get(i).x, polygon.get(i).y);\r
- area3 = area(x3, y3, x1, y1, polygon.get(i).x, polygon.get(i).y);\r
-\r
- if (area1 > 0) if ((area2 > 0) && (area3 > 0)) noPointInTriangle = false;\r
- if (area1 <= 0) if ((area2 <= 0) && (area3 <= 0)) noPointInTriangle = false;\r
+ final int vertexCount = pVertices.size();\r
+ while(i < vertexCount - 1) {\r
+ if((pVertexTypes[i] == CONCAVE)) {\r
+ final Vector2 currentVertex = pVertices.get(i);\r
+\r
+ final float currentVertexX = currentVertex.x;\r
+ final float currentVertexY = currentVertex.y;\r
+\r
+ /* TODO The following condition fails for perpendicular, axis aligned triangles! \r
+ * Removing it doesn't seem to cause problems. \r
+ * Maybe it was an optimization?\r
+ * Maybe it tried to handle collinear pieces ? */\r
+// if(((currentVertexX != pX1) && (currentVertexY != pY1)) || ((currentVertexX != pX2) && (currentVertexY != pY2)) || ((currentVertexX != pX3) && (currentVertexY != pY3))) {\r
+ final int areaSign1 = EarCutTriangulator.computeSpannedAreaSign(pX1, pY1, pX2, pY2, currentVertexX, currentVertexY);\r
+ final int areaSign2 = EarCutTriangulator.computeSpannedAreaSign(pX2, pY2, pX3, pY3, currentVertexX, currentVertexY);\r
+ final int areaSign3 = EarCutTriangulator.computeSpannedAreaSign(pX3, pY3, pX1, pY1, currentVertexX, currentVertexY);\r
+\r
+ if(areaSign1 > 0 && areaSign2 > 0 && areaSign3 > 0) {\r
+ return true;\r
+ } else if(areaSign1 <= 0 && areaSign2 <= 0 && areaSign3 <= 0) {\r
+ return true;\r
+ }\r
+// }\r
}\r
i++;\r
}\r
- return !noPointInTriangle;\r
+ return false;\r
}\r
\r
- /*\r
- * ear: returns true if the point (x2, y2) is an ear, false otherwise\r
- */\r
- boolean ear (List<Vector2> polygon, int[] ptType, float x1, float y1, float x2, float y2, float x3, float y3) {\r
- if (concaveCount != 0)\r
- if (triangleContainsPoint(polygon, ptType, x1, y1, x2, y2, x3, y3))\r
+ private boolean isEarTip(final ArrayList<Vector2> pVertices, final int pEarTipIndex, final int[] pVertexTypes) {\r
+ if(this.concaveVertexCount != 0) {\r
+ final Vector2 previousVertex = pVertices.get(EarCutTriangulator.computePreviousIndex(pVertices, pEarTipIndex));\r
+ final Vector2 currentVertex = pVertices.get(pEarTipIndex);\r
+ final Vector2 nextVertex = pVertices.get(EarCutTriangulator.computeNextIndex(pVertices, pEarTipIndex));\r
+\r
+ if(EarCutTriangulator.isAnyVertexInTriangle(pVertices, pVertexTypes, previousVertex.x, previousVertex.y, currentVertex.x, currentVertex.y, nextVertex.x, nextVertex.y)) {\r
return false;\r
- else\r
+ } else {\r
return true;\r
- else\r
+ }\r
+ } else {\r
return true;\r
+ }\r
}\r
\r
- /*\r
- * cutEar: creates triangle that represents ear for graphics purposes\r
- */\r
- void cutEar (List<Vector2> polygon, List<Vector2> triangles, int index) {\r
- if (index == 0) {\r
- triangles.add(new Vector2(polygon.get(polygon.size() - 1)));\r
- triangles.add(new Vector2(polygon.get(index)));\r
- triangles.add(new Vector2(polygon.get(index + 1)));\r
- } else if ((index > 0) && (index < polygon.size() - 1)) {\r
- triangles.add(new Vector2(polygon.get(index - 1)));\r
- triangles.add(new Vector2(polygon.get(index)));\r
- triangles.add(new Vector2(polygon.get(index + 1)));\r
- } else if (index == polygon.size() - 1) {\r
- triangles.add(new Vector2(polygon.get(index - 1)));\r
- triangles.add(new Vector2(polygon.get(index)));\r
- triangles.add(new Vector2(polygon.get(0)));\r
+ private void cutEarTip(final ArrayList<Vector2> pVertices, final int pEarTipIndex, final ArrayList<Vector2> pTriangles) {\r
+ final int previousIndex = EarCutTriangulator.computePreviousIndex(pVertices, pEarTipIndex);\r
+ final int nextIndex = EarCutTriangulator.computeNextIndex(pVertices, pEarTipIndex);\r
+\r
+ if(!EarCutTriangulator.isCollinear(pVertices, previousIndex, pEarTipIndex, nextIndex)) {\r
+ pTriangles.add(new Vector2(pVertices.get(previousIndex)));\r
+ pTriangles.add(new Vector2(pVertices.get(pEarTipIndex)));\r
+ pTriangles.add(new Vector2(pVertices.get(nextIndex)));\r
+ }\r
+\r
+ pVertices.remove(pEarTipIndex);\r
+ if(pVertices.size() >= 3) {\r
+ EarCutTriangulator.removeCollinearNeighborEarsAfterRemovingEarTip(pVertices, pEarTipIndex);\r
}\r
}\r
\r
- /*\r
- * updatePolygon: creates new polygon without the ear that was cut\r
- */\r
- void updatePolygon (List<Vector2> polygon, int index) {\r
- polygon.remove(index);\r
+ private static void removeCollinearNeighborEarsAfterRemovingEarTip(final ArrayList<Vector2> pVertices, final int pEarTipCutIndex) {\r
+ final int collinearityCheckNextIndex = pEarTipCutIndex % pVertices.size();\r
+ int collinearCheckPreviousIndex = EarCutTriangulator.computePreviousIndex(pVertices, collinearityCheckNextIndex);\r
+\r
+ if(EarCutTriangulator.isCollinear(pVertices, collinearityCheckNextIndex)) {\r
+ pVertices.remove(collinearityCheckNextIndex);\r
+\r
+ if(pVertices.size() > 3) {\r
+ /* Update */\r
+ collinearCheckPreviousIndex = EarCutTriangulator.computePreviousIndex(pVertices, collinearityCheckNextIndex);\r
+ if(EarCutTriangulator.isCollinear(pVertices, collinearCheckPreviousIndex)){\r
+ pVertices.remove(collinearCheckPreviousIndex);\r
+ }\r
+ }\r
+ } else if(EarCutTriangulator.isCollinear(pVertices, collinearCheckPreviousIndex)){\r
+ pVertices.remove(collinearCheckPreviousIndex);\r
+ }\r
+ }\r
+\r
+ private static boolean isCollinear(final ArrayList<Vector2> pVertices, final int pIndex) {\r
+ final int previousIndex = EarCutTriangulator.computePreviousIndex(pVertices, pIndex);\r
+ final int nextIndex = EarCutTriangulator.computeNextIndex(pVertices, pIndex);\r
+\r
+ return EarCutTriangulator.isCollinear(pVertices, previousIndex, pIndex, nextIndex);\r
+ }\r
+\r
+ private static boolean isCollinear(final ArrayList<Vector2> pVertices, final int pPreviousIndex, final int pIndex, final int pNextIndex) {\r
+ final Vector2 previousVertex = pVertices.get(pPreviousIndex);\r
+ final Vector2 vertex = pVertices.get(pIndex);\r
+ final Vector2 nextVertex = pVertices.get(pNextIndex);\r
+\r
+ return EarCutTriangulator.computeSpannedAreaSign(previousVertex.x, previousVertex.y, vertex.x, vertex.y, nextVertex.x, nextVertex.y) == 0;\r
}\r
\r
-}\r
+ private static int computePreviousIndex(final List<Vector2> pVertices, final int pIndex) {\r
+ return pIndex == 0 ? pVertices.size() - 1 : pIndex - 1;\r
+ }\r
+\r
+ private static int computeNextIndex(final List<Vector2> pVertices, final int pIndex) {\r
+ return pIndex == pVertices.size() - 1 ? 0 : pIndex + 1;\r
+ }\r
+}
\ No newline at end of file