2D Booleans
In 2D boolean clipping, a polygon is represented as an ordered sequence of 2D coordinates forming closed boundary contours.
Polygon Structure:
1# 2D Polygon Representation2class Point2D:3 def __init__(self, x, y):4 self.x = x5 self.y = y67class Polygon2D:8 def __init__(self, vertices):9 # Ordered list of Point2D objects forming a closed boundary10 self.vertices = vertices1112 def edges(self):13 n = len(self.vertices)14 return [(self.vertices[i], self.vertices[(i + 1) % n]) for i in range(n)]
Knowing loop orientation is crucial for booleans. Clockwise (CW) vs Counter-Clockwise (CCW) determines outer boundaries and internal holes.
Winding Orientation:
1# Compute signed area to determine contour winding direction2def get_polygon_signed_area(vertices):3 area = 0.04 n = len(vertices)5 for i in range(n):6 p1 = vertices[i]7 p2 = vertices[(i + 1) % n]8 # Sum cross-products of coordinate coordinates9 area += (p1.x * p2.y) - (p2.x * p1.y)10 return 0.5 * area1112# CCW if area > 0, CW if area < 013def is_ccw(vertices):14 return get_polygon_signed_area(vertices) > 0.0
To merge boundaries, we locate all crossings where edges from Polygon A intersect edges of Polygon B.
Intersection Solving:
1# Check segment-segment intersection in 2D2def line_intersection(seg1, seg2):3 p0, p1 = seg14 q0, q1 = seg256 # Solve parametric equations: p0 + t*(p1-p0) == q0 + u*(q1-q0)7 denom = (p1.x - p0.x) * (q1.y - q0.y) - (p1.y - p0.y) * (q1.x - q0.x)8 if abs(denom) < 1e-8:9 return None # Parallel lines1011 t = ((q0.x - p0.x) * (q1.y - q0.y) - (q0.y - p0.y) * (q1.x - q0.x)) / denom12 u = ((q0.x - p0.x) * (p1.y - p0.y) - (q0.y - p0.y) * (p1.x - p0.x)) / denom1314 if 0.0 <= t <= 1.0 and 0.0 <= u <= 1.0:15 return Point2D(p0.x + t * (p1.x - p0.x), p0.y + t * (p1.y - p0.y))16 return None
To stitch polygons together, we insert intersection nodes directly into the coordinate sequence, splitting original edges into segments.
Edge Splitting:
1# Insert intersection nodes into polygon edge list2def split_edges(polygon_vertices, intersection_points):3 updated_vertices = []4 n = len(polygon_vertices)5 for i in range(n):6 p0 = polygon_vertices[i]7 p1 = polygon_vertices[(i + 1) % n]8 updated_vertices.append(p0)910 # Filter intersections lying on this segment, sort by distance from p011 seg_hits = find_intersections_on_segment(p0, p1, intersection_points)12 for hit in sorted(seg_hits, key=lambda h: distance(p0, h)):13 updated_vertices.append(hit)1415 return updated_vertices
Each split segment is evaluated to determine if it lies inside, outside, or on the boundary of the other polygon.
Edge Classification:
1# Classify segments using winding checks or ray casting2def classify_segment(segment, other_polygon):3 midpoint = Point2D(4 (segment[0].x + segment[1].x) / 2,5 (segment[0].y + segment[1].y) / 26 )78 if other_polygon.contains(midpoint):9 return "INSIDE"10 elif other_polygon.on_boundary(midpoint):11 return "BOUNDARY"12 else:13 return "OUTSIDE"
Depending on the chosen boolean operation (Union, Intersection, or Difference), we discard segments that do not meet the containment rules.
Trimming Logic:
1# Selectively keep segments based on the boolean operation type2def trim_segments(segments_A, segments_B, operation):3 trimmed = []45 if operation == "UNION":6 # Keep outside segments of both shapes7 trimmed.extend([s for s in segments_A if s.classification == "OUTSIDE"])8 trimmed.extend([s for s in segments_B if s.classification == "OUTSIDE"])9 elif operation == "INTERSECTION":10 # Keep inside segments of both shapes11 trimmed.extend([s for s in segments_A if s.classification == "INSIDE"])12 trimmed.extend([s for s in segments_B if s.classification == "INSIDE"])13 elif operation == "DIFFERENCE":14 # Keep outside of A, and inside of B (inverted direction)15 trimmed.extend([s for s in segments_A if s.classification == "OUTSIDE"])16 trimmed.extend([s.inverted() for s in segments_B if s.classification == "INSIDE"])1718 return trimmed
Finally, the trimmed segments are linked head-to-tail to reconstruct closed loops, yielding the final clipped polygon geometry boundaries.
Stitching Algorithm:
1# Stitch trimmed segments into closed loops2def stitch_loops(segments):3 loops = []4 while segments:5 current_segment = segments.pop(0)6 loop = [current_segment.start, current_segment.end]78 while True:9 # Find next segment that shares the end coordinate of current loop10 next_idx = find_next_segment_index(loop[-1], segments)11 if next_idx is None:12 break # Open loop or error1314 next_seg = segments.pop(next_idx)15 loop.append(next_seg.end)1617 # Check if loop is closed back to start point18 if distance(loop[-1], loop[0]) < 1e-8:19 loops.append(loop[:-1])20 break21 return loops
Complex polygons can have multiple disjoint parts (islands) and hollow regions (holes). We structure loops into a parent-child hierarchy to correctly define boundaries.
Hole Nesting:
1# Determine nested parent-child island and hole tree structure2def nest_loops(polygon_loops):3 # Sort loops by bounding area descending4 sorted_loops = sorted(polygon_loops, key=lambda l: polygon_area(l), reverse=True)56 tree = {root: []}7 for loop in sorted_loops:8 # Walk down tree to locate deepest parent loop enclosing this loop9 parent = find_deepest_enclosing_parent(loop, tree)10 parent.children.append(loop)1112 # Loops at odd depths in the tree represent internal hollow holes13 return tree