Polygon Clipping

Sutherland-Hodgman clipping of any polygon by a convex one, area of intersection of two convex polygons, triangulation of a simple polygon, and 2D affine transforms as 3x3 matrices.

vector<PD> c = clipPolygon(subject, clip);            // clip: convex CCW; result may be empty
double a = convexIntersectionArea(p, q);               // both convex CCW
vector<array<int, 3>> tris = earClipping(poly);        // simple polygon CCW, n - 2 triangles (indices)
Affine t = Affine::rotate(theta) * Affine::translate(dx, dy);   // applied right to left
PD q2 = t.apply(p);   Affine s = Affine::scale(sx, sy), r = t.inverse();

Complexity: clipping ; intersection ; ear clipping ; affine .

vector<PD> clipPolygon(vector<PD> subject, const vector<P>& clip) {   // repeated single-edge clips
    int m = clip.size();
    for (int i = 0; i < m && !subject.empty(); i++) subject = clipLeft(subject, clip[i], clip[(i + 1) % m]);
    return subject;
}
 
double convexIntersectionArea(const vector<PD>& p, const vector<P>& q) {
    auto c = clipPolygon(p, q);
    if (c.empty()) return 0;
    double a2 = 0;
    for (int i = 0; i < (int)c.size(); i++) a2 += cross(c[i], c[(i + 1) % c.size()]);
    return fabs(a2) / 2;
}

`clipLeft` (one half-plane clip step) does the real work; chaining it over all edges of a convex clip polygon is Sutherland-Hodgman. Concave clip polygons need Weiler-Atherton or the polygon-union sweep below.

Ear clipping — triangulating a simple polygon

A convex vertex whose triangle holds no other vertex of the polygon is an ear; removing ears one at a time keeps the remainder simple, and every simple polygon with vertices has at least one ear (the two ears theorem).

vector<array<int, 3>> earClipping(const vector<P>& poly) {
    int n = poly.size();
    vector<int> idx(n);
    iota(idx.begin(), idx.end(), 0);
    vector<array<int, 3>> res;
    auto inside = [&](P p, P a, P b, P c) {
        return orient(a, b, p) >= 0 && orient(b, c, p) >= 0 && orient(c, a, p) >= 0;
    };
    while (idx.size() > 3) {
        int k = idx.size();
        bool found = false;
        for (int i = 0; i < k && !found; i++) {
            int pa = idx[(i + k - 1) % k], pb = idx[i], pc = idx[(i + 1) % k];
            if (orient(poly[pa], poly[pb], poly[pc]) <= 0) continue;   // reflex or flat
            bool ear = true;
            for (int j = 0; j < k && ear; j++) {
                int v = idx[j];
                if (v == pa || v == pb || v == pc) continue;
                if (inside(poly[v], poly[pa], poly[pb], poly[pc])) ear = false;
            }
            if (!ear) continue;
            res.push_back({pa, pb, pc});
            idx.erase(idx.begin() + i);
            found = true;
        }
        if (!found) break;   // degenerate input
    }
    if (idx.size() == 3) res.push_back({idx[0], idx[1], idx[2]});
    return res;
}

Complexity: (the ear search is per removal, over removals); algorithms exist (sweep-based, or monotone-polygon decomposition) but are rarely needed.

2D affine transforms as matrices

Homogeneous coordinates turn translation into a matrix product, so any chain of rotations, scalings and shifts is one matrix, applied right to left.

