# cython: language_level=3 # Source: https://github.com/mapbox/earcut # License: ISC License (MIT compatible) # # Copyright (c) 2016, Mapbox # # Permission to use, copy, modify, and/or distribute this software for any purpose # with or without fee is hereby granted, provided that the above copyright notice # and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND # FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS # OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF # THIS SOFTWARE. # # Cython implementation of module ezdxf.math._mapbox_earcut.py # Copyright (c) 2022, Manfred Moitzi # License: MIT License # type: ignore from libc.math cimport fmin, fmax, fabs, INFINITY cdef class Node: cdef: int i double x double y int z bint steiner object point Node prev Node next Node prev_z Node next_z def __cinit__(self, int i, object point): self.i = i self.point = point self.x = point.x self.y = point.y self.z = 0 self.steiner = False self.prev = None self.next = None self.prev_z = None self.next_z = None cdef bint equals(self, Node other): return self.x == other.x and self.y == other.y def node_key(Node node): return node.x, node.y def earcut(list exterior, list holes): """Implements a modified ear slicing algorithm, optimized by z-order curve hashing and extended to handle holes, twisted polygons, degeneracies and self-intersections in a way that doesn't guarantee correctness of triangulation, but attempts to always produce acceptable results for practical data. Source: https://github.com/mapbox/earcut Args: exterior: outer path as list of points as objects which provide a `x`- and a `y`-attribute holes: list of holes, each hole is list of points, a hole with a single points is a Steiner point Returns: Returns a list of triangles, each triangle is a tuple of three points, the output points are the same objects as the input points. """ cdef: Node outer_node list triangles = [] double max_x, max_y, x, y double min_x = 0.0 double min_y = 0.0 double inv_size = 0.0 if not exterior: return triangles outer_node = linked_list(exterior, 0, ccw=True) if outer_node is None or outer_node.next is outer_node.prev: return triangles if holes: outer_node = eliminate_holes(holes, len(exterior), outer_node) # if the shape is not too simple, we'll use z-order curve hash later # calculate polygon bbox if len(exterior) > 80: min_x = max_x = exterior[0].x min_y = max_y = exterior[0].y for point in exterior: x = point.x y = point.y min_x = fmin(min_x, x) min_y = fmin(min_y, y) max_x = fmax(max_x, x) max_y = fmax(max_y, y) # min_x, min_y and inv_size are later used to transform coords into # integers for z-order calculation inv_size = fmax(max_x - min_x, max_y - min_y) inv_size = 32767 / inv_size if inv_size != 0 else 0 earcut_linked(outer_node, triangles, min_x, min_y, inv_size, 0) return triangles cdef Node linked_list(list points, int start, bint ccw): """Create a circular doubly linked list from polygon points in the specified winding order """ cdef: Node last = None int end if ccw is (signed_area(points) < 0): for point in points: last = insert_node(start, point, last) start += 1 else: end = start + len(points) for point in reversed(points): last = insert_node(end, point, last) end -= 1 # open polygon: where the 1st vertex is not coincident with the last vertex if last and last.equals(last.next): remove_node(last) last = last.next return last cdef double signed_area(list points): cdef: double s = 0.0 double point_x, prev_x, point_y, prev_y if not len(points): return s prev = points[-1] prev_x = prev.x prev_y = prev.y for point in points: point_x = point.x point_y = point.y s += (point_x - prev_x) * (point_y + prev_y) prev_x = point_x prev_y = point_y # s < 0 is counter-clockwise # s > 0 is clockwise return s cdef double area(Node p, Node q, Node r): """Returns signed area of a triangle""" return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y) cdef bint is_valid_diagonal(Node a, Node b): """Check if a diagonal between two polygon nodes is valid (lies in polygon interior) """ return ( a.next.i != b.i and a.prev.i != b.i and not intersects_polygon(a, b) # doesn't intersect other edges and ( locally_inside(a, b) and locally_inside(b, a) and middle_inside(a, b) and ( area(a.prev, a, b.prev) or area(a, b.prev, b) ) # does not create opposite-facing sectors or a.equals(b) and area(a.prev, a, a.next) > 0 and area(b.prev, b, b.next) > 0 ) # special zero-length case ) cdef bint intersects_polygon(Node a, Node b): """Check if a polygon diagonal intersects any polygon segments""" cdef Node p = a while True: 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) ): return True p = p.next if p is a: break return False cdef int sign(double num): if num < 0.0: return -1 if num > 0.0: return 1 return 0 cdef bint on_segment(p: Node, q: Node, r: Node): return fmax(p.x, r.x) >= q.x >= fmin(p.x, r.x) and fmax( p.y, r.y ) >= q.y >= fmin(p.y, r.y) cdef bint intersects(Node p1, Node q1, Node p2, Node q2): """check if two segments intersect""" cdef: int o1 = sign(area(p1, q1, p2)) int o2 = sign(area(p1, q1, q2)) int o3 = sign(area(p2, q2, p1)) int o4 = sign(area(p2, q2, q1)) if o1 != o2 and o3 != o4: return True # general case if o1 == 0 and on_segment(p1, p2, q1): return True # p1, q1 and p2 are collinear and p2 lies on p1q1 if o2 == 0 and on_segment(p1, q2, q1): return True # p1, q1 and q2 are collinear and q2 lies on p1q1 if o3 == 0 and on_segment(p2, p1, q2): return True # p2, q2 and p1 are collinear and p1 lies on p2q2 if o4 == 0 and on_segment(p2, q1, q2): return True # p2, q2 and q1 are collinear and q1 lies on p2q2 return False cdef Node insert_node(int i, point, Node last): """create a node and optionally link it with previous one (in a circular doubly linked list) """ cdef Node p = Node(i, point) if last is None: p.prev = p p.next = p else: p.next = last.next p.prev = last last.next.prev = p last.next = p return p cdef remove_node(Node p): p.next.prev = p.prev p.prev.next = p.next if p.prev_z is not None: p.prev_z.next_z = p.next_z if p.next_z is not None: p.next_z.prev_z = p.prev_z cdef Node eliminate_holes(list holes, int start, Node outer_node): """link every hole into the outer loop, producing a single-ring polygon without holes """ cdef: list queue = [] list hole for hole in holes: if len(hole) < 1: # skip empty holes continue # hole vertices in clockwise order _list = linked_list(hole, start, ccw=False) if _list is _list.next: _list.steiner = True start += len(hole) queue.append(get_leftmost(_list)) queue.sort(key=node_key) # process holes from left to right for hole_ in queue: outer_node = eliminate_hole(hole_, outer_node) return outer_node cdef Node eliminate_hole(Node hole, Node outer_node): """Find a bridge between vertices that connects hole with an outer ring and link it """ cdef: Node bridge = find_hole_bridge(hole, outer_node) Node bridge_reverse if bridge is None: return outer_node bridge_reverse = split_polygon(bridge, hole) # filter collinear points around the cuts filter_points(bridge_reverse, bridge_reverse.next) return filter_points(bridge, bridge.next) cdef Node filter_points(Node start, Node end = None): """eliminate colinear or duplicate points""" cdef: Node p bint again if start is None: return start if end is None: end = start p = start while True: again = False if not p.steiner and ( p.equals(p.next) or area(p.prev, p, p.next) == 0 ): remove_node(p) p = end = p.prev if p is p.next: break again = True else: p = p.next if not (again or p is not end): break return end # main ear slicing loop which triangulates a polygon (given as a linked list) cdef earcut_linked( Node ear, list triangles, double min_x, double min_y, double inv_size, int pass_, ): cdef: Node stop, prev, next bint _is_ear if ear is None: return # interlink polygon nodes in z-order if not pass_ and inv_size: index_curve(ear, min_x, min_y, inv_size) stop = ear # iterate through ears, slicing them one by one while ear.prev is not ear.next: prev = ear.prev next = ear.next _is_ear = ( is_ear_hashed(ear, min_x, min_y, inv_size) if inv_size else is_ear(ear) ) if _is_ear: # cut off the triangle triangles.append((prev.point, ear.point, next.point)) remove_node(ear) # skipping the next vertex 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 is stop: # try filtering points and slicing again if not pass_: earcut_linked( filter_points(ear), triangles, min_x, min_y, inv_size, 1, ) # if this didn't work, try curing all small self-intersections locally elif pass_ == 1: ear = cure_local_intersections(filter_points(ear), triangles) earcut_linked(ear, triangles, min_x, min_y, inv_size, 2) # as a last resort, try splitting the remaining polygon into two elif pass_ == 2: split_ear_cut(ear, triangles, min_x, min_y, inv_size) break cdef bint is_ear(Node ear): """check whether a polygon node forms a valid ear with adjacent nodes""" cdef: Node a = ear.prev Node b = ear Node c = ear.next Node p double x0, x1, y0, y1 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 # triangle bbox x0 = fmin(a.x, fmin(b.x, c.x)) x1 = fmax(a.x, fmax(b.x, c.x)) y0 = fmin(a.y, fmin(b.y, c.y)) y1 = fmax(a.y, fmax(b.y, c.y)) p = c.next while p is not a: if ( x0 <= p.x <= x1 and y0 <= p.y <= y1 and point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) and area(p.prev, p, p.next) >= 0 ): return False p = p.next return True cdef bint is_ear_hashed(Node ear, double min_x, double min_y, double inv_size): cdef: Node a = ear.prev Node b = ear Node c = ear.next double x0, x1, y0, y1, min_z, max_z Node p, n if area(a, b, c) >= 0: return False # reflex, can't be an ear # triangle bbox x0 = fmin(a.x, fmin(b.x, c.x)) x1 = fmax(a.x, fmax(b.x, c.x)) y0 = fmin(a.y, fmin(b.y, c.y)) y1 = fmax(a.y, fmax(b.y, c.y)) # z-order range for the current triangle bbox; min_z = z_order(x0, y0, min_x, min_y, inv_size) max_z = z_order(x1, y1, min_x, min_y, inv_size) p = ear.prev_z n = ear.next_z # look for points inside the triangle in both directions while p and p.z >= min_z and n and n.z <= max_z: if ( x0 <= p.x <= x1 and y0 <= p.y <= y1 and p is not a and p is not c and point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) and area(p.prev, p, p.next) >= 0 ): return False p = p.prev_z if ( x0 <= n.x <= x1 and y0 <= n.y <= y1 and n is not a and n is not c and point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) and area(n.prev, n, n.next) >= 0 ): return False n = n.next_z # look for remaining points in decreasing z-order while p and p.z >= min_z: if ( x0 <= p.x <= x1 and y0 <= p.y <= y1 and p is not a and p is not c and point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) and area(p.prev, p, p.next) >= 0 ): return False p = p.prev_z # look for remaining points in increasing z-order while n and n.z <= max_z: if ( x0 <= n.x <= x1 and y0 <= n.y <= y1 and n is not a and n is not c and point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) and area(n.prev, n, n.next) >= 0 ): return False n = n.next_z return True cdef Node get_leftmost(Node start): """Find the leftmost node of a polygon ring""" cdef: Node p = start Node leftmost = start while True: if p.x < leftmost.x or (p.x == leftmost.x and p.y < leftmost.y): leftmost = p p = p.next if p is start: break return leftmost cdef bint point_in_triangle( double ax, double ay, double bx, double by_, double cx, double cy, double px, double py, ): """Check if a point lies within a convex triangle""" return ( (cx - px) * (ay - py) >= (ax - px) * (cy - py) and (ax - px) * (by_ - py) >= (bx - px) * (ay - py) and (bx - px) * (cy - py) >= (cx - px) * (by_ - py) ) cdef bint sector_contains_sector(Node m, Node p): """Whether sector in vertex m contains sector in vertex p in the same coordinates. """ return area(m.prev, m, p.prev) < 0 and area(p.next, m, m.next) < 0 cdef index_curve(Node start, double min_x, double min_y, double inv_size): """Interlink polygon nodes in z-order""" cdef Node p = start while True: if p.z == 0: p.z = z_order(p.x, p.y, min_x, min_y, inv_size) p.prev_z = p.prev p.next_z = p.next p = p.next if p is start: break p.prev_z.next_z = None p.prev_z = None sort_linked(p) cdef int z_order( double x0, double y0, double min_x, double min_y, double inv_size ): """Z-order of a point given coords and inverse of the longer side of data bbox. """ # coords are transformed into non-negative 15-bit integer range cdef: int x = int((x0 - min_x) * inv_size) int y = int ((y0 - min_y) * inv_size) 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) # Simon Tatham's linked list merge sort algorithm # http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html cdef Node sort_linked(Node head): cdef: int in_size = 1 int num_merges, p_size, q_size, i Node tail, p, q, e while True: p = head head = None tail = None num_merges = 0 while p: num_merges += 1 q = p p_size = 0 for i in range(in_size): p_size += 1 q = q.next_z if not q: break q_size = in_size while p_size > 0 or (q_size > 0 and q): if p_size != 0 and (q_size == 0 or not q or p.z <= q.z): e = p p = p.next_z p_size -= 1 else: e = q q = q.next_z q_size -= 1 if tail: tail.next_z = e else: head = e e.prev_z = tail tail = e p = q tail.next_z = None in_size *= 2 if num_merges <= 1: break return head cdef Node split_polygon(Node a, Node b): """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. """ cdef : Node a2 = Node(a.i, a.point) Node b2 = Node(b.i, b.point) Node an = a.next Node 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 # go through all polygon nodes and cure small local self-intersections cdef Node cure_local_intersections(Node start, list triangles): cdef: Node p = start Node a, b while True: a = p.prev b = p.next.next if ( not a.equals(b) and intersects(a, p, p.next, b) and locally_inside(a, b) and locally_inside(b, a) ): triangles.append((a.point, p.point, b.point)) # remove two nodes involved remove_node(p) remove_node(p.next) p = start = b p = p.next if p is start: break return filter_points(p) cdef split_ear_cut( Node start, list triangles, double min_x, double min_y, double inv_size, ): """Try splitting polygon into two and triangulate them independently""" # look for a valid diagonal that divides the polygon into two cdef: Node a = start Node b, c while True: b = a.next.next while b is not a.prev: if a.i != b.i and is_valid_diagonal(a, b): # split the polygon in two by the diagonal c = split_polygon(a, b) # filter colinear points around the cuts a = filter_points(a, a.next) c = filter_points(c, c.next) # run earcut on each half earcut_linked(a, triangles, min_x, min_y, inv_size, 0) earcut_linked(c, triangles, min_x, min_y, inv_size, 0) return b = b.next a = a.next if a is start: break # David Eberly's algorithm for finding a bridge between hole and outer polygon cdef Node find_hole_bridge(Node hole, Node outer_node): cdef: Node p = outer_node Node m = None Node stop double hx = hole.x double hy = hole.y double qx = -INFINITY double mx, my, tan_min, tan # 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 while True: if p.y >= hy >= p.next.y != p.y: x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y) if hx >= x > qx: qx = x m = p if p.x < p.next.x else p.next if x == hx: # hole touches outer segment; pick leftmost endpoint return m p = p.next if p is outer_node: break if m is None: return None # 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 stop = m mx = m.x my = m.y tan_min = INFINITY p = m while True: if ( hx >= p.x >= mx and hx != p.x and point_in_triangle( hx if hy < my else qx, hy, mx, my, qx if hy < my else hx, hy, p.x, p.y, ) ): tan = fabs(hy - p.y) / (hx - p.x) # tangential if locally_inside(p, hole) and ( tan < tan_min or ( tan == tan_min and ( p.x > m.x or (p.x == m.x and sector_contains_sector(m, p)) ) ) ): m = p tan_min = tan p = p.next if p is stop: break return m cdef bint locally_inside(Node a, Node b): """Check if a polygon diagonal is locally inside the polygon""" return ( area(a, b, a.next) >= 0 and area(a, a.prev, b) >= 0 if area(a.prev, a, a.next) < 0 else area(a, b, a.prev) < 0 or area(a, a.next, b) < 0 ) cdef bint middle_inside(Node a, Node b): """Check if the middle point of a polygon diagonal is inside the polygon""" cdef: Node p = a bint inside = False double px = (a.x + b.x) / 2 double py = (a.y + b.y) / 2 while True: if ( ((p.y > py) != (p.next.y > py)) and p.next.y != p.y and (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x) ): inside = not inside p = p.next if p is a: break return inside