Earcut.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791
  1. /**
  2. * Port from https://github.com/mapbox/earcut (v2.2.2)
  3. */
  4. const Earcut = {
  5. triangulate: function ( data, holeIndices, dim ) {
  6. dim = dim || 2;
  7. const hasHoles = holeIndices && holeIndices.length;
  8. const outerLen = hasHoles ? holeIndices[ 0 ] * dim : data.length;
  9. let outerNode = linkedList( data, 0, outerLen, dim, true );
  10. const triangles = [];
  11. if ( ! outerNode || outerNode.next === outerNode.prev ) return triangles;
  12. let minX, minY, maxX, maxY, x, y, invSize;
  13. if ( hasHoles ) outerNode = eliminateHoles( data, holeIndices, outerNode, dim );
  14. // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
  15. if ( data.length > 80 * dim ) {
  16. minX = maxX = data[ 0 ];
  17. minY = maxY = data[ 1 ];
  18. for ( let i = dim; i < outerLen; i += dim ) {
  19. x = data[ i ];
  20. y = data[ i + 1 ];
  21. if ( x < minX ) minX = x;
  22. if ( y < minY ) minY = y;
  23. if ( x > maxX ) maxX = x;
  24. if ( y > maxY ) maxY = y;
  25. }
  26. // minX, minY and invSize are later used to transform coords into integers for z-order calculation
  27. invSize = Math.max( maxX - minX, maxY - minY );
  28. invSize = invSize !== 0 ? 1 / invSize : 0;
  29. }
  30. earcutLinked( outerNode, triangles, dim, minX, minY, invSize );
  31. return triangles;
  32. }
  33. };
  34. // create a circular doubly linked list from polygon points in the specified winding order
  35. function linkedList( data, start, end, dim, clockwise ) {
  36. let i, last;
  37. if ( clockwise === ( signedArea( data, start, end, dim ) > 0 ) ) {
  38. for ( i = start; i < end; i += dim ) last = insertNode( i, data[ i ], data[ i + 1 ], last );
  39. } else {
  40. for ( i = end - dim; i >= start; i -= dim ) last = insertNode( i, data[ i ], data[ i + 1 ], last );
  41. }
  42. if ( last && equals( last, last.next ) ) {
  43. removeNode( last );
  44. last = last.next;
  45. }
  46. return last;
  47. }
  48. // eliminate colinear or duplicate points
  49. function filterPoints( start, end ) {
  50. if ( ! start ) return start;
  51. if ( ! end ) end = start;
  52. let p = start,
  53. again;
  54. do {
  55. again = false;
  56. if ( ! p.steiner && ( equals( p, p.next ) || area( p.prev, p, p.next ) === 0 ) ) {
  57. removeNode( p );
  58. p = end = p.prev;
  59. if ( p === p.next ) break;
  60. again = true;
  61. } else {
  62. p = p.next;
  63. }
  64. } while ( again || p !== end );
  65. return end;
  66. }
  67. // main ear slicing loop which triangulates a polygon (given as a linked list)
  68. function earcutLinked( ear, triangles, dim, minX, minY, invSize, pass ) {
  69. if ( ! ear ) return;
  70. // interlink polygon nodes in z-order
  71. if ( ! pass && invSize ) indexCurve( ear, minX, minY, invSize );
  72. let stop = ear,
  73. prev, next;
  74. // iterate through ears, slicing them one by one
  75. while ( ear.prev !== ear.next ) {
  76. prev = ear.prev;
  77. next = ear.next;
  78. if ( invSize ? isEarHashed( ear, minX, minY, invSize ) : isEar( ear ) ) {
  79. // cut off the triangle
  80. triangles.push( prev.i / dim );
  81. triangles.push( ear.i / dim );
  82. triangles.push( next.i / dim );
  83. removeNode( ear );
  84. // skipping the next vertex leads to less sliver triangles
  85. ear = next.next;
  86. stop = next.next;
  87. continue;
  88. }
  89. ear = next;
  90. // if we looped through the whole remaining polygon and can't find any more ears
  91. if ( ear === stop ) {
  92. // try filtering points and slicing again
  93. if ( ! pass ) {
  94. earcutLinked( filterPoints( ear ), triangles, dim, minX, minY, invSize, 1 );
  95. // if this didn't work, try curing all small self-intersections locally
  96. } else if ( pass === 1 ) {
  97. ear = cureLocalIntersections( filterPoints( ear ), triangles, dim );
  98. earcutLinked( ear, triangles, dim, minX, minY, invSize, 2 );
  99. // as a last resort, try splitting the remaining polygon into two
  100. } else if ( pass === 2 ) {
  101. splitEarcut( ear, triangles, dim, minX, minY, invSize );
  102. }
  103. break;
  104. }
  105. }
  106. }
  107. // check whether a polygon node forms a valid ear with adjacent nodes
  108. function isEar( ear ) {
  109. const a = ear.prev,
  110. b = ear,
  111. c = ear.next;
  112. if ( area( a, b, c ) >= 0 ) return false; // reflex, can't be an ear
  113. // now make sure we don't have other points inside the potential ear
  114. let p = ear.next.next;
  115. while ( p !== ear.prev ) {
  116. if ( pointInTriangle( a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y ) &&
  117. area( p.prev, p, p.next ) >= 0 ) return false;
  118. p = p.next;
  119. }
  120. return true;
  121. }
  122. function isEarHashed( ear, minX, minY, invSize ) {
  123. const a = ear.prev,
  124. b = ear,
  125. c = ear.next;
  126. if ( area( a, b, c ) >= 0 ) return false; // reflex, can't be an ear
  127. // triangle bbox; min & max are calculated like this for speed
  128. const minTX = a.x < b.x ? ( a.x < c.x ? a.x : c.x ) : ( b.x < c.x ? b.x : c.x ),
  129. minTY = a.y < b.y ? ( a.y < c.y ? a.y : c.y ) : ( b.y < c.y ? b.y : c.y ),
  130. maxTX = a.x > b.x ? ( a.x > c.x ? a.x : c.x ) : ( b.x > c.x ? b.x : c.x ),
  131. maxTY = a.y > b.y ? ( a.y > c.y ? a.y : c.y ) : ( b.y > c.y ? b.y : c.y );
  132. // z-order range for the current triangle bbox;
  133. const minZ = zOrder( minTX, minTY, minX, minY, invSize ),
  134. maxZ = zOrder( maxTX, maxTY, minX, minY, invSize );
  135. let p = ear.prevZ,
  136. n = ear.nextZ;
  137. // look for points inside the triangle in both directions
  138. while ( p && p.z >= minZ && n && n.z <= maxZ ) {
  139. if ( p !== ear.prev && p !== ear.next &&
  140. pointInTriangle( a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y ) &&
  141. area( p.prev, p, p.next ) >= 0 ) return false;
  142. p = p.prevZ;
  143. if ( n !== ear.prev && n !== ear.next &&
  144. pointInTriangle( a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y ) &&
  145. area( n.prev, n, n.next ) >= 0 ) return false;
  146. n = n.nextZ;
  147. }
  148. // look for remaining points in decreasing z-order
  149. while ( p && p.z >= minZ ) {
  150. if ( p !== ear.prev && p !== ear.next &&
  151. pointInTriangle( a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y ) &&
  152. area( p.prev, p, p.next ) >= 0 ) return false;
  153. p = p.prevZ;
  154. }
  155. // look for remaining points in increasing z-order
  156. while ( n && n.z <= maxZ ) {
  157. if ( n !== ear.prev && n !== ear.next &&
  158. pointInTriangle( a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y ) &&
  159. area( n.prev, n, n.next ) >= 0 ) return false;
  160. n = n.nextZ;
  161. }
  162. return true;
  163. }
  164. // go through all polygon nodes and cure small local self-intersections
  165. function cureLocalIntersections( start, triangles, dim ) {
  166. let p = start;
  167. do {
  168. const a = p.prev,
  169. b = p.next.next;
  170. if ( ! equals( a, b ) && intersects( a, p, p.next, b ) && locallyInside( a, b ) && locallyInside( b, a ) ) {
  171. triangles.push( a.i / dim );
  172. triangles.push( p.i / dim );
  173. triangles.push( b.i / dim );
  174. // remove two nodes involved
  175. removeNode( p );
  176. removeNode( p.next );
  177. p = start = b;
  178. }
  179. p = p.next;
  180. } while ( p !== start );
  181. return filterPoints( p );
  182. }
  183. // try splitting polygon into two and triangulate them independently
  184. function splitEarcut( start, triangles, dim, minX, minY, invSize ) {
  185. // look for a valid diagonal that divides the polygon into two
  186. let a = start;
  187. do {
  188. let b = a.next.next;
  189. while ( b !== a.prev ) {
  190. if ( a.i !== b.i && isValidDiagonal( a, b ) ) {
  191. // split the polygon in two by the diagonal
  192. let c = splitPolygon( a, b );
  193. // filter colinear points around the cuts
  194. a = filterPoints( a, a.next );
  195. c = filterPoints( c, c.next );
  196. // run earcut on each half
  197. earcutLinked( a, triangles, dim, minX, minY, invSize );
  198. earcutLinked( c, triangles, dim, minX, minY, invSize );
  199. return;
  200. }
  201. b = b.next;
  202. }
  203. a = a.next;
  204. } while ( a !== start );
  205. }
  206. // link every hole into the outer loop, producing a single-ring polygon without holes
  207. function eliminateHoles( data, holeIndices, outerNode, dim ) {
  208. const queue = [];
  209. let i, len, start, end, list;
  210. for ( i = 0, len = holeIndices.length; i < len; i ++ ) {
  211. start = holeIndices[ i ] * dim;
  212. end = i < len - 1 ? holeIndices[ i + 1 ] * dim : data.length;
  213. list = linkedList( data, start, end, dim, false );
  214. if ( list === list.next ) list.steiner = true;
  215. queue.push( getLeftmost( list ) );
  216. }
  217. queue.sort( compareX );
  218. // process holes from left to right
  219. for ( i = 0; i < queue.length; i ++ ) {
  220. eliminateHole( queue[ i ], outerNode );
  221. outerNode = filterPoints( outerNode, outerNode.next );
  222. }
  223. return outerNode;
  224. }
  225. function compareX( a, b ) {
  226. return a.x - b.x;
  227. }
  228. // find a bridge between vertices that connects hole with an outer ring and and link it
  229. function eliminateHole( hole, outerNode ) {
  230. outerNode = findHoleBridge( hole, outerNode );
  231. if ( outerNode ) {
  232. const b = splitPolygon( outerNode, hole );
  233. // filter collinear points around the cuts
  234. filterPoints( outerNode, outerNode.next );
  235. filterPoints( b, b.next );
  236. }
  237. }
  238. // David Eberly's algorithm for finding a bridge between hole and outer polygon
  239. function findHoleBridge( hole, outerNode ) {
  240. let p = outerNode;
  241. const hx = hole.x;
  242. const hy = hole.y;
  243. let qx = - Infinity, m;
  244. // find a segment intersected by a ray from the hole's leftmost point to the left;
  245. // segment's endpoint with lesser x will be potential connection point
  246. do {
  247. if ( hy <= p.y && hy >= p.next.y && p.next.y !== p.y ) {
  248. const x = p.x + ( hy - p.y ) * ( p.next.x - p.x ) / ( p.next.y - p.y );
  249. if ( x <= hx && x > qx ) {
  250. qx = x;
  251. if ( x === hx ) {
  252. if ( hy === p.y ) return p;
  253. if ( hy === p.next.y ) return p.next;
  254. }
  255. m = p.x < p.next.x ? p : p.next;
  256. }
  257. }
  258. p = p.next;
  259. } while ( p !== outerNode );
  260. if ( ! m ) return null;
  261. if ( hx === qx ) return m; // hole touches outer segment; pick leftmost endpoint
  262. // look for points inside the triangle of hole point, segment intersection and endpoint;
  263. // if there are no points found, we have a valid connection;
  264. // otherwise choose the point of the minimum angle with the ray as connection point
  265. const stop = m,
  266. mx = m.x,
  267. my = m.y;
  268. let tanMin = Infinity, tan;
  269. p = m;
  270. do {
  271. if ( hx >= p.x && p.x >= mx && hx !== p.x &&
  272. pointInTriangle( hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y ) ) {
  273. tan = Math.abs( hy - p.y ) / ( hx - p.x ); // tangential
  274. if ( locallyInside( p, hole ) && ( tan < tanMin || ( tan === tanMin && ( p.x > m.x || ( p.x === m.x && sectorContainsSector( m, p ) ) ) ) ) ) {
  275. m = p;
  276. tanMin = tan;
  277. }
  278. }
  279. p = p.next;
  280. } while ( p !== stop );
  281. return m;
  282. }
  283. // whether sector in vertex m contains sector in vertex p in the same coordinates
  284. function sectorContainsSector( m, p ) {
  285. return area( m.prev, m, p.prev ) < 0 && area( p.next, m, m.next ) < 0;
  286. }
  287. // interlink polygon nodes in z-order
  288. function indexCurve( start, minX, minY, invSize ) {
  289. let p = start;
  290. do {
  291. if ( p.z === null ) p.z = zOrder( p.x, p.y, minX, minY, invSize );
  292. p.prevZ = p.prev;
  293. p.nextZ = p.next;
  294. p = p.next;
  295. } while ( p !== start );
  296. p.prevZ.nextZ = null;
  297. p.prevZ = null;
  298. sortLinked( p );
  299. }
  300. // Simon Tatham's linked list merge sort algorithm
  301. // http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
  302. function sortLinked( list ) {
  303. let i, p, q, e, tail, numMerges, pSize, qSize,
  304. inSize = 1;
  305. do {
  306. p = list;
  307. list = null;
  308. tail = null;
  309. numMerges = 0;
  310. while ( p ) {
  311. numMerges ++;
  312. q = p;
  313. pSize = 0;
  314. for ( i = 0; i < inSize; i ++ ) {
  315. pSize ++;
  316. q = q.nextZ;
  317. if ( ! q ) break;
  318. }
  319. qSize = inSize;
  320. while ( pSize > 0 || ( qSize > 0 && q ) ) {
  321. if ( pSize !== 0 && ( qSize === 0 || ! q || p.z <= q.z ) ) {
  322. e = p;
  323. p = p.nextZ;
  324. pSize --;
  325. } else {
  326. e = q;
  327. q = q.nextZ;
  328. qSize --;
  329. }
  330. if ( tail ) tail.nextZ = e;
  331. else list = e;
  332. e.prevZ = tail;
  333. tail = e;
  334. }
  335. p = q;
  336. }
  337. tail.nextZ = null;
  338. inSize *= 2;
  339. } while ( numMerges > 1 );
  340. return list;
  341. }
  342. // z-order of a point given coords and inverse of the longer side of data bbox
  343. function zOrder( x, y, minX, minY, invSize ) {
  344. // coords are transformed into non-negative 15-bit integer range
  345. x = 32767 * ( x - minX ) * invSize;
  346. y = 32767 * ( y - minY ) * invSize;
  347. x = ( x | ( x << 8 ) ) & 0x00FF00FF;
  348. x = ( x | ( x << 4 ) ) & 0x0F0F0F0F;
  349. x = ( x | ( x << 2 ) ) & 0x33333333;
  350. x = ( x | ( x << 1 ) ) & 0x55555555;
  351. y = ( y | ( y << 8 ) ) & 0x00FF00FF;
  352. y = ( y | ( y << 4 ) ) & 0x0F0F0F0F;
  353. y = ( y | ( y << 2 ) ) & 0x33333333;
  354. y = ( y | ( y << 1 ) ) & 0x55555555;
  355. return x | ( y << 1 );
  356. }
  357. // find the leftmost node of a polygon ring
  358. function getLeftmost( start ) {
  359. let p = start,
  360. leftmost = start;
  361. do {
  362. if ( p.x < leftmost.x || ( p.x === leftmost.x && p.y < leftmost.y ) ) leftmost = p;
  363. p = p.next;
  364. } while ( p !== start );
  365. return leftmost;
  366. }
  367. // check if a point lies within a convex triangle
  368. function pointInTriangle( ax, ay, bx, by, cx, cy, px, py ) {
  369. return ( cx - px ) * ( ay - py ) - ( ax - px ) * ( cy - py ) >= 0 &&
  370. ( ax - px ) * ( by - py ) - ( bx - px ) * ( ay - py ) >= 0 &&
  371. ( bx - px ) * ( cy - py ) - ( cx - px ) * ( by - py ) >= 0;
  372. }
  373. // check if a diagonal between two polygon nodes is valid (lies in polygon interior)
  374. function isValidDiagonal( a, b ) {
  375. return a.next.i !== b.i && a.prev.i !== b.i && ! intersectsPolygon( a, b ) && // dones't intersect other edges
  376. ( locallyInside( a, b ) && locallyInside( b, a ) && middleInside( a, b ) && // locally visible
  377. ( area( a.prev, a, b.prev ) || area( a, b.prev, b ) ) || // does not create opposite-facing sectors
  378. equals( a, b ) && area( a.prev, a, a.next ) > 0 && area( b.prev, b, b.next ) > 0 ); // special zero-length case
  379. }
  380. // signed area of a triangle
  381. function area( p, q, r ) {
  382. return ( q.y - p.y ) * ( r.x - q.x ) - ( q.x - p.x ) * ( r.y - q.y );
  383. }
  384. // check if two points are equal
  385. function equals( p1, p2 ) {
  386. return p1.x === p2.x && p1.y === p2.y;
  387. }
  388. // check if two segments intersect
  389. function intersects( p1, q1, p2, q2 ) {
  390. const o1 = sign( area( p1, q1, p2 ) );
  391. const o2 = sign( area( p1, q1, q2 ) );
  392. const o3 = sign( area( p2, q2, p1 ) );
  393. const o4 = sign( area( p2, q2, q1 ) );
  394. if ( o1 !== o2 && o3 !== o4 ) return true; // general case
  395. if ( o1 === 0 && onSegment( p1, p2, q1 ) ) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1
  396. if ( o2 === 0 && onSegment( p1, q2, q1 ) ) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1
  397. if ( o3 === 0 && onSegment( p2, p1, q2 ) ) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2
  398. if ( o4 === 0 && onSegment( p2, q1, q2 ) ) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2
  399. return false;
  400. }
  401. // for collinear points p, q, r, check if point q lies on segment pr
  402. function onSegment( p, q, r ) {
  403. return q.x <= Math.max( p.x, r.x ) && q.x >= Math.min( p.x, r.x ) && q.y <= Math.max( p.y, r.y ) && q.y >= Math.min( p.y, r.y );
  404. }
  405. function sign( num ) {
  406. return num > 0 ? 1 : num < 0 ? - 1 : 0;
  407. }
  408. // check if a polygon diagonal intersects any polygon segments
  409. function intersectsPolygon( a, b ) {
  410. let p = a;
  411. do {
  412. if ( p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&
  413. intersects( p, p.next, a, b ) ) return true;
  414. p = p.next;
  415. } while ( p !== a );
  416. return false;
  417. }
  418. // check if a polygon diagonal is locally inside the polygon
  419. function locallyInside( a, b ) {
  420. return area( a.prev, a, a.next ) < 0 ?
  421. area( a, b, a.next ) >= 0 && area( a, a.prev, b ) >= 0 :
  422. area( a, b, a.prev ) < 0 || area( a, a.next, b ) < 0;
  423. }
  424. // check if the middle point of a polygon diagonal is inside the polygon
  425. function middleInside( a, b ) {
  426. let p = a,
  427. inside = false;
  428. const px = ( a.x + b.x ) / 2,
  429. py = ( a.y + b.y ) / 2;
  430. do {
  431. if ( ( ( p.y > py ) !== ( p.next.y > py ) ) && p.next.y !== p.y &&
  432. ( px < ( p.next.x - p.x ) * ( py - p.y ) / ( p.next.y - p.y ) + p.x ) )
  433. inside = ! inside;
  434. p = p.next;
  435. } while ( p !== a );
  436. return inside;
  437. }
  438. // link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;
  439. // if one belongs to the outer ring and another to a hole, it merges it into a single ring
  440. function splitPolygon( a, b ) {
  441. const a2 = new Node( a.i, a.x, a.y ),
  442. b2 = new Node( b.i, b.x, b.y ),
  443. an = a.next,
  444. bp = b.prev;
  445. a.next = b;
  446. b.prev = a;
  447. a2.next = an;
  448. an.prev = a2;
  449. b2.next = a2;
  450. a2.prev = b2;
  451. bp.next = b2;
  452. b2.prev = bp;
  453. return b2;
  454. }
  455. // create a node and optionally link it with previous one (in a circular doubly linked list)
  456. function insertNode( i, x, y, last ) {
  457. const p = new Node( i, x, y );
  458. if ( ! last ) {
  459. p.prev = p;
  460. p.next = p;
  461. } else {
  462. p.next = last.next;
  463. p.prev = last;
  464. last.next.prev = p;
  465. last.next = p;
  466. }
  467. return p;
  468. }
  469. function removeNode( p ) {
  470. p.next.prev = p.prev;
  471. p.prev.next = p.next;
  472. if ( p.prevZ ) p.prevZ.nextZ = p.nextZ;
  473. if ( p.nextZ ) p.nextZ.prevZ = p.prevZ;
  474. }
  475. function Node( i, x, y ) {
  476. // vertex index in coordinates array
  477. this.i = i;
  478. // vertex coordinates
  479. this.x = x;
  480. this.y = y;
  481. // previous and next vertex nodes in a polygon ring
  482. this.prev = null;
  483. this.next = null;
  484. // z-order curve value
  485. this.z = null;
  486. // previous and next nodes in z-order
  487. this.prevZ = null;
  488. this.nextZ = null;
  489. // indicates whether this is a steiner point
  490. this.steiner = false;
  491. }
  492. function signedArea( data, start, end, dim ) {
  493. let sum = 0;
  494. for ( let i = start, j = end - dim; i < end; i += dim ) {
  495. sum += ( data[ j ] - data[ i ] ) * ( data[ i + 1 ] + data[ j + 1 ] );
  496. j = i;
  497. }
  498. return sum;
  499. }
  500. export { Earcut };
粤ICP备19079148号