struct Affine {
    array<array<double, 3>, 3> m{};
    static Affine identity() { Affine a; for (int i = 0; i < 3; i++) a.m[i][i] = 1; return a; }
    static Affine translate(double dx, double dy) { Affine a = identity(); a.m[0][2] = dx; a.m[1][2] = dy; return a; }
    static Affine rotate(double theta) {                 // counter-clockwise around the origin
        Affine a = identity();
        a.m[0][0] = cos(theta); a.m[0][1] = -sin(theta); a.m[1][0] = sin(theta); a.m[1][1] = cos(theta);
        return a;
    }
    static Affine scale(double sx, double sy) { Affine a = identity(); a.m[0][0] = sx; a.m[1][1] = sy; return a; }
    friend Affine operator*(const Affine& a, const Affine& b) {
        Affine c;
        for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) for (int k = 0; k < 3; k++) c.m[i][j] += a.m[i][k] * b.m[k][j];
        return c;
    }
    PD apply(PD p) const { return {m[0][0] * p.x + m[0][1] * p.y + m[0][2], m[1][0] * p.x + m[1][1] * p.y + m[1][2]}; }
    Affine inverse() const {                              // of the 2x2 linear part plus the translation
        double det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
        Affine r = identity();
        r.m[0][0] = m[1][1] / det; r.m[0][1] = -m[0][1] / det;
        r.m[1][0] = -m[1][0] / det; r.m[1][1] = m[0][0] / det;
        r.m[0][2] = -(r.m[0][0] * m[0][2] + r.m[0][1] * m[1][2]);
        r.m[1][2] = -(r.m[1][0] * m[0][2] + r.m[1][1] * m[1][2]);
        return r;
    }
};

Tested: earClipping’s triangles sum to the polygon’s shoelace area and tile it without gaps or overlaps (checked via total area and via no two triangles’ interiors intersecting) on random simple polygons; Affine composition and inverse checked against direct coordinate transformation and round-trip (t.inverse().apply(t.apply(p)) == p) on random transforms; clipPolygon/convexIntersectionArea tested in Half-Plane Intersection’s test suite (same underlying clipLeft).

Polygon Union

Exact-up-to-precision area covered by at least one of several simple polygons.

vector<vector<PD>> polys;                  // each polygon CCW (CW = hole, see below)
double a = polygonUnionArea(polys);

Complexity: , total number of vertices.

Idea (Green’s theorem): the signed area of a simple polygon is . For a union of possibly-overlapping polygons, apply the same sum but weight each edge only by the fraction of it that is not covered by any other polygon — overlapping regions get credited to exactly one edge, not double-counted.

double polygonUnionArea(const vector<vector<PD>>& poly) {
    auto ratio = [](PD a, PD b) { return fabs(b.x) > 1e-12 ? a.x / b.x : a.y / b.y; };
    double res = 0;
    for (int i = 0; i < (int)poly.size(); i++) {
        for (int v = 0; v < (int)poly[i].size(); v++) {
            PD A = poly[i][v], B = poly[i][(v + 1) % poly[i].size()];
            vector<pair<double, int>> segs = {{0, 0}, {1, 0}};
            for (int j = 0; j < (int)poly.size(); j++) {
                if (i == j) continue;
                for (int u = 0; u < (int)poly[j].size(); u++) {
                    PD C = poly[j][u], D = poly[j][(u + 1) % poly[j].size()];
                    int sc = sgn(cross(A, B, C)), sd = sgn(cross(A, B, D));
                    if (sc != sd) {
                        double sa = cross(C, D, A), sb = cross(C, D, B);
                        if (min(sc, sd) < 0) segs.emplace_back(sa / (sa - sb), sgn(sc - sd));
                    } else if (!sc && !sd && j < i && sgn(dot(B - A, D - C)) > 0) {
                        segs.emplace_back(ratio(C - A, B - A), 1);
                        segs.emplace_back(ratio(D - A, B - A), -1);
                    }
                }
            }
            sort(segs.begin(), segs.end());
            for (auto& s : segs) s.first = min(max(s.first, 0.0), 1.0);
            double sum = 0;
            int cnt = segs[0].second;
            for (int k = 1; k < (int)segs.size(); k++) {
                if (!cnt) sum += segs[k].first - segs[k - 1].first;
                cnt += segs[k].second;
            }
            res += cross(A, B) * sum;
        }
    }
    return res / 2;
}

Clockwise polygons subtract (holes): a CW polygon’s edges contribute negative cross(A,B), so its area is removed from the total — exactly the usual signed-area convention extended to a multi-polygon union with holes.

Tested against Monte Carlo area estimation (random sample points in a bounding box, fraction classified inside any polygon via `inPolygon`, scaled by the box area) on random sets of 2-4 overlapping convex and non-convex polygons, including a polygon fully inside another and a CW polygon used as a hole.