|
@@ -31,39 +31,17 @@ class Earcut {
|
|
|
public function new() {
|
|
|
}
|
|
|
|
|
|
- @:generic public function triangulate<T:{x:Float,y:Float}>( points : Array<T> ) : Array<Int> {
|
|
|
- var outerLen = points.length;
|
|
|
+ @:generic public function triangulate < T: { x:Float, y:Float } > ( points : Array<T>, ?holes : Array<Int> ) : Array<Int> {
|
|
|
|
|
|
- if( outerLen < 3 )
|
|
|
- return [];
|
|
|
+ var hasHoles = holes != null && holes.length > 0;
|
|
|
+ var outerLen = hasHoles ? holes[0] : points.length;
|
|
|
+ if( outerLen < 3 ) return [];
|
|
|
|
|
|
- var root : EarNode = allocNode( -1, 0, 0, null);
|
|
|
- var first = root;
|
|
|
+ var root = setLinkedList(points, 0, outerLen, true);
|
|
|
+ //eliminate holes
|
|
|
+ if(holes != null)
|
|
|
+ root = eliminateHoles(points, holes, root);
|
|
|
|
|
|
- // check polygon winding
|
|
|
- var sum = 0.;
|
|
|
- var p = points[points.length - 1];
|
|
|
- for( p2 in points ) {
|
|
|
- sum += (p.x - p2.x) * (p.y + p2.y);
|
|
|
- p = p2;
|
|
|
- }
|
|
|
- if( sum > 0 ) {
|
|
|
- // clockwise
|
|
|
- for( i in 0...points.length ) {
|
|
|
- var p = points[i];
|
|
|
- root = allocNode(i, p.x, p.y, root);
|
|
|
- }
|
|
|
- } else {
|
|
|
- var last = points.length - 1;
|
|
|
- for( i in 0...points.length ) {
|
|
|
- var p = points[last - i];
|
|
|
- root = allocNode(last - i, p.x, p.y, root);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // close loop
|
|
|
- root.next = first.next;
|
|
|
- root.next.prev = root;
|
|
|
return triangulateNode(root, points.length > 80);
|
|
|
}
|
|
|
|
|
@@ -109,6 +87,132 @@ class Earcut {
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
+ function setLinkedList < T: { x:Float, y:Float } > (points : Array<T>, start : Int, end : Int, clockwise : Bool) {
|
|
|
+
|
|
|
+ // check polygon winding
|
|
|
+ var sum = 0.;
|
|
|
+ var j = end - 1;
|
|
|
+ for (i in start...end) {
|
|
|
+ sum += (points[j].x - points[i].x) * (points[i].y + points[j].y);
|
|
|
+ j = i;
|
|
|
+ }
|
|
|
+
|
|
|
+ // link points into circular doubly-linked list in the specified winding order
|
|
|
+ var node = allocNode(-1, 0, 0, null);
|
|
|
+ var first = node;
|
|
|
+ if (clockwise == (sum > 0)) {
|
|
|
+ for (i in start...end) {
|
|
|
+ var p = points[i];
|
|
|
+ node = allocNode(i, p.x, p.y, node);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ var i = end - 1;
|
|
|
+ while(i >= start) {
|
|
|
+ var p = points[i];
|
|
|
+ node = allocNode(i, p.x, p.y, node);
|
|
|
+ i--;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ node.next = first.next;
|
|
|
+ node.next.prev = node;
|
|
|
+ return node;
|
|
|
+ }
|
|
|
+
|
|
|
+ // link every hole into the outer loop, producing a single-ring polygon without holes
|
|
|
+ function eliminateHoles < T: { x:Float, y:Float } > (points : Array<T>, holes : Array<Int>, root : EarNode) {
|
|
|
+ var queue = [];
|
|
|
+
|
|
|
+ for(i in 0...holes.length) {
|
|
|
+ var s = holes[i];
|
|
|
+ var e = i == holes.length - 1 ? points.length : holes[i + 1];
|
|
|
+ var node = setLinkedList(points, s, e, false);
|
|
|
+ if (node == node.next) node.steiner = true;
|
|
|
+ queue.push(getLeftmost(node));
|
|
|
+ }
|
|
|
+
|
|
|
+ queue.sort(compareX);
|
|
|
+
|
|
|
+ // process holes from left to right
|
|
|
+ for( q in queue) {
|
|
|
+ eliminateHole(q, root);
|
|
|
+ root = filterPoints(root, root.next);
|
|
|
+ }
|
|
|
+
|
|
|
+ return root;
|
|
|
+ }
|
|
|
+
|
|
|
+ // find a bridge between vertices that connects hole with an outer ring and and link it
|
|
|
+ function eliminateHole(hole, root) {
|
|
|
+ root = findHoleBridge(hole, root);
|
|
|
+ if (root != null) {
|
|
|
+ var b = splitPolygon(root, hole);
|
|
|
+ filterPoints(b, b.next);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // David Eberly's algorithm for finding a bridge between hole and outer polygon
|
|
|
+ function findHoleBridge(hole : EarNode, root : EarNode) {
|
|
|
+ var p = root;
|
|
|
+ var hx = hole.x;
|
|
|
+ var hy = hole.y;
|
|
|
+ var qx = Math.NEGATIVE_INFINITY;
|
|
|
+ var m = null;
|
|
|
+
|
|
|
+ // find a segment intersected by a ray from the hole's leftmost point to the left;
|
|
|
+ // segment's endpoint with lesser x will be potential connection point
|
|
|
+ do {
|
|
|
+ if (hy <= p.y && hy >= p.next.y) {
|
|
|
+ var x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
|
|
|
+ if (x <= hx && x > qx) {
|
|
|
+ qx = x;
|
|
|
+ m = p.x < p.next.x ? p : p.next;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ p = p.next;
|
|
|
+ } while (p != root);
|
|
|
+
|
|
|
+ if (m == null) return null;
|
|
|
+
|
|
|
+ // look for points inside the triangle of hole point, segment intersection and endpoint;
|
|
|
+ // if there are no points found, we have a valid connection;
|
|
|
+ // otherwise choose the point of the minimum angle with the ray as connection point
|
|
|
+ var stop = m;
|
|
|
+ var tanMin = Math.POSITIVE_INFINITY;
|
|
|
+ var tan;
|
|
|
+
|
|
|
+ p = m.next;
|
|
|
+ while (p != stop) {
|
|
|
+ if (hx >= p.x && p.x >= m.x && pointInTriangle(hy < m.y ? hx : qx, hy, m.x, m.y, hy < m.y ? qx : hx, hy, p.x, p.y)) {
|
|
|
+ tan = Math.abs(hy - p.y) / (hx - p.x); // tangential
|
|
|
+ if ((tan < tanMin || (tan == tanMin && p.x > m.x)) && locallyInside(p, hole)) {
|
|
|
+ m = p;
|
|
|
+ tanMin = tan;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ p = p.next;
|
|
|
+ }
|
|
|
+
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ // find the leftmost node of a polygon ring
|
|
|
+ function getLeftmost(node : EarNode) {
|
|
|
+ var p = node, leftmost = node;
|
|
|
+ do {
|
|
|
+ if (p.x < leftmost.x) leftmost = p;
|
|
|
+ p = p.next;
|
|
|
+ } while (p != node);
|
|
|
+
|
|
|
+ return leftmost;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ inline function compareX(a : EarNode, b : EarNode) {
|
|
|
+ return a.x - b.x > 0 ? 1 : -1;
|
|
|
+ }
|
|
|
+
|
|
|
inline function equals(p1:EarNode, p2:EarNode) {
|
|
|
return p1.x == p2.x && p1.y == p2.y;
|
|
|
}
|