Skip to content
Home DSA Float Collinearity Causes Self-Intersecting Polygons

Float Collinearity Causes Self-Intersecting Polygons

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Geometry → Topic 1 of 4
Naive line intersection using raw float equality failed on nearly collinear vertices, causing a self-intersecting polygon destroying three prototypes.
⚙️ Intermediate — basic DSA knowledge assumed
In this tutorial, you'll learn
Naive line intersection using raw float equality failed on nearly collinear vertices, causing a self-intersecting polygon destroying three prototypes.
  • 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.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • 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.
🚨 START HERE

Geometry Intersection Triage Cheat Sheet

Fast diagnostics for intersection test failures in production geometry code.
🟡

Parallel segments reported as intersecting.

Immediate ActionCheck epsilon threshold in collinear on_segment test.
Commands
Print orientation values for all four point triples to verify they are near-zero
Check if on_segment bounding-box test uses raw float or epsilon comparison
Fix NowAdd epsilon to on_segment: if abs(orientation_val) > EPS, skip collinear branch. Tune EPS to coordinate scale.
🟡

Shared endpoint intersection missed.

Immediate ActionVerify all four collinear checks are present in segments_intersect.
Commands
Add debug print: print(f'o1={o1} o2={o2} o3={o3} o4={o4}') before return
Test with segments sharing exactly one endpoint: ((0,0),(1,1)) and ((1,1),(2,0))
Fix NowEnsure all four if oN==0 and on_segment(...) checks exist. Do not shortcut the collinear cases.
🟡

Intersection point is wildly wrong for nearly parallel lines.

Immediate ActionCheck if denominator is near zero before dividing.
Commands
Print denom value: should be > EPS for valid intersection
If denom < EPS, treat as parallel — return None instead of dividing
Fix NowAdd: if abs(denom) < 1e-10: return None. Choose EPS based on coordinate scale.
🟠

O(n^2) intersection checks are too slow for large polygon sets.

Immediate ActionSwitch to sweep line algorithm for batch intersection detection.
Commands
Profile: count total segment pairs being tested
Implement Bentley-Ottmann sweep line to reduce to O((n+k) log n)
Fix NowUse sweep line for n > 1000 segments. O(n^2) is fine for small inputs.
Production Incident

Self-Intersecting Polygon in CAD Export Crashes CNC Machine

A CAD software's polygon simplification algorithm produced self-intersecting polygons due to missed collinear segment detection, causing a CNC milling machine to cut through material incorrectly, destroying a $45,000 aerospace prototype.
SymptomCNC machine cut paths that intersected themselves, creating holes in the prototype where solid material should exist. QA flagged the issue after the third destroyed prototype. The CAD export showed valid polygons on screen but the simplified vertex output contained crossing edges.
AssumptionThe team assumed the polygon simplification (Ramer-Douglas-Peucker algorithm) always produced valid simple polygons. They tested with hand-crafted inputs but never with real-world CAD data containing near-collinear vertices from curved surface tessellation.
Root causeThe Ramer-Douglas-Peucker implementation used a naive segment intersection check that did not handle collinear cases. When three consecutive vertices were nearly collinear (within floating-point epsilon), the middle vertex was removed, but the resulting simplified edge crossed another edge of the polygon. The orientation test returned zero (collinear) but the on-segment check used raw float comparison instead of epsilon, so it failed to detect the overlap. The simplified polygon had a self-intersection that the downstream CNC path generator faithfully followed.
Fix1. Replaced raw float comparison in on_segment with epsilon-based check: if abs(val) < EPS, treat as collinear. 2. Added a post-simplification validation pass that runs O(n^2) intersection checks on all non-adjacent edges of the output polygon. 3. Switched to exact arithmetic (rational numbers) for the orientation test when coordinates are derived from CAD data. 4. Added adversarial test cases with near-collinear vertices (angle < 0.001 degrees) to the regression suite.
Key Lesson
Floating-point equality in geometry is never safe. Always use epsilon comparisons or exact arithmetic.Polygon simplification algorithms can produce self-intersecting output if the intersection predicate is imprecise.Post-condition validation (checking that output polygons are simple) catches bugs that the algorithm itself misses.Test with adversarial inputs: near-collinear vertices, near-parallel edges, degenerate triangles. Real CAD data is full of these.
Production Debug Guide

When intersection tests return wrong results, miss edge cases, or produce degenerate polygons.

