Source code for py3dtiles.earcut

import math
from typing import Union

__all__ = ['earcut', 'deviation', 'flatten']


class Node:
    def __init__(self, i, x, y):
        # vertex index in coordinates array
        self.i = i

        # vertex coordinates

        self.x = x
        self.y = y

        # previous and next vertice nodes in a polygon ring
        self.prev = None
        self.next = None

        # z-order curve value
        self.z = None

        # previous and next nodes in z-order
        self.prevZ = None
        self.nextZ = None

        # indicates whether this is a steiner point
        self.steiner = False


[docs] def earcut(data, hole_indices=None, dim=None): dim = dim or 2 has_holes = hole_indices and len(hole_indices) outer_len = hole_indices[0] * dim if has_holes else len(data) outer_node = linked_list(data, 0, outer_len, dim, True) triangles = [] if not outer_node: return triangles min_x = None min_y = None size = None if has_holes: outer_node = eliminate_holes(data, hole_indices, outer_node, dim) # if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox if len(data) > 80 * dim: min_x = max_x = data[0] min_y = max_y = data[1] for i in range(dim, outer_len, dim): x = data[i] y = data[i + 1] if x < min_x: min_x = x if y < min_y: min_y = y if x > max_x: max_x = x if y > max_y: max_y = y # min_x, min_y and size are later used to transform coords into integers for z-order calculation size = max(max_x - min_x, max_y - min_y) earcut_linked(outer_node, triangles, dim, min_x, min_y, size) return triangles
# create a circular doubly linked _list from polygon points in the specified winding order def linked_list(data, start, end, dim, clockwise) -> Union[Node, None]: last = None if clockwise == (signed_area(data, start, end, dim) > 0): for i in range(start, end, dim): last = insert_node(i, data[i], data[i + 1], last) else: for i in reversed(range(start, end, dim)): last = insert_node(i, data[i], data[i + 1], last) if last and equals(last, last.next): remove_node(last) last = last.next return last # eliminate collinear or duplicate points def filter_points(start, end=None): if not start: return start if not end: end = start p = start again = True while again or p != end: again = False if not p.steiner and (equals(p, p.next) or area(p.prev, p, p.next) == 0): remove_node(p) p = end = p.prev if p == p.next: return None again = True else: p = p.next return end # main ear slicing loop which triangulates a polygon (given as a linked _list) def earcut_linked(ear, triangles, dim, min_x, min_y, size, _pass=None): if not ear: return # interlink polygon nodes in z-order if not _pass and size: index_curve(ear, min_x, min_y, size) stop = ear # iterate through ears, slicing them one by one while ear.prev != ear.next: prev_ear = ear.prev next_ear = ear.next if is_ear_hashed(ear, min_x, min_y, size) if size else is_ear(ear): # cut off the triangle triangles.append(prev_ear.i // dim) triangles.append(ear.i // dim) triangles.append(next_ear.i // dim) remove_node(ear) # skipping the next vertex leads to less sliver triangles ear = next_ear.next stop = next_ear.next continue ear = next_ear # if we looped through the whole remaining polygon and can't find any more ears if ear == stop: # try filtering points and slicing again if not _pass: earcut_linked(filter_points(ear), triangles, dim, min_x, min_y, size, 1) # if this didn't work, try curing all small self-intersections locally elif _pass == 1: ear = cure_local_intersections(ear, triangles, dim) earcut_linked(ear, triangles, dim, min_x, min_y, size, 2) # as a last resort, try splitting the remaining polygon into two elif _pass == 2: split_earcut(ear, triangles, dim, min_x, min_y, size) break # check whether a polygon node forms a valid ear with adjacent nodes def is_ear(ear): a = ear.prev b = ear c = ear.next 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 p = ear.next.next while p != ear.prev: if 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 def is_ear_hashed(ear, min_x, min_y, size): a = ear.prev b = ear c = ear.next if area(a, b, c) >= 0: return False # reflex, can't be an ear # triangle bbox; min & max are calculated like this for speed min_tx = (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) min_ty = (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) max_tx = (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) max_ty = (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) # z-order range for the current triangle bbox; min_z = z_order(min_tx, min_ty, min_x, min_y, size) max_z = z_order(max_tx, max_ty, min_x, min_y, size) # first look for points inside the triangle in increasing z-order p = ear.nextZ while p and p.z <= max_z: if ( p != ear.prev and p != ear.next 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.nextZ # then look for points in decreasing z-order p = ear.prevZ while p and p.z >= min_z: if ( p != ear.prev and p != ear.next 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.prevZ return True # go through all polygon nodes and cure small local self-intersections def cure_local_intersections(start, triangles, dim): do = True p = start while do or p != start: do = False a = p.prev b = p.next.next if not equals(a, b) and intersects(a, p, p.next, b) and locally_inside(a, b) and locally_inside(b, a): triangles.append(a.i // dim) triangles.append(p.i // dim) triangles.append(b.i // dim) # remove two nodes involved remove_node(p) remove_node(p.next) p = start = b p = p.next return p # try splitting polygon into two and triangulate them independently def split_earcut(start, triangles, dim, min_x, min_y, size): # look for a valid diagonal that divides the polygon into two do = True a = start while do or a != start: do = False b = a.next.next while b != 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 collinear 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, dim, min_x, min_y, size) earcut_linked(c, triangles, dim, min_x, min_y, size) return b = b.next a = a.next # link every hole into the outer loop, producing a single-ring polygon without holes def eliminate_holes(data, hole_indices, outer_node, dim): queue = [] _len = len(hole_indices) _list = None for i in range(len(hole_indices)): start = hole_indices[i] * dim end = hole_indices[i + 1] * dim if i < _len - 1 else len(data) _list = linked_list(data, start, end, dim, False) if _list is not None and _list == _list.next: _list.steiner = True queue.append(get_leftmost(_list)) queue = sorted(queue, key=lambda i: i.x) # process holes from left to right for i in range(len(queue)): eliminate_hole(queue[i], outer_node) outer_node = filter_points(outer_node, outer_node.next) return outer_node def compare_x(a, b): return a.x - b.x # find a bridge between vertices that connects hole with an outer ring and and link it def eliminate_hole(hole, outer_node): outer_node = find_hole_bridge(hole, outer_node) if outer_node: b = split_polygon(outer_node, hole) filter_points(b, b.next) # David Eberly's algorithm for finding a bridge between hole and outer polygon def find_hole_bridge(hole, outer_node): do = True p = outer_node hx = hole.x hy = hole.y qx = -math.inf m = None # 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 do or p != outer_node: do = False if p.y >= hy >= p.next.y and p.next.y - p.y != 0: x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y) if hx >= x > qx: qx = x if x == hx: if hy == p.y: return p if hy == p.next.y: return p.next m = p if p.x < p.next.x else p.next p = p.next if not m: return None if hx == qx: return m.prev # hole touches outer segment; pick lower endpoint # 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 = math.inf p = m.next while p != stop: hx_or_qx = hx if hy < my else qx qx_or_hx = qx if hy < my else hx if hx >= p.x >= mx and point_in_triangle(hx_or_qx, hy, mx, my, qx_or_hx, hy, p.x, p.y): tan = abs(hy - p.y) / (hx - p.x) # tangential if (tan < tan_min or (tan == tan_min and p.x > m.x)) and locally_inside(p, hole): m = p tan_min = tan p = p.next return m # interlink polygon nodes in z-order def index_curve(start, min_x, min_y, size): do = True p = start while do or p != start: do = False if p.z is None: p.z = z_order(p.x, p.y, min_x, min_y, size) p.prevZ = p.prev p.nextZ = p.next p = p.next p.prevZ.nextZ = None p.prevZ = None sort_linked(p) # Simon Tatham's linked _list merge sort algorithm # http:#www.chiark.greenend.org.uk/~sgtatham/algorithms/_listsort.html def sort_linked(_list): do = True num_merges = None in_size = 1 while do or num_merges > 1: do = False p = _list _list = None tail = None num_merges = 0 while p: num_merges += 1 q = p p_size = 0 for _ in range(in_size): p_size += 1 q = q.nextZ if not q: break q_size = in_size while p_size > 0 or (q_size > 0 and q): if p_size == 0: e = q q = q.nextZ q_size -= 1 elif q_size == 0 or not q: e = p p = p.nextZ p_size -= 1 elif p.z <= q.z: e = p p = p.nextZ p_size -= 1 else: e = q q = q.nextZ q_size -= 1 if tail: tail.nextZ = e else: _list = e e.prevZ = tail tail = e p = q tail.nextZ = None in_size *= 2 return _list # z-order of a point given coords and size of the data bounding box def z_order(x, y, min_x, min_y, size): # coords are transformed into non-negative 15-bit integer range x = int(32767 * (x - min_x) // size) y = int(32767 * (y - min_y) // 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) # find the leftmost node of a polygon ring def get_leftmost(start): do = True p = start leftmost = start while do or p != start: do = False if p.x < leftmost.x: leftmost = p p = p.next return leftmost # check if a point lies within a convex triangle def point_in_triangle(ax, ay, bx, by, cx, cy, px, py): return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 and \ (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 and \ (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0 # check if a diagonal between two polygon nodes is valid (lies in polygon interior) def is_valid_diagonal(a, b): return a.next.i != b.i and a.prev.i != b.i and not intersects_polygon(a, b) and \ locally_inside(a, b) and locally_inside(b, a) and middle_inside(a, b) # signed area of a triangle def area(p, q, r): return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y) # check if two points are equal def equals(p1, p2): return p1.x == p2.x and p1.y == p2.y # check if two segments intersect def intersects(p1, q1, p2, q2): if (equals(p1, q1) and equals(p2, q2)) or (equals(p1, q2) and equals(p2, q1)): return True return area(p1, q1, p2) > 0 != area(p1, q1, q2) > 0 and \ area(p2, q2, p1) > 0 != area(p2, q2, q1) > 0 # check if a polygon diagonal intersects any polygon segments def intersects_polygon(a, b): do = True p = a while do or p != a: do = False 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 return False # check if a polygon diagonal is locally inside the polygon def locally_inside(a, b): if area(a.prev, a, a.next) < 0: return area(a, b, a.next) >= 0 and area(a, a.prev, b) >= 0 else: return area(a, b, a.prev) < 0 or area(a, a.next, b) < 0 # check if the middle point of a polygon diagonal is inside the polygon def middle_inside(a, b): do = True p = a inside = False px = (a.x + b.x) / 2 py = (a.y + b.y) / 2 while do or p != a: do = False 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): inside = not inside p = p.next return inside # 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 def split_polygon(a, b): a2 = Node(a.i, a.x, a.y) b2 = Node(b.i, b.x, b.y) an = a.next 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 # create a node and optionally link it with previous one (in a circular doubly linked _list) def insert_node(i, x, y, last) -> Node: p = Node(i, x, y) if not last: p.prev = p p.next = p else: p.next = last.next p.prev = last last.next.prev = p last.next = p return p def remove_node(p): p.next.prev = p.prev p.prev.next = p.next if p.prevZ: p.prevZ.nextZ = p.nextZ if p.nextZ: p.nextZ.prevZ = p.prevZ # return a percentage difference between the polygon area and its triangulation area; # used to verify correctness of triangulation
[docs] def deviation(data, hole_indices, dim, triangles): _len = len(hole_indices) has_holes = hole_indices and len(hole_indices) outer_len = hole_indices[0] * dim if has_holes else len(data) polygon_area = abs(signed_area(data, 0, outer_len, dim)) if has_holes: for i in range(_len): start = hole_indices[i] * dim end = hole_indices[i + 1] * dim if i < _len - 1 else len(data) polygon_area -= abs(signed_area(data, start, end, dim)) triangles_area = 0 for i in range(0, len(triangles), 3): a = triangles[i] * dim b = triangles[i + 1] * dim c = triangles[i + 2] * dim triangles_area += abs( (data[a] - data[c]) * (data[b + 1] - data[a + 1]) - (data[a] - data[b]) * (data[c + 1] - data[a + 1]) ) if polygon_area == 0 and triangles_area == 0: return 0 return abs((triangles_area - polygon_area) / polygon_area)
def signed_area(data, start, end, dim): sum_area = 0 j = end - dim for i in range(start, end, dim): sum_area += (data[j] - data[i]) * (data[i + 1] + data[j + 1]) j = i return sum_area # turn a polygon in a multi-dimensional array form (e.g. as in GeoJSON) into a form Earcut accepts
[docs] def flatten(data): dim = len(data[0][0]) result = { 'vertices': [], 'holes': [], 'dimensions': dim } hole_index = 0 for i in range(len(data)): for j in range(len(data[i])): for d in range(dim): result['vertices'].append(data[i][j][d]) if i > 0: hole_index += len(data[i - 1]) result['holes'].append(hole_index) return result
def unflatten(data): result = [] for i in range(0, len(data), 3): result.append(tuple(data[i:i + 3])) return result