فهرست منبع

Merge pull request #12661 from Mugen87/dev6

ShapeUtils: Use earcut for polygon triangulation (2nd attempt)
Mr.doob 7 سال پیش
والد
کامیت
e523785b46
5فایلهای تغییر یافته به همراه597 افزوده شده و 1687 حذف شده
  1. 4 30
      docs/api/extras/ShapeUtils.html
  2. 0 1
      examples/files.js
  3. 0 654
      examples/js/libs/earcut.js
  4. 0 617
      examples/webgl_geometry_text_earcut.html
  5. 593 385
      src/extras/ShapeUtils.js

+ 4 - 30
docs/api/extras/ShapeUtils.html

@@ -28,30 +28,6 @@
 
 
 		</div>
 		</div>
 
 
-		<h3>[method:Number b2]( t, p0, p1, p2 )</h3>
-		<div>
-		t -- number<br />
-		p0, p1, p2 -- x, y, z or w components of a quadratic bezier curve.<br /><br />
-
-		Note that this is a linear function so it is neccessary to calculate separately for
-		x, y (and z for 3D curves) components of a curve.<br /><br />
-
-		Used internally by [page:QuadraticBezierCurve QuadraticBezierCurve],
-		[page:QuadraticBezierCurve3 QuadraticBezierCurve3] and [page:Font Font].
-		</div>
-
-		<h3>[method:Number b3]( t, p0, p1, p2, p3 )</h3>
-		<div>
-		t -- number. <br />
-		p0, p1, p2, p3 -- x, y or z components of a cubic bezier curve..<br /><br />
-
-		Note that this is a linear function so it is neccessary to calculate separately for
-		x, y (and z for 3D curves) components of a curve.<br /><br />
-
-		Used internally by [page:CubicBezierCurve CubicBezierCurve],
-		[page:CubicBezierCurve3 CubicBezierCurve3] and [page:Font Font].
-		</div>
-
 		<h3>[method:Boolean isClockwise]( pts )</h3>
 		<h3>[method:Boolean isClockwise]( pts )</h3>
 		<div>
 		<div>
 		pts -- points defining a 2D polygon<br /><br />
 		pts -- points defining a 2D polygon<br /><br />
@@ -65,11 +41,10 @@
 
 
 		<h3>[method:null triangulate]( contour, indices )</h3>
 		<h3>[method:null triangulate]( contour, indices )</h3>
 		<div>
 		<div>
