Skip to content

circle_polygon_area.hpp

SECTIONGeometry INCLUDEnoya/geometry/circle_polygon_area.hpp

计算两圆重叠面积及圆与简单多边形的交面积;处理相切、内含、零半径和任意多边形朝向。

Complexity: Time: O(1) for two circles, O(n) for a circle and polygon. Space: O(1), excluding the input polygon.

跳到代码 · GitHub ↗

Implementation

当前头文件,省略 include guard;依赖见 #include。

/// @complexity Time: O(1) for two circles, O(n) for a circle and polygon.
/// Space: O(1), excluding the input polygon.

#include "noya/geometry/geometry_base.hpp"

#include <algorithm>
#include <array>
#include <cassert>
#include <cmath>
#include <numbers>
#include <vector>

namespace noya {

/// @brief Area of intersection of two closed disks with nonnegative radii.
/// Uses long double; inputs and intermediate squared lengths/areas must be finite.
template <class T, class U>
long double circle_overlap_area(const circle<T> &a, const circle<U> &b) {
  assert(a.r1 >= T{} && b.r1 >= U{});
  long double r = static_cast<long double>(a.r1);
  long double s = static_cast<long double>(b.r1);
  long double d = std::hypot(static_cast<long double>(a.o.x) - b.o.x,
                             static_cast<long double>(a.o.y) - b.o.y);
  const long double pi = std::numbers::pi_v<long double>;
  if (r == 0 || s == 0 || d >= r + s) {
    return 0;
  }
  if (r > s) {
    std::swap(r, s);
  }
  long double cap = pi * std::min(r, s) * std::min(r, s);
  if (d <= std::abs(r - s)) {
    return cap;
  }
  // Measure the chord from the smaller disk. Factoring d^2-s^2 avoids
  // subtracting nearly equal squared lengths for very unequal radii.
  long double x = (r * r + (d - s) * (d + s)) / (2 * d);
  long double y = d - x;
  long double height = std::sqrt(std::max(0.0L, (r - x) * (r + x)));
  long double first = std::atan2(height, x);
  long double second = std::atan2(height, y);
  auto segment = [](long double radius, long double angle) {
    // theta-sin(theta)cos(theta) loses precision for a shallow circular cap.
    long double value;
    if (angle < 1e-3L) {
      long double sq = angle * angle;
      value = angle * sq * (2.0L / 3 + sq * (-2.0L / 15 +
              sq * (4.0L / 315 - sq * (2.0L / 2835))));
    } else {
      value = angle - std::sin(angle) * std::cos(angle);
    }
    return radius * radius * value;
  };
  long double area = segment(r, first) + segment(s, second);
  return std::clamp(area, 0.0L, cap);
}

/// @brief Area shared by a closed disk and a simple polygon, in either winding.
/// Empty/point/segment polygons and zero-radius disks have area zero. Repeating
/// the first vertex at the end is allowed. Each edge is split at circle crossings;
/// inside pieces contribute triangle area, outside pieces contribute sector area.
/// Uses long double; inputs and intermediate squared lengths/areas must be finite.
template <class T, class U>
long double circle_polygon_intersection_area(
    const circle<T> &disk, const std::vector<point<U>> &pg) {
  assert(disk.r1 >= T{});
  long double r = static_cast<long double>(disk.r1);
  if (r == 0 || pg.size() < 3) {
    return 0;
  }
  point<long double> center{static_cast<long double>(disk.o.x),
                             static_cast<long double>(disk.o.y)};
  auto relative = [&](const point<U> &p) {
    return point<long double>{static_cast<long double>(p.x) - center.x,
                               static_cast<long double>(p.y) - center.y};
  };
  long double sum = 0;
  for (int i = 0; i < int(pg.size()); i++) {
    auto a = relative(pg[i]);
    auto b = relative(pg[(i + 1) % pg.size()]);
    auto dir = b - a;
    long double length = std::hypot(dir.x, dir.y);
    if (length == 0) {
      continue;
    }
    dir /= length;
    // Parameterize by signed distance from the line's perpendicular foot.
    // Reconstructing a + (b-a)*t would lose the crossings on very long edges.
    long double height = cross(dir, a);
    long double first = dot(a, dir), last = dot(b, dir);
    std::array<long double, 4> cuts{first, last};
    int count = 2;
    long double remaining = (r - std::abs(height)) * (r + std::abs(height));
    if (remaining > 0) {
      long double offset = std::sqrt(remaining);
      for (long double t : {-offset, offset}) {
        if (first < t && t < last) {
          cuts[count++] = t;
        }
      }
    }
    std::sort(cuts.begin(), cuts.begin() + count);
    for (int j = 0; j + 1 < count; j++) {
      long double lo = cuts[j], hi = cuts[j + 1];
      long double mid = lo / 2 + hi / 2;
      long double cr = height * (lo - hi);
      if (mid * mid < remaining) {
        sum += cr;
      } else {
        sum += r * r * std::atan2(cr, height * height + lo * hi);
      }
    }
  }
  return std::abs(sum) / 2;
}

} // namespace noya

 // NOYA_CIRCLE_POLYGON_AREA_HPP
