diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index 4dee42381..a30d02533 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -49,8 +49,8 @@ pub use self::{ mesh::{Mesh, Mesh16, Vertex}, shadow::Shadow, shape::{ - CircleShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape, RectShape, - Rounding, Shape, TextShape, + ArcPieShape, CircleShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape, + RectShape, Rounding, Shape, TextShape, }, stats::PaintStats, stroke::{PathStroke, Stroke}, diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 6f67a2bc6..de2717f55 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -28,6 +28,9 @@ pub enum Shape { /// For performance reasons it is better to avoid it. Vec(Vec), + /// An arc or pie with a given start and end angle. + ArcPie(ArcPieShape), + /// Circle with optional outline and fill. Circle(CircleShape), @@ -233,6 +236,80 @@ impl Shape { Self::Path(PathShape::convex_polygon(points, fill, stroke)) } + /// Generates an arc with a given start and end angle. + /// + /// This function creates an arc centered at a specified point, with a specified radius. + /// The arc starts at the `start_angle` and ends at the `end_angle`. + /// Angles are specified in radians, with positive angles indicating clockwise rotation and negative angles indicating counterclockwise rotation. + /// + /// # Arguments + /// + /// * `center` - The center point of the arc. + /// * `radius` - The radius of the arc. + /// * `start_angle` - The start angle of the arc, in radians. + /// * `end_angle` - The end angle of the arc, in radians. + /// * `stroke` - The stroke of the arc. + /// + /// # Example + /// + /// ```no_run + /// # use epaint::{pos2, Color32, Shape, Stroke}; + /// let arc = Shape::arc(pos2(100.0, 100.0), 50.0, 0.0, std::f32::consts::PI, Stroke::new(3.0, Color32::RED)); + /// ``` + pub fn arc( + center: Pos2, + radius: f32, + start_angle: f32, + end_angle: f32, + stroke: impl Into, + ) -> Self { + Self::ArcPie(ArcPieShape::arc( + center, + radius, + start_angle, + end_angle, + stroke, + )) + } + + /// Generates an pie with a given start and end angle. + /// + /// This function creates an arc centered at a specified point, with a specified radius. + /// The pie starts at the `start_angle` and ends at the `end_angle`. + /// Angles are specified in radians, with positive angles indicating clockwise rotation and negative angles indicating counterclockwise rotation. + /// + /// # Arguments + /// + /// * `center` - The center point of the pie. + /// * `radius` - The radius of the pie. + /// * `start_angle` - The start angle of the pie, in radians. + /// * `end_angle` - The end angle of the pie, in radians. + /// * `stroke` - The stroke of the pie. + /// + /// # Example + /// + /// ```no_run + /// # use epaint::{pos2, Color32, Shape, Stroke}; + /// let pie = Shape::pie(pos2(100.0, 100.0), 50.0, 0.0, std::f32::consts::PI, Color32::BLUE, Stroke::new(3.0, Color32::RED)); + /// ``` + pub fn pie( + center: Pos2, + radius: f32, + start_angle: f32, + end_angle: f32, + fill: impl Into, + stroke: impl Into, + ) -> Self { + Self::ArcPie(ArcPieShape::pie( + center, + radius, + start_angle, + end_angle, + fill, + stroke, + )) + } + #[inline] pub fn circle_filled(center: Pos2, radius: f32, fill_color: impl Into) -> Self { Self::Circle(CircleShape::filled(center, radius, fill_color)) @@ -340,6 +417,7 @@ impl Shape { } rect } + Self::ArcPie(arc_pie_shape) => arc_pie_shape.visual_bounding_rect(), Self::Circle(circle_shape) => circle_shape.visual_bounding_rect(), Self::Ellipse(ellipse_shape) => ellipse_shape.visual_bounding_rect(), Self::LineSegment { points, stroke } => { @@ -428,6 +506,11 @@ impl Shape { rect_shape.stroke.width *= transform.scaling; rect_shape.rounding *= transform.scaling; } + Self::ArcPie(arc_pie_shape) => { + arc_pie_shape.center = transform * arc_pie_shape.center; + arc_pie_shape.radius *= transform.scaling; + arc_pie_shape.stroke.width *= transform.scaling; + } Self::Text(text_shape) => { text_shape.pos = transform * text_shape.pos; @@ -467,6 +550,114 @@ impl Shape { // ---------------------------------------------------------------------------- +/// A arc or pie slice with a given start and end angle. +#[derive(Clone, Debug, PartialEq)] +pub struct ArcPieShape { + pub center: Pos2, + pub radius: f32, + pub start_angle: f32, + pub end_angle: f32, + pub closed: bool, + pub fill: Color32, + pub stroke: PathStroke, +} + +impl ArcPieShape { + /// Create a new arc or pie shape. + /// + /// # Arguments + /// + /// * `center` - The center of the arc or pie. + /// * `radius` - The radius of the arc or pie. + /// * `start_angle` - The start angle of the arc or pie, in radians. + /// * `end_angle` - The end angle of the arc or pie, in radians. + /// * `closed` - If true, connect the center with the start and end points. + /// * `fill` - The fill color of the arc or pie. + /// * `stroke` - The stroke of the arc or pie. + pub fn new( + center: Pos2, + radius: f32, + start_angle: f32, + end_angle: f32, + closed: bool, + fill: impl Into, + stroke: impl Into, + ) -> Self { + Self { + center, + radius, + start_angle, + end_angle, + closed, + fill: fill.into(), + stroke: stroke.into(), + } + } + + /// Create a new arc shape. + /// + /// # Arguments + /// + /// * `center` - The center of the arc. + /// * `radius` - The radius of the arc. + /// * `start_angle` - The start angle of the arc, in radians. + /// * `end_angle` - The end angle of the arc, in radians. + /// * `stroke` - The stroke of the arc. + pub fn arc( + center: Pos2, + radius: f32, + start_angle: f32, + end_angle: f32, + stroke: impl Into, + ) -> Self { + Self::new( + center, + radius, + start_angle, + end_angle, + false, + Color32::TRANSPARENT, + stroke, + ) + } + + /// Create a new pie shape. + /// + /// # Arguments + /// + /// * `center` - The center of the pie. + /// * `radius` - The radius of the pie. + /// * `start_angle` - The start angle of the pie, in radians. + /// * `end_angle` - The end angle of the pie, in radians. + /// * `fill` - The fill color of the pie. + /// * `stroke` - The stroke of the pie. + pub fn pie( + center: Pos2, + radius: f32, + start_angle: f32, + end_angle: f32, + fill: impl Into, + stroke: impl Into, + ) -> Self { + Self::new(center, radius, start_angle, end_angle, true, fill, stroke) + } + + /// 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 { + let rect = Rect::from_center_size(self.center, vec2(self.radius, self.radius)); + let start = + self.center + vec2(self.start_angle.cos(), self.start_angle.sin()) * self.radius; + let end = self.center + vec2(self.end_angle.cos(), self.end_angle.sin()) * self.radius; + rect.union(Rect::from_two_pos(start, end)) + } + } +} + +// ---------------------------------------------------------------------------- + /// How to paint a circle. #[derive(Copy, 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 f072393f5..e74a9ebdb 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -46,6 +46,31 @@ pub fn adjust_colors( adjust_color_mode(&mut stroke.color, adjust_color); } + Shape::ArcPie(ArcPieShape { + center: _, + radius: _, + start_angle: _, + end_angle: _, + closed, + fill, + stroke, + }) => { + if *closed { + adjust_color(fill); + } + match &stroke.color { + color::ColorMode::Solid(mut col) => adjust_color(&mut col), + color::ColorMode::UV(callback) => { + let callback = callback.clone(); + stroke.color = color::ColorMode::UV(Arc::new(Box::new(move |rect, pos| { + let mut col = callback(rect, pos); + adjust_color(&mut col); + col + }))); + } + } + } + Shape::Circle(CircleShape { center: _, radius: _, diff --git a/crates/epaint/src/stats.rs b/crates/epaint/src/stats.rs index 68bba622e..bb97ca7cd 100644 --- a/crates/epaint/src/stats.rs +++ b/crates/epaint/src/stats.rs @@ -200,6 +200,7 @@ impl PaintStats { } } Shape::Noop + | Shape::ArcPie { .. } | Shape::Circle { .. } | Shape::Ellipse { .. } | Shape::LineSegment { .. } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index fdbe27091..c5a491913 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -477,6 +477,59 @@ impl Path { } } + fn add_arc_pie( + &mut self, + center: Pos2, + radius: f32, + start_angle: f32, + end_angle: f32, + closed: bool, + ) { + use std::f32::consts::TAU; + + let num_segs = if radius <= 2.0 { + 8 + } else if radius <= 5.0 { + 16 + } else if radius < 18.0 { + 32 + } else if radius < 50.0 { + 64 + } else { + 128 + }; + + let angle = (end_angle - start_angle).clamp(-TAU + f32::EPSILON, TAU - f32::EPSILON); + let mut points = Vec::with_capacity(num_segs + 3); + let step = angle / num_segs as f32; + if closed { + points.push(center); + } + for i in 0..=num_segs { + let a = start_angle + step * i as f32; + points.push(pos2( + center.x + radius * a.cos(), + center.y + radius * a.sin(), + )); + } + if closed { + points.push(center); + self.add_line_loop(&points[..]); + } else { + self.add_open_points(&points[..]); + } + } + + /// Add an arc line. + pub fn add_arc(&mut self, center: Pos2, radius: f32, start_angle: f32, end_angle: f32) { + self.add_arc_pie(center, radius, start_angle, end_angle, false); + } + + /// Add a pie slice. + pub fn add_pie(&mut self, center: Pos2, radius: f32, start_angle: f32, end_angle: f32) { + self.add_arc_pie(center, radius, start_angle, end_angle, true); + } + /// Open-ended. pub fn stroke_open(&mut self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { stroke_path(feathering, &mut self.0, PathType::Open, stroke, out); @@ -1356,6 +1409,9 @@ impl Tessellator { self.tessellate_shape(shape, out); } } + Shape::ArcPie(arc_pie_shape) => { + self.tessellate_arc_pie(arc_pie_shape, out); + } Shape::Circle(circle) => { self.tessellate_circle(circle, out); } @@ -1406,6 +1462,74 @@ impl Tessellator { } } + /// Tessellate a single [`ArcPieShape`] into a [`Mesh`]. + /// + /// * `arc_pie_shape`: the arc or pie to tessellate. + /// * `out`: triangles are appended to this. + pub fn tessellate_arc_pie(&mut self, arc_pie_shape: ArcPieShape, out: &mut Mesh) { + let ArcPieShape { + center, + radius, + start_angle, + end_angle, + closed, + fill, + stroke, + } = arc_pie_shape; + + if radius <= 0.0 + || start_angle == end_angle + || stroke.width <= 0.0 && (!closed || fill == Color32::TRANSPARENT) + { + return; + } + + if self.options.coarse_tessellation_culling + && !self + .clip_rect + .expand(radius + stroke.width) + .contains(center) + { + return; + } + + // If the arc is a full circle, we can just use the circle function. + if (end_angle - start_angle).abs() >= std::f32::consts::TAU { + let stroke_color = match stroke.color { + ColorMode::Solid(color) => color, + ColorMode::UV(callback) => { + // TODO: Currently, CircleShape does not support PathStroke. + // As a workaround, the stroke color is set to the center color. + // This needs to be revisited once CircleShape gains PathStroke support. + callback(Rect::from_center_size(center, Vec2::splat(radius)), center) + } + }; + let stroke = Stroke::new(stroke.width, stroke_color); + let circle = CircleShape { + center, + radius, + fill, + stroke, + }; + return self.tessellate_circle(circle, out); + } + + self.scratchpad_path.clear(); + + if closed { + self.scratchpad_path + .add_pie(center, radius, start_angle, end_angle); + self.scratchpad_path.fill(self.feathering, fill, out); + self.scratchpad_path + .stroke_closed(self.feathering, &stroke, out); + } else { + self.scratchpad_path + .add_arc(center, radius, start_angle, end_angle); + self.scratchpad_path + .stroke_open(self.feathering, &stroke, out); + } + } + /// Tessellate a single [`CircleShape`] into a [`Mesh`]. /// /// * `shape`: the circle to tessellate. @@ -2041,6 +2165,7 @@ impl Tessellator { Shape::Noop | Shape::Text(_) + | Shape::ArcPie(_) | Shape::Circle(_) | Shape::Mesh(_) | Shape::LineSegment { .. }