-		contour --  2D polygon.<br />
-		indices -- <br /><br />
+		vertices -- flat array of vertices like [ x0,y0, x1,y1, x2,y2, ... ].<br />
+		holeIndices -- array of hole indices.<br /><br />
 
 
-		Used internally by [page:ExtrudeGeometry ExtrudeGeometry]
-		and [page:ShapeBufferGeometry ShapeBufferGeometry] to calculate faces.
+		Performs a polygon triangulation based on earcut. The implementation is a port of [link:https://github.com/mapbox/earcut mapbox/earcut].
 		</div>
 		</div>
 
 
 		<h3>[method:null triangulateShape]( contour, holes )</h3>
 		<h3>[method:null triangulateShape]( contour, holes )</h3>
@@ -77,8 +52,7 @@
 		contour -- 2D polygon.<br />
 		contour -- 2D polygon.<br />
 		holes -- array of holes<br /><br />
 		holes -- array of holes<br /><br />
 
 
-		Used internally by [page:ExtrudeGeometry ExtrudeGeometry]
-		and [page:ShapeBufferGeometry ShapeBufferGeometry] to calculate faces in shapes with holes.
+		Used by [page:ExtrudeGeometry ExtrudeGeometry] and [page:ShapeBufferGeometry ShapeBufferGeometry] to calculate faces in shapes with holes.
 		</div>
 		</div>
 
 
 		<h2>Source</h2>
 		<h2>Source</h2>

+ 0 - 1
examples/files.js

@@ -42,7 +42,6 @@ var files = {
 		"webgl_geometry_terrain_fog",
 		"webgl_geometry_terrain_fog",
 		"webgl_geometry_terrain_raycast",
 		"webgl_geometry_terrain_raycast",
 		"webgl_geometry_text",
 		"webgl_geometry_text",
-		"webgl_geometry_text_earcut",
 		"webgl_geometry_text_pnltri",
 		"webgl_geometry_text_pnltri",
 		"webgl_geometry_text_shapes",
 		"webgl_geometry_text_shapes",
 		"webgl_gpgpu_birds",
 		"webgl_gpgpu_birds",

+ 0 - 654
examples/js/libs/earcut.js

@@ -1,654 +0,0 @@
-/**
- *
- * Earcut https://github.com/mapbox/earcut
- *
- * Copyright (c) 2016, Mapbox
- *
- * Permission to use, copy, modify, and/or distribute this software for any purpose
- * with or without fee is hereby granted, provided that the above copyright notice
- * and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
- * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
- * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
- * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
- * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
- * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
- * THIS SOFTWARE.
- */
-'use strict';
-
-//module.exports = earcut;
-
-function earcut(data, holeIndices, dim) {
-
-    dim = dim || 2;
-
-    var hasHoles = holeIndices && holeIndices.length,
-        outerLen = hasHoles ? holeIndices[0] * dim : data.length,
-        outerNode = linkedList(data, 0, outerLen, dim, true),
-        triangles = [];
-
-    if (!outerNode) return triangles;
-
-    var minX, minY, maxX, maxY, x, y, size;
-
-    if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim);
-
-    // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
-    if (data.length > 80 * dim) {
-        minX = maxX = data[0];
-        minY = maxY = data[1];
-
-        for (var i = dim; i < outerLen; i += dim) {
-            x = data[i];
-            y = data[i + 1];
-            if (x < minX) minX = x;
-            if (y < minY) minY = y;
-            if (x > maxX) maxX = x;
-            if (y > maxY) maxY = y;
-        }
-
-        // minX, minY and size are later used to transform coords into integers for z-order calculation
-        size = Math.max(maxX - minX, maxY - minY);
-    }
-
-    earcutLinked(outerNode, triangles, dim, minX, minY, size);
-
-    return triangles;
-}
-
-// create a circular doubly linked list from polygon points in the specified winding order
-function linkedList(data, start, end, dim, clockwise) {
-    var i, last;
-
-    if (clockwise === (signedArea(data, start, end, dim) > 0)) {
-        for (i = start; i < end; i += dim) last = insertNode(i, data[i], data[i + 1], last);
-    } else {
-        for (i = end - dim; i >= start; i -= dim) last = insertNode(i, data[i], data[i + 1], last);
-    }
-
-    if (last && equals(last, last.next)) {
-        removeNode(last);
-        last = last.next;
-    }
-
-    return last;
-}
-
-// eliminate colinear or duplicate points
-function filterPoints(start, end) {
-    if (!start) return start;
-    if (!end) end = start;
-
-    var p = start,
-        again;
-    do {
-        again = false;
-
-        if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
-            removeNode(p);
-            p = end = p.prev;
-            if (p === p.next) return null;
-            again = true;
-
-        } else {
-            p = p.next;
-        }
-    } while (again || p !== end);
-
-    return end;
-}
-
-// main ear slicing loop which triangulates a polygon (given as a linked list)
-function earcutLinked(ear, triangles, dim, minX, minY, size, pass) {
-    if (!ear) return;
-
-    // interlink polygon nodes in z-order
-    if (!pass && size) indexCurve(ear, minX, minY, size);
-
-    var stop = ear,
-        prev, next;
-
-    // iterate through ears, slicing them one by one
-    while (ear.prev !== ear.next) {
-        prev = ear.prev;
-        next = ear.next;
-
-        if (size ? isEarHashed(ear, minX, minY, size) : isEar(ear)) {
-            // cut off the triangle
-            triangles.push(prev.i / dim);
-            triangles.push(ear.i / dim);
-            triangles.push(next.i / dim);
-
-            removeNode(ear);
-
-            // skipping the next vertice leads to less sliver triangles
-            ear = next.next;
-            stop = next.next;
-
-            continue;
-        }
-
-        ear = next;
-
-        // if we looped through the whole remaining polygon and can't find any more ears
-        if (ear === stop) {
-            // try filtering points and slicing again
-            if (!pass) {
-                earcutLinked(filterPoints(ear), triangles, dim, minX, minY, size, 1);
-
-            // if this didn't work, try curing all small self-intersections locally
-            } else if (pass === 1) {
-                ear = cureLocalIntersections(ear, triangles, dim);
-                earcutLinked(ear, triangles, dim, minX, minY, size, 2);
-
-            // as a last resort, try splitting the remaining polygon into two
-            } else if (pass === 2) {
-                splitEarcut(ear, triangles, dim, minX, minY, size);
-            }
-
-            break;
-        }
-    }
-}
-
-// check whether a polygon node forms a valid ear with adjacent nodes
-function isEar(ear) {
-    var a = ear.prev,
-        b = ear,
-        c = ear.next;
-
-    if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
-
-    // now make sure we don't have other points inside the potential ear
-    var p = ear.next.next;
-
-    while (p !== ear.prev) {
-        if (pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
-            area(p.prev, p, p.next) >= 0) return false;
-        p = p.next;
-    }
-
-    return true;
-}
-
-function isEarHashed(ear, minX, minY, size) {
-    var a = ear.prev,
-        b = ear,
-        c = ear.next;
-
-    if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
-
-    // triangle bbox; min & max are calculated like this for speed
-    var minTX = a.x < b.x ? (a.x < c.x ? a.x : c.x) : (b.x < c.x ? b.x : c.x),
-        minTY = a.y < b.y ? (a.y < c.y ? a.y : c.y) : (b.y < c.y ? b.y : c.y),
-        maxTX = a.x > b.x ? (a.x > c.x ? a.x : c.x) : (b.x > c.x ? b.x : c.x),
-        maxTY = a.y > b.y ? (a.y > c.y ? a.y : c.y) : (b.y > c.y ? b.y : c.y);
-
-    // z-order range for the current triangle bbox;
-    var minZ = zOrder(minTX, minTY, minX, minY, size),
-        maxZ = zOrder(maxTX, maxTY, minX, minY, size);
-
-    // first look for points inside the triangle in increasing z-order
-    var p = ear.nextZ;
-
-    while (p && p.z <= maxZ) {
-        if (p !== ear.prev && p !== ear.next &&
-            pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
-            area(p.prev, p, p.next) >= 0) return false;
-        p = p.nextZ;
-    }
-
-    // then look for points in decreasing z-order
-    p = ear.prevZ;
-
-    while (p && p.z >= minZ) {
-        if (p !== ear.prev && p !== ear.next &&
-            pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
-            area(p.prev, p, p.next) >= 0) return false;
-        p = p.prevZ;
-    }
-
-    return true;
-}
-
-// go through all polygon nodes and cure small local self-intersections
-function cureLocalIntersections(start, triangles, dim) {
-    var p = start;
-    do {
-        var a = p.prev,
-            b = p.next.next;
-
-        if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {
-
-            triangles.push(a.i / dim);
-            triangles.push(p.i / dim);
-            triangles.push(b.i / dim);
-
-            // remove two nodes involved
-            removeNode(p);
-            removeNode(p.next);
-
-            p = start = b;
-        }
-        p = p.next;
-    } while (p !== start);
-
-    return p;
-}
-
-// try splitting polygon into two and triangulate them independently
-function splitEarcut(start, triangles, dim, minX, minY, size) {
-    // look for a valid diagonal that divides the polygon into two
-    var a = start;
-    do {
-        var b = a.next.next;
-        while (b !== a.prev) {
-            if (a.i !== b.i && isValidDiagonal(a, b)) {
-                // split the polygon in two by the diagonal
-                var c = splitPolygon(a, b);
-
-                // filter colinear points around the cuts
-                a = filterPoints(a, a.next);
-                c = filterPoints(c, c.next);
-
-                // run earcut on each half
-                earcutLinked(a, triangles, dim, minX, minY, size);
-                earcutLinked(c, triangles, dim, minX, minY, size);
-                return;
-            }
-            b = b.next;
-        }
-        a = a.next;
-    } while (a !== start);
-}
-
-// link every hole into the outer loop, producing a single-ring polygon without holes
-function eliminateHoles(data, holeIndices, outerNode, dim) {
-    var queue = [],
-        i, len, start, end, list;
-
-    for (i = 0, len = holeIndices.length; i < len; i++) {
-        start = holeIndices[i] * dim;
-        end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
-        list = linkedList(data, start, end, dim, false);
-        if (list === list.next) list.steiner = true;
-        queue.push(getLeftmost(list));
-    }
-
-    queue.sort(compareX);
-
-    // process holes from left to right
-    for (i = 0; i < queue.length; i++) {
-        eliminateHole(queue[i], outerNode);
-        outerNode = filterPoints(outerNode, outerNode.next);
-    }
-
-    return outerNode;
-}
-
-function compareX(a, b) {
-    return a.x - b.x;
-}
-
-// find a bridge between vertices that connects hole with an outer ring and and link it
-function eliminateHole(hole, outerNode) {
-    outerNode = findHoleBridge(hole, outerNode);
-    if (outerNode) {
-        var b = splitPolygon(outerNode, hole);
-        filterPoints(b, b.next);
-    }
-}
-
-// David Eberly's algorithm for finding a bridge between hole and outer polygon
-function findHoleBridge(hole, outerNode) {
-    var p = outerNode,
-        hx = hole.x,
-        hy = hole.y,
-        qx = -Infinity,
-        m;
-
-    // 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 && p.next.y !== p.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;
-                if (x === hx) {
-                    if (hy === p.y) return p;
-                    if (hy === p.next.y) return p.next;
-                }
-                m = p.x < p.next.x ? p : p.next;
-            }
-        }
-        p = p.next;
-    } while (p !== outerNode);
-
-    if (!m) return null;
-
-    if (hx === qx) return m.prev; // hole touches outer segment; pick lower endpoint
-
-    // 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,
-        mx = m.x,
-        my = m.y,
-        tanMin = Infinity,
-        tan;
-
-    p = m.next;
-
-    while (p !== stop) {
-        if (hx >= p.x && p.x >= mx && hx !== p.x &&
-                pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? 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;
-}
-
-// interlink polygon nodes in z-order
-function indexCurve(start, minX, minY, size) {
-    var p = start;
-    do {
-        if (p.z === null) p.z = zOrder(p.x, p.y, minX, minY, size);
-        p.prevZ = p.prev;
-        p.nextZ = p.next;
-        p = p.next;
-    } while (p !== start);
-
-    p.prevZ.nextZ = null;
-    p.prevZ = null;
-
-    sortLinked(p);
-}
-
-// Simon Tatham's linked list merge sort algorithm
-// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
-function sortLinked(list) {
-    var i, p, q, e, tail, numMerges, pSize, qSize,
-        inSize = 1;
-
-    do {
-        p = list;
-        list = null;
-        tail = null;
-        numMerges = 0;
-
-        while (p) {
-            numMerges++;
-            q = p;
-            pSize = 0;
-            for (i = 0; i < inSize; i++) {
-                pSize++;
-                q = q.nextZ;
-                if (!q) break;
-            }
-            qSize = inSize;
-
-            while (pSize > 0 || (qSize > 0 && q)) {
-
-                if (pSize !== 0 && (qSize === 0 || !q || p.z <= q.z)) {
-                    e = p;
-                    p = p.nextZ;
-                    pSize--;
-                } else {
-                    e = q;
-                    q = q.nextZ;
-                    qSize--;
-                }
-
-                if (tail) tail.nextZ = e;
-                else list = e;
-
-                e.prevZ = tail;
-                tail = e;
-            }
-
-            p = q;
-        }
-
-        tail.nextZ = null;
-        inSize *= 2;
-
-    } while (numMerges > 1);
-
-    return list;
-}
-
-// z-order of a point given coords and size of the data bounding box
-function zOrder(x, y, minX, minY, size) {
-    // coords are transformed into non-negative 15-bit integer range
-    x = 32767 * (x - minX) / size;
-    y = 32767 * (y - minY) / size;
-
-    x = (x | (x << 8)) & 0x00FF00FF;
-    x = (x | (x << 4)) & 0x0F0F0F0F;
-    x = (x | (x << 2)) & 0x33333333;
-    x = (x | (x << 1)) & 0x55555555;
-
-    y = (y | (y << 8)) & 0x00FF00FF;
-    y = (y | (y << 4)) & 0x0F0F0F0F;
-    y = (y | (y << 2)) & 0x33333333;
-    y = (y | (y << 1)) & 0x55555555;
-
-    return x | (y << 1);
-}
-
-// find the leftmost node of a polygon ring
-function getLeftmost(start) {
-    var p = start,
-        leftmost = start;
-    do {
-        if (p.x < leftmost.x) leftmost = p;
-        p = p.next;
-    } while (p !== start);
-
-    return leftmost;
-}
-
-// check if a point lies within a convex triangle
-function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {
-    return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 &&
-           (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 &&
-           (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0;
-}
-
-// check if a diagonal between two polygon nodes is valid (lies in polygon interior)
-function isValidDiagonal(a, b) {
-    return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) &&
-           locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b);
-}
-
-// signed area of a triangle
-function area(p, q, r) {
-    return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
-}
-
-// check if two points are equal
-function equals(p1, p2) {
-    return p1.x === p2.x && p1.y === p2.y;
-}
-
-// check if two segments intersect
-function intersects(p1, q1, p2, q2) {
-    if ((equals(p1, q1) && equals(p2, q2)) ||
-        (equals(p1, q2) && equals(p2, q1))) return true;
-    return area(p1, q1, p2) > 0 !== area(p1, q1, q2) > 0 &&
-           area(p2, q2, p1) > 0 !== area(p2, q2, q1) > 0;
-}
-
-// check if a polygon diagonal intersects any polygon segments
-function intersectsPolygon(a, b) {
-    var p = a;
-    do {
-        if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&
-                intersects(p, p.next, a, b)) return true;
-        p = p.next;
-    } while (p !== a);
-
-    return false;
-}
-
-// check if a polygon diagonal is locally inside the polygon
-function locallyInside(a, b) {
-    return area(a.prev, a, a.next) < 0 ?
-        area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 :
-        area(a, b, a.prev) < 0 || area(a, a.next, b) < 0;
-}
-
-// check if the middle point of a polygon diagonal is inside the polygon
-function middleInside(a, b) {
-    var p = a,
-        inside = false,
-        px = (a.x + b.x) / 2,
-        py = (a.y + b.y) / 2;
-    do {
-        if (((p.y > py) !== (p.next.y > py)) && p.next.y !== p.y &&
-                (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x))
-            inside = !inside;
-        p = p.next;
-    } while (p !== a);
-
-    return inside;
-}
-
-// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;
-// if one belongs to the outer ring and another to a hole, it merges it into a single ring
-function splitPolygon(a, b) {
-    var a2 = new Node(a.i, a.x, a.y),
-        b2 = new Node(b.i, b.x, b.y),
-        an = a.next,
-        bp = b.prev;
-
-    a.next = b;
-    b.prev = a;
-
-    a2.next = an;
-    an.prev = a2;
-
-    b2.next = a2;
-    a2.prev = b2;
-
-    bp.next = b2;
-    b2.prev = bp;
-
-    return b2;
-}
-
-// create a node and optionally link it with previous one (in a circular doubly linked list)
-function insertNode(i, x, y, last) {
-    var p = new Node(i, x, y);
-
-    if (!last) {
-        p.prev = p;
-        p.next = p;
-
-    } else {
-        p.next = last.next;
-        p.prev = last;
-        last.next.prev = p;
-        last.next = p;
-    }
-    return p;
-}
-
-function removeNode(p) {
-    p.next.prev = p.prev;
-    p.prev.next = p.next;
-
-    if (p.prevZ) p.prevZ.nextZ = p.nextZ;
-    if (p.nextZ) p.nextZ.prevZ = p.prevZ;
-}
-
-function Node(i, x, y) {
-    // vertice index in coordinates array
-    this.i = i;
-
-    // vertex coordinates
-    this.x = x;
-    this.y = y;
-
-    // previous and next vertice nodes in a polygon ring
-    this.prev = null;
-    this.next = null;
-
-    // z-order curve value
-    this.z = null;
-
-    // previous and next nodes in z-order
-    this.prevZ = null;
-    this.nextZ = null;
-
-    // indicates whether this is a steiner point
-    this.steiner = false;
-}
-
-// return a percentage difference between the polygon area and its triangulation area;
-// used to verify correctness of triangulation
-earcut.deviation = function (data, holeIndices, dim, triangles) {
-    var hasHoles = holeIndices && holeIndices.length;
-    var outerLen = hasHoles ? holeIndices[0] * dim : data.length;
-
-    var polygonArea = Math.abs(signedArea(data, 0, outerLen, dim));
-    if (hasHoles) {
-        for (var i = 0, len = holeIndices.length; i < len; i++) {
-            var start = holeIndices[i] * dim;
-            var end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
-            polygonArea -= Math.abs(signedArea(data, start, end, dim));
-        }
-    }
-
-    var trianglesArea = 0;
-    for (i = 0; i < triangles.length; i += 3) {
-        var a = triangles[i] * dim;
-        var b = triangles[i + 1] * dim;
-        var c = triangles[i + 2] * dim;
-        trianglesArea += Math.abs(
-            (data[a] - data[c]) * (data[b + 1] - data[a + 1]) -
-            (data[a] - data[b]) * (data[c + 1] - data[a + 1]));
-    }
-
-    return polygonArea === 0 && trianglesArea === 0 ? 0 :
-        Math.abs((trianglesArea - polygonArea) / polygonArea);
-};
-
-function signedArea(data, start, end, dim) {
-    var sum = 0;
-    for (var i = start, j = end - dim; i < end; i += dim) {
-        sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]);
-        j = i;
-    }
-    return sum;
-}
-
-// turn a polygon in a multi-dimensional array form (e.g. as in GeoJSON) into a form Earcut accepts
-earcut.flatten = function (data) {
-    var dim = data[0][0].length,
-        result = {vertices: [], holes: [], dimensions: dim},
-        holeIndex = 0;
-
-    for (var i = 0; i < data.length; i++) {
-        for (var j = 0; j < data[i].length; j++) {
-            for (var d = 0; d < dim; d++) result.vertices.push(data[i][j][d]);
-        }
-        if (i > 0) {
-            holeIndex += data[i - 1].length;
-            result.holes.push(holeIndex);
-        }
-    }
-    return result;
-};

