mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-31 22:36:51 -06:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user