Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions DIRECTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,7 @@
* [Dodecahedron](maths/dodecahedron.py)
* [Double Factorial](maths/double_factorial.py)
* [Dual Number Automatic Differentiation](maths/dual_number_automatic_differentiation.py)
* [Ear Clipping Polygon Triangulation](maths/ear_clipping_polygon_triangulation.py)
* [Entropy](maths/entropy.py)
* [Euclidean Distance](maths/euclidean_distance.py)
* [Euler Method](maths/euler_method.py)
Expand Down Expand Up @@ -876,6 +877,7 @@
* [Two Pointer](maths/two_pointer.py)
* [Two Sum](maths/two_sum.py)
* [Volume](maths/volume.py)
* [Weighted Average](maths/weighted_average.py)
* [Zellers Congruence](maths/zellers_congruence.py)

## [Matrix](matrix)
Expand Down
255 changes: 255 additions & 0 deletions maths/ear_clipping_polygon_triangulation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
"""
An implementation of the ear clipping method for triangulating a simple polygon.

Wikipedia : https://en.wikipedia.org/wiki/Polygon_triangulation
"""


def is_ear(polygon: list[tuple[float, float]], point_idx: int, direction: str) -> bool:
"""
This function determines whether three points form an ear.

>>> is_ear([(0, 2), (2, 2), (2,0), (1, 0), (1, 1), (0, 1)], 4,
... "counter-clockwise")
False
>>> is_ear([(0, 2), (2, 2), (2,0), (1, 0), (1, 1), (0, 1)], 3,
... "counter-clockwise")
True
>>> is_ear([(0, 3), (2, 2), (3, 0), (3, 3)], 3, "clockwise")
False
>>> is_ear([(0, 0), (1, 0), (1, 1), (0, 1)], 0, "clockwise")
True
"""
# Calculate indices for the previous and next vertices in the polygon.
prev_idx = (point_idx - 1) % len(polygon)
next_idx = (point_idx + 1) % len(polygon)

# Retrieve the coordinates of the previous, current, and next vertices.
prev_point = polygon[prev_idx]
point = polygon[point_idx]
next_point = polygon[next_idx]

# Check if the vertex is convex based on the polygon's orientation.
if is_convex(prev_point, point, next_point, direction):
# Check if there are any points inside the triangle formed by the current vertex
# and its neighbors.
for j in range(len(polygon)):
if j not in (prev_idx, point_idx, next_idx) and is_point_inside_triangle(
prev_point, point, next_point, polygon[j]
):
return False # The 'ear' is not valid because there's a point
# inside the triangle.
return True # The vertex is an 'ear' because it's convex and no points are
# inside the triangle.
return False # The vertex is not an 'ear' because it's not convex.


def is_convex(
point: tuple[float, float],
prev_p: tuple[float, float],
next_p: tuple[float, float],
direction: str,
) -> bool:
"""
Determine with the ccw, if 3 points are convex.
>>> is_convex((1,1), (2, 2), (1, 2), "clockwise")
True
>>> is_convex((1,1), (2, 2), (1, 2), "counter-clockwise")
False
>>> is_convex((1,1), (2, 2), (3, 3), "clockwise")
True
>>> is_convex((1,1), (2, 2), (3, 3), "counter-clockwise")
True
"""
# Calculate the cross product based on the polygon's orientation.
if direction == "counter-clockwise":
cross_product = (next_p[0] - point[0]) * (prev_p[1] - point[1]) - (
prev_p[0] - point[0]
) * (next_p[1] - point[1])
else:
cross_product = (prev_p[0] - point[0]) * (next_p[1] - point[1]) - (
next_p[0] - point[0]
) * (prev_p[1] - point[1])
# Determine if the angle is convex (cross product is non-negative).
return cross_product >= 0


def cross_product(
p1: tuple[float, float], p2: tuple[float, float], p3: tuple[float, float]
) -> float:
"""
This function computes the product of two vectors, with 3 points.
If the vectors are collinear, the output is 0.
If the three points rotate counterclockwise, the output is positive.
If three points rotate clockwise, the output is negative.

>>> cross_product((0, 0), (1, 0), (1.5, 0.5))
0.5
>>> cross_product((1.5, 0.5), (1, 1), (1.5, 1.5))
-0.5
>>> cross_product((0, 0), (1, 1), (2, 2))
0
"""
return (p2[0] - p1[0]) * (p3[1] - p1[1]) - (p2[1] - p1[1]) * (p3[0] - p1[0])


def direction(polygon: list[tuple[float, float]]) -> str:
"""
Determine the orientation (clockwise or counterclockwise) of a polygon defined
by a list of points.
>>> direction([(1, 1), (2, 2), (3, 4)])
'clockwise'
>>> direction([(1, 1), (2, 2), (3, -1)])
'counter-clockwise'
"""
# Find the point with the lowest y-coordinate (and leftmost if tied).
point_0 = min(polygon, key=lambda point: (point[1], point[0]))
idx_p0 = polygon.index(point_0)

# Calculate the indices of the previous and next points.
prev_idx = (idx_p0 - 1) % len(polygon)
next_idx = (idx_p0 + 1) % len(polygon)

