Skip to content

Commit dfe2eef

Browse files
committed
extend docs and tests for is_point_in_polygon_2d()
1 parent 4689414 commit dfe2eef

File tree

2 files changed

+118
-19
lines changed

2 files changed

+118
-19
lines changed

src/ezdxf/math/construct2d.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,9 @@ def is_point_in_polygon_2d(
249249
"""Test if `point` is inside `polygon`. Returns ``-1`` (for outside) if the
250250
polygon is degenerated, no exception will be raised.
251251
252+
Supports convex and concave polygons with clockwise or counter-clockwise oriented
253+
polygon vertices.
254+
252255
Args:
253256
point: 2D point to test as :class:`Vec2`
254257
polygon: sequence of 2D points as :class:`Vec2`
@@ -260,6 +263,7 @@ def is_point_in_polygon_2d(
260263
"""
261264
# Source: http://www.faqs.org/faqs/graphics/algorithms-faq/
262265
# Subject 2.03: How do I find if a point lies within a polygon?
266+
# TODO: Cython implementation
263267
if not polygon: # empty polygon
264268
return -1
265269

@@ -281,17 +285,17 @@ def is_point_in_polygon_2d(
281285
if (c <= y <= d) and math.fabs(
282286
(y2 - y1) * x - (x2 - x1) * y + (x2 * y1 - y2 * x1)
283287
) <= abs_tol:
284-
return 0
288+
return 0 # on boundary line
285289
if ((y1 <= y < y2) or (y2 <= y < y1)) and (
286290
x < (x2 - x1) * (y - y1) / (y2 - y1) + x1
287291
):
288292
inside = not inside
289293
x1 = x2
290294
y1 = y2
291295
if inside:
292-
return 1
296+
return 1 # inside polygon
293297
else:
294-
return -1
298+
return -1 # outside polygon
295299

296300

297301
def circle_radius_3p(a: Vec3, b: Vec3, c: Vec3) -> float:
@@ -355,16 +359,16 @@ def has_matrix_2d_stretching(m: Matrix44) -> bool:
355359
def is_convex_polygon_2d(polygon: list[Vec2], *, strict=False, epsilon=1e-6) -> bool:
356360
"""Returns ``True`` if the 2D `polygon` is convex.
357361
358-
This function works with
359-
open and closed polygons and clockwise or counter-clockwise vertex
360-
orientation.
361-
Coincident vertices will always be skipped and if argument `strict`
362-
is ``True``, polygons with collinear vertices are not considered as
363-
convex.
362+
This function supports open and closed polygons with clockwise or counter-clockwise
363+
vertex orientation.
364+
365+
Coincident vertices will always be skipped and if argument `strict` is ``True``,
366+
polygons with collinear vertices are not considered as convex.
364367
365368
This solution works only for simple non-self-intersecting polygons!
366369
367370
"""
371+
# TODO: Cython implementation
368372
if len(polygon) < 3:
369373
return False
370374

@@ -378,15 +382,15 @@ def is_convex_polygon_2d(polygon: list[Vec2], *, strict=False, epsilon=1e-6) ->
378382

379383
det = (prev - vertex).det(prev_prev - prev)
380384
if abs(det) >= epsilon:
381-
current_sign = -1 if det < 0.0 else +1
385+
current_sign = -1 if det < 0.0 else +1
382386
if not global_sign:
383387
global_sign = current_sign
384388
# do all determinants have the same sign?
385389
if global_sign != current_sign:
386-
return False
390+
return False
387391
elif strict: # collinear vertices
388-
return False
389-
392+
return False
393+
390394
prev_prev = prev
391395
prev = vertex
392396
return bool(global_sign)

tests/test_06_math/test_613_point_in_poygon.py renamed to tests/test_06_math/test_613_is_point_in_polygon_2d.py

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
1-
# Copyright (c) 2020, Manfred Moitzi
1+
# Copyright (c) 2020-2024, Manfred Moitzi
22
# License: MIT License
33
import pytest
4-
from ezdxf.math import is_point_in_polygon_2d, Vec2
4+
from ezdxf.math import is_point_in_polygon_2d, Vec2, is_convex_polygon_2d
55

66

7-
def test_inside_horiz_box():
7+
def test_inside_horizontal_box():
88
square = Vec2.list([(0, 0), (1, 0), (1, 1), (0, 1)])
99
assert is_point_in_polygon_2d(Vec2(0.5, 0.5), square) == 1
1010

1111

12-
def test_outside_horiz_box():
12+
def test_outside_horizontal_box():
1313
square = Vec2.list([(0, 0), (1, 0), (1, 1), (0, 1)])
1414
assert is_point_in_polygon_2d(Vec2(-0.5, 0.5), square) == -1
1515
assert is_point_in_polygon_2d(Vec2(1.5, 0.5), square) == -1
1616
assert is_point_in_polygon_2d(Vec2(0.5, -0.5), square) == -1
1717
assert is_point_in_polygon_2d(Vec2(0.5, 1.5), square) == -1
1818

1919

20-
def test_colinear_outside_horiz_box():
20+
def test_colinear_outside_horizontal_box():
2121
square = Vec2.list([(0, 0), (1, 0), (1, 1), (0, 1)])
2222
assert is_point_in_polygon_2d(Vec2(1.5, 0), square) == -1
2323
assert is_point_in_polygon_2d(Vec2(-0.5, 0), square) == -1
2424
assert is_point_in_polygon_2d(Vec2(0, 1.5), square) == -1
2525
assert is_point_in_polygon_2d(Vec2(0, -0.5), square) == -1
2626

2727

28-
def test_corners_horiz_box():
28+
def test_corners_horizontal_box():
2929
square = Vec2.list([(0, 0), (1, 0), (1, 1), (0, 1)])
3030
assert is_point_in_polygon_2d(Vec2(0, 0), square) == 0
3131
assert is_point_in_polygon_2d(Vec2(0, 1), square) == 0
@@ -62,5 +62,100 @@ def test_borders_slanted_box_stable():
6262
assert is_point_in_polygon_2d(Vec2(-0.5, 0.5), square) == 0
6363

6464

65+
# Test concave polygon:
66+
#
67+
# 0123456
68+
# 8 .......
69+
# 7 .7---6.
70+
# 6 .|...|.
71+
# 5 .|.4-5.
72+
# 4 .|.|...
73+
# 3 .|.3-2.
74+
# 2 .|...|.
75+
# 1 .0---1.
76+
# 0 .......
77+
# 0123456
78+
79+
SHAPE_C_CCW = Vec2.list(
80+
# 0 1 2 3 4 5 6 7
81+
[(1, 1), (5, 1), (5, 3), (3, 3), (3, 5), (5, 5), (5, 7), (1, 7)]
82+
)
83+
SHAPE_C_CW = list(reversed(SHAPE_C_CCW))
84+
85+
# 0123456
86+
# 8 .......
87+
# 7 .+---+.
88+
# 6 .|678|.
89+
# 5 .|5+-+.
90+
# 4 .|4|...
91+
# 3 .|3+-+.
92+
# 2 .|012|.
93+
# 1 .+---+.
94+
# 0 .......
95+
# 0123456
96+
97+
POINTS_INSIDE = Vec2.list(
98+
# 0 1 2 3 4 5 6 7 8
99+
[(2, 2), (3, 2), (4, 2), (2, 3), (2, 4), (2, 5), (2, 6), (3, 6), (4, 6)]
100+
)
101+
102+
# 0123456
103+
# 8 b.c.d.e
104+
# 7 .+---+.
105+
# 6 9|...|a
106+
# 5 .|.+-+.
107+
# 4 6|.|7.8
108+
# 3 .|.+-+.
109+
# 2 4|...|5
110+
# 1 .+---+.
111+
# 0 0.1.2.3
112+
# 0123456
113+
114+
POINTS_OUTSIDE = Vec2.list(
115+
[
116+
(0, 0), # 0
117+
(2, 0), # 1
118+
(4, 0), # 2
119+
(6, 0), # 3
120+
(0, 2), # 4
121+
(6, 2), # 5
122+
(0, 4), # 6
123+
(4, 4), # 7
124+
(6, 2), # 8
125+
(0, 6), # 9
126+
(6, 6), # a
127+
(0, 8), # b
128+
(2, 8), # c
129+
(4, 8), # d
130+
(6, 8), # e
131+
]
132+
)
133+
134+
135+
def test_shape_c_is_not_convex():
136+
assert is_convex_polygon_2d(SHAPE_C_CCW) is False
137+
assert is_convex_polygon_2d(SHAPE_C_CW) is False
138+
139+
140+
@pytest.mark.parametrize("point", POINTS_INSIDE)
141+
def test_is_inside_ccw_polygon(point: Vec2):
142+
assert is_point_in_polygon_2d(point, SHAPE_C_CCW) >= 0
143+
144+
145+
@pytest.mark.parametrize("point", POINTS_OUTSIDE)
146+
def test_is_outside_ccw_polygon(point: Vec2):
147+
assert is_point_in_polygon_2d(point, SHAPE_C_CCW) == -1
148+
149+
150+
@pytest.mark.parametrize("point", POINTS_INSIDE)
151+
def test_is_inside_cw_polygon(point: Vec2):
152+
assert is_point_in_polygon_2d(point, SHAPE_C_CW) >= 0
153+
154+
155+
@pytest.mark.parametrize("point", POINTS_OUTSIDE)
156+
def test_is_outside_cw_polygon(point: Vec2):
157+
assert is_point_in_polygon_2d(point, SHAPE_C_CW) == -1
158+
159+
65160
if __name__ == "__main__":
66161
pytest.main([__file__])

0 commit comments

Comments
 (0)