+ 0 - 617
examples/webgl_geometry_text_earcut.html

@@ -1,617 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-	<head>
-		<title>three.js webgl - geometry - text</title>
-		<meta charset="utf-8">
-		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
-		<style>
-			body {
-				font-family: Monospace;
-				background-color: #000;
-				color: #fff;
-				margin: 0px;
-				overflow: hidden;
-			}
-			#info {
-				position: absolute;
-				top: 10px;
-				width: 100%;
-				text-align: center;
-				z-index: 100;
-				display:block;
-			}
-			#info a, .button { color: #f00; font-weight: bold; text-decoration: underline; cursor: pointer }
-		</style>
-	</head>
-	<body>
-
-		<div id="info">
-		<a href="http://threejs.org" target="_blank" rel="noopener">three.js</a> - procedural 3D text by <a href="http://www.lab4games.net/zz85/blog" target="_blank" rel="noopener">zz85</a> &amp; alteredq
-		<br/>built-in shape triangulation has been replaced with <a href="https://github.com/mapbox/earcut">Earcut</a> by <a href="https://github.com/mourner" target="_blank" rel="noopener">mourner</a>
-		<br/>type to enter new text, drag to spin the text
-		<br/><span class="button" id="color">change color</span>,
-			<span class="button" id="font">change font</span>,
-			<span class="button" id="weight">change weight</span>,
-			<span class="button" id="bevel">change bevel</span>
-			<a id="permalink" href="#">permalink</a>
-		</div>
-
-
-		<script src="../build/three.js"></script>
-		<script src="js/utils/GeometryUtils.js"></script>
-
-		<script src="js/Detector.js"></script>
-		<script src="js/libs/stats.min.js"></script>
-
-
-		<!-- replace built-in triangulation with Earcut -->
-		<script src="js/libs/earcut.js"></script>
-		<script>
-			THREE.ShapeUtils.triangulateShape = function ( contour, holes ) {
-
-				function removeDupEndPts( points ) {
-
-					var l = points.length;
-					if ( l > 2 && points[ l - 1 ].equals( points[ 0 ] ) ) {
-
-						points.pop();
-
-					}
-
-				}
-
-				function addContour( vertices, contour ) {
-
-					for ( var i = 0; i < contour.length; i ++ ) {
-
-						vertices.push( contour[ i ].x );
-						vertices.push( contour[ i ].y );
-
-					}
-
-				}
-
-				removeDupEndPts( contour );
-				holes.forEach( removeDupEndPts );
-
-				var vertices = [];
-				addContour( vertices, contour );
-				var holeIndices = [];
-				var holeIndex = contour.length;
-				for ( i = 0; i < holes.length; i ++ ) {
-
-					holeIndices.push( holeIndex );
-					holeIndex += holes[ i ].length;
-					addContour( vertices, holes[ i ] );
-
-				}
-
-				var result = earcut( vertices, holeIndices, 2 );
-				var grouped = [];
-				for ( var i = 0; i < result.length; i += 3 ) {
-
-					grouped.push( result.slice( i, i + 3 ) );
-
-				}
-
-				return grouped;
-
-			};
-
-		</script>
-
-		<script>
-
-			if ( ! Detector.webgl ) Detector.addGetWebGLMessage();
-
-			THREE.Cache.enabled = true;
-
-			var container, stats, permalink, hex, color;
-
-			var camera, cameraTarget, scene, renderer;
-
-			var group, textMesh1, textMesh2, textGeo, materials;
-
-			var firstLetter = true;
-
-			var text = "Earcut",
-
-				height = 20,
-				size = 70,
-				hover = 30,
-
-				curveSegments = 4,
-
-				bevelThickness = 2,
-				bevelSize = 1.5,
-				bevelSegments = 3,
-				bevelEnabled = true,
-
-				font = undefined,
-
-				fontName = "optimer", // helvetiker, optimer, gentilis, droid sans, droid serif
-				fontWeight = "bold"; // normal bold
-
-			var mirror = true;
-
-			var fontMap = {
-
-				"helvetiker": 0,
-				"optimer": 1,
-				"gentilis": 2,
-				"droid/droid_sans": 3,
-				"droid/droid_serif": 4
-
-			};
-
-			var weightMap = {
-
-				"regular": 0,
-				"bold": 1
-
-			};
-
-			var reverseFontMap = [];
-			var reverseWeightMap = [];
-
-			for ( var i in fontMap ) reverseFontMap[ fontMap[i] ] = i;
-			for ( var i in weightMap ) reverseWeightMap[ weightMap[i] ] = i;
-
-			var targetRotation = 0;
-			var targetRotationOnMouseDown = 0;
-
-			var mouseX = 0;
-			var mouseXOnMouseDown = 0;
-
-			var windowHalfX = window.innerWidth / 2;
-			var windowHalfY = window.innerHeight / 2;
-
-			var fontIndex = 1;
-
-			init();
-			animate();
-
-			function decimalToHex( d ) {
-
-				var hex = Number( d ).toString( 16 );
-				hex = "000000".substr( 0, 6 - hex.length ) + hex;
-				return hex.toUpperCase();
-
-			}
-
-			function init() {
-
-				container = document.createElement( 'div' );
-				document.body.appendChild( container );
-
-				permalink = document.getElementById( "permalink" );
-
-				// CAMERA
-
-				camera = new THREE.PerspectiveCamera( 30, window.innerWidth / window.innerHeight, 1, 1500 );
-				camera.position.set( 0, 400, 700 );
-
-				cameraTarget = new THREE.Vector3( 0, 150, 0 );
-
-				// SCENE
-
-				scene = new THREE.Scene();
-				scene.background = new THREE.Color( 0x000000 );
-				scene.fog = new THREE.Fog( 0x000000, 250, 1400 );
-
-				// LIGHTS
-
-				var dirLight = new THREE.DirectionalLight( 0xffffff, 0.125 );
-				dirLight.position.set( 0, 0, 1 ).normalize();
-				scene.add( dirLight );
-
-				var pointLight = new THREE.PointLight( 0xffffff, 1.5 );
-				pointLight.position.set( 0, 100, 90 );
-				scene.add( pointLight );
-
-				// Get text from hash
-
-				var hash = document.location.hash.substr( 1 );
-
-				if ( hash.length !== 0 ) {
-
-					var colorhash  = hash.substring( 0, 6 );
-					var fonthash   = hash.substring( 6, 7 );
-					var weighthash = hash.substring( 7, 8 );
-					var bevelhash  = hash.substring( 8, 9 );
-					var texthash   = hash.substring( 10 );
-
-					hex = colorhash;
-					pointLight.color.setHex( parseInt( colorhash, 16 ) );
-
-					fontName = reverseFontMap[ parseInt( fonthash ) ];
-					fontWeight = reverseWeightMap[ parseInt( weighthash ) ];
-
-					bevelEnabled = parseInt( bevelhash );
-
-					text = decodeURI( texthash );
-
-					updatePermalink();
-
-				} else {
-
-					pointLight.color.setHSL( Math.random(), 1, 0.5 );
-					hex = decimalToHex( pointLight.color.getHex() );
-
-				}
-
-				materials = [
-					new THREE.MeshPhongMaterial( { color: 0xffffff, flatShading: true } ), // front
-					new THREE.MeshPhongMaterial( { color: 0xffffff } ) // side
-				];
-
-				group = new THREE.Group();
-				group.position.y = 100;
-
-				scene.add( group );
-
-				loadFont();
-
-				var plane = new THREE.Mesh(
-					new THREE.PlaneBufferGeometry( 10000, 10000 ),
-					new THREE.MeshBasicMaterial( { color: 0xffffff, opacity: 0.5, transparent: true } )
-				);
-				plane.position.y = 100;
-				plane.rotation.x = - Math.PI / 2;
-				scene.add( plane );
-
-				// RENDERER
-
-				renderer = new THREE.WebGLRenderer( { antialias: true } );
-				renderer.setPixelRatio( window.devicePixelRatio );
-				renderer.setSize( window.innerWidth, window.innerHeight );
-				container.appendChild( renderer.domElement );
-
-				// STATS
-
-				stats = new Stats();
-				//container.appendChild( stats.dom );
-
-				// EVENTS
-
-				document.addEventListener( 'mousedown', onDocumentMouseDown, false );
-				document.addEventListener( 'touchstart', onDocumentTouchStart, false );
-				document.addEventListener( 'touchmove', onDocumentTouchMove, false );
-				document.addEventListener( 'keypress', onDocumentKeyPress, false );
-				document.addEventListener( 'keydown', onDocumentKeyDown, false );
-
-				document.getElementById( "color" ).addEventListener( 'click', function() {
-
-					pointLight.color.setHSL( Math.random(), 1, 0.5 );
-					hex = decimalToHex( pointLight.color.getHex() );
-
-					updatePermalink();
-
-				}, false );
-
-				document.getElementById( "font" ).addEventListener( 'click', function() {
-
-					fontIndex ++;
-
-					fontName = reverseFontMap[ fontIndex % reverseFontMap.length ];
-
-					loadFont();
-
-				}, false );
-
-
-				document.getElementById( "weight" ).addEventListener( 'click', function() {
-
-					if ( fontWeight === "bold" ) {
-
-						fontWeight = "regular";
-
-					} else {
-
-						fontWeight = "bold";
-
-					}
-
-					loadFont();
-
-				}, false );
-
-				document.getElementById( "bevel" ).addEventListener( 'click', function() {
-
-					bevelEnabled = !bevelEnabled;
-
-					refreshText();
-
-				}, false );
-
-				//
-
-				window.addEventListener( 'resize', onWindowResize, false );
-
-			}
-
-			function onWindowResize() {
-
-				windowHalfX = window.innerWidth / 2;
-				windowHalfY = window.innerHeight / 2;
-
-				camera.aspect = window.innerWidth / window.innerHeight;
-				camera.updateProjectionMatrix();
-
-				renderer.setSize( window.innerWidth, window.innerHeight );
-
-			}
-
-			//
-
-			function boolToNum( b ) {
-
-				return b ? 1 : 0;
-
-			}
-
-			function updatePermalink() {
-
-				var link = hex + fontMap[ fontName ] + weightMap[ fontWeight ] + boolToNum( bevelEnabled ) + "#" + encodeURI( text );
-
-				permalink.href = "#" + link;
-				window.location.hash = link;
-
-			}
-
-			function onDocumentKeyDown( event ) {
-
-				if ( firstLetter ) {
-
-					firstLetter = false;
-					text = "";
-
-				}
-
-				var keyCode = event.keyCode;
-
-				// backspace
-
-				if ( keyCode == 8 ) {
-
-					event.preventDefault();
-
-					text = text.substring( 0, text.length - 1 );
-					refreshText();
-
-					return false;
-
-				}
-
-			}
-
-			function onDocumentKeyPress( event ) {
-
-				var keyCode = event.which;
-
-				// backspace
-
-				if ( keyCode == 8 ) {
-
-					event.preventDefault();
-
-				} else {
-
-					var ch = String.fromCharCode( keyCode );
-					text += ch;
-
-					refreshText();
-
-				}
-
-			}
-
-			function loadFont() {
-
-				var loader = new THREE.FontLoader();
-				loader.load( 'fonts/' + fontName + '_' + fontWeight + '.typeface.json', function ( response ) {
-
-					font = response;
-
-					refreshText();
-
-				} );
-
-			}
-
-			function createText() {
-
-				textGeo = new THREE.TextGeometry( text, {
-
-					font: font,
-
-					size: size,
-					height: height,
-					curveSegments: curveSegments,
-
-					bevelThickness: bevelThickness,
-					bevelSize: bevelSize,
-					bevelEnabled: bevelEnabled,
-
-					material: 0,
-					extrudeMaterial: 1
-
-				});
-
-				textGeo.computeBoundingBox();
-				textGeo.computeVertexNormals();
-
-				// "fix" side normals by removing z-component of normals for side faces
-				// (this doesn't work well for beveled geometry as then we lose nice curvature around z-axis)
-
-				if ( ! bevelEnabled ) {
-
-					var triangleAreaHeuristics = 0.1 * ( height * size );
-
-					for ( var i = 0; i < textGeo.faces.length; i ++ ) {
-
-						var face = textGeo.faces[ i ];
-
-						if ( face.materialIndex == 1 ) {
-
-							for ( var j = 0; j < face.vertexNormals.length; j ++ ) {
-
-								face.vertexNormals[ j ].z = 0;
-								face.vertexNormals[ j ].normalize();
-
-							}
-
-							var va = textGeo.vertices[ face.a ];
-							var vb = textGeo.vertices[ face.b ];
-							var vc = textGeo.vertices[ face.c ];
-
-							var s = THREE.GeometryUtils.triangleArea( va, vb, vc );
-
-							if ( s > triangleAreaHeuristics ) {
-
-								for ( var j = 0; j < face.vertexNormals.length; j ++ ) {
-
-									face.vertexNormals[ j ].copy( face.normal );
-
-								}
-
-							}
-
-						}
-
-					}
-
-				}
-
-				var centerOffset = -0.5 * ( textGeo.boundingBox.max.x - textGeo.boundingBox.min.x );
-
-				textMesh1 = new THREE.Mesh( textGeo, materials );
-
-				textMesh1.position.x = centerOffset;
-				textMesh1.position.y = hover;
-				textMesh1.position.z = 0;
-
-				textMesh1.rotation.x = 0;
-				textMesh1.rotation.y = Math.PI * 2;
-
-				group.add( textMesh1 );
-
-				if ( mirror ) {
-
-					textMesh2 = new THREE.Mesh( textGeo, materials );
-
-					textMesh2.position.x = centerOffset;
-					textMesh2.position.y = -hover;
-					textMesh2.position.z = height;
-
-					textMesh2.rotation.x = Math.PI;
-					textMesh2.rotation.y = Math.PI * 2;
-
-					group.add( textMesh2 );
-
-				}
-
-			}
-
-			function refreshText() {
-
-				updatePermalink();
-
-				group.remove( textMesh1 );
-				if ( mirror ) group.remove( textMesh2 );
-
-				if ( !text ) return;
-
-				createText();
-
-			}
-
-			function onDocumentMouseDown( event ) {
-
-				event.preventDefault();
-
-				document.addEventListener( 'mousemove', onDocumentMouseMove, false );
-				document.addEventListener( 'mouseup', onDocumentMouseUp, false );
-				document.addEventListener( 'mouseout', onDocumentMouseOut, false );
-
-				mouseXOnMouseDown = event.clientX - windowHalfX;
-				targetRotationOnMouseDown = targetRotation;
-
-			}
-
-			function onDocumentMouseMove( event ) {
-
-				mouseX = event.clientX - windowHalfX;
-
-				targetRotation = targetRotationOnMouseDown + ( mouseX - mouseXOnMouseDown ) * 0.02;
-
-			}
-
-			function onDocumentMouseUp( event ) {
-
-				document.removeEventListener( 'mousemove', onDocumentMouseMove, false );
-				document.removeEventListener( 'mouseup', onDocumentMouseUp, false );
-				document.removeEventListener( 'mouseout', onDocumentMouseOut, false );
-
-			}
-
-			function onDocumentMouseOut( event ) {
-
-				document.removeEventListener( 'mousemove', onDocumentMouseMove, false );
-				document.removeEventListener( 'mouseup', onDocumentMouseUp, false );
-				document.removeEventListener( 'mouseout', onDocumentMouseOut, false );
-
-			}
-
-			function onDocumentTouchStart( event ) {
-
-				if ( event.touches.length == 1 ) {
-
-					event.preventDefault();
-
-					mouseXOnMouseDown = event.touches[ 0 ].pageX - windowHalfX;
-					targetRotationOnMouseDown = targetRotation;
-
-				}
-
-			}
-
-			function onDocumentTouchMove( event ) {
-
-				if ( event.touches.length == 1 ) {
-
-					event.preventDefault();
-
-					mouseX = event.touches[ 0 ].pageX - windowHalfX;
-					targetRotation = targetRotationOnMouseDown + ( mouseX - mouseXOnMouseDown ) * 0.05;
-
-				}
-
-			}
-
-			//
-
-			function animate() {
-
-				requestAnimationFrame( animate );
-
-				render();
-				stats.update();
-
-			}
-
-			function render() {
-
-				group.rotation.y += ( targetRotation - group.rotation.y ) * 0.05;
-
-				camera.lookAt( cameraTarget );
-
-				renderer.clear();
-				renderer.render( scene, camera );
-
-			}
-
-		</script>
-
-	</body>
-</html>

