From 25fdbcb0524c5d95ffbbb08e3f177018dcfdeb96 Mon Sep 17 00:00:00 2001 From: Varphone Wong Date: Wed, 17 Jul 2024 16:41:26 +0800 Subject: [PATCH] `epaint`: Introduce `ArcPieShape` for Simplified Arc and Pie Rendering Enables straightforward painting of arcs and pies using specified start and end angles. --- crates/epaint/src/lib.rs | 4 +- crates/epaint/src/shape_transform.rs | 29 ++++- crates/epaint/src/shapes/arc_pie_shape.rs | 107 +++++++++++++++++ crates/epaint/src/shapes/mod.rs | 2 + crates/epaint/src/shapes/shape.rs | 87 +++++++++++++- crates/epaint/src/stats.rs | 1 + crates/epaint/src/tessellator.rs | 133 +++++++++++++++++++++- 7 files changed, 353 insertions(+), 10 deletions(-) create mode 100644 crates/epaint/src/shapes/arc_pie_shape.rs diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index f84a8caff..e530b8423 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -56,8 +56,8 @@ pub use self::{ mesh::{Mesh, Mesh16, Vertex}, shadow::Shadow, shapes::{ - CircleShape, CubicBezierShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape, - QuadraticBezierShape, RectShape, Shape, TextShape, + ArcPieShape, CircleShape, CubicBezierShape, EllipseShape, PaintCallback, PaintCallbackInfo, + PathShape, QuadraticBezierShape, RectShape, Shape, TextShape, }, stats::PaintStats, stroke::{PathStroke, Stroke, StrokeKind}, diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index 57de14969..047ad93ee 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use crate::{ - color, CircleShape, Color32, ColorMode, CubicBezierShape, EllipseShape, Mesh, PathShape, - QuadraticBezierShape, RectShape, Shape, TextShape, + color, ArcPieShape, CircleShape, Color32, ColorMode, CubicBezierShape, EllipseShape, Mesh, + PathShape, QuadraticBezierShape, RectShape, Shape, TextShape, }; /// Remember to handle [`Color32::PLACEHOLDER`] specially! @@ -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/shapes/arc_pie_shape.rs b/crates/epaint/src/shapes/arc_pie_shape.rs new file mode 100644 index 000000000..5f12a7a26 --- /dev/null +++ b/crates/epaint/src/shapes/arc_pie_shape.rs @@ -0,0 +1,107 @@ +use crate::*; + +/// 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)) + } + } +} diff --git a/crates/epaint/src/shapes/mod.rs b/crates/epaint/src/shapes/mod.rs index 8a42b2c9b..3fbb57f87 100644 --- a/crates/epaint/src/shapes/mod.rs +++ b/crates/epaint/src/shapes/mod.rs @@ -1,3 +1,4 @@ +mod arc_pie_shape; mod bezier_shape; mod circle_shape; mod ellipse_shape; @@ -8,6 +9,7 @@ mod shape; mod text_shape; pub use self::{ + arc_pie_shape::ArcPieShape, bezier_shape::{CubicBezierShape, QuadraticBezierShape}, circle_shape::CircleShape, ellipse_shape::EllipseShape, diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index a855d653a..ace742b17 100644 --- a/crates/epaint/src/shapes/shape.rs +++ b/crates/epaint/src/shapes/shape.rs @@ -11,8 +11,8 @@ use crate::{ }; use super::{ - CircleShape, CubicBezierShape, EllipseShape, PaintCallback, PathShape, QuadraticBezierShape, - RectShape, TextShape, + ArcPieShape, CircleShape, CubicBezierShape, EllipseShape, PaintCallback, PathShape, + QuadraticBezierShape, RectShape, TextShape, }; /// A paint primitive such as a circle or a piece of text. @@ -32,6 +32,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), @@ -255,6 +258,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)) @@ -366,6 +443,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 } => { @@ -427,6 +505,11 @@ impl Shape { shape.transform(transform); } } + 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::Circle(circle_shape) => { circle_shape.center = transform * circle_shape.center; circle_shape.radius *= transform.scaling; diff --git a/crates/epaint/src/stats.rs b/crates/epaint/src/stats.rs index 2acf1e93c..901d54ec7 100644 --- a/crates/epaint/src/stats.rs +++ b/crates/epaint/src/stats.rs @@ -203,6 +203,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 2b24869ae..6741ffb29 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -8,10 +8,10 @@ use emath::{pos2, remap, vec2, GuiRounding as _, NumExt, Pos2, Rect, Rot2, Vec2}; use crate::{ - color::ColorMode, emath, stroke::PathStroke, texture_atlas::PreparedDisc, CircleShape, - ClippedPrimitive, ClippedShape, Color32, CornerRadiusF32, CubicBezierShape, EllipseShape, Mesh, - PathShape, Primitive, QuadraticBezierShape, RectShape, Shape, Stroke, StrokeKind, TextShape, - TextureId, Vertex, WHITE_UV, + color::ColorMode, emath, stroke::PathStroke, texture_atlas::PreparedDisc, ArcPieShape, + CircleShape, ClippedPrimitive, ClippedShape, Color32, CornerRadiusF32, CubicBezierShape, + EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, Shape, Stroke, + StrokeKind, TextShape, TextureId, Vertex, WHITE_UV, }; // ---------------------------------------------------------------------------- @@ -475,6 +475,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); + } + /// The path is taken to be closed (i.e. returning to the start again). /// /// Calling this may reverse the vertices in the path if they are wrong winding order. @@ -1429,6 +1482,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); } @@ -1481,6 +1537,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. @@ -2288,6 +2412,7 @@ impl Tessellator { Shape::Noop | Shape::Text(_) + | Shape::ArcPie(_) | Shape::Circle(_) | Shape::Mesh(_) | Shape::LineSegment { .. }