diff --git a/crates/geom/src/curve.rs b/crates/geom/src/curve.rs index c6be8da..9d45453 100644 --- a/crates/geom/src/curve.rs +++ b/crates/geom/src/curve.rs @@ -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 { diff --git a/crates/geom/src/tests.rs b/crates/geom/src/tests.rs index d6f1320..a9f5daf 100644 --- a/crates/geom/src/tests.rs +++ b/crates/geom/src/tests.rs @@ -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); +}