#ifndef NOYA_CIRCLE_POLYGON_AREA_HPP
#define NOYA_CIRCLE_POLYGON_AREA_HPP 1

/// @complexity Time: O(1) for two circles, O(n) for a circle and polygon.
/// Space: O(1), excluding the input polygon.

#include "noya/geometry/geometry_base.hpp"

#include <algorithm>
#include <array>
#include <cassert>
#include <cmath>
#include <numbers>
#include <vector>

namespace noya {

/// @brief Area of intersection of two closed disks with nonnegative radii.
/// Uses long double; inputs and intermediate squared lengths/areas must be finite.
template <class T, class U>
long double circle_overlap_area(const circle<T> &a, const circle<U> &b) {
  assert(a.r1 >= T{} && b.r1 >= U{});
  long double r = static_cast<long double>(a.r1);
  long double s = static_cast<long double>(b.r1);
  long double d = std::hypot(static_cast<long double>(a.o.x) - b.o.x,
                             static_cast<long double>(a.o.y) - b.o.y);
  const long double pi = std::numbers::pi_v<long double>;
  if (r == 0 || s == 0 || d >= r + s) {
    return 0;
  }
  if (r > s) {
    std::swap(r, s);
  }
  long double cap = pi * std::min(r, s) * std::min(r, s);
  if (d <= std::abs(r - s)) {
    return cap;
  }
  // Measure the chord from the smaller disk. Factoring d^2-s^2 avoids
  // subtracting nearly equal squared lengths for very unequal radii.
  long double x = (r * r + (d - s) * (d + s)) / (2 * d);
  long double y = d - x;
  long double height = std::sqrt(std::max(0.0L, (r - x) * (r + x)));
  long double first = std::atan2(height, x);
  long double second = std::atan2(height, y);
  auto segment = [](long double radius, long double angle) {
    // theta-sin(theta)cos(theta) loses precision for a shallow circular cap.
    long double value;
    if (angle < 1e-3L) {
      long double sq = angle * angle;
      value = angle * sq * (2.0L / 3 + sq * (-2.0L / 15 +
              sq * (4.0L / 315 - sq * (2.0L / 2835))));
    } else {
      value = angle - std::sin(angle) * std::cos(angle);
    }
    return radius * radius * value;
  };
  long double area = segment(r, first) + segment(s, second);
  return std::clamp(area, 0.0L, cap);
}

/// @brief Area shared by a closed disk and a simple polygon, in either winding.
/// Empty/point/segment polygons and zero-radius disks have area zero. Repeating
/// the first vertex at the end is allowed. Each edge is split at circle crossings;
/// inside pieces contribute triangle area, outside pieces contribute sector area.
/// Uses long double; inputs and intermediate squared lengths/areas must be finite.
template <class T, class U>
long double circle_polygon_intersection_area(
    const circle<T> &disk, const std::vector<point<U>> &pg) {
  assert(disk.r1 >= T{});
  long double r = static_cast<long double>(disk.r1);
  if (r == 0 || pg.size() < 3) {
    return 0;
  }
  point<long double> center{static_cast<long double>(disk.o.x),
                             static_cast<long double>(disk.o.y)};
  auto relative = [&](const point<U> &p) {
    return point<long double>{static_cast<long double>(p.x) - center.x,
                               static_cast<long double>(p.y) - center.y};
  };
  long double sum = 0;
  for (int i = 0; i < int(pg.size()); i++) {
    auto a = relative(pg[i]);
    auto b = relative(pg[(i + 1) % pg.size()]);
    auto dir = b - a;
    long double length = std::hypot(dir.x, dir.y);
    if (length == 0) {
      continue;
    }
    dir /= length;
    // Parameterize by signed distance from the line's perpendicular foot.
    // Reconstructing a + (b-a)*t would lose the crossings on very long edges.
    long double height = cross(dir, a);
    long double first = dot(a, dir), last = dot(b, dir);
    std::array<long double, 4> cuts{first, last};
    int count = 2;
    long double remaining = (r - std::abs(height)) * (r + std::abs(height));
    if (remaining > 0) {
      long double offset = std::sqrt(remaining);
      for (long double t : {-offset, offset}) {
        if (first < t && t < last) {
          cuts[count++] = t;
        }
      }
    }
    std::sort(cuts.begin(), cuts.begin() + count);
    for (int j = 0; j + 1 < count; j++) {
      long double lo = cuts[j], hi = cuts[j + 1];
      long double mid = lo / 2 + hi / 2;
      long double cr = height * (lo - hi);
      if (mid * mid < remaining) {
        sum += cr;
      } else {
        sum += r * r * std::atan2(cr, height * height + lo * hi);
      }
    }
  }
  return std::abs(sum) / 2;
}

} // namespace noya

