From 8424d63009e606056778857da1d886cbc632bb62 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Mischler=20=28aka=20skarab=29?=
 <skarab@bluewin.ch>
Date: Fri, 18 Mar 2016 17:17:49 +0100
Subject: [PATCH] better triangulation algorithm with earcut

---
 index.html                      |   3 +
 js/main.js                      |  54 ++-
 vendor/earcut.js                | 644 ++++++++++++++++++++++++++++++++
 vendor/triangulation.js         | 319 ++++++++++++++++
 vendor/triangulation/.gitignore |   8 +-
 5 files changed, 1019 insertions(+), 9 deletions(-)
 create mode 100644 vendor/earcut.js
 create mode 100644 vendor/triangulation.js

diff --git a/index.html b/index.html
index b0d7ba2..b0f327c 100644
--- a/index.html
+++ b/index.html
@@ -195,12 +195,15 @@ <h3 class="panel-title">Screen</h3>
             </div><!-- #main -->
 
         </div><!-- .body -->
+
         <script src="vendor/jquery-2.1.4.min.js" type="text/javascript"></script>
         <script src="vendor/jquery-ui/jquery-ui.min.js" type="text/javascript"></script>
         <script src="vendor/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
         <script src="vendor/bootstrap-slider/bootstrap-slider.min.js" type="text/javascript"></script>
         <script src="vendor/lodash.min.js" type="text/javascript"></script>
         <script src="vendor/three.min.js" type="text/javascript"></script>
+        <script src="vendor/earcut.js" type="text/javascript"></script>
+        <script src="vendor/triangulation.js" type="text/javascript"></script>
         <script src="js/slacer/settings.js" type="text/javascript"></script>
         <script src="js/slacer/mesh.js" type="text/javascript"></script>
         <script src="js/slacer/viewer.js" type="text/javascript"></script>
