Spaces:
Running
Running
/** | |
* @author Mugen87 / https://github.com/Mugen87 | |
* Port from https://github.com/mapbox/earcut (v2.1.2) | |
*/ | |
var Earcut = { | |
triangulate: function ( 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, invSize; | |
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 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; | |
} | |
earcutLinked( outerNode, triangles, dim, minX, minY, invSize ); | |
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 ) break; | |
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, invSize, pass ) { | |
if ( ! ear ) return; | |
// interlink polygon nodes in z-order | |
if ( ! pass && invSize ) indexCurve( ear, minX, minY, invSize ); | |
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 ( 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 ); | |
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, invSize, 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, 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; | |
} | |
} | |
} | |
// 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, 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; | |
} | |
// 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, invSize ) { | |
// 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, invSize ); | |
earcutLinked( c, triangles, dim, minX, minY, invSize ); | |
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, invSize ) { | |
var p = start; | |
do { | |
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; | |
} 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 inverse of the longer side of data bbox | |
function zOrder( x, y, minX, minY, invSize ) { | |
// coords are transformed into non-negative 15-bit integer range | |
x = 32767 * ( x - minX ) * invSize; | |
y = 32767 * ( y - minY ) * invSize; | |
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; | |
} | |
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; | |
} | |
export { Earcut }; | |