Skip to content

Commit 69b68a5

Browse files
fix: duplicate points handled
1 parent 0ead700 commit 69b68a5

File tree

1 file changed

+42
-11
lines changed

1 file changed

+42
-11
lines changed

geometry/jarvis_march.py

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,42 +85,59 @@ def jarvis_march(points: list[Point]) -> list[Point]:
8585
if len(points) <= 2:
8686
return []
8787

88+
# Remove duplicate points to avoid infinite loops
89+
unique_points = list(set(points))
90+
91+
if len(unique_points) <= 2:
92+
return []
93+
8894
convex_hull: list[Point] = []
8995

9096
# Find the leftmost point (and bottom-most in case of tie)
9197
left_point_idx = 0
92-
for i in range(1, len(points)):
93-
if points[i].x < points[left_point_idx].x or (
94-
points[i].x == points[left_point_idx].x
95-
and points[i].y < points[left_point_idx].y
98+
for i in range(1, len(unique_points)):
99+
if unique_points[i].x < unique_points[left_point_idx].x or (
100+
unique_points[i].x == unique_points[left_point_idx].x
101+
and unique_points[i].y < unique_points[left_point_idx].y
96102
):
97103
left_point_idx = i
98104

99-
convex_hull.append(Point(points[left_point_idx].x, points[left_point_idx].y))
105+
convex_hull.append(Point(unique_points[left_point_idx].x, unique_points[left_point_idx].y))
100106

101107
current_idx = left_point_idx
102108
while True:
103109
# Find the next counter-clockwise point
104-
next_idx = (current_idx + 1) % len(points)
105-
for i in range(len(points)):
106-
if _cross_product(points[current_idx], points[i], points[next_idx]) > 0:
110+
next_idx = (current_idx + 1) % len(unique_points)
111+
# Make sure next_idx is not the same as current_idx (handle duplicates)
112+
while next_idx == current_idx:
113+
next_idx = (next_idx + 1) % len(unique_points)
114+
115+
for i in range(len(unique_points)):
116+
# Skip the current point itself (handles duplicates)
117+
if i == current_idx:
118+
continue
119+
if _cross_product(unique_points[current_idx], unique_points[i], unique_points[next_idx]) > 0:
107120
next_idx = i
108121

109122
if next_idx == left_point_idx:
110123
# Completed constructing the hull
111124
break
112125

126+
# Safety check: if next_idx == current_idx, we have duplicates causing issues
127+
if next_idx == current_idx:
128+
break
129+
113130
current_idx = next_idx
114131

115132
# Check if the last point is collinear with new point and second-to-last
116133
last = len(convex_hull) - 1
117134
if len(convex_hull) > 1 and _is_point_on_segment(
118-
convex_hull[last - 1], convex_hull[last], points[current_idx]
135+
convex_hull[last - 1], convex_hull[last], unique_points[current_idx]
119136
):
120137
# Remove the last point from the hull
121-
convex_hull[last] = Point(points[current_idx].x, points[current_idx].y)
138+
convex_hull[last] = Point(unique_points[current_idx].x, unique_points[current_idx].y)
122139
else:
123-
convex_hull.append(Point(points[current_idx].x, points[current_idx].y))
140+
convex_hull.append(Point(unique_points[current_idx].x, unique_points[current_idx].y))
124141

125142
# Check for edge case: last point collinear with first and second-to-last
126143
if len(convex_hull) <= 2:
@@ -132,6 +149,20 @@ def jarvis_march(points: list[Point]) -> list[Point]:
132149
if len(convex_hull) == 2:
133150
return []
134151

152+
# Final check: verify the hull forms a valid polygon (at least one non-zero cross product)
153+
# If all cross products are zero, all points are collinear
154+
has_turn = False
155+
for i in range(len(convex_hull)):
156+
p1 = convex_hull[i]
157+
p2 = convex_hull[(i + 1) % len(convex_hull)]
158+
p3 = convex_hull[(i + 2) % len(convex_hull)]
159+
if abs(_cross_product(p1, p2, p3)) > 1e-9:
160+
has_turn = True
161+
break
162+
163+
if not has_turn:
164+
return []
165+
135166
return convex_hull
136167

137168

0 commit comments

Comments
 (0)