+ 593 - 385
src/extras/ShapeUtils.js

@@ -21,670 +21,878 @@ var ShapeUtils = {
 
 
 	},
 	},
 
 
-	triangulate: ( function () {
+	isClockWise: function ( pts ) {
 
 
-		/**
-		 * This code is a quick port of code written in C++ which was submitted to
-		 * flipcode.com by John W. Ratcliff  // July 22, 2000
-		 * See original code and more information here:
-		 * http://www.flipcode.com/archives/Efficient_Polygon_Triangulation.shtml
-		 *
-		 * ported to actionscript by Zevan Rosser
-		 * www.actionsnippet.com
-		 *
-		 * ported to javascript by Joshua Koo
-		 * http://www.lab4games.net/zz85/blog
-		 *
-		 */
+		return ShapeUtils.area( pts ) < 0;
 
 
-		function snip( contour, u, v, w, n, verts ) {
+	},
 
 
-			var p;
-			var ax, ay, bx, by;
-			var cx, cy, px, py;
+	triangulate: function ( vertices, holeIndices ) {
 
 
-			ax = contour[ verts[ u ] ].x;
-			ay = contour[ verts[ u ] ].y;
+		// Port from: https://github.com/mapbox/earcut (v2.1.2)
 
 
-			bx = contour[ verts[ v ] ].x;
-			by = contour[ verts[ v ] ].y;
+		function earcut( data, holeIndices, dim ) {
 
 
-			cx = contour[ verts[ w ] ].x;
-			cy = contour[ verts[ w ] ].y;
+			dim = dim || 2;
 
 
-			if ( ( bx - ax ) * ( cy - ay ) - ( by - ay ) * ( cx - ax ) <= 0 ) return false;
+			var hasHoles = holeIndices && holeIndices.length,
+				outerLen = hasHoles ? holeIndices[ 0 ] * dim : data.length,
+				outerNode = linkedList( data, 0, outerLen, dim, true ),
+				triangles = [];
 
 
-			var aX, aY, bX, bY, cX, cY;
-			var apx, apy, bpx, bpy, cpx, cpy;
-			var cCROSSap, bCROSScp, aCROSSbp;
+			if ( ! outerNode ) return triangles;
 
 
-			aX = cx - bx; aY = cy - by;
-			bX = ax - cx; bY = ay - cy;
-			cX = bx - ax; cY = by - ay;
+			var minX, minY, maxX, maxY, x, y, invSize;
 
 
-			for ( p = 0; p < n; p ++ ) {
+			if ( hasHoles ) outerNode = eliminateHoles( data, holeIndices, outerNode, dim );
 
 
-				px = contour[ verts[ p ] ].x;
-				py = contour[ verts[ p ] ].y;
+			// if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
 
 
-				if ( ( ( px === ax ) && ( py === ay ) ) ||
-					 ( ( px === bx ) && ( py === by ) ) ||
-					 ( ( px === cx ) && ( py === cy ) ) )	continue;
+			if ( data.length > 80 * dim ) {
 
 
-				apx = px - ax; apy = py - ay;
-				bpx = px - bx; bpy = py - by;
-				cpx = px - cx; cpy = py - cy;
+				minX = maxX = data[ 0 ];
+				minY = maxY = data[ 1 ];
 
 
-				// see if p is inside triangle abc
+				for ( var i = dim; i < outerLen; i += dim ) {
 
 
-				aCROSSbp = aX * bpy - aY * bpx;
-				cCROSSap = cX * apy - cY * apx;
-				bCROSScp = bX * cpy - bY * cpx;
+					x = data[ i ];
+					y = data[ i + 1 ];
+					if ( x < minX ) minX = x;
+					if ( y < minY ) minY = y;
+					if ( x > maxX ) maxX = x;
+					if ( y > maxY ) maxY = y;
 
 
-				if ( ( aCROSSbp >= - Number.EPSILON ) && ( bCROSScp >= - Number.EPSILON ) && ( cCROSSap >= - Number.EPSILON ) ) return false;
+				}
+
+				// minX, minY and invSize are later used to transform coords into integers for z-order calculation
+
+				invSize = Math.max( maxX - minX, maxY - minY );
+				invSize = invSize !== 0 ? 1 / invSize : 0;
 
 
 			}
 			}
 
 
-			return true;
+			earcutLinked( outerNode, triangles, dim, minX, minY, invSize );
+
+			return triangles;
 
 
 		}
 		}
 
 
-		// takes in an contour array and returns
+		// create a circular doubly linked list from polygon points in the specified winding order
 
 
-		return function triangulate( contour, indices ) {
+		function linkedList( data, start, end, dim, clockwise ) {
 
 
-			var n = contour.length;
+			var i, last;
 
 
-			if ( n < 3 ) return null;
+			if ( clockwise === ( signedArea( data, start, end, dim ) > 0 ) ) {
 
 
-			var result = [],
-				verts = [],
-				vertIndices = [];
+				for ( i = start; i < end; i += dim ) last = insertNode( i, data[ i ], data[ i + 1 ], last );
 
 
-			/* we want a counter-clockwise polygon in verts */
+			} else {
 
 
-			var u, v, w;
+				for ( i = end - dim; i >= start; i -= dim ) last = insertNode( i, data[ i ], data[ i + 1 ], last );
 
 
-			if ( ShapeUtils.area( contour ) > 0.0 ) {
+			}
 
 
-				for ( v = 0; v < n; v ++ ) verts[ v ] = v;
+			if ( last && equals( last, last.next ) ) {
 
 
-			} else {
-
-				for ( v = 0; v < n; v ++ ) verts[ v ] = ( n - 1 ) - v;
+				removeNode( last );
+				last = last.next;
 
 
 			}
 			}
 
 
-			var nv = n;
+			return last;
+
+		}
+
+		// eliminate colinear or duplicate points
+
+		function filterPoints( start, end ) {
 
 
-			/*  remove nv - 2 vertices, creating 1 triangle every time */
+			if ( ! start ) return start;
+			if ( ! end ) end = start;
 
 
-			var count = 2 * nv; /* error detection */
+			var p = start, again;
 
 
-			for ( v = nv - 1; nv > 2; ) {
+			do {
 
 
-				/* if we loop, it is probably a non-simple polygon */
+				again = false;
 
 
-				if ( ( count -- ) <= 0 ) {
+				if ( ! p.steiner && ( equals( p, p.next ) || area( p.prev, p, p.next ) === 0 ) ) {
 
 
-					//** Triangulate: ERROR - probable bad polygon!
+					removeNode( p );
+					p = end = p.prev;
+					if ( p === p.next ) break;
+					again = true;
 
 
-					//throw ( "Warning, unable to triangulate polygon!" );
-					//return null;
-					// Sometimes warning is fine, especially polygons are triangulated in reverse.
-					console.warn( 'THREE.ShapeUtils: Unable to triangulate polygon! in triangulate()' );
+				} else {
 
 
-					if ( indices ) return vertIndices;
-					return result;
+					p = p.next;
 
 
 				}
 				}
 
 
-				/* three consecutive vertices in current polygon, <u,v,w> */
+			} while ( again || p !== end );
 
 
-				u = v; if ( nv <= u ) u = 0; /* previous */
-				v = u + 1; if ( nv <= v ) v = 0; /* new v    */
-				w = v + 1; if ( nv <= w ) w = 0; /* next     */
+			return end;
 
 
-				if ( snip( contour, u, v, w, nv, verts ) ) {
+		}
 
 
-					var a, b, c, s, t;
+		// main ear slicing loop which triangulates a polygon (given as a linked list)
 
 
-					/* true names of the vertices */
+		function earcutLinked( ear, triangles, dim, minX, minY, invSize, pass ) {
 
 
-					a = verts[ u ];
-					b = verts[ v ];
-					c = verts[ w ];
+			if ( ! ear ) return;
 
 
-					/* output Triangle */
+			// interlink polygon nodes in z-order
 
 
-					result.push( [ contour[ a ],
-						contour[ b ],
-						contour[ c ] ] );
+			if ( ! pass && invSize ) indexCurve( ear, minX, minY, invSize );
 
 
+			var stop = ear, prev, next;
 
 
-					vertIndices.push( [ verts[ u ], verts[ v ], verts[ w ] ] );
+			// iterate through ears, slicing them one by one
 
 
-					/* remove v from the remaining polygon */
+			while ( ear.prev !== ear.next ) {
 
 
-					for ( s = v, t = v + 1; t < nv; s ++, t ++ ) {
+				prev = ear.prev;
+				next = ear.next;
 
 
-						verts[ s ] = verts[ t ];
+				if ( invSize ? isEarHashed( ear, minX, minY, invSize ) : isEar( ear ) ) {
 
 
-					}
+					// cut off the triangle
+					triangles.push( prev.i / dim );
+					triangles.push( ear.i / dim );
+					triangles.push( next.i / dim );
 
 
-					nv --;
+					removeNode( ear );
 
 
-					/* reset error detection counter */
+					// skipping the next vertice leads to less sliver triangles
+					ear = next.next;
+					stop = next.next;
 
 
-					count = 2 * nv;
+					continue;
 
 
 				}
 				}
 
 
-			}
+				ear = next;
 
 
-			if ( indices ) return vertIndices;
-			return result;
+				// if we looped through the whole remaining polygon and can't find any more ears
 
 
-		};
+				if ( ear === stop ) {
 
 
-	} )(),
+					// try filtering points and slicing again
 
 
-	triangulateShape: function ( contour, holes ) {
+					if ( ! pass ) {
 
 
-		function removeDupEndPts( points ) {
+						earcutLinked( filterPoints( ear ), triangles, dim, minX, minY, invSize, 1 );
 
 
-			var l = points.length;
+						// if this didn't work, try curing all small self-intersections locally
 
 
-			if ( l > 2 && points[ l - 1 ].equals( points[ 0 ] ) ) {
+					} else if ( pass === 1 ) {
 
 
-				points.pop();
+						ear = cureLocalIntersections( ear, triangles, dim );
+						earcutLinked( ear, triangles, dim, minX, minY, invSize, 2 );
+
+					// as a last resort, try splitting the remaining polygon into two
+
+					} else if ( pass === 2 ) {
+
+						splitEarcut( ear, triangles, dim, minX, minY, invSize );
+
+					}
+
+					break;
+
+				}
 
 
 			}
 			}
 
 
 		}
 		}
 
 
-		removeDupEndPts( contour );
-		holes.forEach( removeDupEndPts );
+		// check whether a polygon node forms a valid ear with adjacent nodes
 
 
-		function point_in_segment_2D_colin( inSegPt1, inSegPt2, inOtherPt ) {
+		function isEar( ear ) {
 
 
-			// inOtherPt needs to be collinear to the inSegment
-			if ( inSegPt1.x !== inSegPt2.x ) {
+			var a = ear.prev,
+				b = ear,
+				c = ear.next;
 
 
-				if ( inSegPt1.x < inSegPt2.x ) {
+			if ( area( a, b, c ) >= 0 ) return false; // reflex, can't be an ear
 
 
-					return	( ( inSegPt1.x <= inOtherPt.x ) && ( inOtherPt.x <= inSegPt2.x ) );
+			// now make sure we don't have other points inside the potential ear
+			var p = ear.next.next;
 
 
-				} else {
+			while ( p !== ear.prev ) {
 
 
-					return	( ( inSegPt2.x <= inOtherPt.x ) && ( inOtherPt.x <= inSegPt1.x ) );
+				if ( pointInTriangle( a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y ) && area( p.prev, p, p.next ) >= 0 ) {
+
+					return false;
 
 
 				}
 				}
 
 
-			} else {
+				p = p.next;
 
 
-				if ( inSegPt1.y < inSegPt2.y ) {
+			}
 
 
-					return	( ( inSegPt1.y <= inOtherPt.y ) && ( inOtherPt.y <= inSegPt2.y ) );
+			return true;
 
 
-				} else {
+		}
 
 
-					return	( ( inSegPt2.y <= inOtherPt.y ) && ( inOtherPt.y <= inSegPt1.y ) );
+		function isEarHashed( ear, minX, minY, invSize ) {
 
 
-				}
+			var a = ear.prev,
+				b = ear,
+				c = ear.next;
+
+			if ( area( a, b, c ) >= 0 ) return false; // reflex, can't be an ear
+
+			// triangle bbox; min & max are calculated like this for speed
+
+			var minTX = a.x < b.x ? ( a.x < c.x ? a.x : c.x ) : ( b.x < c.x ? b.x : c.x ),
+				minTY = a.y < b.y ? ( a.y < c.y ? a.y : c.y ) : ( b.y < c.y ? b.y : c.y ),
+				maxTX = a.x > b.x ? ( a.x > c.x ? a.x : c.x ) : ( b.x > c.x ? b.x : c.x ),
+				maxTY = a.y > b.y ? ( a.y > c.y ? a.y : c.y ) : ( b.y > c.y ? b.y : c.y );
+
+			// z-order range for the current triangle bbox;
+
+			var minZ = zOrder( minTX, minTY, minX, minY, invSize ),
+				maxZ = zOrder( maxTX, maxTY, minX, minY, invSize );
+
+			// first look for points inside the triangle in increasing z-order
+
+			var p = ear.nextZ;
+
+			while ( p && p.z <= maxZ ) {
+
+				if ( p !== ear.prev && p !== ear.next &&
+						pointInTriangle( a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y ) &&
+						area( p.prev, p, p.next ) >= 0 ) return false;
+				p = p.nextZ;
+
+			}
+
+			// then look for points in decreasing z-order
+
+			p = ear.prevZ;
+
+			while ( p && p.z >= minZ ) {
+
+				if ( p !== ear.prev && p !== ear.next &&
+						pointInTriangle( a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y ) &&
+						area( p.prev, p, p.next ) >= 0 ) return false;
+
+				p = p.prevZ;
 
 
 			}
 			}
 
 
+			return true;
+
 		}
 		}
 
 
-		function intersect_segments_2D( inSeg1Pt1, inSeg1Pt2, inSeg2Pt1, inSeg2Pt2, inExcludeAdjacentSegs ) {
+		// go through all polygon nodes and cure small local self-intersections
 
 
-			var seg1dx = inSeg1Pt2.x - inSeg1Pt1.x, seg1dy = inSeg1Pt2.y - inSeg1Pt1.y;
-			var seg2dx = inSeg2Pt2.x - inSeg2Pt1.x, seg2dy = inSeg2Pt2.y - inSeg2Pt1.y;
+		function cureLocalIntersections( start, triangles, dim ) {
 
 
-			var seg1seg2dx = inSeg1Pt1.x - inSeg2Pt1.x;
-			var seg1seg2dy = inSeg1Pt1.y - inSeg2Pt1.y;
+			var p = start;
 
 
-			var limit		= seg1dy * seg2dx - seg1dx * seg2dy;
-			var perpSeg1	= seg1dy * seg1seg2dx - seg1dx * seg1seg2dy;
+			do {
 
 
-			if ( Math.abs( limit ) > Number.EPSILON ) {
+				var a = p.prev, b = p.next.next;
 
 
-				// not parallel
+				if ( ! equals( a, b ) && intersects( a, p, p.next, b ) && locallyInside( a, b ) && locallyInside( b, a ) ) {
 
 
-				var perpSeg2;
-				if ( limit > 0 ) {
+					triangles.push( a.i / dim );
+					triangles.push( p.i / dim );
+					triangles.push( b.i / dim );
 
 
-					if ( ( perpSeg1 < 0 ) || ( perpSeg1 > limit ) ) 		return [];
-					perpSeg2 = seg2dy * seg1seg2dx - seg2dx * seg1seg2dy;
-					if ( ( perpSeg2 < 0 ) || ( perpSeg2 > limit ) ) 		return [];
+					// remove two nodes involved
 
 
-				} else {
+					removeNode( p );
+					removeNode( p.next );
 
 
-					if ( ( perpSeg1 > 0 ) || ( perpSeg1 < limit ) ) 		return [];
-					perpSeg2 = seg2dy * seg1seg2dx - seg2dx * seg1seg2dy;
-					if ( ( perpSeg2 > 0 ) || ( perpSeg2 < limit ) ) 		return [];
+					p = start = b;
 
 
 				}
 				}
 
 
-				// i.e. to reduce rounding errors
-				// intersection at endpoint of segment#1?
-				if ( perpSeg2 === 0 ) {
+				p = p.next;
 
 
-					if ( ( inExcludeAdjacentSegs ) &&
-						 ( ( perpSeg1 === 0 ) || ( perpSeg1 === limit ) ) )		return [];
-					return [ inSeg1Pt1 ];
+			} while ( p !== start );
 
 
-				}
-				if ( perpSeg2 === limit ) {
+			return p;
 
 
-					if ( ( inExcludeAdjacentSegs ) &&
-						 ( ( perpSeg1 === 0 ) || ( perpSeg1 === limit ) ) )		return [];
-					return [ inSeg1Pt2 ];
+		}
 
 
-				}
-				// intersection at endpoint of segment#2?
-				if ( perpSeg1 === 0 )		return [ inSeg2Pt1 ];
-				if ( perpSeg1 === limit )	return [ inSeg2Pt2 ];
+		// try splitting polygon into two and triangulate them independently
 
 
-				// return real intersection point
-				var factorSeg1 = perpSeg2 / limit;
-				return	[ { x: inSeg1Pt1.x + factorSeg1 * seg1dx, y: inSeg1Pt1.y + factorSeg1 * seg1dy } ];
+		function splitEarcut( start, triangles, dim, minX, minY, invSize ) {
 
 
-			} else {
+			// look for a valid diagonal that divides the polygon into two
 
 
-				// parallel or collinear
-				if ( ( perpSeg1 !== 0 ) ||
-					 ( seg2dy * seg1seg2dx !== seg2dx * seg1seg2dy ) ) 			return [];
+			var a = start;
 
 
-				// they are collinear or degenerate
-				var seg1Pt = ( ( seg1dx === 0 ) && ( seg1dy === 0 ) );	// segment1 is just a point?
-				var seg2Pt = ( ( seg2dx === 0 ) && ( seg2dy === 0 ) );	// segment2 is just a point?
-				// both segments are points
-				if ( seg1Pt && seg2Pt ) {
+			do {
 
 
-					if ( ( inSeg1Pt1.x !== inSeg2Pt1.x ) ||
-						 ( inSeg1Pt1.y !== inSeg2Pt1.y ) )		return [];	// they are distinct  points
-					return [ inSeg1Pt1 ];	// they are the same point
+				var b = a.next.next;
 
 
-				}
-				// segment#1  is a single point
-				if ( seg1Pt ) {
+				while ( b !== a.prev ) {
 
 
-					if ( ! point_in_segment_2D_colin( inSeg2Pt1, inSeg2Pt2, inSeg1Pt1 ) )		return [];		// but not in segment#2
-					return [ inSeg1Pt1 ];
+					if ( a.i !== b.i && isValidDiagonal( a, b ) ) {
 
 
-				}
-				// segment#2  is a single point
-				if ( seg2Pt ) {
+						// split the polygon in two by the diagonal
+
+						var c = splitPolygon( a, b );
 
 
-					if ( ! point_in_segment_2D_colin( inSeg1Pt1, inSeg1Pt2, inSeg2Pt1 ) )		return [];		// but not in segment#1
-					return [ inSeg2Pt1 ];
+						// filter colinear points around the cuts
+
+						a = filterPoints( a, a.next );
+						c = filterPoints( c, c.next );
+
+						// run earcut on each half
+
+						earcutLinked( a, triangles, dim, minX, minY, invSize );
+						earcutLinked( c, triangles, dim, minX, minY, invSize );
+						return;
+
+					}
+
+					b = b.next;
 
 
 				}
 				}
 
 
-				// they are collinear segments, which might overlap
-				var seg1min, seg1max, seg1minVal, seg1maxVal;
-				var seg2min, seg2max, seg2minVal, seg2maxVal;
-				if ( seg1dx !== 0 ) {
+				a = a.next;
 
 
-					// the segments are NOT on a vertical line
-					if ( inSeg1Pt1.x < inSeg1Pt2.x ) {
+			} while ( a !== start );
 
 
-						seg1min = inSeg1Pt1; seg1minVal = inSeg1Pt1.x;
-						seg1max = inSeg1Pt2; seg1maxVal = inSeg1Pt2.x;
+		}
 
 
-					} else {
+		// link every hole into the outer loop, producing a single-ring polygon without holes
 
 
-						seg1min = inSeg1Pt2; seg1minVal = inSeg1Pt2.x;
-						seg1max = inSeg1Pt1; seg1maxVal = inSeg1Pt1.x;
+		function eliminateHoles( data, holeIndices, outerNode, dim ) {
 
 
-					}
-					if ( inSeg2Pt1.x < inSeg2Pt2.x ) {
+			var queue = [], i, len, start, end, list;
 
 
-						seg2min = inSeg2Pt1; seg2minVal = inSeg2Pt1.x;
-						seg2max = inSeg2Pt2; seg2maxVal = inSeg2Pt2.x;
+			for ( i = 0, len = holeIndices.length; i < len; i ++ ) {
 
 
-					} else {
+				start = holeIndices[ i ] * dim;
+				end = i < len - 1 ? holeIndices[ i + 1 ] * dim : data.length;
+				list = linkedList( data, start, end, dim, false );
+				if ( list === list.next ) list.steiner = true;
+				queue.push( getLeftmost( list ) );
 
 
-						seg2min = inSeg2Pt2; seg2minVal = inSeg2Pt2.x;
-						seg2max = inSeg2Pt1; seg2maxVal = inSeg2Pt1.x;
+			}
 
 
-					}
+			queue.sort( compareX );
 
 
-				} else {
+			// process holes from left to right
 
 
-					// the segments are on a vertical line
-					if ( inSeg1Pt1.y < inSeg1Pt2.y ) {
+			for ( i = 0; i < queue.length; i ++ ) {
 
 
-						seg1min = inSeg1Pt1; seg1minVal = inSeg1Pt1.y;
-						seg1max = inSeg1Pt2; seg1maxVal = inSeg1Pt2.y;
+				eliminateHole( queue[ i ], outerNode );
+				outerNode = filterPoints( outerNode, outerNode.next );
 
 
-					} else {
+			}
 
 
-						seg1min = inSeg1Pt2; seg1minVal = inSeg1Pt2.y;
-						seg1max = inSeg1Pt1; seg1maxVal = inSeg1Pt1.y;
+			return outerNode;
 
 
-					}
-					if ( inSeg2Pt1.y < inSeg2Pt2.y ) {
+		}
 
 
-						seg2min = inSeg2Pt1; seg2minVal = inSeg2Pt1.y;
-						seg2max = inSeg2Pt2; seg2maxVal = inSeg2Pt2.y;
+		function compareX( a, b ) {
 
 
-					} else {
+			return a.x - b.x;
 
 
-						seg2min = inSeg2Pt2; seg2minVal = inSeg2Pt2.y;
-						seg2max = inSeg2Pt1; seg2maxVal = inSeg2Pt1.y;
+		}
+
+		// find a bridge between vertices that connects hole with an outer ring and and link it
+
+		function eliminateHole( hole, outerNode ) {
+
+			outerNode = findHoleBridge( hole, outerNode );
+
+			if ( outerNode ) {
+
+				var b = splitPolygon( outerNode, hole );
+
+				filterPoints( b, b.next );
+
+			}
+
+		}
+
+		// David Eberly's algorithm for finding a bridge between hole and outer polygon
+
+		function findHoleBridge( hole, outerNode ) {
+
+			var p = outerNode,
+				hx = hole.x,
+				hy = hole.y,
+				qx = - Infinity,
+				m;
+
+			// 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 && p.next.y !== p.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;
+
+						if ( x === hx ) {
+
+							if ( hy === p.y ) return p;
+							if ( hy === p.next.y ) return p.next;
+
+						}
+
+						m = p.x < p.next.x ? p : p.next;
 
 
 					}
 					}
 
 
 				}
 				}
-				if ( seg1minVal <= seg2minVal ) {
 
 
-					if ( seg1maxVal < seg2minVal )	return [];
-					if ( seg1maxVal === seg2minVal )	{
+				p = p.next;
 
 
-						if ( inExcludeAdjacentSegs )		return [];
-						return [ seg2min ];
+			} while ( p !== outerNode );
 
 
-					}
-					if ( seg1maxVal <= seg2maxVal )	return [ seg2min, seg1max ];
-					return	[ seg2min, seg2max ];
+			if ( ! m ) return null;
 
 
-				} else {
+			if ( hx === qx ) return m.prev; // hole touches outer segment; pick lower endpoint
+
+			// 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
 
 
-					if ( seg1minVal > seg2maxVal )	return [];
-					if ( seg1minVal === seg2maxVal )	{
+			var stop = m,
+				mx = m.x,
+				my = m.y,
+				tanMin = Infinity,
+				tan;
 
 
-						if ( inExcludeAdjacentSegs )		return [];
-						return [ seg1min ];
+			p = m.next;
+
+			while ( p !== stop ) {
+
+				if ( hx >= p.x && p.x >= mx && hx !== p.x &&
+								pointInTriangle( hy < my ? hx : qx, hy, mx, my, hy < my ? 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;
 
 
 					}
 					}
-					if ( seg1maxVal <= seg2maxVal )	return [ seg1min, seg1max ];
-					return	[ seg1min, seg2max ];
 
 
 				}
 				}
 
 
+				p = p.next;
+
 			}
 			}
 
 
+			return m;
+
 		}
 		}
 
 
