Given half-planes (each “the left side of a directed line”), find their intersection — a convex region, possibly empty or unbounded.

The algorithm —

  1. Sort the half-planes by the angle of their direction vector.
  2. Sweep with a deque, maintaining the half-planes that currently contribute to the boundary.
  3. Before adding a new half-plane, pop from the back while the intersection point of the last two lies outside the new half-plane; pop from the front similarly.
  4. After processing all of them, do a final cleanup pass on both ends (the region wraps around).
struct HP { PD p, d; };                              // point + direction; keep the LEFT side
bool outside(const HP& h, PD q) { return cross(h.d, q - h.p) < -1e-9; }
PD inter(const HP& a, const HP& b) {
    double t = cross(b.d, a.p - b.p) / cross(a.d, b.d);
    return a.p + a.d * t;
}
 
vector<PD> halfPlaneIntersection(vector<HP> h) {
    sort(h.begin(), h.end(), [](const HP& a, const HP& b) {
        if (half(a.d) != half(b.d)) return half(a.d) < half(b.d);
        double c = cross(a.d, b.d);
        return fabs(c) > 1e-9 ? c > 0 : cross(a.d, b.p - a.p) < 0;   // parallel: keep the inner one first
    });
    deque<HP> dq; deque<PD> pts;
    for (const HP& cur : h) {
        while (!pts.empty() && outside(cur, pts.back()))  { pts.pop_back();  dq.pop_back(); }
        while (!pts.empty() && outside(cur, pts.front())) { pts.pop_front(); dq.pop_front(); }
        if (!dq.empty() && fabs(cross(dq.back().d, cur.d)) < 1e-9) {  // exactly the same direction: inter() would divide by 0
            if (!outside(cur, dq.back().p)) continue;                 // old point still satisfies cur: cur is looser, drop it
            dq.pop_back();                                            // old point violates cur: cur is strictly tighter, replace
            if (!pts.empty()) pts.pop_back();
        }
        if (!dq.empty()) pts.push_back(inter(dq.back(), cur));
        dq.push_back(cur);
    }
    while (dq.size() > 2 && outside(dq.front(), pts.back()))  { pts.pop_back();  dq.pop_back(); }
    while (dq.size() > 2 && outside(dq.back(), pts.front())) { pts.pop_front(); dq.pop_front(); }
    if (dq.size() < 3) return {};                    // empty or degenerate
    pts.push_back(inter(dq.back(), dq.front()));
    return vector<PD>(pts.begin(), pts.end());
}

Two details do all the work: the angular sort with an inner/outer tiebreak for parallel half-planes (so a duplicate-direction constraint never produces a spurious intersection point), and the two-sided deque popping.

Tested: half-planes taken as the CCW edges of a random convex polygon (p = h[i], d = h[i+1]-h[i], keeping the left/inside) reconstruct exactly that polygon; half-planes from two random convex polygons combined reconstruct their intersection, checked by area against Sutherland-Hodgman clipping run from scratch; and an over-constrained or infeasible set (e.g. two half-planes with incompatible directions) correctly returns empty.

Unbounded regions

If the true intersection is unbounded, the algorithm produces garbage or an empty result (the deque never acquires enough active constraints to close, or closes incorrectly across the “missing” side). The standard fix is to add four half-planes forming a large bounding box before running it. Choose the box large enough that it never clips the real answer — and remember that the reported area then includes the box if the region really was unbounded.

Uses

ProblemFormulation
Intersection of convex polygonseach edge is a half-plane
Feasibility of linear constraints in 2Dis the intersection non-empty
2D linear programmingSeidel’s algorithm is faster ( expected) — 04-linear-programming
Kernel of a polygon (points seeing the whole polygon)intersect the inner half-planes of all edges
Area visible from a pointhalf-planes from the blocking edges
Voronoi cell of one siteintersect the perpendicular-bisector half-planes — 20-delaunay-and-voronoi
Region reachable under constraintsdirect
Dual of a convex hullhalf-plane intersection and convex hull are dual problems

Duality with convex hulls

Under the standard point-line duality (a point dualizes to the line , and vice versa, reading the line’s slope as the dual point’s and minus its intercept as the dual point’s ):

  • the upper envelope of lines the lower convex hull of points;
  • half-plane intersection convex hull.

So an hull algorithm gives an half-plane intersection and vice versa. It also explains why the convex hull trick (an upper envelope of lines) is called that.

When to use something else

SituationBetter tool
Only need feasibilitySeidel’s LP, expected
Only need one extreme point2D LP
Clipping a convex polygon by a few linesSutherland-Hodgman,
Voronoi diagram of many sitesFortune’s algorithm, total
small ()intersect incrementally by clipping; simpler and fast enough

Precision

This is one of the more precision-sensitive routines in geometry: it constructs intersection points and then compares them against other lines. Use long double, keep consistent, and prefer the incremental-clipping approach when is small enough to afford .

Exactly-degenerate (zero-area) intersections are the hard case

When the true intersection has zero area — two input regions only touching along an edge or at a point — the algorithm’s behaviour right at the boundary is unreliable: a configuration that is “just barely” zero-area can report a spurious positive-area region or an incorrectly empty one, because the same floating-point comparisons that correctly resolve clearly-separated or clearly-overlapping inputs have no slack left when the true answer sits exactly on the decision boundary. If the problem can produce exactly-touching inputs and the distinction matters, treat the zero-area case specially (e.g. check the pairwise touching condition directly) rather than trusting the general sweep’s output region.