├── bhx.jpg ├── .gitattributes ├── README.md ├── LICENSE.txt ├── .gitignore └── Earcut.hx /bhx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vujadin/EarcutHx/HEAD/bhx.jpg -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EarcutHx 2 | Polygon triangulation library for Haxe. Ported from https://github.com/mapbox/earcut 3 | 4 | Used in https://github.com/vujadin/BabylonHx 5 | 6 | ![alt tag](https://github.com/vujadin/EarcutHx/blob/master/bhx.jpg?raw=true) 7 | 8 | 9 | Usage: 10 | ``` 11 | // Triangulating a polygon with a hole 12 | var data = Earcut.earcut([0,0, 100,0, 100,100, 0,100, 20,20, 80,20, 80,80, 20,80], [4]); 13 | // data: [3,0,4, 5,4,0, 3,4,7, 5,0,1, 2,3,7, 6,5,1, 2,7,6, 6,1,2] 14 | 15 | // Triangulating a polygon with 3d coords 16 | var data = Earcut.earcut([10,0,1, 0,50,2, 60,60,3, 70,10,4], null, 3); 17 | // data: [1,0,3, 3,2,1] 18 | ``` 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2016, Mapbox 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose 6 | with or without fee is hereby granted, provided that the above copyright notice 7 | and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 11 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 13 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 14 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 15 | THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | -------------------------------------------------------------------------------- /Earcut.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | /** 4 | * ... 5 | * @author Krtolica Vujadin 6 | */ 7 | 8 | /** 9 | * Ported from https://github.com/mapbox/earcut 10 | */ 11 | class Earcut { 12 | 13 | static public function earcut(data:Array, ?holeIndices:Array, dim:Int = 2):Array { 14 | var hasHoles:Bool = holeIndices != null && holeIndices.length > 0; 15 | var outerLen:Int = hasHoles ? Std.int(holeIndices[0] * dim) : data.length; 16 | var outerNode:Node = linkedList(data, 0, outerLen, dim, true); 17 | var triangles:Array = []; 18 | 19 | if (outerNode == null) { 20 | return triangles; 21 | } 22 | 23 | var minX:Float = 0; 24 | var minY:Float = 0; 25 | var maxX:Float = 0; 26 | var maxY:Float = 0; 27 | var x:Float = 0; 28 | var y:Float = 0; 29 | var size:Float = 0; 30 | 31 | if (hasHoles) { 32 | outerNode = eliminateHoles(data, holeIndices, outerNode, dim); 33 | } 34 | 35 | // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox 36 | if (data.length > 80 * dim) { 37 | minX = maxX = data[0]; 38 | minY = maxY = data[1]; 39 | 40 | var i = dim; 41 | while(i < outerLen) { 42 | x = data[i]; 43 | y = data[i + 1]; 44 | if (x < minX) { 45 | minX = x; 46 | } 47 | if (y < minY) { 48 | minY = y; 49 | } 50 | if (x > maxX) { 51 | maxX = x; 52 | } 53 | if (y > maxY) { 54 | maxY = y; 55 | } 56 | 57 | i += dim; 58 | } 59 | 60 | // minX, minY and size are later used to transform coords into integers for z-order calculation 61 | size = Math.max(maxX - minX, maxY - minY); 62 | } 63 | 64 | earcutLinked(outerNode, triangles, dim, minX, minY, size); 65 | 66 | return triangles; 67 | } 68 | 69 | // create a circular doubly linked list from polygon points in the specified winding order 70 | static function linkedList(data:Array, start:Int, end:Int, dim:Int, clockwise:Bool):Node { 71 | var last:Node = null; 72 | 73 | if (clockwise == (signedArea(data, start, end, dim) > 0)) { 74 | var i:Int = start; 75 | while (i < end) { 76 | last = insertNode(i, data[i], data[i + 1], last); 77 | 78 | i += dim; 79 | } 80 | } 81 | else { 82 | var i:Int = end - dim; 83 | while (i >= start) { 84 | last = insertNode(i, data[i], data[i + 1], last); 85 | i -= dim; 86 | } 87 | } 88 | 89 | if (last != null && equals(last, last.next)) { 90 | removeNode(last); 91 | last = last.next; 92 | } 93 | 94 | return last; 95 | } 96 | 97 | // eliminate colinear or duplicate points 98 | static function filterPoints(?start:Node, ?end:Node):Node { 99 | if (start == null) { 100 | return start; 101 | } 102 | if (end == null) { 103 | end = start; 104 | } 105 | 106 | var p:Node = start; 107 | var again:Bool = false; 108 | do { 109 | again = false; 110 | 111 | if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) == 0)) { 112 | removeNode(p); 113 | p = end = p.prev; 114 | if (p == p.next) { 115 | return null; 116 | } 117 | again = true; 118 | } 119 | else { 120 | p = p.next; 121 | } 122 | } 123 | while (again || p != end); 124 | 125 | return end; 126 | } 127 | 128 | // main ear slicing loop which triangulates a polygon (given as a linked list) 129 | static function earcutLinked(ear:Node, triangles:Array, dim:Float, ?minX:Float, ?minY:Float, ?size:Float, ?pass:Int) { 130 | if (ear == null) { 131 | return; 132 | } 133 | 134 | // interlink polygon nodes in z-order 135 | if (pass == null && size != null) { 136 | indexCurve(ear, minX, minY, size); 137 | } 138 | 139 | var stop = ear; 140 | var prev:Node = null; 141 | var next:Node = null; 142 | 143 | // iterate through ears, slicing them one by one 144 | while (ear.prev != ear.next) { 145 | prev = ear.prev; 146 | next = ear.next; 147 | 148 | if (size != null ? isEarHashed(ear, minX, minY, size) : isEar(ear)) { 149 | // cut off the triangle 150 | triangles.push(Std.int(prev.i / dim)); 151 | triangles.push(Std.int(ear.i / dim)); 152 | triangles.push(Std.int(next.i / dim)); 153 | 154 | removeNode(ear); 155 | 156 | // skipping the next vertice leads to less sliver triangles 157 | ear = next.next; 158 | stop = next.next; 159 | 160 | continue; 161 | } 162 | 163 | ear = next; 164 | 165 | // if we looped through the whole remaining polygon and can't find any more ears 166 | if (ear == stop) { 167 | // try filtering points and slicing again 168 | if (pass == null) { 169 | earcutLinked(filterPoints(ear), triangles, dim, minX, minY, size, 1); 170 | } // if this didn't work, try curing all small self-intersections locally 171 | else if (pass == 1) { 172 | ear = cureLocalIntersections(ear, triangles, dim); 173 | earcutLinked(ear, triangles, dim, minX, minY, size, 2); 174 | } // as a last resort, try splitting the remaining polygon into two 175 | else if (pass == 2) { 176 | splitEarcut(ear, triangles, dim, minX, minY, size); 177 | } 178 | 179 | break; 180 | } 181 | } 182 | } 183 | 184 | // check whether a polygon node forms a valid ear with adjacent nodes 185 | static function isEar(ear:Node):Bool { 186 | var a = ear.prev; 187 | var b = ear; 188 | var c = ear.next; 189 | 190 | if (area(a, b, c) >= 0) { 191 | return false; // reflex, can't be an ear 192 | } 193 | 194 | // now make sure we don't have other points inside the potential ear 195 | var p = ear.next.next; 196 | 197 | while (p != ear.prev) { 198 | 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) { 199 | return false; 200 | } 201 | p = p.next; 202 | } 203 | 204 | return true; 205 | } 206 | 207 | static function isEarHashed(ear:Node, minX:Float, minY:Float, size:Float) { 208 | var a = ear.prev; 209 | var b = ear; 210 | var c = ear.next; 211 | 212 | if (area(a, b, c) >= 0) { 213 | return false; // reflex, can't be an ear 214 | } 215 | 216 | // triangle bbox; min & max are calculated like this for speed 217 | var minTX = a.x < b.x ? (a.x < c.x ? a.x : c.x) : (b.x < c.x ? b.x : c.x); 218 | var minTY = a.y < b.y ? (a.y < c.y ? a.y : c.y) : (b.y < c.y ? b.y : c.y); 219 | var maxTX = a.x > b.x ? (a.x > c.x ? a.x : c.x) : (b.x > c.x ? b.x : c.x); 220 | var maxTY = a.y > b.y ? (a.y > c.y ? a.y : c.y) : (b.y > c.y ? b.y : c.y); 221 | 222 | // z-order range for the current triangle bbox; 223 | var minZ = zOrder(cast minTX, cast minTY, cast minX, cast minY, cast size); 224 | var maxZ = zOrder(cast maxTX, cast maxTY, cast minX, cast minY, cast size); 225 | 226 | // first look for points inside the triangle in increasing z-order 227 | var p = ear.nextZ; 228 | 229 | while (p != null && p.z <= maxZ) { 230 | 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) { 231 | return false; 232 | } 233 | p = p.nextZ; 234 | } 235 | 236 | // then look for points in decreasing z-order 237 | p = ear.prevZ; 238 | 239 | while (p != null && p.z >= minZ) { 240 | 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) { 241 | return false; 242 | } 243 | p = p.prevZ; 244 | } 245 | 246 | return true; 247 | } 248 | 249 | // go through all polygon nodes and cure small local self-intersections 250 | static function cureLocalIntersections(start:Node, triangles:Array, dim:Float) { 251 | var p = start; 252 | do { 253 | var a = p.prev; 254 | var b = p.next.next; 255 | 256 | if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) { 257 | triangles.push(Std.int(a.i / dim)); 258 | triangles.push(Std.int(p.i / dim)); 259 | triangles.push(Std.int(b.i / dim)); 260 | 261 | // remove two nodes involved 262 | removeNode(p); 263 | removeNode(p.next); 264 | 265 | p = start = b; 266 | } 267 | p = p.next; 268 | } 269 | while (p != start); 270 | 271 | return p; 272 | } 273 | 274 | // try splitting polygon into two and triangulate them independently 275 | static function splitEarcut(start:Node, triangles:Array, dim:Float, minX:Float, minY:Float, size:Float) { 276 | // look for a valid diagonal that divides the polygon into two 277 | var a = start; 278 | do { 279 | var b = a.next.next; 280 | while (b != a.prev) { 281 | if (a.i != b.i && isValidDiagonal(a, b)) { 282 | // split the polygon in two by the diagonal 283 | var c = splitPolygon(a, b); 284 | 285 | // filter colinear points around the cuts 286 | a = filterPoints(a, a.next); 287 | c = filterPoints(c, c.next); 288 | 289 | // run earcut on each half 290 | earcutLinked(a, triangles, dim, minX, minY, size); 291 | earcutLinked(c, triangles, dim, minX, minY, size); 292 | 293 | return; 294 | } 295 | 296 | b = b.next; 297 | } 298 | 299 | a = a.next; 300 | } 301 | while (a != start); 302 | } 303 | 304 | // link every hole into the outer loop, producing a single-ring polygon without holes 305 | static function eliminateHoles(data:Array, holeIndices:Array, outerNode:Node, dim:Int):Node { 306 | var queue:Array = []; 307 | var len:Int = holeIndices.length; 308 | var start:Int = 0; 309 | var end:Int = 0; 310 | var list:Node = null; 311 | 312 | for (i in 0...len) { 313 | start = Std.int(holeIndices[i] * dim); 314 | end = i < len - 1 ? Std.int(holeIndices[i + 1] * dim) : data.length; 315 | list = linkedList(data, start, end, dim, false); 316 | 317 | if (list == list.next) { 318 | list.steiner = true; 319 | } 320 | queue.push(getLeftmost(list)); 321 | } 322 | 323 | queue.sort(compareX); 324 | 325 | // process holes from left to right 326 | for (i in 0...queue.length) { 327 | eliminateHole(queue[i], outerNode); 328 | outerNode = filterPoints(outerNode, outerNode.next); 329 | } 330 | 331 | return outerNode; 332 | } 333 | 334 | static inline function compareX(a:Node, b:Node):Int { 335 | return Std.int(a.x - b.x); 336 | } 337 | 338 | // find a bridge between vertices that connects hole with an outer ring and and link it 339 | static inline function eliminateHole(holeNode:Node, outerNode:Node) { 340 | outerNode = findHoleBridge(holeNode, outerNode); 341 | if (outerNode != null) { 342 | var b = splitPolygon(outerNode, holeNode); 343 | filterPoints(b, b.next); 344 | } 345 | } 346 | 347 | // David Eberly's algorithm for finding a bridge between hole and outer polygon 348 | static function findHoleBridge(hole:Node, outerNode:Node):Node { 349 | var p = outerNode; 350 | var hx = hole.x; 351 | var hy = hole.y; 352 | var qx = Math.NEGATIVE_INFINITY; 353 | var m:Node = null; 354 | 355 | // find a segment intersected by a ray from the hole's leftmost point to the left; 356 | // segment's endpoint with lesser x will be potential connection point 357 | do { 358 | if (hy <= p.y && hy >= p.next.y) { 359 | var x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y); 360 | if (x <= hx && x > qx) { 361 | qx = x; 362 | if (x == hx) { 363 | if (hy == p.y) { 364 | return p; 365 | } 366 | if (hy == p.next.y) { 367 | return p.next; 368 | } 369 | } 370 | 371 | m = p.x < p.next.x ? p : p.next; 372 | } 373 | } 374 | 375 | p = p.next; 376 | } 377 | while (p != outerNode); 378 | 379 | if (m == null) { 380 | return null; 381 | } 382 | 383 | if (hx == qx) { 384 | return m.prev; // hole touches outer segment; pick lower endpoint 385 | } 386 | 387 | // look for points inside the triangle of hole point, segment intersection and endpoint; 388 | // if there are no points found, we have a valid connection; 389 | // otherwise choose the point of the minimum angle with the ray as connection point 390 | 391 | var stop:Node = m; 392 | var mx:Float = m.x; 393 | var my:Float = m.y; 394 | var tanMin:Float = Math.POSITIVE_INFINITY; 395 | var tan:Float = 0; 396 | 397 | p = m.next; 398 | 399 | while (p != stop) { 400 | if (hx >= p.x && p.x >= mx && pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) { 401 | tan = Math.abs(hy - p.y) / (hx - p.x); // tangential 402 | if ((tan < tanMin || (tan == tanMin && p.x > m.x)) && locallyInside(p, hole)) { 403 | m = p; 404 | tanMin = tan; 405 | } 406 | } 407 | 408 | p = p.next; 409 | } 410 | 411 | return m; 412 | } 413 | 414 | // interlink polygon nodes in z-order 415 | static function indexCurve(start:Node, minX:Float, minY:Float, size:Float) { 416 | var p = start; 417 | 418 | do { 419 | if (p.z == -99999999) { 420 | p.z = zOrder(cast p.x, cast p.y, cast minX, cast minY, cast size); 421 | } 422 | 423 | p.prevZ = p.prev; 424 | p.nextZ = p.next; 425 | p = p.next; 426 | } 427 | while (p != start); 428 | 429 | p.prevZ.nextZ = null; 430 | p.prevZ = null; 431 | 432 | sortLinked(p); 433 | } 434 | 435 | // Simon Tatham's linked list merge sort algorithm 436 | // http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html 437 | static function sortLinked(list:Node):Node { 438 | var p:Node = null; 439 | var q:Node = null; 440 | var e:Node = null; 441 | var tail:Node; 442 | var numMerges:Int = 0; 443 | var pSize:Int = 0; 444 | var qSize:Int = 0; 445 | var inSize = 1; 446 | 447 | do { 448 | p = list; 449 | list = null; 450 | tail = null; 451 | numMerges = 0; 452 | 453 | while (p != null) { 454 | numMerges++; 455 | q = p; 456 | pSize = 0; 457 | for (i in 0...inSize) { 458 | pSize++; 459 | q = q.nextZ; 460 | if (q == null) { 461 | break; 462 | } 463 | } 464 | 465 | qSize = inSize; 466 | 467 | while (pSize > 0 || (qSize > 0 && q != null)) { 468 | 469 | if (pSize == 0) { 470 | e = q; 471 | q = q.nextZ; 472 | qSize--; 473 | } 474 | else if (qSize == 0 || q == null) { 475 | e = p; 476 | p = p.nextZ; 477 | pSize--; 478 | } 479 | else if (p.z <= q.z) { 480 | e = p; 481 | p = p.nextZ; 482 | pSize--; 483 | } 484 | else { 485 | e = q; 486 | q = q.nextZ; 487 | qSize--; 488 | } 489 | 490 | if (tail != null) { 491 | tail.nextZ = e; 492 | } 493 | else { 494 | list = e; 495 | } 496 | 497 | e.prevZ = tail; 498 | tail = e; 499 | } 500 | 501 | p = q; 502 | } 503 | 504 | tail.nextZ = null; 505 | inSize *= 2; 506 | } 507 | while (numMerges > 1); 508 | 509 | return list; 510 | } 511 | 512 | // z-order of a point given coords and size of the data bounding box 513 | static function zOrder(x:Int, y:Int, minX:Int, minY:Int, size:Int):Int { 514 | // coords are transformed into non-negative 15-bit integer range 515 | x = Std.int(32767 * (x - minX) / size); 516 | y = Std.int(32767 * (y - minY) / size); 517 | 518 | x = (x | (x << 8)) & 0x00FF00FF; 519 | x = (x | (x << 4)) & 0x0F0F0F0F; 520 | x = (x | (x << 2)) & 0x33333333; 521 | x = (x | (x << 1)) & 0x55555555; 522 | 523 | y = (y | (y << 8)) & 0x00FF00FF; 524 | y = (y | (y << 4)) & 0x0F0F0F0F; 525 | y = (y | (y << 2)) & 0x33333333; 526 | y = (y | (y << 1)) & 0x55555555; 527 | 528 | return x | (y << 1); 529 | } 530 | 531 | // find the leftmost node of a polygon ring 532 | static function getLeftmost(start:Node):Node { 533 | var p = start; 534 | var leftmost = start; 535 | 536 | do { 537 | if (p.x < leftmost.x) { 538 | leftmost = p; 539 | } 540 | 541 | p = p.next; 542 | } 543 | while (p != start); 544 | 545 | return leftmost; 546 | } 547 | 548 | // check if a point lies within a convex triangle 549 | static inline function pointInTriangle(ax:Float, ay:Float, bx:Float, by:Float, cx:Float, cy:Float, px:Float, py:Float):Bool { 550 | return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 && 551 | (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 && 552 | (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0; 553 | } 554 | 555 | // check if a diagonal between two polygon nodes is valid (lies in polygon interior) 556 | static inline function isValidDiagonal(a:Node, b:Node):Bool { 557 | return a.next.i != b.i && a.prev.i != b.i && !intersectsPolygon(a, b) && 558 | locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b); 559 | } 560 | 561 | // signed area of a triangle 562 | static inline function area(p:Node, q:Node, r:Node):Float { 563 | return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y); 564 | } 565 | 566 | // check if two points are equal 567 | static inline function equals(p1:Node, p2:Node):Bool { 568 | return return p1.x == p2.x && p1.y == p2.y; 569 | } 570 | 571 | // check if two segments intersect 572 | static function intersects(p1:Node, q1:Node, p2:Node, q2:Node):Bool { 573 | if ((equals(p1, q1) && equals(p2, q2)) || (equals(p1, q2) && equals(p2, q1))) { 574 | return true; 575 | } 576 | 577 | return ((area(p1, q1, p2) > 0) != (area(p1, q1, q2) > 0)) && ((area(p2, q2, p1) > 0) != (area(p2, q2, q1) > 0)); 578 | } 579 | 580 | // check if a polygon diagonal intersects any polygon segments 581 | static function intersectsPolygon(a:Node, b:Node):Bool { 582 | var p = a; 583 | do { 584 | 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)) { 585 | return true; 586 | } 587 | p = p.next; 588 | } 589 | while (p != a); 590 | 591 | return false; 592 | } 593 | 594 | // check if a polygon diagonal is locally inside the polygon 595 | static inline function locallyInside(a:Node, b:Node) { 596 | return area(a.prev, a, a.next) < 0 ? 597 | area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 : 598 | area(a, b, a.prev) < 0 || area(a, a.next, b) < 0; 599 | } 600 | 601 | // check if the middle point of a polygon diagonal is inside the polygon 602 | static function middleInside(a:Node, b:Node):Bool { 603 | var p:Node = a; 604 | var inside:Bool = false; 605 | var px:Float = (a.x + b.x) / 2; 606 | var py:Float = (a.y + b.y) / 2; 607 | 608 | do { 609 | if (((p.y > py) != (p.next.y > py)) && (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x)) { 610 | inside = !inside; 611 | } 612 | 613 | p = p.next; 614 | } 615 | while (p != a); 616 | 617 | return inside; 618 | } 619 | 620 | // link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two; 621 | // if one belongs to the outer ring and another to a hole, it merges it into a single ring 622 | static function splitPolygon(a:Node, b:Node):Node { 623 | var a2:Node = new Node(a.i, a.x, a.y); 624 | var b2:Node = new Node(b.i, b.x, b.y); 625 | var an:Node = a.next; 626 | var bp:Node = b.prev; 627 | 628 | a.next = b; 629 | b.prev = a; 630 | 631 | a2.next = an; 632 | an.prev = a2; 633 | 634 | b2.next = a2; 635 | a2.prev = b2; 636 | 637 | bp.next = b2; 638 | b2.prev = bp; 639 | 640 | return b2; 641 | } 642 | 643 | // create a node and optionally link it with previous one (in a circular doubly linked list) 644 | static function insertNode(i:Int, x:Float, y:Float, ?last:Node):Node { 645 | var node = new Node(i, x, y); 646 | 647 | if (last == null) { 648 | node.prev = node; 649 | node.next = node; 650 | } 651 | else { 652 | node.next = last.next; 653 | node.prev = last; 654 | last.next.prev = node; 655 | last.next = node; 656 | } 657 | 658 | return node; 659 | } 660 | 661 | static function removeNode(p:Node) { 662 | p.next.prev = p.prev; 663 | p.prev.next = p.next; 664 | 665 | if (p.prevZ != null) { 666 | p.prevZ.nextZ = p.nextZ; 667 | } 668 | if (p.nextZ != null) { 669 | p.nextZ.prevZ = p.prevZ; 670 | } 671 | } 672 | 673 | static function signedArea(data:Array, start:Int, end:Int, dim:Int):Float { 674 | var sum:Float = 0; 675 | var i:Int = start; 676 | var j:Int = end - dim; 677 | while (i < end) { 678 | sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]); 679 | j = i; 680 | i += dim; 681 | } 682 | 683 | return sum; 684 | } 685 | 686 | // return a percentage difference between the polygon area and its triangulation area; 687 | // used to verify correctness of triangulation 688 | static function deviation(data:Array, holeIndices:Array, dim:Int, triangles:Array):Float { 689 | var hasHoles:Bool = holeIndices != null && holeIndices.length > 0; 690 | var outerLen:Int = hasHoles ? Std.int(holeIndices[0] * dim) : data.length; 691 | 692 | var polygonArea = Math.abs(signedArea(data, 0, outerLen, dim)); 693 | if (hasHoles) { 694 | var len:Int = holeIndices.length; 695 | for (i in 0...len) { 696 | var start = holeIndices[i] * dim; 697 | var end = i < len - 1 ? holeIndices[i + 1] * dim : data.length; 698 | polygonArea -= Math.abs(signedArea(data, start, end, dim)); 699 | } 700 | } 701 | 702 | var trianglesArea = 0.0; 703 | var i:Int = 0; 704 | while (i < triangles.length) { 705 | var a:Int = cast triangles[i] * dim; 706 | var b:Int = cast triangles[i + 1] * dim; 707 | var c:Int = cast triangles[i + 2] * dim; 708 | trianglesArea += Math.abs( 709 | (data[a] - data[c]) * (data[b + 1] - data[a + 1]) - 710 | (data[a] - data[b]) * (data[c + 1] - data[a + 1])); 711 | 712 | i += 3; 713 | } 714 | 715 | return polygonArea == 0 && trianglesArea == 0 ? 0 : Math.abs((trianglesArea - polygonArea) / polygonArea); 716 | } 717 | 718 | // turn a polygon in a multi-dimensional array form (e.g. as in GeoJSON) into a form Earcut accepts 719 | static public function flatten(data:Array>>) { 720 | var dim:Int = data[0][0].length; 721 | var vertices:Array = []; 722 | var holes:Array = []; 723 | var result:Dynamic = { }; 724 | result.vertices = vertices; 725 | result.holes = holes; 726 | result.dimensions = dim; 727 | var holeIndex:Int = 0; 728 | 729 | for (i in 0...data.length) { 730 | for (j in 0...data[i].length) { 731 | for (d in 0...dim) { 732 | result.vertices.push(data[i][j][d]); 733 | } 734 | } 735 | if (i > 0) { 736 | holeIndex += data[i - 1].length; 737 | result.holes.push(holeIndex); 738 | } 739 | } 740 | 741 | return result; 742 | } 743 | 744 | } 745 | 746 | class Node { 747 | 748 | public var i:Int; 749 | 750 | public var x:Float; 751 | public var y:Float; 752 | 753 | public var prev:Node; 754 | public var next:Node; 755 | 756 | public var z:Int = -99999999; 757 | 758 | public var prevZ:Node; 759 | public var nextZ:Node; 760 | 761 | public var steiner:Bool; 762 | 763 | 764 | public function new(i:Int, x:Float, y:Float) { 765 | // vertice index in coordinates array 766 | this.i = i; 767 | 768 | // vertex coordinates 769 | this.x = x; 770 | this.y = y; 771 | 772 | // previous and next vertice nodes in a polygon ring 773 | this.prev = null; 774 | this.next = null; 775 | 776 | // z-order curve value 777 | this.z = -99999999; 778 | 779 | // previous and next nodes in z-order 780 | this.prevZ = null; 781 | this.nextZ = null; 782 | 783 | // indicates whether this is a steiner point 784 | this.steiner = false; 785 | } 786 | 787 | } 788 | --------------------------------------------------------------------------------