earcut.py 18 KB

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