-		function isPointInsideAngle( inVertex, inLegFromPt, inLegToPt, inOtherPt ) {
+		// interlink polygon nodes in z-order
 
 
-			// The order of legs is important
+		function indexCurve( start, minX, minY, invSize ) {
 
 
-			// translation of all points, so that Vertex is at (0,0)
-			var legFromPtX	= inLegFromPt.x - inVertex.x, legFromPtY = inLegFromPt.y - inVertex.y;
-			var legToPtX	= inLegToPt.x	- inVertex.x, legToPtY = inLegToPt.y	- inVertex.y;
-			var otherPtX	= inOtherPt.x	- inVertex.x, otherPtY = inOtherPt.y	- inVertex.y;
+			var p = start;
 
 
-			// main angle >0: < 180 deg.; 0: 180 deg.; <0: > 180 deg.
-			var from2toAngle	= legFromPtX * legToPtY - legFromPtY * legToPtX;
-			var from2otherAngle	= legFromPtX * otherPtY - legFromPtY * otherPtX;
+			do {
 
 
-			if ( Math.abs( from2toAngle ) > Number.EPSILON ) {
+				if ( p.z === null ) p.z = zOrder( p.x, p.y, minX, minY, invSize );
+				p.prevZ = p.prev;
+				p.nextZ = p.next;
+				p = p.next;
 
 
-				// angle != 180 deg.
+			} while ( p !== start );
 
 
-				var other2toAngle		= otherPtX * legToPtY - otherPtY * legToPtX;
-				// console.log( "from2to: " + from2toAngle + ", from2other: " + from2otherAngle + ", other2to: " + other2toAngle );
+			p.prevZ.nextZ = null;
+			p.prevZ = null;
 
 
-				if ( from2toAngle > 0 ) {
+			sortLinked( p );
 
 
-					// main angle < 180 deg.
-					return	( ( from2otherAngle >= 0 ) && ( other2toAngle >= 0 ) );
+		}
 
 
-				} else {
+		// Simon Tatham's linked list merge sort algorithm
+		// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
+
+		function sortLinked( list ) {
+
+			var i, p, q, e, tail, numMerges, pSize, qSize, inSize = 1;
+
+			do {
+
+				p = list;
+				list = null;
+				tail = null;
+				numMerges = 0;
+
+				while ( p ) {
+
+					numMerges ++;
+					q = p;
+					pSize = 0;
+
+					for ( i = 0; i < inSize; i ++ ) {
+
+						pSize ++;
+						q = q.nextZ;
+						if ( ! q ) break;
+
+					}
+
+					qSize = inSize;
+
+					while ( pSize > 0 || ( qSize > 0 && q ) ) {
+
+						if ( pSize !== 0 && ( qSize === 0 || ! q || p.z <= q.z ) ) {
+
+							e = p;
+							p = p.nextZ;
+							pSize --;
+
+						} else {
+
+							e = q;
+							q = q.nextZ;
+							qSize --;
+
+						}
+
+						if ( tail ) tail.nextZ = e;
+						else list = e;
 
 
-					// main angle > 180 deg.
-					return	( ( from2otherAngle >= 0 ) || ( other2toAngle >= 0 ) );
+						e.prevZ = tail;
+						tail = e;
+
+					}
+
+					p = q;
 
 
 				}
 				}
 
 
-			} else {
+				tail.nextZ = null;
+				inSize *= 2;
 
 
-				// angle == 180 deg.
-				// console.log( "from2to: 180 deg., from2other: " + from2otherAngle  );
-				return	( from2otherAngle > 0 );
+			} while ( numMerges > 1 );
 
 
-			}
+			return list;
 
 
 		}
 		}
 
 
