├── LICENSE ├── README.md └── earcut ├── __init__.py └── earcut.py /LICENSE: -------------------------------------------------------------------------------- 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## earcut-python 2 | 3 | A pure Python port of the earcut JavaScript triangulation library. The latest version is based off of the earcut 2.1.1 release, and is functionally identical. 4 | 5 | The original project can be found here: 6 | https://github.com/mapbox/earcut 7 | 8 | #### Usage 9 | 10 | ```python 11 | triangles = earcut([10,0, 0,50, 60,60, 70,10]) # Returns [1,0,3, 3,2,1] 12 | ``` 13 | 14 | Signature: `earcut(vertices[, holes, dimensions = 2])`. 15 | 16 | * `vertices` is a flat array of vertex coordinates like `[x0,y0, x1,y1, x2,y2, ...]`. 17 | * `holes` is an array of hole _indices_ if any 18 | (e.g. `[5, 8]` for a 12-vertex input would mean one hole with vertices 5–7 and another with 8–11). 19 | * `dimensions` is the number of coordinates per vertex in the input array (`2` by default). 20 | 21 | Each group of three vertex indices in the resulting array forms a triangle. 22 | 23 | ```python 24 | # Triangulating a polygon with a hole 25 | earcut([0,0, 100,0, 100,100, 0,100, 20,20, 80,20, 80,80, 20,80], [4]) 26 | # [3,0,4, 5,4,0, 3,4,7, 5,0,1, 2,3,7, 6,5,1, 2,7,6, 6,1,2] 27 | 28 | # Triangulating a polygon with 3d coords 29 | earcut([10,0,1, 0,50,2, 60,60,3, 70,10,4], null, 3) 30 | # [1,0,3, 3,2,1] 31 | ``` 32 | 33 | If you pass a single vertex as a hole, Earcut treats it as a Steiner point. 34 | 35 | If your input is a multi-dimensional array, you can convert it to the format expected by Earcut with `earcut.flatten`: 36 | 37 | ```python 38 | # The first sequence of vertices is treated as the outer hull, the following sequneces are treated as holes. 39 | data = earcut.flatten([[(0,0), (100,0), (100,100), (0,100)], [(20,20), (80,20), (80,80), (20,80)]]) 40 | triangles = earcut(data['vertices'], data['holes'], data['dimensions']) 41 | ``` 42 | 43 | After getting a triangulation, you can verify its correctness with `earcut.deviation`: 44 | 45 | ```python 46 | deviation = earcut.deviation(vertices, holes, dimensions, triangles) 47 | ``` 48 | 49 | Returns the relative difference between the total area of triangles and the area of the input polygon. 50 | `0` means the triangulation is fully correct. -------------------------------------------------------------------------------- /earcut/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshuaskelly/earcut-python/c0d27600d5423d06686eeeb5e24c5f43595c7c1d/earcut/__init__.py -------------------------------------------------------------------------------- /earcut/earcut.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | __all__ = ['earcut', 'deviation', 'flatten'] 4 | 5 | 6 | def earcut(data, holeIndices=None, dim=None): 7 | dim = dim or 2 8 | 9 | hasHoles = holeIndices and len(holeIndices) 10 | outerLen = holeIndices[0] * dim if hasHoles else len(data) 11 | outerNode = linkedList(data, 0, outerLen, dim, True) 12 | triangles = [] 13 | 14 | if not outerNode: 15 | return triangles 16 | 17 | minX = None 18 | minY = None 19 | maxX = None 20 | maxY = None 21 | x = None 22 | y = None 23 | size = None 24 | 25 | if hasHoles: 26 | outerNode = eliminateHoles(data, holeIndices, outerNode, dim) 27 | 28 | # if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox 29 | if (len(data) > 80 * dim): 30 | minX = maxX = data[0] 31 | minY = maxY = data[1] 32 | 33 | for i in range(dim, outerLen, dim): 34 | x = data[i] 35 | y = data[i + 1] 36 | if x < minX: 37 | minX = x 38 | if y < minY: 39 | minY = y 40 | if x > maxX: 41 | maxX = x 42 | if y > maxY: 43 | maxY = y 44 | 45 | # minX, minY and size are later used to transform coords into integers for z-order calculation 46 | size = max(maxX - minX, maxY - minY) 47 | 48 | earcutLinked(outerNode, triangles, dim, minX, minY, size) 49 | 50 | return triangles 51 | 52 | 53 | # create a circular doubly linked _list from polygon points in the specified winding order 54 | def linkedList(data, start, end, dim, clockwise): 55 | i = None 56 | last = None 57 | 58 | if (clockwise == (signedArea(data, start, end, dim) > 0)): 59 | for i in range(start, end, dim): 60 | last = insertNode(i, data[i], data[i + 1], last) 61 | 62 | else: 63 | for i in reversed(range(start, end, dim)): 64 | last = insertNode(i, data[i], data[i + 1], last) 65 | 66 | if (last and equals(last, last.next)): 67 | removeNode(last) 68 | last = last.next 69 | 70 | return last 71 | 72 | 73 | # eliminate colinear or duplicate points 74 | def filterPoints(start, end=None): 75 | if not start: 76 | return start 77 | if not end: 78 | end = start 79 | 80 | p = start 81 | again = True 82 | 83 | while again or p != end: 84 | again = False 85 | 86 | if (not p.steiner and (equals(p, p.next) or area(p.prev, p, p.next) == 0)): 87 | removeNode(p) 88 | p = end = p.prev 89 | if (p == p.next): 90 | return None 91 | 92 | again = True 93 | 94 | else: 95 | p = p.next 96 | 97 | return end 98 | 99 | # main ear slicing loop which triangulates a polygon (given as a linked _list) 100 | def earcutLinked(ear, triangles, dim, minX, minY, size, _pass=None): 101 | if not ear: 102 | return 103 | 104 | # interlink polygon nodes in z-order 105 | if not _pass and size: 106 | indexCurve(ear, minX, minY, size) 107 | 108 | stop = ear 109 | prev = None 110 | next = None 111 | 112 | # iterate through ears, slicing them one by one 113 | while ear.prev != ear.next: 114 | prev = ear.prev 115 | next = ear.next 116 | 117 | if isEarHashed(ear, minX, minY, size) if size else isEar(ear): 118 | # cut off the triangle 119 | triangles.append(prev.i // dim) 120 | triangles.append(ear.i // dim) 121 | triangles.append(next.i // dim) 122 | 123 | removeNode(ear) 124 | 125 | # skipping the next vertice leads to less sliver triangles 126 | ear = next.next 127 | stop = next.next 128 | 129 | continue 130 | 131 | ear = next 132 | 133 | # if we looped through the whole remaining polygon and can't find any more ears 134 | if ear == stop: 135 | # try filtering points and slicing again 136 | if not _pass: 137 | earcutLinked(filterPoints(ear), triangles, dim, minX, minY, size, 1) 138 | 139 | # if this didn't work, try curing all small self-intersections locally 140 | elif _pass == 1: 141 | ear = cureLocalIntersections(ear, triangles, dim) 142 | earcutLinked(ear, triangles, dim, minX, minY, size, 2) 143 | 144 | # as a last resort, try splitting the remaining polygon into two 145 | elif _pass == 2: 146 | splitEarcut(ear, triangles, dim, minX, minY, size) 147 | 148 | break 149 | 150 | # check whether a polygon node forms a valid ear with adjacent nodes 151 | def isEar(ear): 152 | a = ear.prev 153 | b = ear 154 | c = ear.next 155 | 156 | if area(a, b, c) >= 0: 157 | return False # reflex, can't be an ear 158 | 159 | # now make sure we don't have other points inside the potential ear 160 | p = ear.next.next 161 | 162 | while p != ear.prev: 163 | if pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) and area(p.prev, p, p.next) >= 0: 164 | return False 165 | p = p.next 166 | 167 | return True 168 | 169 | def isEarHashed(ear, minX, minY, size): 170 | a = ear.prev 171 | b = ear 172 | c = ear.next 173 | 174 | if area(a, b, c) >= 0: 175 | return False # reflex, can't be an ear 176 | 177 | # triangle bbox; min & max are calculated like this for speed 178 | minTX = (a.x if a.x < c.x else c.x) if a.x < b.x else (b.x if b.x < c.x else c.x) 179 | minTY = (a.y if a.y < c.y else c.y) if a.y < b.y else (b.y if b.y < c.y else c.y) 180 | maxTX = (a.x if a.x > c.x else c.x) if a.x > b.x else (b.x if b.x > c.x else c.x) 181 | maxTY = (a.y if a.y > c.y else c.y) if a.y > b.y else (b.y if b.y > c.y else c.y) 182 | 183 | # z-order range for the current triangle bbox; 184 | minZ = zOrder(minTX, minTY, minX, minY, size) 185 | maxZ = zOrder(maxTX, maxTY, minX, minY, size) 186 | 187 | # first look for points inside the triangle in increasing z-order 188 | p = ear.nextZ 189 | 190 | while p and p.z <= maxZ: 191 | if p != ear.prev and p != ear.next and pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) and area(p.prev, p, p.next) >= 0: 192 | return False 193 | p = p.nextZ 194 | 195 | # then look for points in decreasing z-order 196 | p = ear.prevZ 197 | 198 | while p and p.z >= minZ: 199 | if p != ear.prev and p != ear.next and pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) and area(p.prev, p, p.next) >= 0: 200 | return False 201 | p = p.prevZ 202 | 203 | return True 204 | 205 | # go through all polygon nodes and cure small local self-intersections 206 | def cureLocalIntersections(start, triangles, dim): 207 | do = True 208 | p = start 209 | 210 | while do or p != start: 211 | do = False 212 | 213 | a = p.prev 214 | b = p.next.next 215 | 216 | if not equals(a, b) and intersects(a, p, p.next, b) and locallyInside(a, b) and locallyInside(b, a): 217 | triangles.append(a.i // dim) 218 | triangles.append(p.i // dim) 219 | triangles.append(b.i // dim) 220 | 221 | # remove two nodes involved 222 | removeNode(p) 223 | removeNode(p.next) 224 | 225 | p = start = b 226 | 227 | p = p.next 228 | 229 | return p 230 | 231 | # try splitting polygon into two and triangulate them independently 232 | def splitEarcut(start, triangles, dim, minX, minY, size): 233 | # look for a valid diagonal that divides the polygon into two 234 | do = True 235 | a = start 236 | 237 | while do or a != start: 238 | do = False 239 | b = a.next.next 240 | 241 | while b != a.prev: 242 | if a.i != b.i and isValidDiagonal(a, b): 243 | # split the polygon in two by the diagonal 244 | c = splitPolygon(a, b) 245 | 246 | # filter colinear points around the cuts 247 | a = filterPoints(a, a.next) 248 | c = filterPoints(c, c.next) 249 | 250 | # run earcut on each half 251 | earcutLinked(a, triangles, dim, minX, minY, size) 252 | earcutLinked(c, triangles, dim, minX, minY, size) 253 | return 254 | 255 | b = b.next 256 | 257 | a = a.next 258 | 259 | # link every hole into the outer loop, producing a single-ring polygon without holes 260 | def eliminateHoles(data, holeIndices, outerNode, dim): 261 | queue = [] 262 | i = None 263 | _len = len(holeIndices) 264 | start = None 265 | end = None 266 | _list = None 267 | 268 | for i in range(len(holeIndices)): 269 | start = holeIndices[i] * dim 270 | end = holeIndices[i + 1] * dim if i < _len - 1 else len(data) 271 | _list = linkedList(data, start, end, dim, False) 272 | 273 | if (_list == _list.next): 274 | _list.steiner = True 275 | 276 | queue.append(getLeftmost(_list)) 277 | 278 | queue = sorted(queue, key=lambda i: i.x) 279 | 280 | # process holes from left to right 281 | for i in range(len(queue)): 282 | eliminateHole(queue[i], outerNode) 283 | outerNode = filterPoints(outerNode, outerNode.next) 284 | 285 | return outerNode 286 | 287 | def compareX(a, b): 288 | return a.x - b.x 289 | 290 | # find a bridge between vertices that connects hole with an outer ring and and link it 291 | def eliminateHole(hole, outerNode): 292 | outerNode = findHoleBridge(hole, outerNode) 293 | if outerNode: 294 | b = splitPolygon(outerNode, hole) 295 | filterPoints(b, b.next) 296 | 297 | # David Eberly's algorithm for finding a bridge between hole and outer polygon 298 | def findHoleBridge(hole, outerNode): 299 | do = True 300 | p = outerNode 301 | hx = hole.x 302 | hy = hole.y 303 | qx = -math.inf 304 | m = None 305 | 306 | # find a segment intersected by a ray from the hole's leftmost point to the left; 307 | # segment's endpoint with lesser x will be potential connection point 308 | while do or p != outerNode: 309 | do = False 310 | if hy <= p.y and hy >= p.next.y and p.next.y - p.y != 0: 311 | x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y) 312 | 313 | if x <= hx and x > qx: 314 | qx = x 315 | 316 | if (x == hx): 317 | if hy == p.y: 318 | return p 319 | if hy == p.next.y: 320 | return p.next 321 | 322 | m = p if p.x < p.next.x else p.next 323 | 324 | p = p.next 325 | 326 | if not m: 327 | return None 328 | 329 | if hx == qx: 330 | return m.prev # hole touches outer segment; pick lower endpoint 331 | 332 | # look for points inside the triangle of hole point, segment intersection and endpoint; 333 | # if there are no points found, we have a valid connection; 334 | # otherwise choose the point of the minimum angle with the ray as connection point 335 | 336 | stop = m 337 | mx = m.x 338 | my = m.y 339 | tanMin = math.inf 340 | tan = None 341 | 342 | p = m.next 343 | 344 | while p != stop: 345 | hx_or_qx = hx if hy < my else qx 346 | qx_or_hx = qx if hy < my else hx 347 | 348 | if hx >= p.x and p.x >= mx and pointInTriangle(hx_or_qx, hy, mx, my, qx_or_hx, hy, p.x, p.y): 349 | 350 | tan = abs(hy - p.y) / (hx - p.x) # tangential 351 | 352 | if (tan < tanMin or (tan == tanMin and p.x > m.x)) and locallyInside(p, hole): 353 | m = p 354 | tanMin = tan 355 | 356 | p = p.next 357 | 358 | return m 359 | 360 | # interlink polygon nodes in z-order 361 | def indexCurve(start, minX, minY, size): 362 | do = True 363 | p = start 364 | 365 | while do or p != start: 366 | do = False 367 | 368 | if p.z == None: 369 | p.z = zOrder(p.x, p.y, minX, minY, size) 370 | 371 | p.prevZ = p.prev 372 | p.nextZ = p.next 373 | p = p.next 374 | 375 | p.prevZ.nextZ = None 376 | p.prevZ = None 377 | 378 | sortLinked(p) 379 | 380 | # Simon Tatham's linked _list merge sort algorithm 381 | # http:#www.chiark.greenend.org.uk/~sgtatham/algorithms/_listsort.html 382 | def sortLinked(_list): 383 | do = True 384 | i = None 385 | p = None 386 | q = None 387 | e = None 388 | tail = None 389 | numMerges = None 390 | pSize = None 391 | qSize = None 392 | inSize = 1 393 | 394 | while do or numMerges > 1: 395 | do = False 396 | p = _list 397 | _list = None 398 | tail = None 399 | numMerges = 0 400 | 401 | while p: 402 | numMerges += 1 403 | q = p 404 | pSize = 0 405 | for i in range(inSize): 406 | pSize += 1 407 | q = q.nextZ 408 | if not q: 409 | break 410 | 411 | qSize = inSize 412 | 413 | while pSize > 0 or (qSize > 0 and q): 414 | 415 | if pSize == 0: 416 | e = q 417 | q = q.nextZ 418 | qSize -= 1 419 | 420 | elif (qSize == 0 or not q): 421 | e = p 422 | p = p.nextZ 423 | pSize -= 1 424 | 425 | elif (p.z <= q.z): 426 | e = p 427 | p = p.nextZ 428 | pSize -= 1 429 | 430 | else: 431 | e = q 432 | q = q.nextZ 433 | qSize -= 1 434 | 435 | if tail: 436 | tail.nextZ = e 437 | 438 | else: 439 | _list = e 440 | 441 | e.prevZ = tail 442 | tail = e 443 | 444 | p = q 445 | 446 | tail.nextZ = None 447 | inSize *= 2 448 | 449 | return _list 450 | 451 | 452 | # z-order of a point given coords and size of the data bounding box 453 | def zOrder(x, y, minX, minY, size): 454 | # coords are transformed into non-negative 15-bit integer range 455 | x = 32767 * (x - minX) // size 456 | y = 32767 * (y - minY) // size 457 | 458 | x = (x | (x << 8)) & 0x00FF00FF 459 | x = (x | (x << 4)) & 0x0F0F0F0F 460 | x = (x | (x << 2)) & 0x33333333 461 | x = (x | (x << 1)) & 0x55555555 462 | 463 | y = (y | (y << 8)) & 0x00FF00FF 464 | y = (y | (y << 4)) & 0x0F0F0F0F 465 | y = (y | (y << 2)) & 0x33333333 466 | y = (y | (y << 1)) & 0x55555555 467 | 468 | return x | (y << 1) 469 | 470 | # find the leftmost node of a polygon ring 471 | def getLeftmost(start): 472 | do = True 473 | p = start 474 | leftmost = start 475 | 476 | while do or p != start: 477 | do = False 478 | if p.x < leftmost.x: 479 | leftmost = p 480 | p = p.next 481 | 482 | return leftmost 483 | 484 | # check if a point lies within a convex triangle 485 | def pointInTriangle(ax, ay, bx, by, cx, cy, px, py): 486 | return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 and \ 487 | (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 and \ 488 | (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0 489 | 490 | # check if a diagonal between two polygon nodes is valid (lies in polygon interior) 491 | def isValidDiagonal(a, b): 492 | return a.next.i != b.i and a.prev.i != b.i and not intersectsPolygon(a, b) and \ 493 | locallyInside(a, b) and locallyInside(b, a) and middleInside(a, b) 494 | 495 | # signed area of a triangle 496 | def area(p, q, r): 497 | return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y) 498 | 499 | # check if two points are equal 500 | def equals(p1, p2): 501 | return p1.x == p2.x and p1.y == p2.y 502 | 503 | 504 | # check if two segments intersect 505 | def intersects(p1, q1, p2, q2): 506 | if (equals(p1, q1) and equals(p2, q2)) or (equals(p1, q2) and equals(p2, q1)): 507 | return True 508 | 509 | return area(p1, q1, p2) > 0 != area(p1, q1, q2) > 0 and \ 510 | area(p2, q2, p1) > 0 != area(p2, q2, q1) > 0 511 | 512 | # check if a polygon diagonal intersects any polygon segments 513 | def intersectsPolygon(a, b): 514 | do = True 515 | p = a 516 | 517 | while do or p != a: 518 | do = False 519 | if (p.i != a.i and p.next.i != a.i and p.i != b.i and p.next.i != b.i and intersects(p, p.next, a, b)): 520 | return True 521 | 522 | p = p.next 523 | 524 | return False 525 | 526 | # check if a polygon diagonal is locally inside the polygon 527 | def locallyInside(a, b): 528 | if area(a.prev, a, a.next) < 0: 529 | return area(a, b, a.next) >= 0 and area(a, a.prev, b) >= 0 530 | else: 531 | return area(a, b, a.prev) < 0 or area(a, a.next, b) < 0 532 | 533 | # check if the middle point of a polygon diagonal is inside the polygon 534 | def middleInside(a, b): 535 | do = True 536 | p = a 537 | inside = False 538 | px = (a.x + b.x) / 2 539 | py = (a.y + b.y) / 2 540 | 541 | while do or p != a: 542 | do = False 543 | if ((p.y > py) != (p.next.y > py)) and (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x): 544 | inside = not inside 545 | 546 | p = p.next 547 | 548 | return inside 549 | 550 | # link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two; 551 | # if one belongs to the outer ring and another to a hole, it merges it into a single ring 552 | def splitPolygon(a, b): 553 | a2 = Node(a.i, a.x, a.y) 554 | b2 = Node(b.i, b.x, b.y) 555 | an = a.next 556 | bp = b.prev 557 | 558 | a.next = b 559 | b.prev = a 560 | 561 | a2.next = an 562 | an.prev = a2 563 | 564 | b2.next = a2 565 | a2.prev = b2 566 | 567 | bp.next = b2 568 | b2.prev = bp 569 | 570 | return b2 571 | 572 | 573 | # create a node and optionally link it with previous one (in a circular doubly linked _list) 574 | def insertNode(i, x, y, last): 575 | p = Node(i, x, y) 576 | 577 | if not last: 578 | p.prev = p 579 | p.next = p 580 | 581 | else: 582 | p.next = last.next 583 | p.prev = last 584 | last.next.prev = p 585 | last.next = p 586 | 587 | return p 588 | 589 | def removeNode(p): 590 | p.next.prev = p.prev 591 | p.prev.next = p.next 592 | 593 | if p.prevZ: 594 | p.prevZ.nextZ = p.nextZ 595 | 596 | if p.nextZ: 597 | p.nextZ.prevZ = p.prevZ 598 | 599 | class Node(object): 600 | def __init__(self, i, x, y): 601 | # vertice index in coordinates array 602 | self.i = i 603 | 604 | # vertex coordinates 605 | 606 | self.x = x 607 | self.y = y 608 | 609 | # previous and next vertice nodes in a polygon ring 610 | self.prev = None 611 | self.next = None 612 | 613 | # z-order curve value 614 | self.z = None 615 | 616 | # previous and next nodes in z-order 617 | self.prevZ = None 618 | self.nextZ = None 619 | 620 | # indicates whether this is a steiner point 621 | self.steiner = False 622 | 623 | 624 | # return a percentage difference between the polygon area and its triangulation area; 625 | # used to verify correctness of triangulation 626 | def deviation(data, holeIndices, dim, triangles): 627 | _len = len(holeIndices) 628 | hasHoles = holeIndices and len(holeIndices) 629 | outerLen = holeIndices[0] * dim if hasHoles else len(data) 630 | 631 | polygonArea = abs(signedArea(data, 0, outerLen, dim)) 632 | 633 | if hasHoles: 634 | for i in range(_len): 635 | start = holeIndices[i] * dim 636 | end = holeIndices[i + 1] * dim if i < _len - 1 else len(data) 637 | polygonArea -= abs(signedArea(data, start, end, dim)) 638 | 639 | trianglesArea = 0 640 | 641 | for i in range(0, len(triangles), 3): 642 | a = triangles[i] * dim 643 | b = triangles[i + 1] * dim 644 | c = triangles[i + 2] * dim 645 | trianglesArea += abs( 646 | (data[a] - data[c]) * (data[b + 1] - data[a + 1]) - 647 | (data[a] - data[b]) * (data[c + 1] - data[a + 1])) 648 | 649 | if polygonArea == 0 and trianglesArea == 0: 650 | return 0 651 | 652 | return abs((trianglesArea - polygonArea) / polygonArea) 653 | 654 | 655 | def signedArea(data, start, end, dim): 656 | sum = 0 657 | j = end - dim 658 | 659 | for i in range(start, end, dim): 660 | sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]) 661 | j = i 662 | 663 | return sum 664 | 665 | 666 | # turn a polygon in a multi-dimensional array form (e.g. as in GeoJSON) into a form Earcut accepts 667 | def flatten(data): 668 | dim = len(data[0][0]) 669 | result = { 670 | 'vertices': [], 671 | 'holes': [], 672 | 'dimensions': dim 673 | } 674 | holeIndex = 0 675 | 676 | for i in range(len(data)): 677 | for j in range(len(data[i])): 678 | for d in range(dim): 679 | result['vertices'].append(data[i][j][d]) 680 | 681 | if i > 0: 682 | holeIndex += len(data[i - 1]) 683 | result['holes'].append(holeIndex) 684 | 685 | return result 686 | 687 | def unflatten(data): 688 | result = [] 689 | 690 | for i in range(0, len(data), 3): 691 | result.append(tuple(data[i:i + 3])) 692 | 693 | return result --------------------------------------------------------------------------------