Skip to content

Commit 72fc1f7

Browse files
authored
Fix checking if polygon is convex (#68)
* add tests, start fixing * fix check for beeing convex * improve centroid * improve docstring * fix orientation usage * improve tests * improve naming and type annotation * more improvements * improve assert information
1 parent 1878399 commit 72fc1f7

4 files changed

Lines changed: 152 additions & 26 deletions

File tree

src/PartSegCore_compiled_backend/triangulation/intersection.hpp

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -112,19 +112,27 @@ inline int64_t double_as_hex(double d) {
112112
return result;
113113
}
114114

115+
enum Orientation {
116+
COLLINEAR = 0,
117+
CLOCKWISE = 1,
118+
COUNTERCLOCKWISE = 2,
119+
};
120+
115121
/**
116122
* Determines the orientation of the triplet (p, q, r).
117123
*
118124
* @param p The first point.
119125
* @param q The second point.
120126
* @param r The third point.
121127
*
122-
* @return 0 if p, q and r are collinear.
123-
* 1 if the triplet (p, q, r) is in a clockwise orientation.
124-
* 2 if the triplet (p, q, r) is in a counterclockwise orientation.
128+
* @return Orientation::COLLINEAR if p, q and r are collinear.
129+
* Orientation::CLOCKWISE if the triplet (p, q, r) is in a clockwise
130+
* orientation.
131+
* Orientation::COUNTERCLOCKWISE if the triplet (p, q, r) is in a
132+
* counterclockwise orientation.
125133
*/
126-
inline int _orientation(const point::Point &p, const point::Point &q,
127-
const point::Point &r) {
134+
inline Orientation _orientation(const point::Point &p, const point::Point &q,
135+
const point::Point &r) {
128136
double val1 = ((q.y - p.y) * (r.x - q.x));
129137
double val2 = ((r.y - q.y) * (q.x - p.x));
130138
// This commented code if for debugging purposes of differences between macOS
@@ -142,8 +150,8 @@ inline int _orientation(const point::Point &p, const point::Point &q,
142150
// }
143151
// Instead of using classical equation, we need to use two variables
144152
// to handle problem with strange behavior on macOS.
145-
if (val1 == val2) return 0;
146-
return (val1 > val2) ? 1 : 2;
153+
if (val1 == val2) return Orientation::COLLINEAR;
154+
return (val1 > val2) ? Orientation::CLOCKWISE : Orientation::COUNTERCLOCKWISE;
147155
}
148156

149157
/**
@@ -163,17 +171,17 @@ inline bool _do_intersect(const point::Segment &s1, const point::Segment &s2) {
163171
const point::Point &p2 = s2.bottom;
164172
const point::Point &q2 = s2.top;
165173

166-
int o1 = _orientation(p1, q1, p2);
167-
int o2 = _orientation(p1, q1, q2);
168-
int o3 = _orientation(p2, q2, p1);
169-
int o4 = _orientation(p2, q2, q1);
174+
Orientation o1 = _orientation(p1, q1, p2);
175+
Orientation o2 = _orientation(p1, q1, q2);
176+
Orientation o3 = _orientation(p2, q2, p1);
177+
Orientation o4 = _orientation(p2, q2, q1);
170178

171179
if (o1 != o2 && o3 != o4) return true;
172180

173-
if (o1 == 0 && _on_segment(p1, p2, q1)) return true;
174-
if (o2 == 0 && _on_segment(p1, q2, q1)) return true;
175-
if (o3 == 0 && _on_segment(p2, p1, q2)) return true;
176-
if (o4 == 0 && _on_segment(p2, q1, q2)) return true;
181+
if (o1 == Orientation::COLLINEAR && _on_segment(p1, p2, q1)) return true;
182+
if (o2 == Orientation::COLLINEAR && _on_segment(p1, q2, q1)) return true;
183+
if (o3 == Orientation::COLLINEAR && _on_segment(p2, p1, q2)) return true;
184+
if (o4 == Orientation::COLLINEAR && _on_segment(p2, q1, q2)) return true;
177185

178186
return false;
179187
}

src/PartSegCore_compiled_backend/triangulation/point.hpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,19 @@ struct Segment {
208208
}
209209
};
210210
};
211+
Point centroid(const std::vector<Point> &point_list) {
212+
if (point_list.empty()) {
213+
return {0, 0};
214+
}
215+
Point res(0, 0);
216+
for (auto &point : point_list) {
217+
res.x += point.x;
218+
res.y += point.y;
219+
}
220+
res.x /= float(point_list.size());
221+
res.y /= float(point_list.size());
222+
return res;
223+
}
211224
} // namespace partsegcore::point
212225

213226
// overload of hash function for

src/PartSegCore_compiled_backend/triangulation/triangulate.hpp

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#define PARTSEGCORE_TRIANGULATE_H
33

44
#include <algorithm>
5+
#include <cmath>
56
#include <map>
67
#include <memory> // memory header is required on linux, and not on macos
78
#include <set>
@@ -602,6 +603,36 @@ struct MonotonePolygonBuilder {
602603
};
603604
};
604605

606+
/*
607+
* Check if the polygon, that all angles have the same orientation
608+
* do not have self-intersections
609+
*
610+
* @param begin Iterator to the first point of the polygon
611+
* @param end Iterator to the end of the polygon
612+
* @param centroid Centroid of the polygon
613+
*
614+
* @return True if the polygon is simple, false otherwise
615+
*/
616+
template <typename Iterator>
617+
bool is_simple_polygon(Iterator begin, Iterator end, point::Point centroid) {
618+
double start_angle = std::atan2(begin->y - centroid.y, begin->x - centroid.x);
619+
double prev_angle = 0;
620+
begin++;
621+
for (; begin != end; begin++) {
622+
double angle =
623+
std::atan2(begin->y - centroid.y, begin->x - centroid.x) - start_angle;
624+
if (angle < 0) {
625+
angle += 2 * M_PI;
626+
}
627+
if (angle < prev_angle) {
628+
return false;
629+
} else {
630+
prev_angle = angle;
631+
}
632+
}
633+
return true;
634+
}
635+
605636
/**
606637
* Checks if a given polygon is convex.
607638
*
@@ -615,26 +646,47 @@ struct MonotonePolygonBuilder {
615646
* @return True if the polygon is convex, false otherwise.
616647
*/
617648
inline bool _is_convex(const std::vector<point::Point> &polygon) {
618-
int orientation = 0;
619-
int triangle_orientation;
620-
for (std::size_t i = 0; i < polygon.size() - 2; i++) {
621-
triangle_orientation =
622-
intersection::_orientation(polygon[i], polygon[i + 1], polygon[i + 2]);
623-
if (triangle_orientation == 0) continue;
624-
if (orientation == 0)
649+
if (polygon.size() < 3) return false;
650+
if (polygon.size() == 3) return true;
651+
intersection::Orientation orientation = intersection::Orientation::COLLINEAR;
652+
intersection::Orientation triangle_orientation;
653+
size_t idx = 0;
654+
for (; idx < polygon.size() - 2; idx++) {
655+
triangle_orientation = intersection::_orientation(
656+
polygon[idx], polygon[(idx + 1)], polygon[(idx + 2)]);
657+
if (triangle_orientation != intersection::Orientation::COLLINEAR) {
625658
orientation = triangle_orientation;
626-
else if (orientation != triangle_orientation)
659+
break;
660+
}
661+
}
662+
if (orientation == intersection::Orientation::COLLINEAR) {
663+
return false;
664+
}
665+
for (; idx < polygon.size() - 2; idx++) {
666+
triangle_orientation = intersection::_orientation(
667+
polygon[idx], polygon[(idx + 1)], polygon[(idx + 2)]);
668+
if (triangle_orientation != 0 && triangle_orientation != orientation) {
627669
return false;
670+
}
628671
}
629672
triangle_orientation = intersection::_orientation(
630673
polygon[polygon.size() - 2], polygon[polygon.size() - 1], polygon[0]);
631-
if (triangle_orientation != 0 && triangle_orientation != orientation)
674+
if (triangle_orientation != 0 && triangle_orientation != orientation) {
632675
return false;
676+
}
633677
triangle_orientation = intersection::_orientation(polygon[polygon.size() - 1],
634678
polygon[0], polygon[1]);
635-
if (triangle_orientation != 0 && triangle_orientation != orientation)
679+
if (triangle_orientation != 0 && triangle_orientation != orientation) {
636680
return false;
637-
return true;
681+
}
682+
683+
point::Point centroid = point::centroid(polygon);
684+
685+
if (orientation == intersection::Orientation::COUNTERCLOCKWISE) {
686+
return is_simple_polygon(polygon.begin(), polygon.end(), centroid);
687+
} else {
688+
return is_simple_polygon(polygon.rbegin(), polygon.rend(), centroid);
689+
}
638690
}
639691

640692
/**

src/tests/test_triangulate.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ def test_triangle_convex_polygon():
208208
],
209209
)
210210
def test_triangulate_polygon_py_convex(polygon, expected):
211+
assert is_convex(polygon)
211212
assert triangulate_polygon_py(polygon)[0] == expected
212213

213214

@@ -691,3 +692,55 @@ def test_splitting_edges(split_edges, triangles):
691692
)
692693
triangles_ = triangulate_polygon_with_edge_numpy_li([polygon], split_edges=split_edges)[1][2]
693694
assert len(triangles_) == triangles
695+
696+
697+
def generate_regular_polygon(n: int, reverse: bool, radius: int = 1) -> np.ndarray:
698+
angles = np.linspace(0, 2 * np.pi, n, endpoint=False)
699+
if reverse:
700+
angles = angles[::-1]
701+
return np.column_stack((radius * np.cos(angles), radius * np.sin(angles)))
702+
703+
704+
def generate_self_intersecting_polygon(n: int, reverse: bool, radius: int = 1) -> np.ndarray:
705+
"""Generate self-intersecting polygon with n vertices
706+
707+
The polygon is generated by doubling the angle range of a regular polygon
708+
"""
709+
assert n % 2 == 1, 'an odd number is required to generate a self-intersecting polygon'
710+
angles = np.linspace(0, 4 * np.pi, n, endpoint=False)
711+
if reverse:
712+
angles = angles[::-1]
713+
return np.column_stack((radius * np.cos(angles), radius * np.sin(angles)))
714+
715+
716+
def rotation_matrix(angle: float) -> np.ndarray:
717+
"""Create a 2D rotation matrix for the given angle in degrees."""
718+
return np.array(
719+
[
720+
[np.cos(np.radians(angle)), -np.sin(np.radians(angle))],
721+
[np.sin(np.radians(angle)), np.cos(np.radians(angle))],
722+
]
723+
)
724+
725+
726+
ANGLES = [0, 5, 75, 95, 355]
727+
728+
729+
@pytest.mark.parametrize('angle', ANGLES, ids=str)
730+
@pytest.mark.parametrize('n_vertex', [5, 7, 19])
731+
@pytest.mark.parametrize('reverse', [False, True])
732+
def test_is_convex_self_intersection(angle, n_vertex, reverse):
733+
p = generate_self_intersecting_polygon(n_vertex, reverse)
734+
rot = rotation_matrix(angle)
735+
data = np.dot(p, rot)
736+
assert not is_convex(data)
737+
738+
739+
@pytest.mark.parametrize('angle', ANGLES, ids=str)
740+
@pytest.mark.parametrize('n_vertex', [3, 4, 7, 12, 15, 20])
741+
@pytest.mark.parametrize('reverse', [False, True])
742+
def test_is_convex_regular_polygon(angle, n_vertex, reverse):
743+
poly = generate_regular_polygon(n_vertex, reverse=reverse)
744+
rot = rotation_matrix(angle)
745+
rotated_poly = np.dot(poly, rot)
746+
assert is_convex(rotated_poly)

0 commit comments

Comments
 (0)