+		// z-order of a point given coords and inverse of the longer side of data bbox
 
 
-		function removeHoles( contour, holes ) {
+		function zOrder( x, y, minX, minY, invSize ) {
 
 
-			var shape = contour.concat(); // work on this shape
-			var hole;
+			// coords are transformed into non-negative 15-bit integer range
 
 
-			function isCutLineInsideAngles( inShapeIdx, inHoleIdx ) {
+			x = 32767 * ( x - minX ) * invSize;
+			y = 32767 * ( y - minY ) * invSize;
 
 
-				// Check if hole point lies within angle around shape point
-				var lastShapeIdx = shape.length - 1;
+			x = ( x | ( x << 8 ) ) & 0x00FF00FF;
+			x = ( x | ( x << 4 ) ) & 0x0F0F0F0F;
+			x = ( x | ( x << 2 ) ) & 0x33333333;
+			x = ( x | ( x << 1 ) ) & 0x55555555;
 
 
-				var prevShapeIdx = inShapeIdx - 1;
-				if ( prevShapeIdx < 0 )			prevShapeIdx = lastShapeIdx;
+			y = ( y | ( y << 8 ) ) & 0x00FF00FF;
+			y = ( y | ( y << 4 ) ) & 0x0F0F0F0F;
+			y = ( y | ( y << 2 ) ) & 0x33333333;
+			y = ( y | ( y << 1 ) ) & 0x55555555;
 
 
-				var nextShapeIdx = inShapeIdx + 1;
-				if ( nextShapeIdx > lastShapeIdx )	nextShapeIdx = 0;
+			return x | ( y << 1 );
 
 
-				var insideAngle = isPointInsideAngle( shape[ inShapeIdx ], shape[ prevShapeIdx ], shape[ nextShapeIdx ], hole[ inHoleIdx ] );
-				if ( ! insideAngle ) {
+		}
 
 
-					// console.log( "Vertex (Shape): " + inShapeIdx + ", Point: " + hole[inHoleIdx].x + "/" + hole[inHoleIdx].y );
-					return	false;
+		// find the leftmost node of a polygon ring
 
 
-				}
+		function getLeftmost( start ) {
 
 
-				// Check if shape point lies within angle around hole point
-				var lastHoleIdx = hole.length - 1;
+			var p = start, leftmost = start;
 
 
-				var prevHoleIdx = inHoleIdx - 1;
-				if ( prevHoleIdx < 0 )			prevHoleIdx = lastHoleIdx;
+			do {
 
 
-				var nextHoleIdx = inHoleIdx + 1;
-				if ( nextHoleIdx > lastHoleIdx )	nextHoleIdx = 0;
+				if ( p.x < leftmost.x ) leftmost = p;
+				p = p.next;
 
 
-				insideAngle = isPointInsideAngle( hole[ inHoleIdx ], hole[ prevHoleIdx ], hole[ nextHoleIdx ], shape[ inShapeIdx ] );
-				if ( ! insideAngle ) {
+			} while ( p !== start );
 
 
-					// console.log( "Vertex (Hole): " + inHoleIdx + ", Point: " + shape[inShapeIdx].x + "/" + shape[inShapeIdx].y );
-					return	false;
+			return leftmost;
 
 
-				}
+		}
 
 
-				return	true;
+		// check if a point lies within a convex triangle
 
 
-			}
+		function pointInTriangle( ax, ay, bx, by, cx, cy, px, py ) {
 
 
-			function intersectsShapeEdge( inShapePt, inHolePt ) {
+			return ( cx - px ) * ( ay - py ) - ( ax - px ) * ( cy - py ) >= 0 &&
+			 ( ax - px ) * ( by - py ) - ( bx - px ) * ( ay - py ) >= 0 &&
+			 ( bx - px ) * ( cy - py ) - ( cx - px ) * ( by - py ) >= 0;
 
 
-				// checks for intersections with shape edges
-				var sIdx, nextIdx, intersection;
-				for ( sIdx = 0; sIdx < shape.length; sIdx ++ ) {
+		}
 
 
-					nextIdx = sIdx + 1; nextIdx %= shape.length;
-					intersection = intersect_segments_2D( inShapePt, inHolePt, shape[ sIdx ], shape[ nextIdx ], true );
-					if ( intersection.length > 0 )		return	true;
+		// check if a diagonal between two polygon nodes is valid (lies in polygon interior)
 
 
-				}
+		function isValidDiagonal( a, b ) {
 
 
-				return	false;
+			return a.next.i !== b.i && a.prev.i !== b.i && ! intersectsPolygon( a, b ) &&
+				locallyInside( a, b ) && locallyInside( b, a ) && middleInside( a, b );
 
 
-			}
+		}
 
 
-			var indepHoles = [];
+		// signed area of a triangle
 
 
-			function intersectsHoleEdge( inShapePt, inHolePt ) {
+		function area( p, q, r ) {
 
 
-				// checks for intersections with hole edges
-				var ihIdx, chkHole,
-					hIdx, nextIdx, intersection;
-				for ( ihIdx = 0; ihIdx < indepHoles.length; ihIdx ++ ) {
+			return ( q.y - p.y ) * ( r.x - q.x ) - ( q.x - p.x ) * ( r.y - q.y );
 
 
-					chkHole = holes[ indepHoles[ ihIdx ] ];
-					for ( hIdx = 0; hIdx < chkHole.length; hIdx ++ ) {
+		}
 
 
-						nextIdx = hIdx + 1; nextIdx %= chkHole.length;
-						intersection = intersect_segments_2D( inShapePt, inHolePt, chkHole[ hIdx ], chkHole[ nextIdx ], true );
-						if ( intersection.length > 0 )		return	true;
+		// check if two points are equal
 
 
-					}
+		function equals( p1, p2 ) {
 
 
-				}
-				return	false;
+			return p1.x === p2.x && p1.y === p2.y;
 
 
-			}
+		}
 
 
-			var holeIndex, shapeIndex,
-				shapePt, holePt,
-				holeIdx, cutKey, failedCuts = [],
-				tmpShape1, tmpShape2,
-				tmpHole1, tmpHole2;
+		// check if two segments intersect
 
 
-			for ( var h = 0, hl = holes.length; h < hl; h ++ ) {
+		function intersects( p1, q1, p2, q2 ) {
 
 
-				indepHoles.push( h );
+			if ( ( equals( p1, q1 ) && equals( p2, q2 ) ) ||
+					( equals( p1, q2 ) && equals( p2, q1 ) ) ) return true;
 
 
-			}
+			return area( p1, q1, p2 ) > 0 !== area( p1, q1, q2 ) > 0 &&
+						 area( p2, q2, p1 ) > 0 !== area( p2, q2, q1 ) > 0;
 
 
-			var minShapeIndex = 0;
-			var counter = indepHoles.length * 2;
-			while ( indepHoles.length > 0 ) {
+		}
 
 
-				counter --;
-				if ( counter < 0 ) {
+		// check if a polygon diagonal intersects any polygon segments
 
 
-					console.log( 'THREE.ShapeUtils: Infinite Loop! Holes left:" + indepHoles.length + ", Probably Hole outside Shape!' );
-					break;
+		function intersectsPolygon( a, b ) {
 
 
-				}
+			var p = a;
 
 
-				// search for shape-vertex and hole-vertex,
-				// which can be connected without intersections
-				for ( shapeIndex = minShapeIndex; shapeIndex < shape.length; shapeIndex ++ ) {
+			do {
 
 
-					shapePt = shape[ shapeIndex ];
-					holeIndex	= - 1;
+				if ( p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&
+								intersects( p, p.next, a, b ) ) {
 
 
-					// search for hole which can be reached without intersections
-					for ( var h = 0; h < indepHoles.length; h ++ ) {
+					return true;
 
 
-						holeIdx = indepHoles[ h ];
+				}
 
 
-						// prevent multiple checks
-						cutKey = shapePt.x + ':' + shapePt.y + ':' + holeIdx;
-						if ( failedCuts[ cutKey ] !== undefined )			continue;
+				p = p.next;
 
 
-						hole = holes[ holeIdx ];
-						for ( var h2 = 0; h2 < hole.length; h2 ++ ) {
+			} while ( p !== a );
 
 
-							holePt = hole[ h2 ];
-							if ( ! isCutLineInsideAngles( shapeIndex, h2 ) )		continue;
-							if ( intersectsShapeEdge( shapePt, holePt ) )		continue;
-							if ( intersectsHoleEdge( shapePt, holePt ) )		continue;
+			return false;
 
 
-							holeIndex = h2;
-							indepHoles.splice( h, 1 );
+		}
 
 
-							tmpShape1 = shape.slice( 0, shapeIndex + 1 );
-							tmpShape2 = shape.slice( shapeIndex );
-							tmpHole1 = hole.slice( holeIndex );
-							tmpHole2 = hole.slice( 0, holeIndex + 1 );
+		// check if a polygon diagonal is locally inside the polygon
 
 
-							shape = tmpShape1.concat( tmpHole1 ).concat( tmpHole2 ).concat( tmpShape2 );
+		function locallyInside( a, b ) {
 
 
-							minShapeIndex = shapeIndex;
+			return area( a.prev, a, a.next ) < 0 ?
+				area( a, b, a.next ) >= 0 && area( a, a.prev, b ) >= 0 :
+				area( a, b, a.prev ) < 0 || area( a, a.next, b ) < 0;
 
 
-							// Debug only, to show the selected cuts
-							// glob_CutLines.push( [ shapePt, holePt ] );
+		}
 
 
-							break;
+		// check if the middle point of a polygon diagonal is inside the polygon
 
 
-						}
-						if ( holeIndex >= 0 )	break;		// hole-vertex found
+		function middleInside( a, b ) {
 
 
-						failedCuts[ cutKey ] = true;			// remember failure
+			var p = a,
+				inside = false,
+				px = ( a.x + b.x ) / 2,
+				py = ( a.y + b.y ) / 2;
 
 
-					}
-					if ( holeIndex >= 0 )	break;		// hole-vertex found
+			do {
+
+				if ( ( ( p.y > py ) !== ( p.next.y > py ) ) && p.next.y !== p.y &&
+								( px < ( p.next.x - p.x ) * ( py - p.y ) / ( p.next.y - p.y ) + p.x ) ) {
+
+					inside = ! inside;
 
 
 				}
 				}
 
 
-			}
+				p = p.next;
+
+			} while ( p !== a );
 
 
-			return shape; 			/* shape with no holes */
+			return inside;
 
 
 		}
 		}
 
 