prev_point = polygon[prev_idx]
next_point = polygon[next_idx]
# Handle cases where multiple points share the same y-coordinate.
while cross_product(point_0, next_point, prev_point) == 0:
next_idx += 1
next_point = polygon[next_idx]
# Determine the polygon's orientation based on the cross product.
if cross_product(point_0, next_point, prev_point) > 0:
return "clockwise" # The polygon is in a clockwise direction.
return "counter-clockwise" # The polygon is in a counter-clockwise direction.


def is_point_inside_triangle(
p1: tuple[float, float],
p2: tuple[float, float],
p3: tuple[float, float],
test_point: tuple[float, float],
) -> bool:
"""
Determine whether a given point is located inside a triangle
formed by three other points.

This function calculates the area of both the triangle formed by
the three input points (p1, p2, p3)
and the sub-triangles formed by replacing one vertex of the main
triangle with the test_point.
If the sum of the areas of the sub-triangles is equal to the area of
the main triangle, the test_point
is considered to be inside the triangle. Otherwise, it is considered outside.

>>> is_point_inside_triangle((0, 0), (0, 2), (2, 0), (3, 3))
False
>>> is_point_inside_triangle((0, 0), (0, 2), (2, 0), (1, 1))
True
>>> is_point_inside_triangle((0, 0), (2, 1), (2, 0), (1, 1))
False
>>> is_point_inside_triangle((0, 0), (2, 1), (2, 0), (2, 0))
True
>>> is_point_inside_triangle((0, 0), (1, 1), (2, 0), (1, 0))
True
"""
# Calculate the area of the main triangle.
area_triangle = abs(
0.5
* (p1[0] * (p2[1] - p3[1]) + p2[0] * (p3[1] - p1[1]) + p3[0] * (p1[1] - p2[1]))
)
# Calculate the areas of the sub-triangles formed by replacing one vertex
# with the test_point.
area1 = abs(
0.5
* (
test_point[0] * (p2[1] - p3[1])
+ p2[0] * (p3[1] - test_point[1])
+ p3[0] * (test_point[1] - p2[1])
)
)
area2 = abs(
0.5
* (
p1[0] * (test_point[1] - p3[1])
+ test_point[0] * (p3[1] - p1[1])
+ p3[0] * (p1[1] - test_point[1])
)
)
area3 = abs(
0.5
* (
p1[0] * (p2[1] - test_point[1])
+ p2[0] * (test_point[1] - p1[1])
+ test_point[0] * (p1[1] - p2[1])
)
)

# Check if the test_point is inside the triangle by comparing areas.
return area_triangle == area1 + area2 + area3


def triangulate_polygon(
coordinates: list[tuple[float, float]]
) -> list[list[tuple[float, float]]]:
"""
Triangulate a polygon and provide the points of the resulting triangles.
This function takes a list of coordinates that represent the vertices of a polygon.
It iteratively finds and removes 'ears' from the polygon to create a list
of triangles that triangulate the entire polygon.
The order of vertices in the coordinates list is assumed to be
consistent (either clockwise or counterclockwise).
The function uses helper functions 'direction' to determine the polygon's
orientation and 'is_ear' to identify 'ear' vertices.

Note: The function assumes that the input coordinates form a valid simple polygon.

>>> triangulate_polygon([(0, 2), (2, 2), (2, 0), (1, 0), (1, 1), (0, 1)])
[[(0, 1), (0, 2), (2, 2)], [(2, 2), (2, 0), (1, 0)], [(2, 2), (1, 0), (1, 1)], \
[(0, 1), (2, 2), (1, 1)]]
>>> triangulate_polygon([(0, 3), (2, 2), (3, 0), (3, 3)])
[[(3, 3), (0, 3), (2, 2)], [(3, 3), (2, 2), (3, 0)]]
>>> triangulate_polygon([(0, 0),(2, 0), (1, 1), (2, 2), (0,2)])
[[(0, 0), (2, 0), (1, 1)], [(0, 2), (0, 0), (1, 1)], [(0, 2), (1, 1), (2, 2)]]

"""
polygon: list[tuple[float, float]] = coordinates.copy()

# Initialize an empty list to store the triangles
triangles: list = []

# Determine the orientation (clockwise or counterclockwise) of the polygon.
orientation: str = direction(polygon)

# Iterate while there are at least three vertices in the polygon.
while len(polygon) >= 3:
ear_found: bool = False

# Find an 'ear' vertex and append the triangle to the list.
for i in range(len(polygon)):
if is_ear(polygon, i, orientation):
ear_found = True
prev_idx: int = (i - 1) % len(polygon)
next_idx: int = (i + 1) % len(polygon)
prev_point: tuple[float, float] = polygon[prev_idx]
point: tuple[float, float] = polygon[i]
next_point: tuple[float, float] = polygon[next_idx]
triangles.append(
[
prev_point,
point,
next_point,
]
)
polygon.pop(i)
break

# If no 'ear' is found, exit the loop.
if not ear_found:
break

return triangles


if __name__ == "__main__":
import doctest

doctest.testmod()
Loading