The Polygon
Polygon
If a line is the smallest geometry you can build by using points, and polyline is a collection of points (or segments), the polygon is the particular polyline that is always closed, therefore defining a contained area.
Data structure
A Polygon is stored in memory as an ordered array of Point3d objects (vertices). The most critical rule of a computational polygon is that it must be Closed. The last point in the array must connect back to the first point, forming a continuous loop.
1class Polygon:2 def __init__(self, vertices: list):3 if len(vertices) < 3:4 raise ValueError("A polygon requires at least 3 vertices.")5 self.Vertices = vertices67 # Ensure it is explicitly closed8 if not self.Vertices[0].EpsilonEquals(self.Vertices[-1], 0.001):9 self.Vertices.append(self.Vertices[0])
WindingOrder:
1def DetermineWindingOrder(pts: list['Point3d']) -> str:2 """Calculates winding order of a 2D polygon using the Shoelace formula."""3 area = 0.04 n = len(pts)5 for i in range(n):6 j = (i + 1) % n7 # Accumulate cross product8 area += (pts[i].X * pts[j].Y) - (pts[j].X * pts[i].Y)910 # CCW has positive signed area, CW has negative11 if area > 0.0:12 return "Counter-Clockwise (CCW)"13 elif area < 0.0:14 return "Clockwise (CW)"15 return "Degenerate / Collinear"
Convexity & Convex Hull:
1def IsConvex(pts: list['Point3d']) -> bool:2 """Checks if a simple 2D polygon is convex."""3 n = len(pts)4 if n < 3: return False56 sign = 07 for i in range(n):8 p1 = pts[i]9 p2 = pts[(i + 1) % n]10 p3 = pts[(i + 2) % n]1112 # Cross product of consecutive edges13 cross = (p2.X - p1.X) * (p3.Y - p2.Y) - (p2.Y - p1.Y) * (p3.X - p2.X)14 if abs(cross) > 1e-9:15 current_sign = 1 if cross > 0 else -116 if sign == 0:17 sign = current_sign18 elif sign != current_sign:19 return False20 return True
The most classic algorithm to calculate the Convex Hull is the Jarvis March (Gift Wrapping) method:
1def ConvexHull2D(pts: list['Point3d']) -> list['Point3d']:2 """Computes the convex hull of 2D points using Jarvis March."""3 n = len(pts)4 if n < 3: return pts56 leftmost = 07 for i in range(1, n):8 if pts[i].X < pts[leftmost].X:9 leftmost = i1011 hull = []12 p = leftmost13 while True:14 hull.append(pts[p])15 q = (p + 1) % n16 for i in range(n):17 # Check if point i is to the left of p-q line18 cross = (pts[i].X - pts[p].X) * (pts[q].Y - pts[p].Y) - (pts[i].Y - pts[p].Y) * (pts[q].X - pts[p].X)19 if cross < 0:20 q = i21 p = q22 if p == leftmost:23 break2425 hull.append(hull[0]) # Close path26 return hull
Area/Centroid:
1class Polygon:2 @property3 def Area(self) -> float:4 """Calculates area using the Shoelace Formula."""5 n = len(self.Vertices)6 area2 = 0.07 for i in range(n):8 j = (i + 1) % n9 area2 += self.Vertices[i].X * self.Vertices[j].Y - self.Vertices[j].X * self.Vertices[i].Y10 return abs(area2) / 2.01112 @property13 def Centroid(self) -> Point3d:14 """The geometric center (average of vertices)."""15 return Point3d.Average(self.Vertices)
Signed Area:
Calculates the area of a polygon where the mathematical sign (+/-) reveals the winding direction.
1# RhinoCommon handles this internally via Curve.ClosedCurveOrientation()2# But the underlying math is the Shoelace formula:34def SignedArea(pts: list[Point3d]) -> float:5 """Calculates area. Positive = CCW, Negative = CW."""6 n = len(pts)7 area2 = 0.08 for i in range(n):9 j = (i + 1) % n10 # Cross product of 2D coordinates11 area2 += pts[i].X * pts[j].Y - pts[j].X * pts[i].Y1213 return area2 / 2.0
This is a failry common method to determine if a polygon is a "hole" (CW) or an "outer boundary" (CCW).
Contains(Point3d):
1class Polygon:2 def Contains(self, test_point: Point3d) -> bool:3 """Point-in-Polygon (PIP) Raycasting Algorithm."""4 n = len(self.Vertices)5 inside = False6 px, py = test_point.X, test_point.Y78 j = n - 19 for i in range(n):10 xi, yi = self.Vertices[i].X, self.Vertices[i].Y11 xj, yj = self.Vertices[j].X, self.Vertices[j].Y1213 if (yi > py) != (yj > py):14 if px < (xj - xi) * (py - yi) / (yj - yi) + xi:15 inside = not inside16 j = i1718 return inside
Move(Vector3d):
1class Polygon:2 def Move(self, vector: Vector3d) -> 'Polygon':3 """Translates all vertices by the given vector."""4 return Polygon([v.Move(vector) for v in self.Vertices])
Scale(factor, center):
1class Polygon:2 def Scale(self, factor: float, center: Point3d) -> 'Polygon':3 """Scales all vertices relative to a center point."""4 return Polygon([v.Scale(factor, center) for v in self.Vertices])
Rotate(angle, axis, center):
1class Polygon:2 def Rotate(self, angle: float, axis: Vector3d, center: Point3d) -> 'Polygon':3 """Rotates all vertices around an axis and center."""4 return Polygon([v.Rotate(angle, axis, center) for v in self.Vertices])
Offset:
Offsetting is the process of creating a new polygon boundary that is a fixed distance away from the original. This is computationally intense because it requires calculatingMiters at every vertex — finding the bisector of the angle to ensure the offset distance remains constant.
1class Polygon:2 def offset(self, distance: float) -> 'Polygon':3 """Calculates an expanded or contracted boundary."""4 offset_vertices = []5 n = len(self.vertices)6 for i in range(n):7 prev = self.vertices[i - 1]8 curr = self.vertices[i]9 next = self.vertices[(i + 1) % n]1011 # Edge normals12 e1 = (curr - prev).normalized()13 e2 = (next - curr).normalized()14 n1 = Vector2(-e1.y, e1.x)15 n2 = Vector2(-e2.y, e2.x)1617 # Miter vector18 miter = (n1 + n2).normalized()19 miter_len = 1.0 / max(0.2, miter.dot(n1))2021 offset_vertices.append(curr + miter * (distance * miter_len))2223 return Polygon(offset_vertices)
Union:
Boolean Union merges two or more overlapping closed polygons into a single combined boundary. It is commonly used in space planning to merge adjacent rooms, zones, or structural boundaries.
1import math2"""Naive approach"""3class Point2D:4 def __init__(self, x: float, y: float):5 self.x = x6 self.y = y78def is_point_inside(pt: Point2D, poly: list[Point2D]) -> bool:9 """Raycasting Point-in-Polygon (PIP) check."""10 inside = False11 n = len(poly)12 for i in range(n):13 p1, p2 = poly[i], poly[(i + 1) % n]14 if p1.y > pt.y != p2.y > pt.y:15 if pt.x < (p2.x - p1.x) * (pt.y - p1.y) / (p2.y - p1.y) + p1.x:16 inside = not inside17 return inside1819def get_intersection(p1: Point2D, p2: Point2D, q1: Point2D, q2: Point2D) -> Point2D | None:20 """Calculates intersection point of segment p1p2 and q1q2."""21 denom = (q2.y - q1.y) * (p2.x - p1.x) - (q2.x - q1.x) * (p2.y - p1.y)22 if abs(denom) < 1e-9:23 return None # Parallel24 ua = ((q2.x - q1.x) * (p1.y - q1.y) - (q2.y - q1.y) * (p1.x - q1.x)) / denom25 ub = ((p2.x - p1.x) * (p1.y - q1.y) - (p2.y - p1.y) * (p1.x - q1.x)) / denom26 if 0.0 <= ua <= 1.0 and 0.0 <= ub <= 1.0:27 return Point2D(p1.x + ua * (p2.x - p1.x), p1.y + ua * (p2.y - p1.y))28 return None2930def polygon_union(poly_a: list[Point2D], poly_b: list[Point2D]) -> list[Point2D]:31 """Calculates the union boundary of two overlapping polygons."""32 intersections = []33 na, nb = len(poly_a), len(poly_b)3435 # 1. Find all edge-edge intersections36 for i in range(na):37 p1, p2 = poly_a[i], poly_a[(i + 1) % na]38 for j in range(nb):39 q1, q2 = poly_b[j], poly_b[(j + 1) % nb]40 intersect = get_intersection(p1, p2, q1, q2)41 if intersect:42 intersections.append(intersect)4344 # 2. Collect outer boundary points45 outer_points = []46 for p in poly_a:47 if not is_point_inside(p, poly_b):48 outer_points.append(p)49 for p in poly_b:50 if not is_point_inside(p, poly_a):51 outer_points.append(p)5253 # 3. Combine and reconstruct the boundary54 boundary = outer_points + intersections55 if not boundary:56 return []5758 cx = sum(p.x for p in boundary) / len(boundary)59 cy = sum(p.y for p in boundary) / len(boundary)60 boundary.sort(key=lambda p: math.atan2(p.y - cy, p.x - cx))6162 return boundary
Difference:
Boolean Difference subtracts the region of one polygon (B) from another (A), written as A - B. It is essential for cutting holes, carving recesses, or creating setbacks in architectural layouts.
1def polygon_difference(poly_a: list[Point2D], poly_b: list[Point2D]) -> list[Point2D]:2 """Naive approach - Calculates the difference (A - B) of two overlapping rectangles analytically."""3 xMinA, xMaxA = min(p.x for p in poly_a), max(p.x for p in poly_a)4 yMinA, yMaxA = min(p.y for p in poly_a), max(p.y for p in poly_a)56 xMinB, xMaxB = min(p.x for p in poly_b), max(p.x for p in poly_b)7 yMinB, yMaxB = min(p.y for p in poly_b), max(p.y for p in poly_b)89 # Overlap region coordinates10 x1, x2 = max(xMinA, xMinB), min(xMaxA, xMaxB)11 y1, y2 = max(yMinA, yMinB), min(yMaxA, yMaxB)1213 if x1 >= x2 or y1 >= y2:14 # No overlap, returns original polygon A15 return poly_a1617 touches_left = abs(x1 - xMinA) < 1e-418 touches_right = abs(x2 - xMaxA) < 1e-419 touches_bottom = abs(y1 - yMinA) < 1e-420 touches_top = abs(y2 - yMaxA) < 1e-42122 if touches_left and touches_right and touches_bottom and touches_top:23 return [] # B fully covers A2425 if touches_left and touches_right and touches_bottom:26 return [Point2D(xMinA, y2), Point2D(xMaxA, y2), Point2D(xMaxA, yMaxA), Point2D(xMinA, yMaxA)]27 if touches_left and touches_right and touches_top:28 return [Point2D(xMinA, yMinA), Point2D(xMaxA, yMinA), Point2D(xMaxA, y1), Point2D(xMinA, y1)]29 if touches_left and touches_bottom and touches_top:30 return [Point2D(x2, yMinA), Point2D(xMaxA, yMinA), Point2D(xMaxA, yMaxA), Point2D(x2, yMaxA)]31 if touches_right and touches_bottom and touches_top:32 return [Point2D(xMinA, yMinA), Point2D(x1, yMinA), Point2D(x1, yMaxA), Point2D(xMinA, yMaxA)]3334 if touches_left and touches_bottom:35 return [Point2D(xMinA, y2), Point2D(x2, y2), Point2D(x2, yMinA), Point2D(xMaxA, yMinA), Point2D(xMaxA, yMaxA), Point2D(xMinA, yMaxA)]36 if touches_right and touches_bottom:37 return [Point2D(xMinA, yMinA), Point2D(x1, yMinA), Point2D(x1, y2), Point2D(xMaxA, y2), Point2D(xMaxA, yMaxA), Point2D(xMinA, yMaxA)]38 if touches_left and touches_top:39 return [Point2D(xMinA, yMinA), Point2D(xMaxA, yMinA), Point2D(xMaxA, yMaxA), Point2D(x2, yMaxA), Point2D(x2, y1), Point2D(xMinA, y1)]40 if touches_right and touches_top:41 return [Point2D(xMinA, yMinA), Point2D(xMaxA, yMinA), Point2D(xMaxA, y1), Point2D(x1, y1), Point2D(x1, yMaxA), Point2D(xMinA, yMaxA)]4243 if touches_left:44 return [Point2D(xMinA, yMinA), Point2D(xMaxA, yMinA), Point2D(xMaxA, yMaxA), Point2D(xMinA, yMaxA), Point2D(xMinA, y2), Point2D(x2, y2), Point2D(x2, y1), Point2D(xMinA, y1)]45 if touches_right:46 return [Point2D(xMinA, yMinA), Point2D(xMaxA, yMinA), Point2D(xMaxA, y1), Point2D(x1, y1), Point2D(x1, y2), Point2D(xMaxA, y2), Point2D(xMaxA, yMaxA), Point2D(xMinA, yMaxA)]47 if touches_bottom:48 return [Point2D(xMinA, yMinA), Point2D(x1, yMinA), Point2D(x1, y2), Point2D(x2, y2), Point2D(x2, yMinA), Point2D(xMaxA, yMinA), Point2D(xMaxA, yMaxA), Point2D(xMinA, yMaxA)]49 if touches_top:50 return [Point2D(xMinA, yMinA), Point2D(xMaxA, yMinA), Point2D(xMaxA, yMaxA), Point2D(x2, yMaxA), Point2D(x2, y1), Point2D(x1, y1), Point2D(x1, yMaxA), Point2D(xMinA, yMaxA)]5152 return poly_a
Intersection:
Boolean Intersection finds the common overlapping region between two or more closed polygons. It is used to find common zone clearances, setbacks, or conflict areas between multiple planning boundaries.
1import math2"""Naive approach"""3class Point2D:4 def __init__(self, x: float, y: float):5 self.x = x6 self.y = y78def is_point_inside(pt: Point2D, poly: list[Point2D]) -> bool:9 """Raycasting Point-in-Polygon (PIP) check."""10 inside = False11 n = len(poly)12 for i in range(n):13 p1, p2 = poly[i], poly[(i + 1) % n]14 if p1.y > pt.y != p2.y > pt.y:15 if pt.x < (p2.x - p1.x) * (pt.y - p1.y) / (p2.y - p1.y) + p1.x:16 inside = not inside17 return inside1819def get_intersection(p1: Point2D, p2: Point2D, q1: Point2D, q2: Point2D) -> Point2D | None:20 """Calculates intersection point of segment p1p2 and q1q2."""21 denom = (q2.y - q1.y) * (p2.x - p1.x) - (q2.x - q1.x) * (p2.y - p1.y)22 if abs(denom) < 1e-9:23 return None # Parallel24 ua = ((q2.x - q1.x) * (p1.y - q1.y) - (q2.y - q1.y) * (p1.x - q1.x)) / denom25 ub = ((p2.x - p1.x) * (p1.y - q1.y) - (p2.y - p1.y) * (p1.x - q1.x)) / denom26 if 0.0 <= ua <= 1.0 and 0.0 <= ub <= 1.0:27 return Point2D(p1.x + ua * (p2.x - p1.x), p1.y + ua * (p2.y - p1.y))28 return None2930def polygon_intersection(poly_a: list[Point2D], poly_b: list[Point2D]) -> list[Point2D]:31 """Calculates the intersection boundary of two overlapping polygons."""32 intersections = []33 na, nb = len(poly_a), len(poly_b)3435 # 1. Find all edge-edge intersections36 for i in range(na):37 p1, p2 = poly_a[i], poly_a[(i + 1) % na]38 for j in range(nb):39 q1, q2 = poly_b[j], poly_b[(j + 1) % nb]40 intersect = get_intersection(p1, p2, q1, q2)41 if intersect:42 intersections.append(intersect)4344 # 2. Collect boundary points for A ∩ B: points of A inside B, and B inside A45 boundary_pts = []46 for p in poly_a:47 if is_point_inside(p, poly_b):48 boundary_pts.append(p)49 for p in poly_b:50 if is_point_inside(p, poly_a):51 boundary_pts.append(p)5253 # 3. Combine with intersections and sort radially around centroid54 boundary = boundary_pts + intersections55 if not boundary:56 return []5758 cx = sum(p.x for p in boundary) / len(boundary)59 cy = sum(p.y for p in boundary) / len(boundary)60 boundary.sort(key=lambda p: math.atan2(p.y - cy, p.x - cx))6162 return boundary
Triangulation:
Triangulation is the process of breaking a complex polygon down into simple triangles. Because rendering engines (like GPUs) only know how to draw triangles, every polygon you see on screen is secretly triangulated under the hood.
1class Point2D:2 def __init__(self, x: float, y: float):3 self.x = x4 self.y = y56def is_ccw(a: Point2D, b: Point2D, c: Point2D) -> bool:7 """Checks if three points form a counter-clockwise turn."""8 return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x) > 0910def point_in_triangle(p: Point2D, a: Point2D, b: Point2D, c: Point2D) -> bool:11 """Checks if point p lies inside the triangle abc using barycentric coordinates."""12 denom = (b.y - c.y) * (a.x - c.x) + (c.x - b.x) * (a.y - c.y)13 if abs(denom) < 1e-9:14 return False15 u = ((b.y - c.y) * (p.x - c.x) + (c.x - b.x) * (p.y - c.y)) / denom16 v = ((c.y - a.y) * (p.x - c.x) + (a.x - c.x) * (p.y - c.y)) / denom17 w = 1 - u - v18 return u >= 0 and v >= 0 and w >= 01920def is_ear(poly: list[Point2D], i: int) -> bool:21 """Checks if vertex i is a valid 'ear' of the polygon to clip."""22 n = len(poly)23 prev_idx = (i - 1) % n24 next_idx = (i + 1) % n2526 a, b, c = poly[prev_idx], poly[i], poly[next_idx]2728 # 1. Ear must be convex (pointing outwards)29 if not is_ccw(a, b, c):30 return False3132 # 2. No other vertices of the polygon should lie inside the triangle abc33 for j in range(n):34 if j in (i, prev_idx, next_idx):35 continue36 if point_in_triangle(poly[j], a, b, c):37 return False3839 return True4041def triangulate_polygon(vertices: list[Point2D]) -> list[tuple[int, int, int]]:42 """43 Triangulates a simple polygon using the Ear Clipping algorithm.44 Returns a list of triangles, each defined by three vertex indices.45 """46 n = len(vertices)47 indices = list(range(n))48 triangles = []4950 # Ensure winding order is CCW51 area = 0.052 for i in range(n):53 p1, p2 = vertices[i], vertices[(i + 1) % n]54 area += p1.x * p2.y - p2.x * p1.y55 if area < 0:56 vertices = vertices[::-1]5758 # Clip ears until only 1 triangle remains59 while len(indices) > 3:60 ear_found = False61 for i in range(len(indices)):62 curr_poly = [vertices[idx] for idx in indices]63 if is_ear(curr_poly, i):64 prev_idx = indices[(i - 1) % len(indices)]65 curr_idx = indices[i]66 next_idx = indices[(i + 1) % len(indices)]6768 triangles.append((prev_idx, curr_idx, next_idx))69 indices.pop(i) # Clip the ear70 ear_found = True71 break7273 if not ear_found:74 break # Fallback for degenerate cases7576 if len(indices) == 3:77 triangles.append((indices[0], indices[1], indices[2]))7879 return triangles
Closest Point:
Finding the Closest Point on a polygon boundary is a search operation. The algorithm decomposes the polygon into a series of line segments, projects the test point onto every segment, and returns the result with the shortest absolute distance.
1class Polygon:2 def closest_point(self, test_point: Point3d) -> Point3d:3 """Finds the nearest point on the boundary."""4 min_dist = float('inf')5 best_point = self.vertices[0]6 n = len(self.vertices)78 for i in range(n):9 a = self.vertices[i]10 b = self.vertices[(i + 1) % n]1112 # Project test_point onto segment ab13 ab = b - a14 ab_len_sq = ab.length_squared()15 if ab_len_sq == 0:16 proj = a17 else:18 t = max(0.0, min(1.0, (test_point - a).dot(ab) / ab_len_sq))19 proj = a + ab * t2021 dist = (test_point - proj).length()22 if dist < min_dist:23 min_dist = dist24 best_point = proj2526 return best_point
Is Simple:
A polygon is considered simple if its boundary does not cross itself. Self-intersecting (complex) polygons break many common algorithms like triangulation and offset, making simplicity verification a critical validation step.
1def is_simple_polygon(polygon: Polyline) -> bool:2 """3 Checks if a polygon is simple (no self-intersections).4 Uses a naive O(N^2) check or Bentley-Ottmann sweep-line algorithm.5 """6 pts = polygon.points7 n = len(pts)8 if n < 3:9 return False1011 def line_intersection(p1, p2, p3, p4):12 # Standard line segment intersection test13 d = (p2.X - p1.X) * (p4.Y - p3.Y) - (p2.Y - p1.Y) * (p4.X - p3.X)14 if d == 0:15 return None # Parallel16 u = ((p3.X - p1.X) * (p4.Y - p3.Y) - (p3.Y - p1.Y) * (p4.X - p3.X)) / d17 v = ((p3.X - p1.X) * (p2.Y - p1.Y) - (p3.Y - p1.Y) * (p2.X - p1.X)) / d18 if 0 < u < 1 and 0 < v < 1:19 return Point3d(p1.X + u * (p2.X - p1.X), p1.Y + u * (p2.Y - p1.Y), 0)20 return None2122 # Compare every pair of non-adjacent segments23 for i in range(n):24 for j in range(i + 2, n):25 if i == 0 and j == n - 1:26 continue # Adjacent segments share a vertex27 pt = line_intersection(pts[i], pts[(i+1)%n], pts[j], pts[(j+1)%n])28 if pt is not None:29 return False # Self-intersection found!30 return True
Contains Polygon:
A polygon A contains another polygon B if all points of B lie within the region of A. This requires checking that all of B's vertices are inside A and that their boundaries do not intersect.
1def contains_polygon(poly_a: Polyline, poly_b: Polyline) -> bool:2 """3 Checks if polygon A completely contains polygon B.4 All vertices of B must lie inside or on the boundary of A,5 and no edges of B may intersect the edges of A.6 """7 # 1. Check if all vertices of B are inside A8 for pt in poly_b.points:9 if not poly_a.contains_point(pt, include_boundary=True):10 return False1112 # 2. Verify no edge intersections between A and B13 for i in range(len(poly_a.points)):14 edge_a = (poly_a.points[i], poly_a.points[(i+1)%len(poly_a.points)])15 for j in range(len(poly_b.points)):16 edge_b = (poly_b.points[j], poly_b.points[(j+1)%len(poly_b.points)])17 if segments_intersect(edge_a, edge_b):18 return False1920 return True