diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index c83be860..8746d564 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -47,8 +47,8 @@ pub use { mesh::{Mesh, Mesh16, Vertex}, shadow::Shadow, shape::{ - CircleShape, PaintCallback, PaintCallbackInfo, PathShape, RectShape, Rounding, Shape, - TextShape, + CircleShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape, RectShape, + Rounding, Shape, TextShape, }, stats::PaintStats, stroke::Stroke, diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index fefae6c9..3c6f1703 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -30,6 +30,9 @@ pub enum Shape { /// Circle with optional outline and fill. Circle(CircleShape), + /// Ellipse with optional outline and fill. + Ellipse(EllipseShape), + /// A line between two points. LineSegment { points: [Pos2; 2], stroke: Stroke }, @@ -236,6 +239,16 @@ impl Shape { Self::Circle(CircleShape::stroke(center, radius, stroke)) } + #[inline] + pub fn ellipse_filled(center: Pos2, radius: Vec2, fill_color: impl Into) -> Self { + Self::Ellipse(EllipseShape::filled(center, radius, fill_color)) + } + + #[inline] + pub fn ellipse_stroke(center: Pos2, radius: Vec2, stroke: impl Into) -> Self { + Self::Ellipse(EllipseShape::stroke(center, radius, stroke)) + } + #[inline] pub fn rect_filled( rect: Rect, @@ -324,6 +337,7 @@ impl Shape { rect } Self::Circle(circle_shape) => circle_shape.visual_bounding_rect(), + Self::Ellipse(ellipse_shape) => ellipse_shape.visual_bounding_rect(), Self::LineSegment { points, stroke } => { if stroke.is_empty() { Rect::NOTHING @@ -388,6 +402,11 @@ impl Shape { circle_shape.radius *= transform.scaling; circle_shape.stroke.width *= transform.scaling; } + Self::Ellipse(ellipse_shape) => { + ellipse_shape.center = transform * ellipse_shape.center; + ellipse_shape.radius *= transform.scaling; + ellipse_shape.stroke.width *= transform.scaling; + } Self::LineSegment { points, stroke } => { for p in points { *p = transform * *p; @@ -497,6 +516,61 @@ impl From for Shape { // ---------------------------------------------------------------------------- +/// How to paint an ellipse. +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct EllipseShape { + pub center: Pos2, + + /// Radius is the vector (a, b) where the width of the Ellipse is 2a and the height is 2b + pub radius: Vec2, + pub fill: Color32, + pub stroke: Stroke, +} + +impl EllipseShape { + #[inline] + pub fn filled(center: Pos2, radius: Vec2, fill_color: impl Into) -> Self { + Self { + center, + radius, + fill: fill_color.into(), + stroke: Default::default(), + } + } + + #[inline] + pub fn stroke(center: Pos2, radius: Vec2, stroke: impl Into) -> Self { + Self { + center, + radius, + fill: Default::default(), + stroke: stroke.into(), + } + } + + /// The visual bounding rectangle (includes stroke width) + pub fn visual_bounding_rect(&self) -> Rect { + if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() { + Rect::NOTHING + } else { + Rect::from_center_size( + self.center, + self.radius * 2.0 + Vec2::splat(self.stroke.width), + ) + } + } +} + +impl From for Shape { + #[inline(always)] + fn from(shape: EllipseShape) -> Self { + Self::Ellipse(shape) + } +} + +// ---------------------------------------------------------------------------- + /// A path which can be stroked and/or filled (if closed). #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index c8edff1f..7f4d3358 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -20,6 +20,12 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { fill, stroke, }) + | Shape::Ellipse(EllipseShape { + center: _, + radius: _, + fill, + stroke, + }) | Shape::Path(PathShape { points: _, closed: _, diff --git a/crates/epaint/src/stats.rs b/crates/epaint/src/stats.rs index 56eaa24a..3bb51e14 100644 --- a/crates/epaint/src/stats.rs +++ b/crates/epaint/src/stats.rs @@ -201,6 +201,7 @@ impl PaintStats { } Shape::Noop | Shape::Circle { .. } + | Shape::Ellipse { .. } | Shape::LineSegment { .. } | Shape::Rect { .. } | Shape::CubicBezier(_) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index c753b737..bf419e99 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1215,6 +1215,9 @@ impl Tessellator { Shape::Circle(circle) => { self.tessellate_circle(circle, out); } + Shape::Ellipse(ellipse) => { + self.tessellate_ellipse(ellipse, out); + } Shape::Mesh(mesh) => { crate::profile_scope!("mesh"); @@ -1315,6 +1318,73 @@ impl Tessellator { .stroke_closed(self.feathering, stroke, out); } + /// Tessellate a single [`EllipseShape`] into a [`Mesh`]. + /// + /// * `shape`: the ellipse to tessellate. + /// * `out`: triangles are appended to this. + pub fn tessellate_ellipse(&mut self, shape: EllipseShape, out: &mut Mesh) { + let EllipseShape { + center, + radius, + fill, + stroke, + } = shape; + + if radius.x <= 0.0 || radius.y <= 0.0 { + return; + } + + if self.options.coarse_tessellation_culling + && !self + .clip_rect + .expand2(radius + Vec2::splat(stroke.width)) + .contains(center) + { + return; + } + + // Get the max pixel radius + let max_radius = (radius.max_elem() * self.pixels_per_point) as u32; + + // Ensure there is at least 8 points in each quarter of the ellipse + let num_points = u32::max(8, max_radius / 16); + + // Create an ease ratio based the ellipses a and b + let ratio = ((radius.y / radius.x) / 2.0).clamp(0.0, 1.0); + + // Generate points between the 0 to pi/2 + let quarter: Vec = (1..num_points) + .map(|i| { + let percent = i as f32 / num_points as f32; + + // Ease the percent value, concentrating points around tight bends + let eased = 2.0 * (percent - percent.powf(2.0)) * ratio + percent.powf(2.0); + + // Scale the ease to the quarter + let t = eased * std::f32::consts::FRAC_PI_2; + Vec2::new(radius.x * f32::cos(t), radius.y * f32::sin(t)) + }) + .collect(); + + // Build the ellipse from the 4 known vertices filling arcs between + // them by mirroring the points between 0 and pi/2 + let mut points = Vec::new(); + points.push(center + Vec2::new(radius.x, 0.0)); + points.extend(quarter.iter().map(|p| center + *p)); + points.push(center + Vec2::new(0.0, radius.y)); + points.extend(quarter.iter().rev().map(|p| center + Vec2::new(-p.x, p.y))); + points.push(center + Vec2::new(-radius.x, 0.0)); + points.extend(quarter.iter().map(|p| center - *p)); + points.push(center + Vec2::new(0.0, -radius.y)); + points.extend(quarter.iter().rev().map(|p| center + Vec2::new(p.x, -p.y))); + + self.scratchpad_path.clear(); + self.scratchpad_path.add_line_loop(&points); + self.scratchpad_path.fill(self.feathering, fill, out); + self.scratchpad_path + .stroke_closed(self.feathering, stroke, out); + } + /// Tessellate a single [`Mesh`] into a [`Mesh`]. /// /// * `mesh`: the mesh to tessellate. @@ -1776,7 +1846,7 @@ impl Tessellator { Shape::Path(path_shape) => 32 < path_shape.points.len(), - Shape::QuadraticBezier(_) | Shape::CubicBezier(_) => true, + Shape::QuadraticBezier(_) | Shape::CubicBezier(_) | Shape::Ellipse(_) => true, Shape::Noop | Shape::Text(_)