#endif // NOYA_CIRCLE_POLYGON_AREA_HPP
/// @complexity Time: O(1) for two circles, O(n) for a circle and polygon.
/// Space: O(1), excluding the input polygon.

/// @complexity Time: O(1) per primitive; O(n log n) for convex hull.
/// Space: O(1) per primitive and O(n) for hull construction.

#include <algorithm>
#include <cmath>
#include <optional>
#include <vector>

namespace noya {

/// @brief Two-dimensional point with vector arithmetic and lexicographic order.
template <class T> struct point {
  T x{};
  T y{};

  point() = default;
  point(T x_, T y_) : x(x_), y(y_) {}

  point &operator+=(const point &rhs) {
    x += rhs.x;
    y += rhs.y;
    return *this;
  }
  point &operator-=(const point &rhs) {
    x -= rhs.x;
    y -= rhs.y;
    return *this;
  }
  point &operator*=(const T &scl) {
    x *= scl;
    y *= scl;
    return *this;
  }
  point &operator/=(const T &scl) {
    x /= scl;
    y /= scl;
    return *this;
  }

  friend point operator+(point l, const point &r) { return l += r; }
  friend point operator-(point l, const point &r) { return l -= r; }
  friend point operator*(point val, const T &scl) { return val *= scl; }
  friend point operator*(const T &scl, point val) { return val *= scl; }
  friend point operator/(point val, const T &scl) { return val /= scl; }
  friend bool operator==(const point &, const point &) = default;
  friend bool operator<(const point &l, const point &r) {
    return l.x < r.x || (l.x == r.x && l.y < r.y);
  }
};

/// @brief Circle represented by a center and a nonnegative radius.
template <class Real> struct circle {
  point<Real> o;
  Real r1{};
};

/// @brief Return the dot product of two vectors.
template <class T> T dot(const point<T> &a, const point<T> &b) {
  return a.x * b.x + a.y * b.y;
}

/// @brief Return the signed cross product of two vectors.
template <class T> T cross(const point<T> &a, const point<T> &b) {
  return a.x * b.y - a.y * b.x;
}

/// @brief Return cross(a - o1, b - o1).
template <class T>
T cross(const point<T> &o1, const point<T> &a, const point<T> &b) {
  return cross(a - o1, b - o1);
}

/// @brief Return the squared Euclidean norm.
template <class T> T norm2(const point<T> &val) { return dot(val, val); }

/// @brief Compare a value with zero using an optional absolute tolerance.
template <class T> int sign(const T &val, const T &eps = T{}) {
  return (val > eps) - (val < -eps);
}

/// @brief Return -1, 0, or 1 for a clockwise, collinear, or counter-clockwise
/// turn.
template <class T>
int orientation(const point<T> &a, const point<T> &b, const point<T> &c,
                const T &eps = T{}) {
  return sign(cross(a, b, c), eps);
}

/// @brief Test whether p lies on the closed segment [a, b].
template <class T>
bool on_segment(const point<T> &p, const point<T> &a, const point<T> &b,
                const T &eps = T{}) {
  if (orientation(a, b, p, eps) != 0) {
    return false;
  }
  return std::min(a.x, b.x) - eps <= p.x && p.x <= std::max(a.x, b.x) + eps &&
         std::min(a.y, b.y) - eps <= p.y && p.y <= std::max(a.y, b.y) + eps;
}

/// @brief Test whether the closed segments [a, b] and [c, d] intersect.
template <class T>
bool segments_intersect(const point<T> &a, const point<T> &b, const point<T> &c,
                        const point<T> &d, const T &eps = T{}) {
  int s1 = orientation(a, b, c, eps);
  int s2 = orientation(a, b, d, eps);
  int s3 = orientation(c, d, a, eps);
  int s4 = orientation(c, d, b, eps);
  if (s1 == 0 && on_segment(c, a, b, eps)) {
    return true;
  }
  if (s2 == 0 && on_segment(d, a, b, eps)) {
    return true;
  }
  if (s3 == 0 && on_segment(a, c, d, eps)) {
    return true;
  }
  if (s4 == 0 && on_segment(b, c, d, eps)) {
    return true;
  }
  return s1 * s2 < 0 && s3 * s4 < 0;
}

/// @brief Intersect the infinite lines through (a, b) and (c, d), returning
/// nullopt when they are parallel or coincident.
template <class T>
std::optional<point<long double>>
line_intersection(const point<T> &a, const point<T> &b, const point<T> &c,
                  const point<T> &d, long double eps = 0) {
  point<long double> lhs{static_cast<long double>(a.x),
                         static_cast<long double>(a.y)};
  point<long double> b1{static_cast<long double>(b.x),
                        static_cast<long double>(b.y)};
  point<long double> z{static_cast<long double>(c.x),
                       static_cast<long double>(c.y)};
  point<long double> d1{static_cast<long double>(d.x),
                        static_cast<long double>(d.y)};
  point<long double> da = b1 - lhs;
  point<long double> db = d1 - z;
  long double den = cross(da, db);
  if (std::abs(den) <= eps) {
    return std::nullopt;
  }
  long double rat = cross(z - lhs, db) / den;
  return lhs + da * rat;
}

/// @brief Return the convex hull in counter-clockwise order without repetition.
template <class T>
std::vector<point<T>> convex_hull(std::vector<point<T>> pt, bool kc = false) {
  std::sort(pt.begin(), pt.end());
  pt.erase(std::unique(pt.begin(), pt.end()), pt.end());
  if (pt.size() <= 1) {
    return pt;
  }
  bool col = true;
  for (int i = 2; i < int(pt.size()); i++) {
    col &= orientation(pt[0], pt[1], pt[i]) == 0;
  }
  if (kc && col) {
    return pt;
  }
  std::vector<point<T>> lo, hi;
  for (const point<T> &p : pt) {
    while (lo.size() >= 2) {
      int rot = orientation(lo[lo.size() - 2], lo.back(), p);
      if (rot > 0 || (kc && rot == 0)) {
        break;
      }
      lo.pop_back();
    }
    lo.push_back(p);
  }
  for (auto it = pt.rbegin(); it != pt.rend(); ++it) {
    while (hi.size() >= 2) {
      int rot = orientation(hi[hi.size() - 2], hi.back(), *it);
      if (rot > 0 || (kc && rot == 0)) {
        break;
      }
      hi.pop_back();
    }
    hi.push_back(*it);
  }
  lo.pop_back();
  hi.pop_back();
  lo.insert(lo.end(), hi.begin(), hi.end());
  return lo;
}

/// @brief Return twice the signed area of a polygon.
template <class T> T polygon_area2(const std::vector<point<T>> &pg) {
  T res{};
  for (int i = 0; i < int(pg.size()); i++) {
    res += cross(pg[i], pg[(i + 1) % pg.size()]);
  }
  return res;
}

/// @brief Classify a point relative to a polygon: -1 outside, 0 boundary, 1
/// inside.
template <class T>
int point_in_polygon(const point<T> &p, const std::vector<point<T>> &pg) {
  bool in = false;
  for (int i = 0; i < int(pg.size()); i++) {
    point<T> a = pg[i];
    point<T> b = pg[(i + 1) % pg.size()];
    if (on_segment(p, a, b)) {
      return 0;
    }
    if (a.y <= p.y && p.y < b.y && orientation(a, b, p) > 0) {
      in = !in;
    }
    if (b.y <= p.y && p.y < a.y && orientation(a, b, p) < 0) {
      in = !in;
    }
  }
  return in ? 1 : -1;
}

/// @brief Return the Euclidean distance between two points.
template <class T> long double distance(const point<T> &a, const point<T> &b) {
  return std::hypot(
      static_cast<long double>(a.x) - static_cast<long double>(b.x),
      static_cast<long double>(a.y) - static_cast<long double>(b.y));
}

} // namespace noya