+		// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;
+		// if one belongs to the outer ring and another to a hole, it merges it into a single ring
 
 
-		var i, il, f, face,
-			key, index,
-			allPointsMap = {};
+		function splitPolygon( a, b ) {
 
 
-		// To maintain reference to old shape, one must match coordinates, or offset the indices from original arrays. It's probably easier to do the first.
+			var a2 = new Node( a.i, a.x, a.y ),
+				b2 = new Node( b.i, b.x, b.y ),
+				an = a.next,
+				bp = b.prev;
 
 
-		var allpoints = contour.concat();
+			a.next = b;
+			b.prev = a;
 
 
-		for ( var h = 0, hl = holes.length; h < hl; h ++ ) {
+			a2.next = an;
+			an.prev = a2;
 
 
-			Array.prototype.push.apply( allpoints, holes[ h ] );
+			b2.next = a2;
+			a2.prev = b2;
+
+			bp.next = b2;
+			b2.prev = bp;
+
+			return b2;
 
 
 		}
 		}
 
 
-		//console.log( "allpoints",allpoints, allpoints.length );
+		// create a node and optionally link it with previous one (in a circular doubly linked list)
+
+		function insertNode( i, x, y, last ) {
 
 
-		// prepare all points map
+			var p = new Node( i, x, y );
 
 
-		for ( i = 0, il = allpoints.length; i < il; i ++ ) {
+			if ( ! last ) {
 
 
-			key = allpoints[ i ].x + ':' + allpoints[ i ].y;
+				p.prev = p;
+				p.next = p;
 
 
-			if ( allPointsMap[ key ] !== undefined ) {
+			} else {
 
 
-				console.warn( 'THREE.ShapeUtils: Duplicate point', key, i );
+				p.next = last.next;
+				p.prev = last;
+				last.next.prev = p;
+				last.next = p;
 
 
 			}
 			}
 
 
-			allPointsMap[ key ] = i;
+			return p;
 
 
 		}
 		}
 
 
