-
-
Notifications
You must be signed in to change notification settings - Fork 51.1k
Expand file tree
/
Copy pathear_clipping_polygon_triangulation.py
More file actions
255 lines (221 loc) · 8.98 KB
/
Copy pathear_clipping_polygon_triangulation.py
File metadata and controls
255 lines (221 loc) · 8.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
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()