Float Collinearity Causes Self-Intersecting Polygons
- Orientation test: cross product sign of (q-p) x (r-p). Positive = CCW, Negative = CW, Zero = collinear. No division — exact for integer coordinates.
- Segment intersection: segments AB, CD intersect iff {A,B} straddle CD AND {C,D} straddle AB. Handle collinear endpoint cases separately.
- No division in orientation test makes it exact for integer coordinates — crucial for robust geometry code.
- Orientation test: cross product of vectors (q-p) x (r-p). Positive = CCW, Negative = CW, Zero = collinear. No division — exact for integer coordinates.
- Segment intersection: segments AB and CD intersect iff {A,B} straddle CD AND {C,D} straddle AB. Collinear cases require an on-segment bounding-box check.
- Intersection point: solve parametric line equations. Denominator near zero means parallel — check with epsilon.
- Performance: O(1) per test. No allocations, no division in orientation. Batch tests dominate sweep line algorithms.
- Production insight: floating-point precision is the #1 source of geometry bugs. Use integer coordinates or epsilon comparisons — never raw float equality.
- Biggest mistake: forgetting collinear endpoint cases. Naive implementations return wrong results when an endpoint lies exactly on the other segment.
Geometry Intersection Triage Cheat Sheet
Parallel segments reported as intersecting.
Print orientation values for all four point triples to verify they are near-zeroCheck if on_segment bounding-box test uses raw float or epsilon comparisonShared endpoint intersection missed.
Add debug print: print(f'o1={o1} o2={o2} o3={o3} o4={o4}') before returnTest with segments sharing exactly one endpoint: ((0,0),(1,1)) and ((1,1),(2,0))Intersection point is wildly wrong for nearly parallel lines.
Print denom value: should be > EPS for valid intersectionIf denom < EPS, treat as parallel — return None instead of dividingO(n^2) intersection checks are too slow for large polygon sets.
Profile: count total segment pairs being testedImplement Bentley-Ottmann sweep line to reduce to O((n+k) log n)Production Incident
Production Debug GuideWhen intersection tests return wrong results, miss edge cases, or produce degenerate polygons.
Line intersection is the foundational primitive of computational geometry. Collision detection, polygon clipping, sweep line algorithms, and convex hull construction all reduce to one question: do two segments intersect? The cross product orientation test answers this in O(1) with no division, making it numerically robust and fast.
Production geometry code fails not on the common case but on edge cases: collinear overlapping segments, endpoints lying exactly on segments, and floating-point precision at near-parallel intersections. A single missed collinear check can corrupt a convex hull, create self-intersecting polygons, or cause collision detection to miss contacts.
A common misconception is that line equations are equivalent to the cross product approach. They are not. Line equations require division, which introduces floating-point error. The cross product uses only multiplication and subtraction — exact for integer coordinates and significantly more robust for floats.
Orientation Test — The Core Primitive
The orientation of three points p, q, r is determined by the cross product of vectors (q-p) and (r-p): - Positive: counterclockwise (left turn) - Negative: clockwise (right turn) - Zero: collinear
This is the single operation that underlies all segment intersection tests. The cross product formula is:
val = (q.y - p.y) (r.x - q.x) - (q.x - p.x) (r.y - q.y)
No division is involved — only multiplication and subtraction. This makes the test exact for integer coordinates and significantly more robust than line-equation approaches that require dividing by a potentially-zero denominator.
def orientation(p, q, r): """ Returns: 1 — counterclockwise (left turn) -1 — clockwise (right turn) 0 — collinear """ val = (q[1]-p[1])*(r[0]-q[0]) - (q[0]-p[0])*(r[1]-q[1]) if val > 0: return 1 if val < 0: return -1 return 0 print(orientation((0,0),(1,0),(0,1))) # 1 (CCW) print(orientation((0,0),(1,0),(2,0))) # 0 (collinear) print(orientation((0,0),(0,1),(1,0))) # -1 (CW)
0
-1
- Cross product = 2 * signed area of triangle (p, q, r)
- Positive area = points are counterclockwise
- Zero area = points are collinear (triangle degenerates to a line)
- No division means no rounding error for integer inputs
Segment Intersection
Two segments AB and CD intersect if their orientations 'straddle' each other. Specifically, A and B must be on opposite sides of line CD, AND C and D must be on opposite sides of line AB. This is checked by verifying that orientation(A,B,C) != orientation(A,B,D) and orientation(C,D,A) != orientation(C,D,B).
Edge case: when one or more orientations are zero (collinear), the segments may still intersect if an endpoint lies on the other segment. The on_segment test checks this by verifying the collinear point falls within the bounding box of the segment.
def on_segment(p, q, r): """Does point q lie on segment pr (given collinearity)?""" return (min(p[0],r[0]) <= q[0] <= max(p[0],r[0]) and min(p[1],r[1]) <= q[1] <= max(p[1],r[1])) def segments_intersect(p1, q1, p2, q2) -> bool: """Do segments p1q1 and p2q2 intersect?""" o1 = orientation(p1, q1, p2) o2 = orientation(p1, q1, q2) o3 = orientation(p2, q2, p1) o4 = orientation(p2, q2, q1) if o1 != o2 and o3 != o4: return True # Collinear cases if o1 == 0 and on_segment(p1, p2, q1): return True if o2 == 0 and on_segment(p1, q2, q1): return True if o3 == 0 and on_segment(p2, p1, q2): return True if o4 == 0 and on_segment(p2, q1, q2): return True return False print(segments_intersect((1,1),(10,1),(1,2),(10,2))) # False (parallel) print(segments_intersect((10,0),(0,10),(0,0),(10,10))) # True (X pattern)
True
Line Intersection Point
Given that two lines intersect, the intersection point is computed by solving the parametric equations of both lines simultaneously. For lines through p1-p2 and p3-p4, the denominator is the cross product of the direction vectors. If the denominator is zero, the lines are parallel. The parameter t gives the position along the first line, and the intersection point is p1 + t*(p2-p1).
This function operates on infinite lines, not segments. To find the intersection of segments, first verify they intersect using segments_intersect, then compute the point. For near-parallel lines, the denominator approaches zero and the intersection point becomes numerically unstable — handle this with an epsilon check.
def line_intersection(p1, p2, p3, p4): """Find intersection point of lines through p1-p2 and p3-p4.""" x1,y1,x2,y2 = *p1, *p2 x3,y3,x4,y4 = *p3, *p4 denom = (x1-x2)*(y3-y4) - (y1-y2)*(x3-x4) if abs(denom) < 1e-10: return None # parallel t = ((x1-x3)*(y3-y4) - (y1-y3)*(x3-x4)) / denom x = x1 + t*(x2-x1) y = y1 + t*(y2-y1) return (x, y) print(line_intersection((0,0),(1,1),(0,1),(1,0))) # (0.5, 0.5)
- line_intersection: where do the infinite lines through these points cross?
- segments_intersect: do the finite segments between these points overlap?
- To get segment intersection point: call segments_intersect first, then line_intersection
- Calling line_intersection alone gives false positives for non-overlapping segments
| Approach | Arithmetic | Handles Collinear | Handles Endpoint | Performance | Best For |
|---|---|---|---|---|---|
| Cross Product Orientation | Multiplication + subtraction only | Yes (with on_segment) | Yes (4 collinear checks) | O(1) per test, no division | General segment intersection, convex hull, sweep line |
| Line Equation (y = mx + b) | Division required | No (slope undefined for vertical) | Requires special cases | O(1) but division overhead | Simple horizontal/vertical cases only |
| Parametric (p + t*d) | Multiplication + division | Yes (denom = 0 check) | Yes (t in [0,1] check) | O(1) per test | Computing intersection POINTS, ray casting |
| Exact Rational Arithmetic | Integer operations on fractions | Exact — no epsilon | Exact — no epsilon | Slower (GCD per operation) | Safety-critical CAD, formal verification, financial geometry |
| Sweep Line (Bentley-Ottmann) | Cross product at core | Yes | Yes | O((n+k) log n) for n segments | Batch intersection detection on large segment sets |
🎯 Key Takeaways
- Orientation test: cross product sign of (q-p) x (r-p). Positive = CCW, Negative = CW, Zero = collinear. No division — exact for integer coordinates.
- Segment intersection: segments AB, CD intersect iff {A,B} straddle CD AND {C,D} straddle AB. Handle collinear endpoint cases separately.
- No division in orientation test makes it exact for integer coordinates — crucial for robust geometry code.
- Foundation for: convex hull (Graham scan, Jarvis march), polygon intersection, sweep line algorithms, collision detection.
- Floating-point precision is the #1 source of geometry bugs. Use epsilon comparisons or exact arithmetic — never raw float equality.
- line_intersection computes where infinite lines cross. Always guard with segments_intersect to verify the segments actually overlap.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhat does the cross product tell you about three points' orientation?
- QHow do you handle the collinear case in segment intersection?
- QWhy is the cross product preferred over direct line equations for intersection testing?
- QWalk me through all five cases of segment intersection: general crossing, T-junction, collinear overlap, collinear non-overlap, and parallel non-collinear. Which test handles each?
- QYou are building a sweep line algorithm for batch segment intersection. What data structure maintains the active segments, and how does the orientation test drive the event queue?
- QA geometry library returns wrong intersection results for segments with coordinates around 1e9. What is the likely cause, and how do you fix it?
- QHow would you detect if a polygon is self-intersecting? What is the time complexity of the naive approach, and how does sweep line improve it?
Frequently Asked Questions
How do you handle floating-point precision in geometry algorithms?
Two approaches: (1) Use integer coordinates and exact arithmetic — cross product is exact for integers. (2) Use epsilon comparisons — treat |val| < eps as collinear. For production computational geometry (CAD, GIS), exact arithmetic libraries like CGAL or Shapely handle this robustly.
What is the difference between line intersection and segment intersection?
Line intersection finds where two infinite lines cross — it returns a point even if the segments between the given points do not overlap. Segment intersection checks if two finite segments share any point. Always use segment intersection for collision detection and polygon operations.
Why does the orientation test use cross product instead of computing slopes?
Slope computation requires division, which introduces floating-point error and fails on vertical lines (infinite slope). The cross product uses only multiplication and subtraction — no division, no special cases for vertical lines, and exact results for integer coordinates.
How do you choose the epsilon value for collinearity checks?
Epsilon should be scaled to your coordinate range. For unit-scale coordinates, 1e-10 works. For pixel coordinates (0-4096), use 1e-6. For GPS coordinates, use 1e-12. A rule of thumb: EPS should be roughly (coordinate_range^2) * 1e-15 to account for the cross product being a squared-area quantity.
What is the Bentley-Ottmann sweep line algorithm?
It is an O((n+k) log n) algorithm for finding all k intersections among n segments, where naive pairwise checking is O(n^2). It sweeps a vertical line from left to right, maintaining an ordered set of active segments and an event queue of endpoints and intersection points. The orientation test is used to order segments and detect new intersections.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.