Skip to content

Commit

Permalink
add archive/tricy.pyx module
Browse files Browse the repository at this point in the history
Added tricy.pyx just for practice and better comparability
of the tripy and the mapbox_earcut algorithm.

The performance advantage of the mapbox_earcut algorithm
for small polygons is 5x and 20x for big polygons.
  • Loading branch information
mozman committed Jul 30, 2022
1 parent ccb8fb2 commit 6f99fc3
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 6 deletions.
4 changes: 4 additions & 0 deletions archive/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
build
*.cpp
*.html
*.pyd
20 changes: 20 additions & 0 deletions archive/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright (c) 2022, Manfred Moitzi
# License: MIT License
from setuptools import Extension, setup

setup(
name="tricy",
ext_modules=[
Extension(
"tricy",
["tricy.pyx"],
language="c++",
include_dirs=[
r"..\src\ezdxf\acc",
],
),
],
)

# build c-extensions in archive:
# py(thon3) setup.py build_ext -i
128 changes: 128 additions & 0 deletions archive/tricy.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# cython: language_level=3
# distutils: language = c++
# Copyright (c) 2017 Sam Bolgert
# License: MIT License
# https://github.com/linuxlewis/tripy
from typing import Iterable, Iterator, List, Tuple
from ezdxf.math import UVec, has_clockwise_orientation
from ezdxf.acc.vector cimport Vec2
from libc.math cimport fabs

cdef double EPSILON = 1e-16

# This module was replaced by the faster ezdxf.math._mapbox_earcut.py module!
# This file just preserves the invested time and effort for the ezdxf
# integration ;)


def earclip(vertices: Iterable[UVec]) -> Iterator[Tuple[Vec2, Vec2, Vec2]]:
"""This function triangulates the given 2d polygon into simple triangles by
the "ear clipping" algorithm. The function yields n-2 triangles for a polygon
with n vertices, each triangle is a 3-tuple of :class:`Vec2` objects.

Implementation Reference:
- https://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf

"""
cdef list ear_vertices = []
cdef list polygon = Vec2.list(vertices)
cdef list groups
cdef int point_count, prev_index, next_next_index, i, j
cdef Vec2 ear, prev_point, next_point, point, prev_prev_point, next_next_point

if len(polygon) == 0:
return

# remove closing vertex -> produces a degenerated last triangle
# [0] -> [-2] -> [-1], where [0] == [-1]
if polygon[0].isclose(polygon[-1]):
polygon.pop()

point_count = len(polygon)
if point_count < 3:
return

if has_clockwise_orientation(polygon):
polygon.reverse()

# "simple" polygons are a requirement, see algorithm description:
# https://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf
if point_count == 3:
yield polygon[0], polygon[1], polygon[2]
return

for i, point in enumerate(polygon):
prev_point = <Vec2> polygon[i - 1]
next_point = <Vec2> polygon[(i + 1) % point_count]
if _is_ear(prev_point, point, next_point, polygon):
ear_vertices.append(point)

while ear_vertices and point_count >= 3:
ear = <Vec2> ear_vertices.pop(0)
i = polygon.index(ear)
prev_index = i - 1
prev_point = <Vec2> polygon[prev_index]
next_point = <Vec2> polygon[(i + 1) % point_count]

polygon.remove(ear)
point_count -= 1
yield prev_point, ear, next_point

if point_count > 3:
prev_prev_point = <Vec2> polygon[prev_index - 1]
next_next_index = (i + 1) % point_count
next_next_point = <Vec2> polygon[next_next_index]

groups = [
(prev_prev_point, prev_point, next_point, polygon),
(prev_point, next_point, next_next_point, polygon),
]
for j in range(len(groups)):
group = groups[j]
p = group[1]
if _is_ear(<Vec2> group[0], <Vec2> p, <Vec2> group[2], <list> group[3]):
if p not in ear_vertices:
ear_vertices.append(p)
elif p in ear_vertices:
ear_vertices.remove(p)


cdef bint _is_convex(Vec2 a, Vec2 b, Vec2 c):
return a.x * (c.y - b.y) + b.x * (a.y - c.y) + c.x * (b.y - a.y) < 0.0


cdef bint _is_ear(Vec2 p1, Vec2 p2, Vec2 p3, list polygon):
return (
_is_convex(p1, p2, p3)
and _triangle_area(p1, p2, p3) > 0.0
and _contains_no_points(p1, p2, p3, polygon)
)


cdef bint _contains_no_points(
Vec2 p1, Vec2 p2, Vec2 p3, list polygon
):
cdef Vec2 pn
cdef int i

for i in range(len(polygon)):
pn = <Vec2> polygon[i]
if pn is p1 or pn is p2 or pn is p3:
continue
elif _is_point_inside(pn, p1, p2, p3):
return False
return True


cdef bint _is_point_inside(Vec2 p, Vec2 a, Vec2 b, Vec2 c):
cdef double area = _triangle_area(a, b, c)
cdef double area1 = _triangle_area(p, b, c)
cdef double area2 = _triangle_area(p, a, c)
cdef double area3 = _triangle_area(p, a, b)
return fabs(area - area1 - area2 - area3) < EPSILON


cdef double _triangle_area(Vec2 a, Vec2 b, Vec2 c):
return fabs(
(a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)) / 2.0
)
2 changes: 0 additions & 2 deletions archive/tripy.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Copyright (c) 2017 Sam Bolgert
# License: MIT License
# https://github.com/linuxlewis/tripy
# ezdxf integration: Copyright (c) 2022, Manfred Moitzi
# License: MIT License

from __future__ import annotations
from typing import Iterable, Iterator, List, Tuple
Expand Down
19 changes: 15 additions & 4 deletions profiling/triangulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
import time

from archive import tripy
try:
from archive import tricy
except ImportError:
print("module 'tricy' from archive not importable!")
print("build module:\n")
print(" cd archive")
print(" python setup.py build_ext -i")
exit(1)

from ezdxf.render import forms
from ezdxf.math.triangulation import mapbox_earcut_2d

Expand Down Expand Up @@ -32,12 +41,14 @@ def profile1(func, *args) -> float:


def profile(text, func, *args):
ear_clipping_2d_time = profile1(func, tripy.earclip, *args)
tripy_time = profile1(func, tripy.earclip, *args)
tricy_time = profile1(func, tricy.earclip, *args)
print(f"tripy.earclip (CPython) - {text} {tripy_time:.3f}s")
print(f"tricy.earclip (Cython) - {text} {tricy_time:.3f}s")
print(f"Ratio tripy/tricy {tripy_time / tricy_time:.1f}x")
mapbox_earcut_2d_time = profile1(func, mapbox_earcut_2d, *args)
ratio = ear_clipping_2d_time / mapbox_earcut_2d_time
print(f"tripy.earclip (CPython) - {text} {ear_clipping_2d_time:.3f}s")
print(f"mapbox_earcut_2d (Cython) - {text} {mapbox_earcut_2d_time:.3f}s")
print(f"Ratio {ratio:.1f}x")
print(f"Ratio tricy/mapbox {tricy_time/mapbox_earcut_2d_time:.1f}x\n")


RUNS = 100
Expand Down

0 comments on commit 6f99fc3

Please sign in to comment.