-		// remove holes by cutting paths to holes and adding them to the shape
-		var shapeWithoutHoles = removeHoles( contour, holes );
+		function removeNode( p ) {
+
+			p.next.prev = p.prev;
+			p.prev.next = p.next;
 
 
-		var triangles = ShapeUtils.triangulate( shapeWithoutHoles, false ); // True returns indices for points of spooled shape
-		//console.log( "triangles",triangles, triangles.length );
+			if ( p.prevZ ) p.prevZ.nextZ = p.nextZ;
+			if ( p.nextZ ) p.nextZ.prevZ = p.prevZ;
 
 
-		// check all face vertices against all points map
+		}
 
 
-		for ( i = 0, il = triangles.length; i < il; i ++ ) {
+		function Node( i, x, y ) {
 
 
-			face = triangles[ i ];
+			// vertice index in coordinates array
+			this.i = i;
 
 
-			for ( f = 0; f < 3; f ++ ) {
+			// vertex coordinates
+			this.x = x;
+			this.y = y;
 
 
-				key = face[ f ].x + ':' + face[ f ].y;
+			// previous and next vertice nodes in a polygon ring
+			this.prev = null;
+			this.next = null;
 
 
-				index = allPointsMap[ key ];
+			// z-order curve value
+			this.z = null;
 
 
-				if ( index !== undefined ) {
+			// previous and next nodes in z-order
+			this.prevZ = null;
+			this.nextZ = null;
 
 
-					face[ f ] = index;
+			// indicates whether this is a steiner point
+			this.steiner = false;
 
 
-				}
+		}
+
+		function signedArea( data, start, end, dim ) {
+
+			var sum = 0;
+
+			for ( var i = start, j = end - dim; i < end; i += dim ) {
+
+				sum += ( data[ j ] - data[ i ] ) * ( data[ i + 1 ] + data[ j + 1 ] );
+				j = i;
 
 
 			}
 			}
 
 
+			return sum;
+
 		}
 		}
 
 
-		return triangles.concat();
+		return earcut( vertices, holeIndices );
 
 
 	},
 	},
 
 
-	isClockWise: function ( pts ) {
+	triangulateShape: function ( contour, holes ) {
 
 
-		return ShapeUtils.area( pts ) < 0;
+		function removeDupEndPts( points ) {
+
+			var l = points.length;
+
+			if ( l > 2 && points[ l - 1 ].equals( points[ 0 ] ) ) {
+
+				points.pop();
+
+			}
+
+		}
+
+		function addContour( vertices, contour ) {
+
+			for ( var i = 0; i < contour.length; i ++ ) {
+
+				vertices.push( contour[ i ].x );
+				vertices.push( contour[ i ].y );
+
+			}
+
+		}
+
+		var vertices = []; // flat array of vertices like [ x0,y0, x1,y1, x2,y2, ... ]
+		var holeIndices = []; // array of hole indices
+		var faces = []; // final array of vertex indices like [ [ a,b,d ], [ b,c,d ] ]
+
+		removeDupEndPts( contour );
+		addContour( vertices, contour );
+
+		//
+
+		var holeIndex = contour.length;
+		holes.forEach( removeDupEndPts );
+
+		for ( i = 0; i < holes.length; i ++ ) {
+
+			holeIndices.push( holeIndex );
+			holeIndex += holes[ i ].length;
+			addContour( vertices, holes[ i ] );
+
+		}
+
+		//
+
+		var triangles = ShapeUtils.triangulate( vertices, holeIndices );
+
+		//
+
+		for ( var i = 0; i < triangles.length; i += 3 ) {
+
+			faces.push( triangles.slice( i, i + 3 ) );
+
+		}
+
+		return faces;
 
 
 	}
 	}