diff --git a/js/main.js b/js/main.js
index 8e05293..de3ce27 100644
--- a/js/main.js
+++ b/js/main.js
@@ -68,14 +68,46 @@ function errorHandler(error) {
 // Slicer
 // -----------------------------------------------------------------------------
 var slicer = new SLAcer.Slicer();
+var shapes;
+
+function removeShapes() {
+    if (shapes && shapes.length) {
+        for (var i = 0, il = shapes.length; i < il; i++) {
+            viewer3d.removeObject(shapes[i]);
+        }
+    }
+}
 
 function slice(layerNumber) {
+    // remove old shapes
+    removeShapes();
+
+    if (layerNumber < 1) {
+        viewer3d.render();
+        return;
+    }
+
     // get slice
     var layerHeight = settings.get('slicer.layers.height') / 1000;
     var zPosition   = layerNumber * layerHeight;
     var slice       = slicer.getFaces(zPosition);
-    console.log('layer number:', layerNumber);
-    console.log('z position  :', zPosition);
+
+    //console.log('layer number:', layerNumber);
+    //console.log('z position  :', zPosition);
+
+    // get new shapes list
+    shapes = slice.shapes;
+    zPosition -= settings.get('buildVolume.size.z') / 2;
+
+    // add new shapes
+    for (var i = 0, il = shapes.length; i < il; i++) {
+        shapes[i].position.z = zPosition;
+        shapes[i].material.depthTest = false;
+        viewer3d.scene.add(shapes[i]);
+    }
+
+    // render
+    viewer3d.render();
 }
 
 // -----------------------------------------------------------------------------
@@ -92,6 +124,10 @@ var viewer3d  = new SLAcer.Viewer3D({
     target     : $viewer3d[0]
 });
 
+// Triangulation algorithm
+//THREE.Triangulation.setTimer(true);
+THREE.Triangulation.setLibrary('earcut');
+
 // Slider
 var $sliderInput = $('#slider input');
 
@@ -100,6 +136,8 @@ $sliderInput.slider({ reversed : true }).on('change', function(e) {
 });
 
 var $sliderElement = $('#slider .slider');
+var $sliderMinValue = $('#slider .min');
+var $sliderMaxValue = $('#slider .max');
 
 
 // Sidebar
@@ -173,8 +211,13 @@ var $meshSizeZ    = $meshBody.find('#mesh-size-z');
 var $meshSizeUnit = $meshBody.find('.mesh-size-unit');
 
 function updateMeshInfoUI(mesh) {
-    var size = mesh.getSize();
-    var unit = settings.get('buildVolume.unit');
+    var size         = mesh.getSize();
+    var unit         = settings.get('buildVolume.unit');
+    var layersHeight = settings.get('slicer.layers.height') / 1000;
+    var layersNumber = Math.ceil(size.z / layersHeight);
+
+    $sliderInput.slider('setAttribute', 'max', layersNumber);
+    $sliderMaxValue.html(layersNumber);
 
     $meshSizeUnit.html(unit);
 
@@ -321,6 +364,9 @@ loader.onGeometry = function(geometry) {
         // remove old mesh and plane
         slicer.mesh && viewer3d.removeObject(slicer.mesh);
 
+        // remove old shapes
+        removeShapes();
+
         // load new mesh in slicer
         slicer.loadMesh(new SLAcer.Mesh(geometry, new THREE.MeshPhongMaterial({
             color: settings.get('mesh.color')
diff --git a/vendor/earcut.js b/vendor/earcut.js
new file mode 100644
index 0000000..52efc69
--- /dev/null
+++ b/vendor/earcut.js
@@ -0,0 +1,644 @@
+'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) {
+            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 &&
+                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) {
+                    e = q;
+                    q = q.nextZ;
+                    qSize--;
+                } else if (qSize === 0 || !q) {
+                    e = p;
+                    p = p.nextZ;
+                    pSize--;
+                } else if (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)) && (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;
+};
diff --git a/vendor/triangulation.js b/vendor/triangulation.js
new file mode 100644
index 0000000..61b89b4
--- /dev/null
+++ b/vendor/triangulation.js
@@ -0,0 +1,319 @@
+'use strict';
+
+if ( ! window.THREE ) throw new Error( 'ERROR: three.js not loaded' );
+
+THREE.Triangulation = ( function() {
+
+    var timer = false;
+
+    var library = 'original';
+
+    /**
+     * Names of the supported libraries
+     *
+     * @type {{original: string, earcut: string, poly2tri: string, libtess: string}}
+     */
+    var libraries = {
+        original: 'original',
+        earcut: 'earcut',
+        poly2tri: 'poly2tri',
+        libtess: 'libtess'
+    };
+
+    /**
+     * Container object for holding the different library adapters
+     *
+     * @type {{}}
+     */
+    var adapters = {};
+
+    adapters[ libraries.original ] = {
+
+        triangulate: THREE.ShapeUtils.triangulate,
+
+        triangulateShape: THREE.ShapeUtils.triangulateShape
+
+    };
+
+    adapters[ libraries.earcut ] = {
+
+        triangulateShape: function( contour, holes ) {
+
+            var i, il, dim = 2, array;
+            var holeIndices = [];
+            var points = [];
+
+            addPoints( contour );
+
+            for ( i = 0, il = holes.length; i < il; i ++ ) {
+
+                holeIndices.push( points.length / dim );
+
+                addPoints( holes[ i ] );
+
+            }
+
+            array = earcut( points, holeIndices, dim );
+
+            var result = [];
+
+            for ( i = 0, il = array.length; i < il; i += 3 ) {
+
+                result.push( array.slice( i, i + 3 ) );
+
+            }
+
+            return result;
+
+            function addPoints( a ) {
+
+                var i, il = a.length;
+
+                for ( i = 0; i < il; i ++ ) {
+
+                    points.push( a[ i ].x, a[ i ].y );
+
+                }
+
+            }
+
+        }
+
+    };
+
+    adapters[ libraries.poly2tri ] = {
+
+        triangulateShape: function( contour, holes ) {
+
+            var i, il, object, sweepContext, triangles;
+            var pointMap = {}, count = 0;
+
+            points = makePoints( contour );
+
+            sweepContext = new poly2tri.SweepContext( points );
+
+            for ( i = 0, il = holes.length; i < il; i ++ ) {
+
+                points = makePoints( holes[ i ] );
+
+                sweepContext.addHole( points );
+
+                points = points.concat( points );
+
+            }
+
+            object = sweepContext.triangulate();
+
+            triangles = object.triangles_;
+
+            var a, b, c, points, result = [];
+
+            for ( i = 0, il = triangles.length; i < il; i ++ ) {
+
+                points = triangles[ i ].points_;
+
+                a = pointMap[ points[ 0 ].x + ',' + points[ 0 ].y ];
+                b = pointMap[ points[ 1 ].x + ',' + points[ 1 ].y ];
+                c = pointMap[ points[ 2 ].x + ',' + points[ 2 ].y ];
+
+                result.push( [ a, b, c ] );
+
+            }
+
+            return result;
+
+            function makePoints( a ) {
+
+                var i, il = a.length,
+                    points = [];
+
+                for ( i = 0; i < il; i ++ ) {
+
+                    points.push( new poly2tri.Point( a[ i ].x, a[ i ].y ) );
+                    pointMap[ a[ i ].x + ',' + a[ i ].y ] = count;
+                    count ++;
+
+                }
+
+                return points;
+
+            }
+
+        }
+
+    };
+
+    adapters[ libraries.libtess ] = {
+
+        triangulateShape: function( contour, holes ) {
+
+            var i, il, triangles = [];
+            var pointMap = {}, count = 0;
+
+            // libtess will take 3d verts and flatten to a plane for tesselation
+            // since only doing 2d tesselation here, provide z=1 normal to skip
+            // iterating over verts only to get the same answer.
+            // comment out to test normal-generation code
+            tessy.gluTessNormal( 0, 0, 1 );
+
+            tessy.gluTessBeginPolygon( triangles );
+
+            points = makePoints( contour );
+
+            for ( i = 0, il = holes.length; i < il; i ++ ) {
+
+                points = makePoints( holes[ i ] );
+
+            }
+
+            tessy.gluTessEndPolygon();
+
+            var a, b, c, points, result = [];
+
+            for ( i = 0, il = triangles.length; i < il; i += 6 ) {
+
+                a = pointMap[ triangles[ i ] + ',' + triangles[ i + 1 ]];
+                b = pointMap[ triangles[ i + 2 ] + ',' + triangles[ i + 3 ]];
+                c = pointMap[ triangles[ i + 4 ] + ',' + triangles[ i + 5 ]];
+
+                result.push( [ a, b, c ] );
+
+            }
+
+            return result;
+
+            function makePoints( a ) {
+
+                var i, il = a.length,
+                    coordinates;
+
+                tessy.gluTessBeginContour();
+
+                for ( i = 0; i < il; i ++ ) {
+
+                    coordinates = [ a[ i ].x, a[ i ].y, 0 ];
+                    tessy.gluTessVertex( coordinates, coordinates );
+                    pointMap[ a[ i ].x + ',' + a[ i ].y ] = count;
+                    count ++;
+
+                }
+
+                tessy.gluTessEndContour();
+
+                return points;
+
+            }
+
+        }
+
+    };
+
+    /**
+     * Initialize the library by attaching the triangulation methods to the three.js API
+     */
+    function init() {
+
+        checkDependencies( library );
+
+        if ( timer ) {
+
+            THREE.ShapeUtils.triangulate = function() {
+
+                return adapters[ library ].triangulate.apply( this, arguments );
+
+            };
+
+            THREE.ShapeUtils.triangulateShape = function() {
+
+                console.time( library );
+
+                var result = adapters[ library ].triangulateShape.apply( this, arguments );
+
+                console.timeEnd( library );
+
+                return result;
+
+            };
+
+        } else {
+
+            THREE.ShapeUtils.triangulate = adapters[ library ].triangulate;
+
+            THREE.ShapeUtils.triangulateShape = adapters[ library ].triangulateShape;
+
+        }
+
+    }
+
+    /**
+     * Checks dependencies needed for the current library
+     *
+     * @param library
+     */
+    function checkDependencies( library ) {
+
+        switch ( library ) {
+
+            case libraries.earcut:
+
+                if ( ! window.earcut ) throw new Error( 'ERROR: earcut not loaded' );
+
+                break;
+
+            case libraries.poly2tri:
+
+                if ( ! window.poly2tri ) throw new Error( 'ERROR: poly2tri not loaded' );
+
+                break;
+
+            case libraries.libtess:
+
+                if ( ! window.tessy ) throw new Error( 'ERROR: libtess not loaded' );
+
+                break;
+
+        }
+
+    }
+
+    /**
+     * Set the current triangulation library
+     *
+     * @param name
+     */
+    function setLibrary( name ) {
+
+        if ( ! libraries.hasOwnProperty( name ) ) throw new Error( 'ERROR: unknown library ' + name );
+
+        library = name;
+
+        init();
+
+    }
+
+    /**
+     * Set timer for triangulation on/off
+     *
+     * @param boolean
+     */
+    function setTimer( boolean ) {
+
+        timer = boolean;
+
+        init();
+
+    }
+
+    init();
+
+    return {
+
+        libraries: libraries,
+
+        setTimer: setTimer,
+
+        setLibrary: setLibrary
+
+    };
+
+} )();
diff --git a/vendor/triangulation/.gitignore b/vendor/triangulation/.gitignore
index 4baffb5..ca6966b 100644
--- a/vendor/triangulation/.gitignore
+++ b/vendor/triangulation/.gitignore
@@ -1,5 +1,3 @@
-.idea
-coverage
-node_modules
-dist
-*.log
+benchmark
+shapes
+viewers
