fix(geom): measure bulged polylines and distant polygons accurately (#212)

* fix(geom): compute polyline bulge areas without tessellation error

* fix(geom): preserve polygon area far from the coordinate origin
This commit is contained in:
Nigel Ihza Firdausy
2026-10-10 14:38:57 -04:00
committed by GitHub
parent f38b3a6447
commit c52f4d190e
2 changed files with 73 additions and 3 deletions
+31 -3
View File
@@ -514,8 +514,31 @@ impl Polyline {
}
/// Signed area (CCW positive) of a closed polyline including bulge areas.
pub fn area(&self) -> f64 {
let pts = self.tessellate(1e-4);
shoelace(&pts)
// Integrate the chords exactly and add the signed circular-segment area
// for every bulge. A tessellated shoelace loses precision on large arcs.
// Working relative to the first vertex avoids cancellation when a small
// shape is located far from the drawing origin.
let Some(first) = self.vertices.first() else {
return 0.0;
};
let origin = first.p;
let n = self.vertices.len();
let mut twice_area = 0.0;
for i in 0..n {
let (Some(a), Some(b)) = (self.vertices.get(i), self.vertices.get((i + 1) % n)) else {
continue;
};
twice_area += (a.p - origin).cross(b.p - origin);
// Open polylines keep their historical implicit straight closing edge.
// The final vertex's bulge is only used for an explicit closing segment.
if (i + 1 < n || self.closed)
&& let Some((arc, ccw)) = bulge_to_arc(a.p, b.p, a.bulge)
{
let sweep = if ccw { arc.sweep() } else { -arc.sweep() };
twice_area += arc.radius * arc.radius * (sweep - sweep.sin());
}
}
twice_area / 2.0
}
pub fn closest(&self, p: Vec2) -> Option<Vec2> {
let mut best: Option<(f64, Vec2)> = None;
@@ -540,10 +563,15 @@ pub fn shoelace(pts: &[Vec2]) -> f64 {
if n < 3 {
return 0.0;
}
let Some(origin) = pts.first().copied() else {
return 0.0;
};
// Measure about a local origin so a small polygon at a large world
// coordinate does not lose its area to cancellation of huge products.
let mut s = 0.0;
for i in 0..n {
if let (Some(a), Some(b)) = (pts.get(i), pts.get((i + 1) % n)) {
s += a.cross(*b);
s += (*a - origin).cross(*b - origin);
}
}
s / 2.0
+42
View File
@@ -236,3 +236,45 @@ fn elliptical_arc_bounds_exclude_extrema_outside_the_sweep() {
assert!((bounds.min.y - e.at_param(e.end).y).abs() < 1e-12);
assert!((bounds.max.y - 18.25_f64.sqrt()).abs() < 1e-12);
}
#[test]
fn polyline_area_is_exact_for_bulge_segments() {
// One semicircular arc from left to right plus a straight closing edge.
let mut p = Polyline::from_points(&[Vec2::new(0.0, 0.0), Vec2::new(2.0, 0.0)], true);
p.vertices[0].bulge = 1.0;
assert!((p.area() - PI / 2.0).abs() < 1e-12, "{}", p.area());
p.vertices[0].bulge = -1.0;
assert!((p.area() + PI / 2.0).abs() < 1e-12, "{}", p.area());
}
#[test]
fn polyline_area_is_stable_far_from_the_origin() {
let o = 1.0e12;
let p = Polyline::from_points(
&[
Vec2::new(o, o),
Vec2::new(o + 10.0, o),
Vec2::new(o + 10.0, o + 10.0),
Vec2::new(o, o + 10.0),
],
true,
);
assert!((p.area() - 100.0).abs() < 1e-12, "{}", p.area());
// An open polyline's measured area retains the implicit straight closing edge.
let mut open = p;
open.closed = false;
assert!((open.area() - 100.0).abs() < 1e-12);
}
#[test]
fn shoelace_preserves_small_polygon_area_at_large_coordinates() {
let o = 1.0e12;
let corners = [
Vec2::new(o, o),
Vec2::new(o + 10.0, o),
Vec2::new(o + 10.0, o + 10.0),
Vec2::new(o, o + 10.0),
];
assert_eq!(shoelace(&corners), 100.0);
assert_eq!(shoelace(&corners.iter().rev().copied().collect::<Vec<_>>()), -100.0);
}