@@ -39,14 +39,25 @@ class Point(NamedTuple):
3939
4040
4141def is_point_on_segment (
42- point : Point , seg_start : Point , seg_end : Point , tolerance : float = 1e-9
42+ point : Point , seg_start : Point , seg_end : Point , tolerance : float = 0.0
4343) -> bool :
4444 """
4545 Determine whether a point lies on the line segment between seg_start and seg_end.
4646
4747 The check verifies that:
48- 1. The point is collinear with the segment endpoints (cross product is near zero).
49- 2. The point lies within the bounding box of the segment.
48+ 1. The point is collinear with the segment endpoints
49+ (cross product within tolerance).
50+ 2. The point lies within the bounding box of the segment (within tolerance).
51+
52+ Parameters:
53+ point: The query Point.
54+ seg_start: The start Point of the line segment.
55+ seg_end: The end Point of the line segment.
56+ tolerance: Non-negative tolerance for collinearity and bounding box
57+ checks (default 0.0 for exact mathematical boundary testing).
58+
59+ Raises:
60+ ValueError: If tolerance is negative.
5061
5162 >>> is_point_on_segment(Point(1.0, 1.0), Point(0.0, 0.0), Point(2.0, 2.0))
5263 True
@@ -60,7 +71,20 @@ def is_point_on_segment(
6071 False
6172 >>> is_point_on_segment(Point(2.0, 0.0), Point(0.0, 0.0), Point(4.0, 0.0))
6273 True
74+ >>> is_point_on_segment(Point(2.0, 1e-10), Point(0.0, 0.0), Point(4.0, 0.0))
75+ False
76+ >>> is_point_on_segment(
77+ ... Point(2.0, 1e-10), Point(0.0, 0.0), Point(4.0, 0.0), tolerance=1e-9
78+ ... )
79+ True
80+ >>> is_point_on_segment(Point(0.0, 0.0), Point(0.0, 0.0), Point(1.0, 1.0), -1.0)
81+ Traceback (most recent call last):
82+ ...
83+ ValueError: tolerance must be non-negative.
6384 """
85+ if tolerance < 0.0 :
86+ raise ValueError ("tolerance must be non-negative." )
87+
6488 # Cross product of vector (seg_start -> seg_end) and (seg_start -> point)
6589 cross_product = (seg_end .x - seg_start .x ) * (point .y - seg_start .y ) - (
6690 seg_end .y - seg_start .y
@@ -83,7 +107,10 @@ def is_point_on_segment(
83107
84108
85109def point_in_polygon (
86- point : Point , polygon : list [Point ], include_boundary : bool = True
110+ point : Point ,
111+ polygon : list [Point ],
112+ include_boundary : bool = True ,
113+ tolerance : float = 0.0 ,
87114) -> bool :
88115 """
89116 Determine whether a 2D point lies inside an arbitrary polygon using ray casting.
@@ -94,13 +121,15 @@ def point_in_polygon(
94121 in cyclic order (clockwise or counter-clockwise). Must have >= 3 vertices.
95122 include_boundary: Whether points on the boundary (edges or vertices)
96123 are considered inside (default True).
124+ tolerance: Non-negative tolerance for boundary testing (default 0.0
125+ for exact mathematical boundary testing).
97126
98127 Returns:
99128 True if the point is inside (or on the boundary if include_boundary=True),
100129 False otherwise.
101130
102131 Raises:
103- ValueError: If the polygon has fewer than 3 vertices.
132+ ValueError: If the polygon has fewer than 3 vertices or tolerance is negative .
104133
105134 Examples:
106135 >>> square = [Point(0.0, 0.0), Point(4.0, 0.0), Point(4.0, 4.0), Point(0.0, 4.0)]
@@ -113,7 +142,7 @@ def point_in_polygon(
113142 >>> point_in_polygon(Point(2.0, 5.0), square)
114143 False
115144
116- Boundary tests:
145+ Boundary tests (exact boundary testing with tolerance=0.0) :
117146 >>> point_in_polygon(Point(0.0, 2.0), square, include_boundary=True)
118147 True
119148 >>> point_in_polygon(Point(0.0, 2.0), square, include_boundary=False)
@@ -127,6 +156,14 @@ def point_in_polygon(
127156 >>> point_in_polygon(Point(2.0, 0.0), square, include_boundary=False)
128157 False
129158
159+ Points very close to boundary:
160+ >>> point_in_polygon(Point(2.0, -1e-10), square)
161+ False
162+ >>> point_in_polygon(Point(2.0, 1e-10), square, include_boundary=False)
163+ True
164+ >>> point_in_polygon(Point(2.0, -1e-10), square, tolerance=1e-9)
165+ True
166+
130167 Concave (arrowhead) polygon:
131168 >>> arrowhead = [Point(0.0, 0.0), Point(5.0, 2.0), Point(0.0, 4.0), Point(2.0, 2.0)]
132169 >>> point_in_polygon(Point(3.0, 2.0), arrowhead)
@@ -143,22 +180,28 @@ def point_in_polygon(
143180 >>> point_in_polygon(Point(-3.0, 0.0), triangle)
144181 False
145182
146- Invalid input (fewer than 3 vertices):
183+ Invalid input (fewer than 3 vertices or negative tolerance ):
147184 >>> point_in_polygon(Point(0.0, 0.0), [Point(0.0, 0.0), Point(1.0, 1.0)])
148185 Traceback (most recent call last):
149186 ...
150187 ValueError: A polygon must have at least 3 vertices.
188+ >>> point_in_polygon(Point(0.0, 0.0), square, tolerance=-1.0)
189+ Traceback (most recent call last):
190+ ...
191+ ValueError: tolerance must be non-negative.
151192 """
152193 if len (polygon ) < 3 :
153194 raise ValueError ("A polygon must have at least 3 vertices." )
195+ if tolerance < 0.0 :
196+ raise ValueError ("tolerance must be non-negative." )
154197
155198 num_vertices = len (polygon )
156199
157200 # Check if point lies on any boundary edge or vertex
158201 for vertex_index in range (num_vertices ):
159202 edge_start = polygon [vertex_index ]
160203 edge_end = polygon [(vertex_index + 1 ) % num_vertices ]
161- if is_point_on_segment (point , edge_start , edge_end ):
204+ if is_point_on_segment (point , edge_start , edge_end , tolerance = tolerance ):
162205 return include_boundary
163206
164207 # Ray casting: cast a horizontal ray from point towards positive x-infinity
0 commit comments