fix(geom): calculate tight ellipse and arc bounds analytically (#211)

This commit is contained in:
Nigel Ihza Firdausy 2026-10-11 01:38:42 +07:00 committed by GitHub
parent a560048dd0
commit f38b3a6447
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 46 additions and 3 deletions

View File

@ -256,9 +256,22 @@ impl Ellipse {
}
}
pub fn bounds(&self) -> Bounds2 {
let mut pts = Vec::new();
self.tessellate(self.major.len() * 1e-4, &mut pts);
Bounds2::from_points(pts)
// Sampled bounds can cut off the true extrema, especially on a small arc or
// a large drawing. The derivative of each coordinate gives two exact
// candidate parameters (opposite sides of the ellipse).
let minor = self.minor();
let mut bounds = Bounds2::from_points([self.at_param(self.start), self.at_param(self.end)]);
for angle in [
minor.x.atan2(self.major.x),
minor.x.atan2(self.major.x) + PI,
minor.y.atan2(self.major.y),
minor.y.atan2(self.major.y) + PI,
] {
if angle_in_sweep(angle, self.start, self.end) {
bounds.add(self.at_param(angle));
}
}
bounds
}
/// Parameter of the point on the ellipse nearest the direction of `p` (approximate).
pub fn param_of(&self, p: Vec2) -> f64 {

View File

@ -206,3 +206,33 @@ fn circle_three_points_accepts_small_valid_triangles() {
assert!(Circle::from_3_points(Vec2::ZERO, Vec2::new(r, 0.0), Vec2::new(2.0 * r, 0.0)).is_none());
assert!(Circle::from_3_points(Vec2::ZERO, Vec2::X, Vec2::new(f64::NAN, 0.0)).is_none());
}
#[test]
fn ellipse_bounds_find_analytic_extrema() {
// A tilted ellipse's extrema generally fall between tessellated samples.
let center = Vec2::new(7.0, -11.0);
let e = Ellipse::full(center, Vec2::new(3.0, 4.0), 0.5);
let bounds = e.bounds();
let dx = 13.0_f64.sqrt(); // hypot(major.x, minor.x)
let dy = 18.25_f64.sqrt(); // hypot(major.y, minor.y)
assert!((bounds.min.x - (center.x - dx)).abs() < 1e-12);
assert!((bounds.max.x - (center.x + dx)).abs() < 1e-12);
assert!((bounds.min.y - (center.y - dy)).abs() < 1e-12);
assert!((bounds.max.y - (center.y + dy)).abs() < 1e-12);
}
#[test]
fn elliptical_arc_bounds_exclude_extrema_outside_the_sweep() {
let e = Ellipse {
center: Vec2::ZERO,
major: Vec2::new(3.0, 4.0),
ratio: 0.5,
start: 0.0,
end: PI / 4.0,
};
let bounds = e.bounds();
assert!((bounds.max.x - 3.0).abs() < 1e-12);
assert!((bounds.min.x - e.at_param(e.end).x).abs() < 1e-12);
assert!((bounds.min.y - e.at_param(e.end).y).abs() < 1e-12);
assert!((bounds.max.y - 18.25_f64.sqrt()).abs() < 1e-12);
}