#include <algorithm>
#include <array>
#include <cassert>
#include <cmath>
#include <numbers>
#include <vector>

namespace noya {

/// @brief Area of intersection of two closed disks with nonnegative radii.
/// Uses long double; inputs and intermediate squared lengths/areas must be finite.
template <class T, class U>
long double circle_overlap_area(const circle<T> &a, const circle<U> &b) {
  assert(a.r1 >= T{} && b.r1 >= U{});
  long double r = static_cast<long double>(a.r1);
  long double s = static_cast<long double>(b.r1);
  long double d = std::hypot(static_cast<long double>(a.o.x) - b.o.x,
                             static_cast<long double>(a.o.y) - b.o.y);
  const long double pi = std::numbers::pi_v<long double>;
  if (r == 0 || s == 0 || d >= r + s) {
    return 0;
  }
  if (r > s) {
    std::swap(r, s);
  }
  long double cap = pi * std::min(r, s) * std::min(r, s);
  if (d <= std::abs(r - s)) {
    return cap;
  }
  // Measure the chord from the smaller disk. Factoring d^2-s^2 avoids
  // subtracting nearly equal squared lengths for very unequal radii.
  long double x = (r * r + (d - s) * (d + s)) / (2 * d);
  long double y = d - x;
  long double height = std::sqrt(std::max(0.0L, (r - x) * (r + x)));
  long double first = std::atan2(height, x);
  long double second = std::atan2(height, y);
  auto segment = [](long double radius, long double angle) {
    // theta-sin(theta)cos(theta) loses precision for a shallow circular cap.
    long double value;
    if (angle < 1e-3L) {
      long double sq = angle * angle;
      value = angle * sq * (2.0L / 3 + sq * (-2.0L / 15 +
              sq * (4.0L / 315 - sq * (2.0L / 2835))));
    } else {
      value = angle - std::sin(angle) * std::cos(angle);
    }
    return radius * radius * value;
  };
  long double area = segment(r, first) + segment(s, second);
  return std::clamp(area, 0.0L, cap);
}

/// @brief Area shared by a closed disk and a simple polygon, in either winding.
/// Empty/point/segment polygons and zero-radius disks have area zero. Repeating
/// the first vertex at the end is allowed. Each edge is split at circle crossings;
/// inside pieces contribute triangle area, outside pieces contribute sector area.
/// Uses long double; inputs and intermediate squared lengths/areas must be finite.
template <class T, class U>
long double circle_polygon_intersection_area(
    const circle<T> &disk, const std::vector<point<U>> &pg) {
  assert(disk.r1 >= T{});
  long double r = static_cast<long double>(disk.r1);
  if (r == 0 || pg.size() < 3) {
    return 0;
  }
  point<long double> center{static_cast<long double>(disk.o.x),
                             static_cast<long double>(disk.o.y)};
  auto relative = [&](const point<U> &p) {
    return point<long double>{static_cast<long double>(p.x) - center.x,
                               static_cast<long double>(p.y) - center.y};
  };
  long double sum = 0;
  for (int i = 0; i < int(pg.size()); i++) {
    auto a = relative(pg[i]);
    auto b = relative(pg[(i + 1) % pg.size()]);
    auto dir = b - a;
    long double length = std::hypot(dir.x, dir.y);
    if (length == 0) {
      continue;
    }
    dir /= length;
    // Parameterize by signed distance from the line's perpendicular foot.
    // Reconstructing a + (b-a)*t would lose the crossings on very long edges.
    long double height = cross(dir, a);
    long double first = dot(a, dir), last = dot(b, dir);
    std::array<long double, 4> cuts{first, last};
    int count = 2;
    long double remaining = (r - std::abs(height)) * (r + std::abs(height));
    if (remaining > 0) {
      long double offset = std::sqrt(remaining);
      for (long double t : {-offset, offset}) {
        if (first < t && t < last) {
          cuts[count++] = t;
        }
      }
    }
    std::sort(cuts.begin(), cuts.begin() + count);
    for (int j = 0; j + 1 < count; j++) {
      long double lo = cuts[j], hi = cuts[j + 1];
      long double mid = lo / 2 + hi / 2;
      long double cr = height * (lo - hi);
      if (mid * mid < remaining) {
        sum += cr;
      } else {
        sum += r * r * std::atan2(cr, height * height + lo * hi);
      }
    }
  }
  return std::abs(sum) / 2;
}

} // namespace noya

 // NOYA_CIRCLE_POLYGON_AREA_HPP