Winding Rules
Determining if a coordinate lies inside a complex, self-intersecting, or nested polygon contour requires robust mathematical boundary definitions.
Winding Number:
1# Winding number definition2# For a point P and curve C, the winding number represents3# the number of times C wraps around P.4class PolygonWinding:5 def __init__(self, vertices):6 self.vertices = vertices
Ray casting counts intersections between a query point's ray and the polygon edges.
Ray Crossing:
1# Determine if ray from P in direction D intersects edge AB2def ray_intersect_edge(p, d, a, b):3 # Solve system: P + t*D = A + u*(B-A)4 # Return u in [0, 1] and t > 05 v1 = p - a6 v2 = b - a7 v3 = [-d[1], d[0]] # Perpendicular direction89 dot = v2[0] * v3[0] + v2[1] * v3[1]10 if abs(dot) < 1e-9:11 return None # Parallel1213 t = (v2[0] * v1[1] - v2[1] * v1[0]) / dot14 u = (v1[0] * v3[0] + v1[1] * v3[1]) / dot1516 if t >= 0.0 and 0.0 <= u <= 1.0:17 return p + t * d18 return None
As rays intersect edges, we track edge direction (upwards vs downwards) to adjust winding weights.
Winding Counter:
1# Determine crossing direction and update winding number2def accumulate_winding(p, a, b, winding_number):3 # Check if edge crosses the ray Y level4 if a[1] <= p[1]:5 if b[1] > p[1]: # Upward crossing6 # Check if point is to the left of the edge7 if is_left(a, b, p) > 0:8 winding_number += 19 else:10 if b[1] <= p[1]: # Downward crossing11 if is_left(a, b, p) < 0:12 winding_number -= 113 return winding_number1415def is_left(a, b, c):16 # > 0 for c left of line ab, < 0 for right, 0 on line17 return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])
The Even-Odd rule toggles interior status with every boundary line crossed. Winding sign is ignored.
Even-Odd Rule:
1# Even-Odd Rule containment check2def is_inside_even_odd(p, polygon):3 winding = 04 # Loop over edges and count intersections5 for i in range(len(polygon)):6 a = polygon[i]7 b = polygon[(i + 1) % len(polygon)]89 # Simple crossing check10 if ((a[1] > p[1]) != (b[1] > p[1])) and \11 (p[0] < (b[0] - a[0]) * (p[1] - a[1]) / (b[1] - a[1] + 1e-9) + a[0]):12 winding += 11314 # Odd count is inside, Even is outside15 return (winding % 2) != 0
The Non-Zero rule classifies regions as interior if the accumulated signed winding number does not equal zero.
Non-Zero Rule:
1# Non-Zero Rule containment check2def is_inside_non_zero(p, polygon):3 winding = 04 for i in range(len(polygon)):5 a = polygon[i]6 b = polygon[(i + 1) % len(polygon)]78 # Track edge direction to accumulate signed crossings9 if a[1] <= p[1]:10 if b[1] > p[1] and is_left(a, b, p) > 0:11 winding += 112 else:13 if b[1] <= p[1] and is_left(a, b, p) < 0:14 winding -= 11516 # Non-zero winding score is inside17 return winding != 01819def is_left(a, b, c):20 return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])
The Positive rule classifies a point as interior only if its winding number is strictly greater than zero. Clockwise-wound regions are excluded.
Positive Rule:
1# Positive winding rule2def is_inside_positive(p, polygon):3 """Inside if winding number > 0 (positive direction only)."""4 winding = compute_winding_number(p, polygon)5 return winding > 067# Useful in font rendering and SVG path filling.8# Only regions wrapped in the positive (CCW) direction9# are considered interior. CW holes become exterior.
The Negative rule classifies a point as interior only if its winding number is strictly less than zero. Only CW-wound regions are filled.
Negative Rule:
1# Negative winding rule2def is_inside_negative(p, polygon):3 """Inside if winding number < 0 (clockwise direction only)."""4 winding = compute_winding_number(p, polygon)5 return winding < 067# Opposite of positive rule. Only regions with net8# clockwise wrapping are considered filled.9# Useful for specific CAD kernel implementations.
A self-intersecting star polygon demonstrates how the four winding rules produce different fill results for the same boundary curve.
Rule Comparison:
1# Compare winding rule results on self-intersecting polygon2def compare_rules(p, polygon):3 winding = compute_winding_number(p, polygon)4 crossings = count_crossings(p, polygon)56 results = {7 'even_odd': (crossings % 2) != 0,8 'non_zero': winding != 0,9 'positive': winding > 0,10 'negative': winding < 0,11 }12 return results1314# The star polygon highlights where rules differ:15# - Even-Odd: center is outside (two crossings)16# - Non-Zero: center is inside (winding = 2)