Initializing 3D Canvas...

Winding Rules

1 min read1 page

Determining if a coordinate lies inside a complex, self-intersecting, or nested polygon contour requires robust mathematical boundary definitions.

Winding Number:

1. Point containment is classified using signed boundary wrapping counts. 2. Different winding rules (Even-Odd vs Non-Zero) interpret inside vs outside differently. 3. The boundary curve direction (clockwise vs counter-clockwise) is critical.
python
1# Winding number definition
2# For a point P and curve C, the winding number represents
3# the number of times C wraps around P.
4class PolygonWinding:
5 def __init__(self, vertices):
6 self.vertices = vertices
3 min read1 page

Ray casting counts intersections between a query point's ray and the polygon edges.

Ray Crossing:

1. Cast semi-infinite horizontal ray from query point. 2. Calculate intersection coordinates against each boundary segment. 3. Determine if intersection lies strictly within the segment parameter range.
python
1# Determine if ray from P in direction D intersects edge AB
2def 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 > 0
5 v1 = p - a
6 v2 = b - a
7 v3 = [-d[1], d[0]] # Perpendicular direction
8
9 dot = v2[0] * v3[0] + v2[1] * v3[1]
10 if abs(dot) < 1e-9:
11 return None # Parallel
12
13 t = (v2[0] * v1[1] - v2[1] * v1[0]) / dot
14 u = (v1[0] * v3[0] + v1[1] * v3[1]) / dot
15
16 if t >= 0.0 and 0.0 <= u <= 1.0:
17 return p + t * d
18 return None
Ray Y Height
0.00
3 min read1 page

As rays intersect edges, we track edge direction (upwards vs downwards) to adjust winding weights.

Winding Counter:

1. Upward edge crossing (dy > 0) adds +1 to the winding score if point is left. 2. Downward edge crossing (dy < 0) subtracts -1 from the winding score if point is left. 3. Non-crossing segments have no effect.
python
1# Determine crossing direction and update winding number
2def accumulate_winding(p, a, b, winding_number):
3 # Check if edge crosses the ray Y level
4 if a[1] <= p[1]:
5 if b[1] > p[1]: # Upward crossing
6 # Check if point is to the left of the edge
7 if is_left(a, b, p) > 0:
8 winding_number += 1
9 else:
10 if b[1] <= p[1]: # Downward crossing
11 if is_left(a, b, p) < 0:
12 winding_number -= 1
13 return winding_number
14
15def is_left(a, b, c):
16 # > 0 for c left of line ab, < 0 for right, 0 on line
17 return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])
Point X Position
0.00
2 min read1 page

The Even-Odd rule toggles interior status with every boundary line crossed. Winding sign is ignored.

Even-Odd Rule:

1. Count total boundary crossings along the ray. 2. If sum is odd, region is inside. 3. If sum is even, region is outside.
python
1# Even-Odd Rule containment check
2def is_inside_even_odd(p, polygon):
3 winding = 0
4 # Loop over edges and count intersections
5 for i in range(len(polygon)):
6 a = polygon[i]
7 b = polygon[(i + 1) % len(polygon)]
8
9 # Simple crossing check
10 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 += 1
13
14 # Odd count is inside, Even is outside
15 return (winding % 2) != 0
Point X
0.00
2 min read1 page

The Non-Zero rule classifies regions as interior if the accumulated signed winding number does not equal zero.

Non-Zero Rule:

1. Calculate the signed winding number using ray crossings or angle sums. 2. If winding number is non-zero (e.g. +1, -1, +2), the point is inside. 3. If winding number is exactly 0, the point is outside.
python
1# Non-Zero Rule containment check
2def is_inside_non_zero(p, polygon):
3 winding = 0
4 for i in range(len(polygon)):
5 a = polygon[i]
6 b = polygon[(i + 1) % len(polygon)]
7
8 # Track edge direction to accumulate signed crossings
9 if a[1] <= p[1]:
10 if b[1] > p[1] and is_left(a, b, p) > 0:
11 winding += 1
12 else:
13 if b[1] <= p[1] and is_left(a, b, p) < 0:
14 winding -= 1
15
16 # Non-zero winding score is inside
17 return winding != 0
18
19def is_left(a, b, c):
20 return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])
Point X
0.00
2 min read1 page

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. Compute signed winding number at query point. 2. If winding > 0, point is interior. 3. Negative winding regions (CW loops) are treated as exterior.
python
1# Positive winding rule
2def is_inside_positive(p, polygon):
3 """Inside if winding number > 0 (positive direction only)."""
4 winding = compute_winding_number(p, polygon)
5 return winding > 0
6
7# Useful in font rendering and SVG path filling.
8# Only regions wrapped in the positive (CCW) direction
9# are considered interior. CW holes become exterior.
Point X
0.00
1 min read1 page

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. Compute signed winding number at query point. 2. If winding < 0, point is interior. 3. Positive winding regions (CCW loops) are treated as exterior.
python
1# Negative winding rule
2def is_inside_negative(p, polygon):
3 """Inside if winding number < 0 (clockwise direction only)."""
4 winding = compute_winding_number(p, polygon)
5 return winding < 0
6
7# Opposite of positive rule. Only regions with net
8# clockwise wrapping are considered filled.
9# Useful for specific CAD kernel implementations.
Point X
0.00
2 min read1 page

A self-intersecting star polygon demonstrates how the four winding rules produce different fill results for the same boundary curve.

Rule Comparison:

1. Cast rays from each grid cell through the star boundary. 2. Compute winding number and crossing count at each sample. 3. Color cells by which rules classify them as interior.
python
1# Compare winding rule results on self-intersecting polygon
2def compare_rules(p, polygon):
3 winding = compute_winding_number(p, polygon)
4 crossings = count_crossings(p, polygon)
5
6 results = {
7 'even_odd': (crossings % 2) != 0,
8 'non_zero': winding != 0,
9 'positive': winding > 0,
10 'negative': winding < 0,
11 }
12 return results
13
14# The star polygon highlights where rules differ:
15# - Even-Odd: center is outside (two crossings)
16# - Non-Zero: center is inside (winding = 2)
Rule (0=EO, 1=NZ, 2=Pos, 3=Neg)
0.00