Intersection test returns True for parallel segments that clearly do not overlap.Check the epsilon threshold in the collinear on_segment test. If epsilon is too large, near-parallel segments are treated as collinear and the bounding-box overlap check triggers incorrectly. Reduce epsilon or switch to exact arithmetic.
Intersection test misses an intersection at a shared endpoint.Verify that the collinear on_segment cases are handled. The general straddling test (o1 != o2 and o3 != o4) does NOT catch endpoint-on-segment cases. All four collinear checks must be present: o1==0, o2==0, o3==0, o4==0.
Convex hull output has collinear points that should have been removed.Check the orientation test in the hull construction. If the hull uses '<= 0' to remove collinear points, it keeps the last collinear point. If it uses '< 0', it keeps all collinear points. Decide based on your spec and be consistent.
Sweep line algorithm produces wrong results on inputs with many vertical segments.Vertical segments have undefined slope, which breaks line-equation-based intersection. Use the cross product orientation test — it handles vertical and horizontal segments correctly with no special cases.
Intersection point calculation returns NaN or Infinity for nearly parallel lines.The denominator (x1-x2)(y3-y4) - (y1-y2)(x3-x4) approaches zero for parallel lines. Add an epsilon check: if abs(denom) < EPS, return None (parallel). Choose EPS based on your coordinate range — 1e-10 for unit-scale, 1e-6 for pixel-scale.

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.

