diff --git a/egui_demo_lib/src/apps/demo/demo_app_windows.rs b/egui_demo_lib/src/apps/demo/demo_app_windows.rs index 6c4b69e7cfb..f6eb1600d06 100644 --- a/egui_demo_lib/src/apps/demo/demo_app_windows.rs +++ b/egui_demo_lib/src/apps/demo/demo_app_windows.rs @@ -16,6 +16,7 @@ struct Demos { impl Default for Demos { fn default() -> Self { Self::from_demos(vec![ + Box::new(super::paint_bezier::PaintBezier::default()), Box::new(super::code_editor::CodeEditor::default()), Box::new(super::code_example::CodeExample::default()), Box::new(super::context_menu::ContextMenus::default()), @@ -25,7 +26,6 @@ impl Default for Demos { Box::new(super::MiscDemoWindow::default()), Box::new(super::multi_touch::MultiTouch::default()), Box::new(super::painting::Painting::default()), - Box::new(super::paint_bezier::PaintBezier::default()), Box::new(super::plot_demo::PlotDemo::default()), Box::new(super::scrolling::Scrolling::default()), Box::new(super::sliders::Sliders::default()), diff --git a/egui_demo_lib/src/apps/demo/paint_bezier.rs b/egui_demo_lib/src/apps/demo/paint_bezier.rs index d315b7b2d9a..d96e68a482b 100644 --- a/egui_demo_lib/src/apps/demo/paint_bezier.rs +++ b/egui_demo_lib/src/apps/demo/paint_bezier.rs @@ -1,52 +1,40 @@ -use egui::emath::RectTransform; -use egui::epaint::{CircleShape, CubicBezierShape, QuadraticBezierShape}; +use egui::epaint::{CubicBezierShape, PathShape, QuadraticBezierShape}; use egui::*; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct PaintBezier { - /// Current bezier curve degree, it can be 3, 4. - bezier: usize, - /// Track the bezier degree before change in order to clean the remaining points. - degree_backup: usize, - /// Points already clicked. once it reaches the 'bezier' degree, it will be pushed into the 'shapes' - points: Vec, - /// Track last points set in order to draw auxiliary lines. - backup_points: Vec, - /// Quadratic shapes already drawn. - q_shapes: Vec, - /// Cubic shapes already drawn. - /// Since `Shape` can't be 'serialized', we can't use Shape as variable type. - c_shapes: Vec, - /// Stroke for auxiliary lines. - aux_stroke: Stroke, - /// Stroke for bezier curve. + /// Bézier curve degree, it can be 3, 4. + degree: usize, + /// The control points. The [`Self::degree`] first of them are used. + control_points: [Pos2; 4], + + /// Stroke for Bézier curve. stroke: Stroke, - /// Fill for bezier curve. + + /// Fill for Bézier curve. fill: Color32, - /// The curve should be closed or not. - closed: bool, - /// Display the bounding box or not. - show_bounding_box: bool, - /// Storke for the bounding box. + + /// Stroke for auxiliary lines. + aux_stroke: Stroke, + bounding_box_stroke: Stroke, } impl Default for PaintBezier { fn default() -> Self { Self { - bezier: 4, // default bezier degree, a cubic bezier curve - degree_backup: 4, - points: Default::default(), - backup_points: Default::default(), - q_shapes: Default::default(), - c_shapes: Default::default(), - aux_stroke: Stroke::new(1.0, Color32::RED), + degree: 4, + control_points: [ + pos2(50.0, 50.0), + pos2(60.0, 150.0), + pos2(140.0, 150.0), + pos2(150.0, 50.0), + ], stroke: Stroke::new(1.0, Color32::LIGHT_BLUE), - fill: Default::default(), - closed: false, - show_bounding_box: false, - bounding_box_stroke: Stroke::new(1.0, Color32::LIGHT_GREEN), + fill: Color32::from_rgb(50, 100, 150).linear_multiply(0.25), + aux_stroke: Stroke::new(1.0, Color32::RED.linear_multiply(0.25)), + bounding_box_stroke: Stroke::new(0.0, Color32::LIGHT_GREEN.linear_multiply(0.25)), } } } @@ -55,187 +43,125 @@ impl PaintBezier { pub fn ui_control(&mut self, ui: &mut egui::Ui) -> egui::Response { ui.horizontal(|ui| { ui.vertical(|ui| { - egui::stroke_ui(ui, &mut self.stroke, "Curve Stroke"); - egui::stroke_ui(ui, &mut self.aux_stroke, "Auxiliary Stroke"); + ui.radio_value(&mut self.degree, 3, "Quadratic Bézier"); + ui.radio_value(&mut self.degree, 4, "Cubic Bézier"); + ui.label("Move the points by dragging them."); + ui.label("Only convex curves can be accurately filled.") + }); + + ui.separator(); + + ui.vertical(|ui| { ui.horizontal(|ui| { - ui.label("Fill Color:"); - if ui.color_edit_button_srgba(&mut self.fill).changed() - && self.fill != Color32::TRANSPARENT - { - self.closed = true; - } - if ui.checkbox(&mut self.closed, "Closed").clicked() && !self.closed { - self.fill = Color32::TRANSPARENT; - } + ui.label("Fill color:"); + ui.color_edit_button_srgba(&mut self.fill); }); + egui::stroke_ui(ui, &mut self.stroke, "Curve Stroke"); + egui::stroke_ui(ui, &mut self.aux_stroke, "Auxiliary Stroke"); egui::stroke_ui(ui, &mut self.bounding_box_stroke, "Bounding Box Stroke"); }); ui.separator(); + ui.vertical(|ui| { - { - let mut tessellation_options = *(ui.ctx().tessellation_options()); - let tessellation_options = &mut tessellation_options; - tessellation_options.ui(ui); - let mut new_tessellation_options = ui.ctx().tessellation_options(); - *new_tessellation_options = *tessellation_options; - } - - ui.checkbox(&mut self.show_bounding_box, "Bounding Box"); + ui.label("Global tessellation options:"); + let mut tessellation_options = *(ui.ctx().tessellation_options()); + let tessellation_options = &mut tessellation_options; + tessellation_options.ui(ui); + let mut new_tessellation_options = ui.ctx().tessellation_options(); + *new_tessellation_options = *tessellation_options; }); - ui.separator(); - ui.vertical(|ui| { - if ui.radio_value(&mut self.bezier, 3, "Quadratic").clicked() - && self.degree_backup != self.bezier - { - self.points.clear(); - self.degree_backup = self.bezier; - }; - if ui.radio_value(&mut self.bezier, 4, "Cubic").clicked() - && self.degree_backup != self.bezier - { - self.points.clear(); - self.degree_backup = self.bezier; - }; - // ui.radio_value(self.bezier, 5, "Quintic"); - ui.label("Click 3 or 4 points to build a bezier curve!"); - if ui.button("Clear Painting").clicked() { - self.points.clear(); - self.backup_points.clear(); - self.q_shapes.clear(); - self.c_shapes.clear(); - } - }) }) .response } pub fn ui_content(&mut self, ui: &mut Ui) -> egui::Response { - let (mut response, painter) = - ui.allocate_painter(ui.available_size_before_wrap(), Sense::click()); + let (response, painter) = + ui.allocate_painter(Vec2::new(ui.available_width(), 300.0), Sense::hover()); let to_screen = emath::RectTransform::from_to( - Rect::from_min_size(Pos2::ZERO, response.rect.square_proportions()), + Rect::from_min_size(Pos2::ZERO, response.rect.size()), response.rect, ); - let from_screen = to_screen.inverse(); - - if response.clicked() { - if let Some(pointer_pos) = response.interact_pointer_pos() { - let canvas_pos = from_screen * pointer_pos; - self.points.push(canvas_pos); - if self.points.len() >= self.bezier { - self.backup_points = self.points.clone(); - let points = self.points.drain(..).collect::>(); - match points.len() { - 3 => { - let quadratic = QuadraticBezierShape::from_points_stroke( - points, - self.closed, - self.fill, - self.stroke, - ); - self.q_shapes.push(quadratic); - } - 4 => { - let cubic = CubicBezierShape::from_points_stroke( - points, - self.closed, - self.fill, - self.stroke, - ); - self.c_shapes.push(cubic); - } - _ => { - unreachable!(); - } - } - } - - response.mark_changed(); - } - } - let mut shapes = Vec::new(); - for shape in self.q_shapes.iter() { - shapes.push(shape.to_screen(&to_screen).into()); - if self.show_bounding_box { - shapes.push(self.build_bounding_box(shape.bounding_rect(), &to_screen)); - } - } - for shape in self.c_shapes.iter() { - shapes.push(shape.to_screen(&to_screen).into()); - if self.show_bounding_box { - shapes.push(self.build_bounding_box(shape.bounding_rect(), &to_screen)); - } - } - painter.extend(shapes); - if !self.points.is_empty() { - painter.extend(build_auxiliary_line( - &self.points, - &to_screen, - &self.aux_stroke, - )); - } else if !self.backup_points.is_empty() { - painter.extend(build_auxiliary_line( - &self.backup_points, - &to_screen, - &self.aux_stroke, + let control_point_radius = 8.0; + + let mut control_point_shapes = vec![]; + + for (i, point) in self.control_points.iter_mut().enumerate().take(self.degree) { + let size = Vec2::splat(2.0 * control_point_radius); + + let point_in_screen = to_screen.transform_pos(*point); + let point_rect = Rect::from_center_size(point_in_screen, size); + let point_id = response.id.with(i); + let point_response = ui.interact(point_rect, point_id, Sense::drag()); + + *point += point_response.drag_delta(); + *point = to_screen.from().clamp(*point); + + let point_in_screen = to_screen.transform_pos(*point); + let stroke = ui.style().interact(&point_response).fg_stroke; + + control_point_shapes.push(Shape::circle_stroke( + point_in_screen, + control_point_radius, + stroke, )); } - response - } - - pub fn build_bounding_box(&self, bbox: Rect, to_screen: &RectTransform) -> Shape { - let bbox = Rect { - min: to_screen * bbox.min, - max: to_screen * bbox.max, + let points_in_screen: Vec = self + .control_points + .iter() + .take(self.degree) + .map(|p| to_screen * *p) + .collect(); + + match self.degree { + 3 => { + let points = points_in_screen.clone().try_into().unwrap(); + let shape = + QuadraticBezierShape::from_points_stroke(points, true, self.fill, self.stroke); + painter.add(epaint::RectShape::stroke( + shape.bounding_rect(), + 0.0, + self.bounding_box_stroke, + )); + painter.add(shape); + } + 4 => { + let points = points_in_screen.clone().try_into().unwrap(); + let shape = + CubicBezierShape::from_points_stroke(points, true, self.fill, self.stroke); + painter.add(epaint::RectShape::stroke( + shape.bounding_rect(), + 0.0, + self.bounding_box_stroke, + )); + painter.add(shape); + } + _ => { + unreachable!(); + } }; - let bbox_shape = epaint::RectShape::stroke(bbox, 0.0, self.bounding_box_stroke); - bbox_shape.into() - } -} -/// An internal function to create auxiliary lines around the current bezier curve -/// or to auxiliary lines (points) before the points meet the bezier curve requirements. -fn build_auxiliary_line( - points: &[Pos2], - to_screen: &RectTransform, - aux_stroke: &Stroke, -) -> Vec { - let mut shapes = Vec::new(); - if points.len() >= 2 { - let points: Vec = points.iter().map(|p| to_screen * *p).collect(); - shapes.push(egui::Shape::line(points, *aux_stroke)); - } - for point in points.iter() { - let center = to_screen * *point; - let radius = aux_stroke.width * 3.0; - let circle = CircleShape { - center, - radius, - fill: aux_stroke.color, - stroke: *aux_stroke, - }; + painter.add(PathShape::line(points_in_screen, self.aux_stroke)); + painter.extend(control_point_shapes); - shapes.push(circle.into()); + response } - - shapes } impl super::Demo for PaintBezier { fn name(&self) -> &'static str { - "✔ Bezier Curve" + ") Bézier Curve" } fn show(&mut self, ctx: &Context, open: &mut bool) { use super::View as _; Window::new(self.name()) .open(open) - .default_size(vec2(512.0, 512.0)) .vscroll(false) + .resizable(false) .show(ctx, |ui| self.ui(ui)); } } diff --git a/epaint/src/bezier.rs b/epaint/src/bezier.rs index b17cf9123bd..d0a63c8a130 100644 --- a/epaint/src/bezier.rs +++ b/epaint/src/bezier.rs @@ -6,9 +6,9 @@ use emath::*; // ---------------------------------------------------------------------------- -/// How to paint a cubic Bezier curve on screen. -/// The definition: [Bezier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). -/// This implementation is only for cubic Bezier curve, or the Bezier curve of degree 3. +/// A cubic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). +/// +/// See also [`QuadraticBezierShape`]. #[derive(Copy, Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct CubicBezierShape { @@ -22,30 +22,29 @@ pub struct CubicBezierShape { } impl CubicBezierShape { - /// Creates a cubic Bezier curve based on 4 points and stroke. + /// Creates a cubic Bézier curve based on 4 points and stroke. + /// /// The first point is the starting point and the last one is the ending point of the curve. /// The middle points are the control points. - /// The number of points must be 4. pub fn from_points_stroke( - points: Vec, + points: [Pos2; 4], closed: bool, fill: Color32, stroke: impl Into, ) -> Self { - crate::epaint_assert!(points.len() == 4, "Cubic needs 4 points"); Self { - points: points.try_into().unwrap(), + points, closed, fill, stroke: stroke.into(), } } - /// Creates a cubic Bezier curve based on the screen coordinates for the 4 points. - pub fn to_screen(&self, to_screen: &RectTransform) -> Self { + /// Transform the curve with the given transform. + pub fn transform(&self, transform: &RectTransform) -> Self { let mut points = [Pos2::default(); 4]; for (i, origin_point) in self.points.iter().enumerate() { - points[i] = to_screen * *origin_point; + points[i] = transform * *origin_point; } CubicBezierShape { points, @@ -55,12 +54,12 @@ impl CubicBezierShape { } } - /// Convert the cubic Bezier curve to one or two `PathShapes`. + /// Convert the cubic Bézier curve to one or two `PathShapes`. /// When the curve is closed and it has to intersect with the base line, it will be converted into two shapes. /// Otherwise, it will be converted into one shape. /// The `tolerance` will be used to control the max distance between the curve and the base line. /// The `epsilon` is used when comparing two floats. - pub fn to_pathshapes(&self, tolerance: Option, epsilon: Option) -> Vec { + pub fn to_path_shapes(&self, tolerance: Option, epsilon: Option) -> Vec { let mut pathshapes = Vec::new(); let mut points_vec = self.flatten_closed(tolerance, epsilon); for points in points_vec.drain(..) { @@ -74,6 +73,7 @@ impl CubicBezierShape { } pathshapes } + /// Screen-space bounding rectangle. pub fn bounding_rect(&self) -> Rect { //temporary solution @@ -256,9 +256,9 @@ impl CubicBezierShape { None } - /// Calculate the point (x,y) at t based on the cubic bezier curve equation. + /// Calculate the point (x,y) at t based on the cubic Bézier curve equation. /// t is in [0.0,1.0] - /// [Bezier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves) + /// [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves) /// pub fn sample(&self, t: f32) -> Pos2 { crate::epaint_assert!( @@ -278,7 +278,7 @@ impl CubicBezierShape { result.to_pos2() } - /// find a set of points that approximate the cubic bezier curve. + /// find a set of points that approximate the cubic Bézier curve. /// the number of points is determined by the tolerance. /// the points may not be evenly distributed in the range [0.0,1.0] (t value) pub fn flatten(&self, tolerance: Option) -> Vec { @@ -290,7 +290,7 @@ impl CubicBezierShape { result } - /// find a set of points that approximate the cubic bezier curve. + /// find a set of points that approximate the cubic Bézier curve. /// the number of points is determined by the tolerance. /// the points may not be evenly distributed in the range [0.0,1.0] (t value) /// this api will check whether the curve will cross the base line or not when closed = true. @@ -358,6 +358,11 @@ impl From for Shape { } } +// ---------------------------------------------------------------------------- + +/// A quadratic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). +/// +/// See also [`CubicBezierShape`]. #[derive(Copy, Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct QuadraticBezierShape { @@ -371,32 +376,30 @@ pub struct QuadraticBezierShape { } impl QuadraticBezierShape { - /// create a new quadratic bezier shape based on the 3 points and stroke. - /// the first point is the starting point and the last one is the ending point of the curve. - /// the middle point is the control points. - /// the points should be in the order [start, control, end] + /// Create a new quadratic Bézier shape based on the 3 points and stroke. /// + /// The first point is the starting point and the last one is the ending point of the curve. + /// The middle point is the control points. + /// The points should be in the order [start, control, end] pub fn from_points_stroke( - points: Vec, + points: [Pos2; 3], closed: bool, fill: Color32, stroke: impl Into, ) -> Self { - crate::epaint_assert!(points.len() == 3, "Quadratic needs 3 points"); - QuadraticBezierShape { - points: points.try_into().unwrap(), // it's safe to unwrap because we just checked + points, closed, fill, stroke: stroke.into(), } } - /// create a new quadratic bezier shape based on the screen coordination for the 3 points. - pub fn to_screen(&self, to_screen: &RectTransform) -> Self { + /// Transform the curve with the given transform. + pub fn transform(&self, transform: &RectTransform) -> Self { let mut points = [Pos2::default(); 3]; for (i, origin_point) in self.points.iter().enumerate() { - points[i] = to_screen * *origin_point; + points[i] = transform * *origin_point; } QuadraticBezierShape { points, @@ -406,9 +409,9 @@ impl QuadraticBezierShape { } } - /// Convert the quadratic Bezier curve to one `PathShape`. + /// Convert the quadratic Bézier curve to one `PathShape`. /// The `tolerance` will be used to control the max distance between the curve and the base line. - pub fn to_pathshape(&self, tolerance: Option) -> PathShape { + pub fn to_path_shape(&self, tolerance: Option) -> PathShape { let points = self.flatten(tolerance); PathShape { points, @@ -417,7 +420,8 @@ impl QuadraticBezierShape { stroke: self.stroke, } } - /// bounding box of the quadratic bezier shape + + /// bounding box of the quadratic Bézier shape pub fn bounding_rect(&self) -> Rect { let (mut min_x, mut max_x) = if self.points[0].x < self.points[2].x { (self.points[0].x, self.points[2].x) @@ -466,9 +470,9 @@ impl QuadraticBezierShape { } } - /// Calculate the point (x,y) at t based on the quadratic bezier curve equation. + /// Calculate the point (x,y) at t based on the quadratic Bézier curve equation. /// t is in [0.0,1.0] - /// [Bezier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B.C3.A9zier_curves) + /// [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B.C3.A9zier_curves) /// pub fn sample(&self, t: f32) -> Pos2 { crate::epaint_assert!( @@ -486,7 +490,7 @@ impl QuadraticBezierShape { result.to_pos2() } - /// find a set of points that approximate the quadratic bezier curve. + /// find a set of points that approximate the quadratic Bézier curve. /// the number of points is determined by the tolerance. /// the points may not be evenly distributed in the range [0.0,1.0] (t value) pub fn flatten(&self, tolerance: Option) -> Vec { @@ -533,6 +537,8 @@ impl From for Shape { } } +// ---------------------------------------------------------------------------- + // lyon_geom::flatten_cubic.rs // copied from https://docs.rs/lyon_geom/latest/lyon_geom/ fn flatten_cubic_bezier_with_t( @@ -567,6 +573,7 @@ fn flatten_cubic_bezier_with_t( callback(point, t); }); } + // from lyon_geom::quadratic_bezier.rs // copied from https://docs.rs/lyon_geom/latest/lyon_geom/ struct FlatteningParameters { @@ -665,7 +672,7 @@ fn single_curve_approximation(curve: &CubicBezierShape) -> QuadraticBezierShape } fn quadratic_for_each_local_extremum(p0: f32, p1: f32, p2: f32, cb: &mut F) { - // A quadratic bezier curve can be derived by a linear function: + // A quadratic Bézier curve can be derived by a linear function: // p(t) = p0 + t(p1 - p0) + t^2(p2 - 2p1 + p0) // The derivative is: // p'(t) = (p1 - p0) + 2(p2 - 2p1 + p0)t or: @@ -685,7 +692,7 @@ fn quadratic_for_each_local_extremum(p0: f32, p1: f32, p2: f32, c fn cubic_for_each_local_extremum(p0: f32, p1: f32, p2: f32, p3: f32, cb: &mut F) { // See www.faculty.idc.ac.il/arik/quality/appendixa.html for an explanation - // A cubic bezier curve can be derivated by the following equation: + // A cubic Bézier curve can be derivated by the following equation: // B'(t) = 3(1-t)^2(p1-p0) + 6(1-t)t(p2-p1) + 3t^2(p3-p2) or // f(x) = a * x² + b * x + c let a = 3.0 * (p3 + 3.0 * (p1 - p2) - p0);