Files
stepanalyser/.venv/lib/python3.12/site-packages/ezdxf/acc/mapbox_earcut.pyx
Christian Anetzberger a197de9456 initial
2026-01-22 20:23:51 +01:00

836 lines
23 KiB
Cython

# 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