orientation.py · PYTHON
123456789101112131415
def orientation(p, q, r):
    """
    Returns:
     1counterclockwise (left turn)
    -1clockwise (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)
▶ Output
1
0
-1
Mental Model
The Cross Product as a Signed Area
Think signed area, not abstract algebra.
  • 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
📊 Production Insight
Cause: Using line equations (y = mx + b) for orientation requires computing slope (division), which introduces floating-point error for near-vertical lines and loses precision for large coordinates. Effect: Near-vertical lines produce infinite or near-infinite slopes, causing orientation tests to return wrong results. A single wrong orientation corrupts the entire convex hull or sweep line output. Action: Always use the cross product formula — no division. For integer coordinates, the result is exact. For floating-point coordinates, add epsilon comparison: treat |val| < EPS as collinear.
🎯 Key Takeaway
The orientation test is the atomic primitive of computational geometry. It uses only multiplication and subtraction — no division — making it exact for integers and robust for floats. Every segment intersection, convex hull, and sweep line algorithm reduces to repeated calls to this single function.
Choosing Arithmetic Precision
IfInput coordinates are integers (pixel coordinates, grid-based, CAD with integer units).
UseUse cross product with integer arithmetic. Result is exact — no epsilon needed.
IfInput coordinates are floats (GPS, physics simulation, real-world measurements).
UseUse cross product with epsilon comparison. Treat |val| < EPS as collinear. Choose EPS based on coordinate magnitude.
IfApplication requires guaranteed correctness (formal verification, financial geometry, safety-critical CAD).
UseUse exact rational arithmetic (Python fractions, Java BigDecimal, or CGAL's exact predicates). No epsilon, no approximation.

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.

segment_intersection.py · PYTHON
123456789101112131415161718192021
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)
▶ Output
False
True
⚠ The Four Collinear Checks Are Not Optional
A common implementation error is to only check the general straddling case (o1 != o2 and o3 != o4) and skip the four collinear cases. This misses all endpoint-on-segment intersections. Every production implementation must include all four collinear checks. Missing even one creates a silent bug that only manifests when segments share an endpoint or one endpoint lies exactly on the other segment.
📊 Production Insight
Cause: Naive implementations only check the general straddling case (o1 != o2 and o3 != o4) and skip the four collinear endpoint checks. Effect: Segments that share an endpoint, or where one endpoint lies exactly on the other segment, are reported as non-intersecting. In polygon clipping, this creates gaps. In collision detection, it allows objects to pass through each other at contact points. Action: Always include all four collinear checks. Test with segments that share endpoints, are collinear but non-overlapping, and are collinear and overlapping.
🎯 Key Takeaway
Segment intersection requires two tests: general straddling (o1 != o2 and o3 != o4) AND four collinear endpoint checks. Missing the collinear cases is the most common bug in geometry code — it silently fails on T-junctions, shared endpoints, and overlapping collinear segments.
Intersection Test Edge Cases
IfSegments cross in an X pattern (general case).
UseGeneral straddling test catches this: o1 != o2 and o3 != o4.
IfSegments share exactly one endpoint (T-junction or corner touch).
UseCollinear check catches this: one orientation is zero, on_segment returns True.
IfSegments are collinear and overlap (partial or full overlap).
UseCollinear check catches this: both orientations on one side are zero, bounding-box overlap detected by on_segment.
IfSegments are collinear but do not overlap (gap between them).
UseCollinear check returns False: on_segment bounding-box test fails — the points are collinear but outside the segment range.
IfSegments are parallel but not collinear (distinct lines).
UseGeneral straddling test returns False: o1 == o2 (same side) and o3 == o4 (same side). No collinear case triggered.

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.

intersection_point.py · PYTHON
123456789101112
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)
▶ Output
(0.5, 0.5)
🔥Lines vs Segments: Know Which You Need
  • 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
📊 Production Insight
Cause: Using line_intersection without first checking segments_intersect. The function returns a point for any non-parallel lines, even if the segments themselves do not overlap. Effect: In collision detection, this reports intersections where none exist — objects that are far apart but whose extended lines cross are reported as colliding. In polygon clipping, this creates phantom intersection points outside both polygon edges. Action: Always guard line_intersection with a segments_intersect check. The intersection point is only valid if the segments actually cross.
🎯 Key Takeaway
line_intersection computes where infinite lines cross — it does NOT check if the segments overlap. Always guard with segments_intersect first. The denominator is the cross product of direction vectors: near-zero means parallel. Choose your epsilon based on coordinate scale.
When to Use Each Function
IfNeed to know IF two segments intersect (boolean question).
UseUse segments_intersect. O(1), no division, handles all edge cases.
IfNeed the exact intersection POINT of two segments.
UseFirst call segments_intersect to verify. Then call line_intersection to compute the point.
IfNeed where two infinite lines cross (ray casting, line-line intersection).
UseUse line_intersection directly. No segment check needed — you want the infinite line intersection.
IfNeed intersection of a ray with a segment (ray casting for visibility).
UseUse parametric ray equation combined with segments_intersect. Check that t >= 0 (intersection is in front of ray origin).
🗂 Intersection Detection Approaches
Choosing the right method for your geometry problem.
ApproachArithmeticHandles CollinearHandles EndpointPerformanceBest For
Cross Product OrientationMultiplication + subtraction onlyYes (with on_segment)Yes (4 collinear checks)O(1) per test, no divisionGeneral segment intersection, convex hull, sweep line
Line Equation (y = mx + b)Division requiredNo (slope undefined for vertical)Requires special casesO(1) but division overheadSimple horizontal/vertical cases only
Parametric (p + t*d)Multiplication + divisionYes (denom = 0 check)Yes (t in [0,1] check)O(1) per testComputing intersection POINTS, ray casting
Exact Rational ArithmeticInteger operations on fractionsExact — no epsilonExact — no epsilonSlower (GCD per operation)Safety-critical CAD, formal verification, financial geometry
Sweep Line (Bentley-Ottmann)Cross product at coreYesYesO((n+k) log n) for n segmentsBatch 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

    Using line equations instead of cross product: Line equations require division, which introduces floating-point error and fails on vertical lines. The cross product uses only multiplication and subtraction — exact for integers, robust for floats.
    Missing collinear endpoint checks: The general straddling test (o1 != o2 and o3 != o4) does NOT catch shared endpoints or endpoint-on-segment cases. All four collinear checks must be present.
    Using raw float equality for collinearity: orientation() returns 0 when the cross product is exactly zero, but floating-point arithmetic rarely produces exact zero. Use epsilon comparison: |val| < EPS means collinear.
    Calling line_intersection without segments_intersect: line_intersection returns a point for any non-parallel lines, even if the segments do not overlap. Always verify intersection first.
    Choosing epsilon without considering coordinate scale: An epsilon of 1e-10 works for unit-scale coordinates but is meaningless for pixel-scale (0-1920) or GPS-scale (lat/lon in degrees). Scale your epsilon to your coordinate range.
    Not testing with degenerate inputs: Near-collinear vertices, zero-length segments, segments sharing endpoints, and near-parallel lines are not edge cases — they are common in real-world geometry data from CAD, GIS, and physics engines.

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.

🔥
Naren Founder & Author

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.

Next →Polygon Area — Shoelace Formula
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged