From f90e5bac6dcd35f61e9b8b0137fcdf7f5470f1f9 Mon Sep 17 00:00:00 2001 From: Xu Desheng <8742037+xudesheng@users.noreply.github.com> Date: Sat, 29 Jan 2022 15:02:55 -0500 Subject: [PATCH 1/4] Bezier Curve Enhancement for #1120 --- README.md | 2 + egui/src/introspection.rs | 6 + .../src/apps/demo/demo_app_windows.rs | 1 + egui_demo_lib/src/apps/demo/mod.rs | 1 + egui_demo_lib/src/apps/demo/paint_bezier.rs | 247 ++++ epaint/src/bezier.rs | 1086 +++++++++++++++++ epaint/src/lib.rs | 2 + epaint/src/shape.rs | 13 + epaint/src/shape_transform.rs | 8 + epaint/src/stats.rs | 8 +- epaint/src/tessellator.rs | 92 ++ 11 files changed, 1464 insertions(+), 2 deletions(-) create mode 100644 egui_demo_lib/src/apps/demo/paint_bezier.rs create mode 100644 epaint/src/bezier.rs diff --git a/README.md b/README.md index a4e8d166d85..c17460d4a9a 100644 --- a/README.md +++ b/README.md @@ -374,6 +374,8 @@ Notable contributions by: egui is licensed under [MIT](LICENSE-MIT) OR [Apache-2.0](LICENSE-APACHE). +* The flattening algorithm for the cubic bezier curve and quadratic bezier curve is from [lyon_geom](https://docs.rs/lyon_geom/latest/lyon_geom/) + Default fonts: * `emoji-icon-font.ttf`: [Copyright (c) 2014 John Slegers](https://github.com/jslegers/emoji-icon-font) , MIT License diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index eb6b7c4ab4f..891237ba440 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -144,6 +144,8 @@ impl Widget for &mut epaint::TessellationOptions { debug_paint_clip_rects, debug_paint_text_rects, debug_ignore_clip_rects, + bezier_tolerence, + epsilon: _, } = self; ui.checkbox(anti_alias, "Antialias") .on_hover_text("Turn off for small performance gain."); @@ -158,6 +160,10 @@ impl Widget for &mut epaint::TessellationOptions { ui.checkbox(debug_ignore_clip_rects, "Ignore clip rectangles"); ui.checkbox(debug_paint_clip_rects, "Paint clip rectangles"); ui.checkbox(debug_paint_text_rects, "Paint text bounds"); + crate::widgets::Slider::new(bezier_tolerence, 0.0001..=10.0) + .logarithmic(true) + .show_value(true) + .text("Tolerance") }); }) .response 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 b905ca34904..6c4b69e7cfb 100644 --- a/egui_demo_lib/src/apps/demo/demo_app_windows.rs +++ b/egui_demo_lib/src/apps/demo/demo_app_windows.rs @@ -25,6 +25,7 @@ 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/mod.rs b/egui_demo_lib/src/apps/demo/mod.rs index 79ef343f748..61118f7d7ee 100644 --- a/egui_demo_lib/src/apps/demo/mod.rs +++ b/egui_demo_lib/src/apps/demo/mod.rs @@ -15,6 +15,7 @@ pub mod font_book; pub mod layout_test; pub mod misc_demo_window; pub mod multi_touch; +pub mod paint_bezier; pub mod painting; pub mod password; pub mod plot_demo; diff --git a/egui_demo_lib/src/apps/demo/paint_bezier.rs b/egui_demo_lib/src/apps/demo/paint_bezier.rs new file mode 100644 index 00000000000..eb24b9550e4 --- /dev/null +++ b/egui_demo_lib/src/apps/demo/paint_bezier.rs @@ -0,0 +1,247 @@ +use egui::emath::RectTransform; +use egui::epaint::{CircleShape, CubicBezierShape, QuadraticBezierShape}; +use egui::*; + +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct PaintBezier { + bezier: usize, // current bezier curve degree, it can be 3,4, + tolerance: f32, // the tolerance for the bezier curve + bezier_backup: usize, //track the bezier degree before change in order to clean the remaining points. + points: Vec, //points already clicked. once it reaches the 'bezier' degree, it will be pushed into the 'shapes' + backup_points: Vec, //track last points set in order to draw auxiliary lines. + q_shapes: Vec, //shapes already drawn. once it reaches the 'bezier' degree, it will be pushed into the 'shapes' + c_shapes: Vec, // since `Shape` can't be 'serilized', we can't use Shape as variable type. + aux_stroke: Stroke, + stroke: Stroke, + fill: Color32, + closed: bool, + show_bounding_box: bool, + bounding_box_stroke: Stroke, +} + +impl Default for PaintBezier { + fn default() -> Self { + Self { + bezier: 4, // default bezier degree, a cubic bezier curve + tolerance: 1.0, // default tolerance 1.0 + bezier_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), + 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), + } + } +} + +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.horizontal(|ui| { + ui.label("Fill Color:"); + if ui.color_edit_button_srgba(&mut self.fill).changed() { + if self.fill != Color32::TRANSPARENT { + self.closed = true; + } + } + if ui.checkbox(&mut self.closed, "Closed").clicked() { + if !self.closed { + self.fill = Color32::TRANSPARENT; + } + } + }) + }); + + ui.separator(); + ui.vertical(|ui| { + ui.add( + egui::Slider::new(&mut self.tolerance, 0.0001..=10.0) + .logarithmic(true) + .show_value(true) + .text("Tolerance:"), + ); + ui.checkbox(&mut self.show_bounding_box, "Bounding Box"); + + egui::stroke_ui(ui, &mut self.bounding_box_stroke, "Bounding Box Stroke"); + }); + ui.separator(); + ui.vertical(|ui| { + if ui.radio_value(&mut self.bezier, 3, "Quadratic").clicked() { + if self.bezier_backup != self.bezier { + self.points.clear(); + self.bezier_backup = self.bezier; + } + }; + if ui.radio_value(&mut self.bezier, 4, "Cubic").clicked() { + if self.bezier_backup != self.bezier { + self.points.clear(); + self.bezier_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 + } + + // 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( + &self, + 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.clone())); + } + 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.clone(), + }; + + shapes.push(circle.into()); + } + + shapes + } + + pub fn ui_content(&mut self, ui: &mut Ui) -> egui::Response { + { + // using a block here to avoid the borrow checker conflict with the next painter borrow. + let mut t_options = ui.ctx().tessellation_options(); + t_options.bezier_tolerence = self.tolerance; + } + + let (mut response, painter) = + ui.allocate_painter(ui.available_size_before_wrap(), Sense::click()); + + let to_screen = emath::RectTransform::from_to( + Rect::from_min_size(Pos2::ZERO, response.rect.square_proportions()), + 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.clone(), + self.stroke.clone(), + ); + self.q_shapes.push(quadratic); + } + 4 => { + let cubic = CubicBezierShape::from_points_stroke( + points, + self.closed, + self.fill.clone(), + self.stroke.clone(), + ); + self.c_shapes.push(cubic); + } + _ => { + todo!(); + } + } + } + + 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.len() > 0 { + painter.extend(self.build_auxiliary_line(&self.points, &to_screen, &self.aux_stroke)); + } else if self.backup_points.len() > 0 { + painter.extend(self.build_auxiliary_line( + &self.backup_points, + &to_screen, + &self.aux_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 bbox_shape = epaint::RectShape::stroke(bbox, 0.0, self.bounding_box_stroke.clone()); + bbox_shape.into() + } +} + +impl super::Demo for PaintBezier { + fn name(&self) -> &'static str { + "✔ Bezier 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) + .show(ctx, |ui| self.ui(ui)); + } +} + +impl super::View for PaintBezier { + fn ui(&mut self, ui: &mut Ui) { + // ui.vertical_centered(|ui| { + // ui.add(crate::__egui_github_link_file!()); + // }); + self.ui_control(ui); + + Frame::dark_canvas(ui.style()).show(ui, |ui| { + self.ui_content(ui); + }); + } +} diff --git a/epaint/src/bezier.rs b/epaint/src/bezier.rs new file mode 100644 index 00000000000..2c1606e534f --- /dev/null +++ b/epaint/src/bezier.rs @@ -0,0 +1,1086 @@ +use std::ops::Range; + +use crate::{ + Color32, Stroke, + shape::Shape, PathShape, +}; +use emath::*; + +// ---------------------------------------------------------------------------- + +/// How to paint a cubic Bezier curve on screen. +/// The definition: https://en.wikipedia.org/wiki/B%C3%A9zier_curve +/// This implementation is only for cubic Bezier curve, or the Bezier curve of degree 3. +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct CubicBezierShape { + /// 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. + pub points: [Pos2; 4], + pub closed: bool, + + pub fill: Color32, + pub stroke: Stroke, +} + +impl CubicBezierShape { + /// Creates a cubic Bezier 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,closed:bool,fill:Color32, stroke: impl Into) -> Self { + crate::epaint_assert!( + points.len() == 4, + "Cubic needs 4 points" + ); + Self { + points:points.try_into().unwrap(), + 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{ + let mut points = [Pos2::default();4]; + for i in 0..4{ + points[i] = to_screen * self.points[i]; + } + CubicBezierShape { + points, + closed: self.closed, + fill: self.fill.clone(), + stroke: self.stroke.clone(), + } + } + + /// Convert the cubic Bezier curve to one or two PathShapes. + /// When the curve is closed and it will cross 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{ + let mut pathshapes = Vec::new(); + let mut points_vec = self.flatten_closed(tolerance, epsilon); + for points in points_vec.drain(..){ + let pathshape = PathShape{ + points, + closed: self.closed, + fill: self.fill.clone(), + stroke: self.stroke.clone(), + }; + pathshapes.push(pathshape); + } + pathshapes + } + /// Screen-space bounding rectangle. + pub fn bounding_rect(&self) -> Rect { + //temporary solution + let (mut min_x,mut max_x) = if self.points[0].x < self.points[3].x { + (self.points[0].x,self.points[3].x)}else{(self.points[3].x,self.points[0].x)}; + let (mut min_y,mut max_y) = if self.points[0].y < self.points[3].y { + (self.points[0].y,self.points[3].y)}else{(self.points[3].y,self.points[0].y)}; + + // find the inflection points and get the x value + cubic_for_each_local_extremum(self.points[0].x,self.points[1].x,self.points[2].x,self.points[3].x,&mut |t|{ + let x = self.sample(t).x; + if x < min_x {min_x = x} + if x > max_x {max_x = x} + }); + + // find the inflection points and get the y value + cubic_for_each_local_extremum(self.points[0].y,self.points[1].y,self.points[2].y,self.points[3].y,&mut |t|{ + let y = self.sample(t).y; + if y < min_y {min_y = y} + if y > max_y {max_y = y} + }); + + + Rect { + min: Pos2 { x: min_x, y: min_y }, + max: Pos2 { x: max_x, y: max_y }, + } + } + + /// split the original cubic curve into a new one within a range. + pub fn split_range(&self, t_range: Range) -> Self { + crate::epaint_assert!( + t_range.start >= 0.0 && t_range.end <= 1.0 && t_range.start <= t_range.end, + "range should be in [0.0,1.0]" + ); + + let from = self.sample(t_range.start); + let to = self.sample(t_range.end); + + let d_from = self.points[1] - self.points[0].to_vec2(); + let d_ctrl = self.points[2] - self.points[1].to_vec2(); + let d_to = self.points[3] - self.points[2].to_vec2(); + let q = QuadraticBezierShape { + points: [d_from, d_ctrl, d_to], + closed: self.closed, + fill: self.fill, + stroke: self.stroke, + }; + let delta_t = t_range.end - t_range.start; + let q_start = q.sample(t_range.start); + let q_end = q.sample(t_range.end); + let ctrl1 = from + q_start.to_vec2() * delta_t; + let ctrl2 = to - q_end.to_vec2() * delta_t; + CubicBezierShape { + points: [from, ctrl1, ctrl2, to], + closed: self.closed, + fill: self.fill, + stroke: self.stroke, + } + } + + // copied from lyon::geom::flattern_cubic.rs + // Computes the number of quadratic bézier segments to approximate a cubic one. + // Derived by Raph Levien from section 10.6 of Sedeberg's CAGD notes + // https://scholarsarchive.byu.edu/cgi/viewcontent.cgi?article=1000&context=facpub#section.10.6 + // and the error metric from the caffein owl blog post http://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html + pub fn num_quadratics(&self, tolerance: f32) -> u32 { + crate::epaint_assert!(tolerance > 0.0, "the tolerance should be positive"); + + let x = + self.points[0].x - 3.0 * self.points[1].x + 3.0 * self.points[2].x - self.points[3].x; + let y = + self.points[0].y - 3.0 * self.points[1].y + 3.0 * self.points[2].y - self.points[3].y; + let err = x * x + y * y; + + (err / (432.0 * tolerance * tolerance)) + .powf(1.0 / 6.0) + .ceil() + .max(1.0) as u32 + } + + /// Find out the t value for the point where the curve is intersected with the base line. + /// The base line is the line from P0 to P3. + /// If the curve only has two intersection points with the base line, they should be 0.0 and 1.0. + /// In this case, the "fill" will be simple since the curve is a contour line. + /// If the curve has more than two intersection points with the base line, the "fill" will be a problem. + /// We need to find out where is the 3rd t value (0 0, there will be one real root, two complex roots + /// when p = 0, there will be two real roots, when p=q=0, there will be three real roots but all 0. + /// when p < 0, there will be three unique real roots. this is what we need. (x1, x2, x3) + /// t = x + b / (3 * a), then we have: t1, t2, t3. + /// the one between 0.0 and 1.0 is what we need. + /// https://baike.baidu.com/item/%E4%B8%80%E5%85%83%E4%B8%89%E6%AC%A1%E6%96%B9%E7%A8%8B/8388473 + /// + pub fn find_cross_t(&self,epsilon:f32) -> Option{ + let p0 = self.points[0]; + let p1 = self.points[1]; + let p2 = self.points[2]; + let p3 = self.points[3]; + + let a = (p3.x-3.0*p2.x+3.0*p1.x-p0.x)*(p3.y-p0.y)-(p3.y-3.0*p2.y+3.0*p1.y-p0.y)*(p3.x-p0.x) ; + let b = (3.0*p2.x-6.0*p1.x+3.0*p0.x)*(p3.y-p0.y)-(3.0*p2.y-6.0*p1.y+3.0*p0.y)*(p3.x-p0.x); + let c = (3.0*p1.x-3.0*p0.x)*(p3.y-p0.y)-(3.0*p1.y-3.0*p0.y)*(p3.x-p0.x) ; + let d = p0.x*(p3.y-p0.y)-p0.y*(p3.x-p0.x)+p0.x*(p0.y-p3.y)+p0.y*(p3.x-p0.x) ; + + let h = -b / (3.0 * a); + let p = (3.0 * a * c - b * b) / (3.0 * a * a); + let q = (2.0 * b * b * b - 9.0 * a * b * c + 27.0 * a * a * d) / (27.0 * a * a * a); + // println!("a:{},b:{},c:{},d:{},h:{},p:{},q:{}",a,b,c,d,h,p,q); + + if p > 0.0 { + return None; + } + let r = (-1.0 * (p/3.0).powi(3)).sqrt(); + let theta = (-1.0 * q / (2.0 * r)).acos()/3.0; + // let discriminant = (q/2.0).powi(2) + (p/3.0).powi(3); // discriminant + // println!("discriminant: {},r:{},theta:{}",discriminant,r,theta); + + let t1 = 2.0 * r.powf(1.0/3.0) * theta.cos() + h; + let t2 = 2.0 * r.powf(1.0/3.0) * (theta+ 120.0 * std::f32::consts::PI / 180.0).cos() + h; + let t3 = 2.0 * r.powf(1.0/3.0) * (theta+ 240.0 * std::f32::consts::PI / 180.0).cos() + h; + + if t1 > epsilon && t1 < 1.0 - epsilon{ + return Some(t1); + } + if t2 > epsilon && t2 < 1.0 - epsilon{ + return Some(t2); + } + if t3 > epsilon && t3 < 1.0 - epsilon{ + return Some(t3); + } + return None; + } + + /// Calculate the point (x,y) at t based on the cubic bezier curve equation. + /// t is in [0.0,1.0] + /// https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves + /// + pub fn sample(&self, t: f32) -> Pos2 { + crate::epaint_assert!( + t >= 0.0 && t <= 1.0, + "the sample value should be in [0.0,1.0]" + ); + + let h = 1.0 - t; + let a = t * t * t; + let b = 3.0 * t * t * h; + let c = 3.0 * t * h * h; + let d = h * h * h; + let result = self.points[3].to_vec2() * a + + self.points[2].to_vec2() * b + + self.points[1].to_vec2() * c + + self.points[0].to_vec2() * d; + result.to_pos2() + } + + /// find a set of points that approximate the cubic bezier 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{ + let tolerance =tolerance.unwrap_or( (self.points[0].x-self.points[3].x).abs()*0.001); + let mut result = Vec::new(); + result.push(self.points[0]); + self.for_each_flattened_with_t(tolerance, &mut |p,_t|{ + result.push(p); + }); + result + } + + /// find a set of points that approximate the cubic bezier 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. + /// The result will be a vec of vec of Pos2. it will store two closed aren in different vec. + /// The epsilon is used to compare a float value. + pub fn flatten_closed(&self, tolerance:Option, epsilon:Option)->Vec>{ + let tolerance =tolerance.unwrap_or( (self.points[0].x-self.points[3].x).abs()*0.001); + let epsilon = epsilon.unwrap_or(1.0e-5); + let mut result = Vec::new(); + let mut first_half = Vec::new(); + let mut second_half = Vec::new(); + let mut flipped = false; + first_half.push(self.points[0]); + + let cross = self.find_cross_t(epsilon); + if self.closed && cross.is_some() { + let cross = cross.unwrap(); + self.for_each_flattened_with_t(tolerance, &mut |p,t|{ + if t < cross { + first_half.push(p); + }else{ + if !flipped{ + // when just crossed the base line, flip the order of the points + // add the cross point to the first half as the last point + // and add the cross point to the second half as the first point + flipped = true; + let cross_point = self.sample(cross); + first_half.push(cross_point.clone()); + second_half.push(cross_point); + } + second_half.push(p); + } + + }); + }else{ + self.for_each_flattened_with_t(tolerance, &mut |p,_t|{ + first_half.push(p); + }); + } + result.push(first_half); + if second_half.len() > 0{ + result.push(second_half); + } + result + } + // from lyon_geom::cubic_bezier.rs + /// Iterates through the curve invoking a callback at each point. + pub fn for_each_flattened_with_t(&self, tolerance: f32, callback: &mut F) { + flatten_cubic_bezier_with_t(self, tolerance, callback); + } +} + +impl From for Shape { + #[inline(always)] + fn from(shape: CubicBezierShape) -> Self { + Self::CubicBezier(shape) + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct QuadraticBezierShape { + /// 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. + pub points: [Pos2; 3], + pub closed: bool, + + pub fill: Color32, + pub stroke: Stroke, +} + +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] + /// + pub fn from_points_stroke(points: Vec,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 + 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{ + let mut points = [Pos2::default();3]; + for i in 0..3{ + points[i] = to_screen * self.points[i]; + } + QuadraticBezierShape { + points, + closed: self.closed, + fill: self.fill.clone(), + stroke: self.stroke.clone(), + } + } + + /// Convert the quadratic Bezier 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{ + let points = self.flatten(tolerance); + PathShape{ + points, + closed: self.closed, + fill: self.fill.clone(), + stroke: self.stroke.clone(), + } + } + /// bounding box of the quadratic bezier 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) + } else { + (self.points[2].x, self.points[0].x) + }; + let (mut min_y, mut max_y) = if self.points[0].y < self.points[2].y { + (self.points[0].y, self.points[2].y) + } else { + (self.points[2].y, self.points[0].y) + }; + + quadratic_for_each_local_extremum(self.points[0].x, self.points[1].x, self.points[2].x, &mut |t|{ + let x = self.sample(t).x; + if x < min_x { + min_x = x; + } + if x > max_x { + max_x = x; + } + }); + + quadratic_for_each_local_extremum(self.points[0].y, self.points[1].y, self.points[2].y, &mut |t|{ + let y = self.sample(t).y; + if y < min_y { + min_y = y; + } + if y > max_y { + max_y = y; + } + }); + + Rect { + min: Pos2 { x: min_x, y: min_y }, + max: Pos2 { x: max_x, y: max_y }, + } + } + + /// Calculate the point (x,y) at t based on the quadratic bezier curve equation. + /// t is in [0.0,1.0] + /// https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B.C3.A9zier_curves + /// + pub fn sample(&self, t: f32) -> Pos2 { + crate::epaint_assert!( + t >= 0.0 && t <= 1.0, + "the sample value should be in [0.0,1.0]" + ); + + let h = 1.0 - t; + let a = t * t; + let b = 2.0 * t * h; + let c = h * h; + let result = self.points[2].to_vec2() * a + + self.points[1].to_vec2() * b + + self.points[0].to_vec2() * c; + result.to_pos2() + } + + /// find a set of points that approximate the quadratic bezier 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{ + let tolerance =tolerance.unwrap_or( (self.points[0].x-self.points[2].x).abs()*0.001); + let mut result = Vec::new(); + result.push(self.points[0]); + self.for_each_flattened_with_t(tolerance, &mut |p,_t|{ + result.push(p); + }); + result + } + + // copied from https://docs.rs/lyon_geom/latest/lyon_geom/ + /// Compute a flattened approximation of the curve, invoking a callback at + /// each step. + /// + /// The callback takes the point and corresponding curve parameter at each step. + /// + /// This implements the algorithm described by Raph Levien at + /// + pub fn for_each_flattened_with_t(&self, tolerance: f32, callback: &mut F) + where + F: FnMut(Pos2, f32), + { + let params = FlatteningParameters::from_curve(self, tolerance); + if params.is_point { + return; + } + + let count = params.count as u32; + for index in 1..count { + let t = params.t_at_iteration(index as f32); + + callback(self.sample(t),t); + } + + callback(self.sample(1.0),1.0); + } +} + +impl From for Shape { + #[inline(always)] + fn from(shape: QuadraticBezierShape) -> Self { + Self::QuadraticBezier(shape) + } +} + +// lyon_geom::flatten_cubic.rs +// copied from https://docs.rs/lyon_geom/latest/lyon_geom/ +fn flatten_cubic_bezier_with_t( + curve: &CubicBezierShape, + tolerance: f32, + callback: &mut F, +) +{ + // debug_assert!(tolerance >= S::EPSILON * S::EPSILON); + let quadratics_tolerance = tolerance * 0.2; + let flattening_tolerance = tolerance * 0.8; + + let num_quadratics = curve.num_quadratics( quadratics_tolerance); + let step = 1.0 / num_quadratics as f32; + let n = num_quadratics; + let mut t0 = 0.0; + for _ in 0..(n - 1) { + let t1 = t0 + step; + + let quadratic = single_curve_approximation(&curve.split_range(t0..t1)); + quadratic.for_each_flattened_with_t(flattening_tolerance, &mut |point, t_sub| { + let t = t0 + step * t_sub; + callback(point, t); + }); + + t0 = t1; + } + + // Do the last step manually to make sure we finish at t = 1.0 exactly. + let quadratic = single_curve_approximation(&curve.split_range(t0..1.0)); + quadratic.for_each_flattened_with_t(flattening_tolerance, &mut |point, t_sub| { + let t = t0 + step * t_sub; + callback(point, t); + }); +} +// from lyon_geom::quadratic_bezier.rs +// copied from https://docs.rs/lyon_geom/latest/lyon_geom/ +struct FlatteningParameters { + count: f32, + integral_from: f32, + integral_step: f32, + inv_integral_from: f32, + div_inv_integral_diff: f32, + is_point: bool, +} + +impl FlatteningParameters { + // https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html + pub fn from_curve(curve: &QuadraticBezierShape, tolerance: f32) -> Self { + // Map the quadratic bézier segment to y = x^2 parabola. + let from = curve.points[0]; + let ctrl = curve.points[1]; + let to = curve.points[2]; + + let ddx = 2.0 * ctrl.x - from.x - to.x; + let ddy = 2.0 * ctrl.y - from.y - to.y; + let cross = (to.x - from.x) * ddy - (to.y - from.y) * ddx; + let inv_cross = 1.0 / cross; + let parabola_from = ((ctrl.x - from.x) * ddx + (ctrl.y - from.y) * ddy) * inv_cross; + let parabola_to = ((to.x - ctrl.x) * ddx + (to.y - ctrl.y) * ddy) * inv_cross; + // Note, scale can be NaN, for example with straight lines. When it happens the NaN will + // propagate to other parameters. We catch it all by setting the iteration count to zero + // and leave the rest as garbage. + let scale = + cross.abs() / ((ddx * ddx + ddy * ddy).sqrt() * (parabola_to - parabola_from).abs()); + + let integral_from = approx_parabola_integral(parabola_from); + let integral_to = approx_parabola_integral(parabola_to); + let integral_diff = integral_to - integral_from; + + let inv_integral_from = approx_parabola_inv_integral(integral_from); + let inv_integral_to = approx_parabola_inv_integral(integral_to); + let div_inv_integral_diff = 1.0 / (inv_integral_to - inv_integral_from); + + // the original author thinks it can be stored as integer if it's not generic. + // but if so, we have to handle the edge case of the integral being infinite. + let mut count = (0.5 * integral_diff.abs() * (scale / tolerance).sqrt()).ceil(); + let mut is_point = false; + // If count is NaN the curve can be approximated by a single straight line or a point. + if !count.is_finite() { + count = 0.0; + is_point = ((to.x - from.x) * (to.x - from.x) + (to.y - from.y) * (to.y - from.y)) + .sqrt() + < tolerance * tolerance; + } + + let integral_step = integral_diff / count; + + FlatteningParameters { + count, + integral_from, + integral_step, + inv_integral_from, + div_inv_integral_diff, + is_point, + } + } + + fn t_at_iteration(&self, iteration: f32) -> f32 { + let u = approx_parabola_inv_integral(self.integral_from + self.integral_step * iteration); + let t = (u - self.inv_integral_from) * self.div_inv_integral_diff; + + t + } +} + +/// Compute an approximation to integral (1 + 4x^2) ^ -0.25 dx used in the flattening code. +fn approx_parabola_integral(x: f32) -> f32 { + let d: f32 = 0.67; + let quarter = 0.25; + x / (1.0 - d + (d.powi(4) + quarter * x * x).sqrt().sqrt()) +} + +/// Approximate the inverse of the function above. +fn approx_parabola_inv_integral(x: f32) -> f32 { + let b = 0.39; + let quarter = 0.25; + x * (1.0 - b + (b * b + quarter * x * x).sqrt()) +} + +fn single_curve_approximation(curve:&CubicBezierShape) -> QuadraticBezierShape { + let c1_x = (curve.points[1].x * 3.0 - curve.points[0].x) * 0.5; + let c1_y = (curve.points[1].y * 3.0 - curve.points[0].y) * 0.5; + let c2_x = (curve.points[2].x * 3.0 - curve.points[3].x) * 0.5; + let c2_y = (curve.points[2].y * 3.0 - curve.points[3].y) * 0.5; + let c = Pos2 { + x: (c1_x + c2_x) * 0.5, + y: (c1_y + c2_y) * 0.5, + }; + QuadraticBezierShape { + points: [curve.points[0], c, curve.points[3]], + closed: curve.closed, + fill: curve.fill, + stroke: curve.stroke, + } +} + +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: + // p(t) = p0 + t(p1 - p0) + t^2(p2 - 2p1 + p0) + // The derivative is: + // p'(t) = (p1 - p0) + 2(p2 - 2p1 + p0)t or: + // f(x) = a* x + b + let a = p2 - 2.0 * p1 + p0; + // let b = p1 - p0; + // no need to check for zero, since we're only interested in local extrema + if a == 0.0 { + return; + } + + let t = (p0 - p1) / a; + if t > 0.0 && t < 1.0 { + cb(t); + } + +} + +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: + // 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); + let b = 6.0 * (p2 - 2.0 * p1 + p0); + let c = 3.0 * (p1-p0); + + let in_range = |t:f32| t<=1.0 && t>=0.0; + + // linear situation + if a == 0.0 { + if b != 0.0 { + let t = - c / b; + if in_range(t) { + cb(t); + } + } + return; + } + + let discr = b * b - 4.0 * a * c; + // no Real solution + if discr < 0.0 { + return; + } + + // one Real solution + if discr == 0.0 { + let t = -b / (2.0 * a); + if in_range(t) { + cb(t); + } + return; + } + + // two Real solutions + let discr = discr.sqrt(); + let t1 = (-b - discr) / (2.0 * a); + let t2 = (-b + discr) / (2.0 * a); + if in_range(t1) { + cb(t1); + } + if in_range(t2) { + cb(t2); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_quadratic_bounding_box(){ + let curve = QuadraticBezierShape { + points: [ + Pos2 { x: 110.0, y: 170.0 }, + Pos2 { x: 10.0, y: 10.0 }, + Pos2 { x: 180.0, y: 30.0 }, + ], + closed: false, + fill: Default::default(), + stroke: Default::default(), + }; + let bbox = curve.bounding_rect(); + assert!( (bbox.min.x-72.96).abs()<0.01); + assert!( (bbox.min.y-27.78).abs()<0.01); + + assert!( (bbox.max.x-180.0).abs() < 0.01); + assert!( (bbox.max.y-170.0).abs() < 0.01); + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.1, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 26); + + let curve = QuadraticBezierShape { + points: [ + Pos2 { x: 110.0, y: 170.0 }, + Pos2 { x: 180.0, y: 30.0 }, + Pos2 { x: 10.0, y: 10.0 }, + ], + closed: false, + fill: Default::default(), + stroke: Default::default(), + }; + let bbox = curve.bounding_rect(); + assert!( (bbox.min.x-10.0).abs()<0.01); + assert!( (bbox.min.y-10.0).abs()<0.01); + + assert!( (bbox.max.x-130.42).abs() < 0.01); + assert!( (bbox.max.y-170.0).abs() < 0.01); + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.1, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 25); + } + + #[test] + fn test_quadratic_dfferent_tolerance(){ + let curve = QuadraticBezierShape { + points: [ + Pos2 { x: 110.0, y: 170.0 }, + Pos2 { x: 180.0, y: 30.0 }, + Pos2 { x: 10.0, y: 10.0 }, + ], + closed: false, + fill: Default::default(), + stroke: Default::default(), + }; + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(1.0, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 9); + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.1, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 25); + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 77); + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.001, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 240); + } + #[test] + fn test_cubic_bounding_box(){ + let curve = CubicBezierShape { + points: [ + Pos2::new(10.0, 10.0), + Pos2::new(110.0, 170.0), + Pos2::new(180.0, 30.0), + Pos2::new(270.0, 210.0), + ], + closed: false, + fill: Default::default(), + stroke: Default::default(), + }; + + let bbox = curve.bounding_rect(); + assert_eq!(bbox.min.x, 10.0); + assert_eq!(bbox.min.y, 10.0); + assert_eq!(bbox.max.x, 270.0); + assert_eq!(bbox.max.y, 210.0); + + let curve = CubicBezierShape { + points: [ + Pos2::new(10.0, 10.0), + Pos2::new(110.0, 170.0), + Pos2::new(270.0, 210.0), + Pos2::new(180.0, 30.0), + ], + closed: false, + fill: Default::default(), + stroke: Default::default(), + }; + + let bbox = curve.bounding_rect(); + assert_eq!(bbox.min.x, 10.0); + assert_eq!(bbox.min.y, 10.0); + assert!( (bbox.max.x-206.50).abs() < 0.01); + assert!( (bbox.max.y-148.48).abs() < 0.01); + + let curve = CubicBezierShape { + points: [ + Pos2::new(110.0, 170.0), + Pos2::new(10.0, 10.0), + Pos2::new(270.0, 210.0), + Pos2::new(180.0, 30.0), + ], + closed: false, + fill: Default::default(), + stroke: Default::default(), + }; + + let bbox = curve.bounding_rect(); + assert!( (bbox.min.x-86.71).abs()<0.01); + assert!( (bbox.min.y-30.0).abs()<0.01); + + assert!( (bbox.max.x-199.27).abs() < 0.01); + assert!( (bbox.max.y-170.0).abs() < 0.01); + } + #[test] + fn test_cubic_different_tolerance_flattening() { + let curve = CubicBezierShape { + points: [ + Pos2::new(0.0, 0.0), + Pos2::new(100.0, 0.0), + Pos2::new(100.0, 100.0), + Pos2::new(100.0, 200.0), + ], + closed: false, + fill: Default::default(), + stroke: Default::default(), + }; + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(1.0, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 10); + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.5, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 13); + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.1, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 28); + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 83); + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.001, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 248); + } + + #[test] + fn test_cubic_different_shape_flattening() { + let curve = CubicBezierShape { + points: [ + Pos2::new(90.0, 110.0), + Pos2::new(30.0, 170.0), + Pos2::new(210.0, 170.0), + Pos2::new(170.0, 110.0), + ], + closed: false, + fill: Default::default(), + stroke: Default::default(), + }; + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 117); + + let curve = CubicBezierShape { + points: [ + Pos2::new(90.0, 110.0), + Pos2::new(90.0, 170.0), + Pos2::new(170.0, 170.0), + Pos2::new(170.0, 110.0), + ], + closed: false, + fill: Default::default(), + stroke: Default::default(), + }; + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 91); + + let curve = CubicBezierShape { + points: [ + Pos2::new(90.0, 110.0), + Pos2::new(110.0, 170.0), + Pos2::new(150.0, 170.0), + Pos2::new(170.0, 110.0), + ], + closed: false, + fill: Default::default(), + stroke: Default::default(), + }; + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 75); + + let curve = CubicBezierShape { + points: [ + Pos2::new(90.0, 110.0), + Pos2::new(110.0, 170.0), + Pos2::new(230.0, 110.0), + Pos2::new(170.0, 110.0), + ], + closed: false, + fill: Default::default(), + stroke: Default::default(), + }; + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 100); + + let curve = CubicBezierShape { + points: [ + Pos2::new(90.0, 110.0), + Pos2::new(110.0, 170.0), + Pos2::new(210.0, 70.0), + Pos2::new(170.0, 110.0), + ], + closed: false, + fill: Default::default(), + stroke: Default::default(), + }; + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 71); + + let curve = CubicBezierShape { + points: [ + Pos2::new(90.0, 110.0), + Pos2::new(110.0, 170.0), + Pos2::new(150.0, 50.0), + Pos2::new(170.0, 110.0), + ], + closed: false, + fill: Default::default(), + stroke: Default::default(), + }; + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 88); + } + + #[test] + fn test_quadrtic_flattening() { + let curve = QuadraticBezierShape { + points: [ + Pos2::new(0.0, 0.0), + Pos2::new(80.0, 200.0), + Pos2::new(100.0, 30.0), + ], + closed: false, + fill: Default::default(), + stroke: Default::default(), + }; + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(1.0, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 9); + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.5, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 11); + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.1, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 24); + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 72); + + let mut result = Vec::new(); + result.push(curve.points[0]); //add the start point + curve.for_each_flattened_with_t(0.001, &mut |pos,_t| { + result.push(pos); + }); + + assert_eq!(result.len(), 223); + } +} diff --git a/epaint/src/lib.rs b/epaint/src/lib.rs index 2207398d974..48df065bb69 100644 --- a/epaint/src/lib.rs +++ b/epaint/src/lib.rs @@ -87,6 +87,7 @@ #![allow(clippy::float_cmp)] #![allow(clippy::manual_range_contains)] +mod bezier; pub mod color; pub mod image; mod mesh; @@ -104,6 +105,7 @@ pub mod textures; pub mod util; pub use { + bezier::{CubicBezierShape, QuadraticBezierShape}, color::{Color32, Rgba}, image::{AlphaImage, ColorImage, ImageData, ImageDelta}, mesh::{Mesh, Mesh16, Vertex}, diff --git a/epaint/src/shape.rs b/epaint/src/shape.rs index 84a363cfea2..4bdf242a466 100644 --- a/epaint/src/shape.rs +++ b/epaint/src/shape.rs @@ -2,6 +2,7 @@ use crate::{ text::{FontId, Fonts, Galley}, Color32, Mesh, Stroke, }; +use crate::{CubicBezierShape, QuadraticBezierShape}; use emath::*; /// A paint primitive such as a circle or a piece of text. @@ -26,6 +27,8 @@ pub enum Shape { Rect(RectShape), Text(TextShape), Mesh(Mesh), + QuadraticBezier(QuadraticBezierShape), + CubicBezier(CubicBezierShape), } /// ## Constructors @@ -187,6 +190,16 @@ impl Shape { Shape::Mesh(mesh) => { mesh.translate(delta); } + Shape::QuadraticBezier(bezier_shape) => { + bezier_shape.points[0] += delta; + bezier_shape.points[1] += delta; + bezier_shape.points[2] += delta; + } + Shape::CubicBezier(cubie_curve) => { + for p in &mut cubie_curve.points { + *p += delta; + } + } } } } diff --git a/epaint/src/shape_transform.rs b/epaint/src/shape_transform.rs index b3ffc19d534..c033a8ee4b2 100644 --- a/epaint/src/shape_transform.rs +++ b/epaint/src/shape_transform.rs @@ -43,5 +43,13 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { adjust_color(&mut v.color); } } + Shape::QuadraticBezier(quatratic) => { + adjust_color(&mut quatratic.fill); + adjust_color(&mut quatratic.stroke.color); + } + Shape::CubicBezier(bezier) => { + adjust_color(&mut bezier.fill); + adjust_color(&mut bezier.stroke.color); + } } } diff --git a/epaint/src/stats.rs b/epaint/src/stats.rs index ad7a0c88389..322c0ddf13e 100644 --- a/epaint/src/stats.rs +++ b/epaint/src/stats.rs @@ -195,8 +195,12 @@ impl PaintStats { self.add(shape); } } - Shape::Noop | Shape::Circle { .. } | Shape::LineSegment { .. } | Shape::Rect { .. } => { - } + Shape::Noop + | Shape::Circle { .. } + | Shape::LineSegment { .. } + | Shape::Rect { .. } + | Shape::CubicBezier(_) + | Shape::QuadraticBezier(_) => {} Shape::Path(path_shape) => { self.shape_path += AllocInfo::from_slice(&path_shape.points); } diff --git a/epaint/src/tessellator.rs b/epaint/src/tessellator.rs index c2301a88de8..5c5fd8ae513 100644 --- a/epaint/src/tessellator.rs +++ b/epaint/src/tessellator.rs @@ -286,6 +286,12 @@ pub struct TessellationOptions { /// If true, no clipping will be done. pub debug_ignore_clip_rects: bool, + + /// The maximum distance between the original curve and the flattened curve. + pub bezier_tolerence: f32, + + /// The default value will be 1.0e-5, it will be used during float compare. + pub epsilon: f32, } impl Default for TessellationOptions { @@ -299,6 +305,8 @@ impl Default for TessellationOptions { debug_paint_text_rects: false, debug_paint_clip_rects: false, debug_ignore_clip_rects: false, + bezier_tolerence: 0.1, + epsilon: 1.0e-5, } } } @@ -710,7 +718,91 @@ impl Tessellator { } self.tessellate_text(tex_size, text_shape, out); } + Shape::QuadraticBezier(quadratic_shape) => { + self.tessellate_quadratic_bezier(quadratic_shape, out); + } + Shape::CubicBezier(cubic_shape) => self.tessellate_cubic_bezier(cubic_shape, out), + } + } + + pub(crate) fn tessellate_quadratic_bezier( + &mut self, + quadratic_shape: QuadraticBezierShape, + out: &mut Mesh, + ) { + let options = &self.options; + let clip_rect = self.clip_rect; + + if options.coarse_tessellation_culling + && !quadratic_shape.bounding_rect().intersects(clip_rect) + { + return; + } + + let points = quadratic_shape.flatten(Some(options.bezier_tolerence)); + + self.tessellate_bezier_complete( + points, + quadratic_shape.fill, + quadratic_shape.closed, + quadratic_shape.stroke, + out, + ); + } + + pub(crate) fn tessellate_cubic_bezier( + &mut self, + cubic_shape: CubicBezierShape, + out: &mut Mesh, + ) { + let options = &self.options; + let clip_rect = self.clip_rect; + if options.coarse_tessellation_culling && !cubic_shape.bounding_rect().intersects(clip_rect) + { + return; + } + + let points_vec = + cubic_shape.flatten_closed(Some(options.bezier_tolerence), Some(options.epsilon)); + + for points in points_vec { + self.tessellate_bezier_complete( + points, + cubic_shape.fill, + cubic_shape.closed, + cubic_shape.stroke, + out, + ); + } + } + + fn tessellate_bezier_complete( + &mut self, + points: Vec, + fill: Color32, + closed: bool, + stroke: Stroke, + out: &mut Mesh, + ) { + self.scratchpad_path.clear(); + if closed { + self.scratchpad_path.add_line_loop(&points); + } else { + self.scratchpad_path.add_open_points(&points); } + if fill != Color32::TRANSPARENT { + crate::epaint_assert!( + closed, + "You asked to fill a path that is not closed. That makes no sense." + ); + self.scratchpad_path.fill(fill, &self.options, out); + } + let typ = if closed { + PathType::Closed + } else { + PathType::Open + }; + self.scratchpad_path.stroke(typ, stroke, &self.options, out); } pub(crate) fn tessellate_path(&mut self, path_shape: PathShape, out: &mut Mesh) { From ba634668de64b1268d95358918fa4b50133adbbe Mon Sep 17 00:00:00 2001 From: Xu Desheng <8742037+xudesheng@users.noreply.github.com> Date: Sun, 30 Jan 2022 18:22:34 -0500 Subject: [PATCH 2/4] fix typo and clean clippy warnings --- egui/src/introspection.rs | 10 +- egui_demo_lib/src/apps/demo/paint_bezier.rs | 144 +++--- epaint/src/bezier.rs | 496 +++++++++++--------- epaint/src/tessellator.rs | 10 +- 4 files changed, 347 insertions(+), 313 deletions(-) diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index 891237ba440..615ac8d2641 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -149,6 +149,12 @@ impl Widget for &mut epaint::TessellationOptions { } = self; ui.checkbox(anti_alias, "Antialias") .on_hover_text("Turn off for small performance gain."); + ui.add( + crate::widgets::Slider::new(bezier_tolerence, 0.0001..=10.0) + .logarithmic(true) + .show_value(true) + .text("Spline Tolerance"), + ); ui.collapsing("debug", |ui| { ui.checkbox( coarse_tessellation_culling, @@ -160,10 +166,6 @@ impl Widget for &mut epaint::TessellationOptions { ui.checkbox(debug_ignore_clip_rects, "Ignore clip rectangles"); ui.checkbox(debug_paint_clip_rects, "Paint clip rectangles"); ui.checkbox(debug_paint_text_rects, "Paint text bounds"); - crate::widgets::Slider::new(bezier_tolerence, 0.0001..=10.0) - .logarithmic(true) - .show_value(true) - .text("Tolerance") }); }) .response diff --git a/egui_demo_lib/src/apps/demo/paint_bezier.rs b/egui_demo_lib/src/apps/demo/paint_bezier.rs index eb24b9550e4..bd66de5096e 100644 --- a/egui_demo_lib/src/apps/demo/paint_bezier.rs +++ b/egui_demo_lib/src/apps/demo/paint_bezier.rs @@ -6,7 +6,6 @@ use egui::*; #[cfg_attr(feature = "serde", serde(default))] pub struct PaintBezier { bezier: usize, // current bezier curve degree, it can be 3,4, - tolerance: f32, // the tolerance for the bezier curve bezier_backup: usize, //track the bezier degree before change in order to clean the remaining points. points: Vec, //points already clicked. once it reaches the 'bezier' degree, it will be pushed into the 'shapes' backup_points: Vec, //track last points set in order to draw auxiliary lines. @@ -23,8 +22,7 @@ pub struct PaintBezier { impl Default for PaintBezier { fn default() -> Self { Self { - bezier: 4, // default bezier degree, a cubic bezier curve - tolerance: 1.0, // default tolerance 1.0 + bezier: 4, // default bezier degree, a cubic bezier curve bezier_backup: 4, points: Default::default(), backup_points: Default::default(), @@ -48,44 +46,43 @@ impl PaintBezier { egui::stroke_ui(ui, &mut self.aux_stroke, "Auxiliary Stroke"); ui.horizontal(|ui| { ui.label("Fill Color:"); - if ui.color_edit_button_srgba(&mut self.fill).changed() { - if self.fill != Color32::TRANSPARENT { - self.closed = true; - } + 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() { - if !self.closed { - self.fill = Color32::TRANSPARENT; - } + if ui.checkbox(&mut self.closed, "Closed").clicked() && !self.closed { + self.fill = Color32::TRANSPARENT; } - }) + }); + egui::stroke_ui(ui, &mut self.bounding_box_stroke, "Bounding Box Stroke"); }); ui.separator(); ui.vertical(|ui| { - ui.add( - egui::Slider::new(&mut self.tolerance, 0.0001..=10.0) - .logarithmic(true) - .show_value(true) - .text("Tolerance:"), - ); - ui.checkbox(&mut self.show_bounding_box, "Bounding Box"); + { + 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; + } - egui::stroke_ui(ui, &mut self.bounding_box_stroke, "Bounding Box Stroke"); + ui.checkbox(&mut self.show_bounding_box, "Bounding Box"); }); ui.separator(); ui.vertical(|ui| { - if ui.radio_value(&mut self.bezier, 3, "Quadratic").clicked() { - if self.bezier_backup != self.bezier { - self.points.clear(); - self.bezier_backup = self.bezier; - } + if ui.radio_value(&mut self.bezier, 3, "Quadratic").clicked() + && self.bezier_backup != self.bezier + { + self.points.clear(); + self.bezier_backup = self.bezier; }; - if ui.radio_value(&mut self.bezier, 4, "Cubic").clicked() { - if self.bezier_backup != self.bezier { - self.points.clear(); - self.bezier_backup = self.bezier; - } + if ui.radio_value(&mut self.bezier, 4, "Cubic").clicked() + && self.bezier_backup != self.bezier + { + self.points.clear(); + self.bezier_backup = self.bezier; }; // ui.radio_value(self.bezier, 5, "Quintic"); ui.label("Click 3 or 4 points to build a bezier curve!"); @@ -100,42 +97,7 @@ impl PaintBezier { .response } - // 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( - &self, - 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.clone())); - } - 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.clone(), - }; - - shapes.push(circle.into()); - } - - shapes - } - pub fn ui_content(&mut self, ui: &mut Ui) -> egui::Response { - { - // using a block here to avoid the borrow checker conflict with the next painter borrow. - let mut t_options = ui.ctx().tessellation_options(); - t_options.bezier_tolerence = self.tolerance; - } - let (mut response, painter) = ui.allocate_painter(ui.available_size_before_wrap(), Sense::click()); @@ -157,8 +119,8 @@ impl PaintBezier { let quadratic = QuadraticBezierShape::from_points_stroke( points, self.closed, - self.fill.clone(), - self.stroke.clone(), + self.fill, + self.stroke, ); self.q_shapes.push(quadratic); } @@ -166,13 +128,13 @@ impl PaintBezier { let cubic = CubicBezierShape::from_points_stroke( points, self.closed, - self.fill.clone(), - self.stroke.clone(), + self.fill, + self.stroke, ); self.c_shapes.push(cubic); } _ => { - todo!(); + unreachable!(); } } } @@ -195,10 +157,14 @@ impl PaintBezier { } painter.extend(shapes); - if self.points.len() > 0 { - painter.extend(self.build_auxiliary_line(&self.points, &to_screen, &self.aux_stroke)); - } else if self.backup_points.len() > 0 { - painter.extend(self.build_auxiliary_line( + 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, @@ -213,11 +179,39 @@ impl PaintBezier { min: to_screen * bbox.min, max: to_screen * bbox.max, }; - let bbox_shape = epaint::RectShape::stroke(bbox, 0.0, self.bounding_box_stroke.clone()); + 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, + }; + + shapes.push(circle.into()); + } + + shapes +} + impl super::Demo for PaintBezier { fn name(&self) -> &'static str { "✔ Bezier Curve" diff --git a/epaint/src/bezier.rs b/epaint/src/bezier.rs index 2c1606e534f..1c9913f239c 100644 --- a/epaint/src/bezier.rs +++ b/epaint/src/bezier.rs @@ -1,15 +1,13 @@ +#![allow(clippy::many_single_char_names)] use std::ops::Range; -use crate::{ - Color32, Stroke, - shape::Shape, PathShape, -}; +use crate::{shape::Shape, Color32, PathShape, Stroke}; use emath::*; // ---------------------------------------------------------------------------- /// How to paint a cubic Bezier curve on screen. -/// The definition: https://en.wikipedia.org/wiki/B%C3%A9zier_curve +/// 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. #[derive(Copy, Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -28,47 +26,49 @@ impl CubicBezierShape { /// 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,closed:bool,fill:Color32, stroke: impl Into) -> Self { - crate::epaint_assert!( - points.len() == 4, - "Cubic needs 4 points" - ); + pub fn from_points_stroke( + points: Vec, + 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: points.try_into().unwrap(), 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{ - let mut points = [Pos2::default();4]; - for i in 0..4{ - points[i] = to_screen * self.points[i]; + pub fn to_screen(&self, to_screen: &RectTransform) -> Self { + let mut points = [Pos2::default(); 4]; + for (i, origin_point) in self.points.iter().enumerate() { + points[i] = to_screen * *origin_point; } CubicBezierShape { points, closed: self.closed, - fill: self.fill.clone(), - stroke: self.stroke.clone(), + fill: self.fill, + stroke: self.stroke, } } - /// Convert the cubic Bezier curve to one or two PathShapes. - /// When the curve is closed and it will cross the base line, it will be converted into two shapes. + /// Convert the cubic Bezier 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_pathshapes(&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(..){ - let pathshape = PathShape{ + for points in points_vec.drain(..) { + let pathshape = PathShape { points, closed: self.closed, - fill: self.fill.clone(), - stroke: self.stroke.clone(), + fill: self.fill, + stroke: self.stroke, }; pathshapes.push(pathshape); } @@ -77,26 +77,51 @@ impl CubicBezierShape { /// Screen-space bounding rectangle. pub fn bounding_rect(&self) -> Rect { //temporary solution - let (mut min_x,mut max_x) = if self.points[0].x < self.points[3].x { - (self.points[0].x,self.points[3].x)}else{(self.points[3].x,self.points[0].x)}; - let (mut min_y,mut max_y) = if self.points[0].y < self.points[3].y { - (self.points[0].y,self.points[3].y)}else{(self.points[3].y,self.points[0].y)}; - + let (mut min_x, mut max_x) = if self.points[0].x < self.points[3].x { + (self.points[0].x, self.points[3].x) + } else { + (self.points[3].x, self.points[0].x) + }; + let (mut min_y, mut max_y) = if self.points[0].y < self.points[3].y { + (self.points[0].y, self.points[3].y) + } else { + (self.points[3].y, self.points[0].y) + }; + // find the inflection points and get the x value - cubic_for_each_local_extremum(self.points[0].x,self.points[1].x,self.points[2].x,self.points[3].x,&mut |t|{ - let x = self.sample(t).x; - if x < min_x {min_x = x} - if x > max_x {max_x = x} - }); + cubic_for_each_local_extremum( + self.points[0].x, + self.points[1].x, + self.points[2].x, + self.points[3].x, + &mut |t| { + let x = self.sample(t).x; + if x < min_x { + min_x = x; + } + if x > max_x { + max_x = x; + } + }, + ); // find the inflection points and get the y value - cubic_for_each_local_extremum(self.points[0].y,self.points[1].y,self.points[2].y,self.points[3].y,&mut |t|{ - let y = self.sample(t).y; - if y < min_y {min_y = y} - if y > max_y {max_y = y} - }); + cubic_for_each_local_extremum( + self.points[0].y, + self.points[1].y, + self.points[2].y, + self.points[3].y, + &mut |t| { + let y = self.sample(t).y; + if y < min_y { + min_y = y; + } + if y > max_y { + max_y = y; + } + }, + ); - Rect { min: Pos2 { x: min_x, y: min_y }, max: Pos2 { x: max_x, y: max_y }, @@ -158,7 +183,7 @@ impl CubicBezierShape { /// Find out the t value for the point where the curve is intersected with the base line. /// The base line is the line from P0 to P3. /// If the curve only has two intersection points with the base line, they should be 0.0 and 1.0. - /// In this case, the "fill" will be simple since the curve is a contour line. + /// In this case, the "fill" will be simple since the curve is a convex line. /// If the curve has more than two intersection points with the base line, the "fill" will be a problem. /// We need to find out where is the 3rd t value (0 0, there will be one real root, two complex roots /// when p = 0, there will be two real roots, when p=q=0, there will be three real roots but all 0. /// when p < 0, there will be three unique real roots. this is what we need. (x1, x2, x3) /// t = x + b / (3 * a), then we have: t1, t2, t3. /// the one between 0.0 and 1.0 is what we need. - /// https://baike.baidu.com/item/%E4%B8%80%E5%85%83%E4%B8%89%E6%AC%A1%E6%96%B9%E7%A8%8B/8388473 - /// - pub fn find_cross_t(&self,epsilon:f32) -> Option{ + /// <`https://baike.baidu.com/item/%E4%B8%80%E5%85%83%E4%B8%89%E6%AC%A1%E6%96%B9%E7%A8%8B/8388473 /`> + /// + pub fn find_cross_t(&self, epsilon: f32) -> Option { let p0 = self.points[0]; let p1 = self.points[1]; let p2 = self.points[2]; let p3 = self.points[3]; - let a = (p3.x-3.0*p2.x+3.0*p1.x-p0.x)*(p3.y-p0.y)-(p3.y-3.0*p2.y+3.0*p1.y-p0.y)*(p3.x-p0.x) ; - let b = (3.0*p2.x-6.0*p1.x+3.0*p0.x)*(p3.y-p0.y)-(3.0*p2.y-6.0*p1.y+3.0*p0.y)*(p3.x-p0.x); - let c = (3.0*p1.x-3.0*p0.x)*(p3.y-p0.y)-(3.0*p1.y-3.0*p0.y)*(p3.x-p0.x) ; - let d = p0.x*(p3.y-p0.y)-p0.y*(p3.x-p0.x)+p0.x*(p0.y-p3.y)+p0.y*(p3.x-p0.x) ; + let a = (p3.x - 3.0 * p2.x + 3.0 * p1.x - p0.x) * (p3.y - p0.y) + - (p3.y - 3.0 * p2.y + 3.0 * p1.y - p0.y) * (p3.x - p0.x); + let b = (3.0 * p2.x - 6.0 * p1.x + 3.0 * p0.x) * (p3.y - p0.y) + - (3.0 * p2.y - 6.0 * p1.y + 3.0 * p0.y) * (p3.x - p0.x); + let c = + (3.0 * p1.x - 3.0 * p0.x) * (p3.y - p0.y) - (3.0 * p1.y - 3.0 * p0.y) * (p3.x - p0.x); + let d = p0.x * (p3.y - p0.y) - p0.y * (p3.x - p0.x) + + p0.x * (p0.y - p3.y) + + p0.y * (p3.x - p0.x); let h = -b / (3.0 * a); let p = (3.0 * a * c - b * b) / (3.0 * a * a); let q = (2.0 * b * b * b - 9.0 * a * b * c + 27.0 * a * a * d) / (27.0 * a * a * a); - // println!("a:{},b:{},c:{},d:{},h:{},p:{},q:{}",a,b,c,d,h,p,q); if p > 0.0 { return None; } - let r = (-1.0 * (p/3.0).powi(3)).sqrt(); - let theta = (-1.0 * q / (2.0 * r)).acos()/3.0; - // let discriminant = (q/2.0).powi(2) + (p/3.0).powi(3); // discriminant - // println!("discriminant: {},r:{},theta:{}",discriminant,r,theta); - - let t1 = 2.0 * r.powf(1.0/3.0) * theta.cos() + h; - let t2 = 2.0 * r.powf(1.0/3.0) * (theta+ 120.0 * std::f32::consts::PI / 180.0).cos() + h; - let t3 = 2.0 * r.powf(1.0/3.0) * (theta+ 240.0 * std::f32::consts::PI / 180.0).cos() + h; - - if t1 > epsilon && t1 < 1.0 - epsilon{ + let r = (-1.0 * (p / 3.0).powi(3)).sqrt(); + let theta = (-1.0 * q / (2.0 * r)).acos() / 3.0; + + let t1 = 2.0 * r.cbrt() * theta.cos() + h; + let t2 = 2.0 * r.cbrt() * (theta + 120.0 * std::f32::consts::PI / 180.0).cos() + h; + let t3 = 2.0 * r.cbrt() * (theta + 240.0 * std::f32::consts::PI / 180.0).cos() + h; + + if t1 > epsilon && t1 < 1.0 - epsilon { return Some(t1); } - if t2 > epsilon && t2 < 1.0 - epsilon{ + if t2 > epsilon && t2 < 1.0 - epsilon { return Some(t2); } - if t3 > epsilon && t3 < 1.0 - epsilon{ + if t3 > epsilon && t3 < 1.0 - epsilon { return Some(t3); } - return None; + None } /// Calculate the point (x,y) at t based on the cubic bezier curve equation. /// t is in [0.0,1.0] - /// https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves - /// + /// [Bezier 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!( t >= 0.0 && t <= 1.0, @@ -254,11 +281,10 @@ impl CubicBezierShape { /// find a set of points that approximate the cubic bezier 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{ - let tolerance =tolerance.unwrap_or( (self.points[0].x-self.points[3].x).abs()*0.001); - let mut result = Vec::new(); - result.push(self.points[0]); - self.for_each_flattened_with_t(tolerance, &mut |p,_t|{ + pub fn flatten(&self, tolerance: Option) -> Vec { + let tolerance = tolerance.unwrap_or((self.points[0].x - self.points[3].x).abs() * 0.001); + let mut result = vec![self.points[0]]; + self.for_each_flattened_with_t(tolerance, &mut |p, _t| { result.push(p); }); result @@ -270,8 +296,8 @@ impl CubicBezierShape { /// this api will check whether the curve will cross the base line or not when closed = true. /// The result will be a vec of vec of Pos2. it will store two closed aren in different vec. /// The epsilon is used to compare a float value. - pub fn flatten_closed(&self, tolerance:Option, epsilon:Option)->Vec>{ - let tolerance =tolerance.unwrap_or( (self.points[0].x-self.points[3].x).abs()*0.001); + pub fn flatten_closed(&self, tolerance: Option, epsilon: Option) -> Vec> { + let tolerance = tolerance.unwrap_or((self.points[0].x - self.points[3].x).abs() * 0.001); let epsilon = epsilon.unwrap_or(1.0e-5); let mut result = Vec::new(); let mut first_half = Vec::new(); @@ -280,32 +306,40 @@ impl CubicBezierShape { first_half.push(self.points[0]); let cross = self.find_cross_t(epsilon); - if self.closed && cross.is_some() { - let cross = cross.unwrap(); - self.for_each_flattened_with_t(tolerance, &mut |p,t|{ - if t < cross { - first_half.push(p); - }else{ - if !flipped{ - // when just crossed the base line, flip the order of the points - // add the cross point to the first half as the last point - // and add the cross point to the second half as the first point - flipped = true; - let cross_point = self.sample(cross); - first_half.push(cross_point.clone()); - second_half.push(cross_point); - } - second_half.push(p); + match cross { + Some(cross) => { + if self.closed { + self.for_each_flattened_with_t(tolerance, &mut |p, t| { + if t < cross { + first_half.push(p); + } else { + if !flipped { + // when just crossed the base line, flip the order of the points + // add the cross point to the first half as the last point + // and add the cross point to the second half as the first point + flipped = true; + let cross_point = self.sample(cross); + first_half.push(cross_point); + second_half.push(cross_point); + } + second_half.push(p); + } + }); + } else { + self.for_each_flattened_with_t(tolerance, &mut |p, _t| { + first_half.push(p); + }); } - - }); - }else{ - self.for_each_flattened_with_t(tolerance, &mut |p,_t|{ - first_half.push(p); - }); + } + None => { + self.for_each_flattened_with_t(tolerance, &mut |p, _t| { + first_half.push(p); + }); + } } + result.push(first_half); - if second_half.len() > 0{ + if !second_half.is_empty() { result.push(second_half); } result @@ -341,12 +375,14 @@ impl QuadraticBezierShape { /// 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,closed:bool,fill:Color32, stroke: impl Into) -> Self { - crate::epaint_assert!( - points.len() == 3, - "Quadratic needs 3 points" - ); + /// + pub fn from_points_stroke( + points: Vec, + 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 @@ -355,30 +391,30 @@ impl QuadraticBezierShape { 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{ - let mut points = [Pos2::default();3]; - for i in 0..3{ - points[i] = to_screen * self.points[i]; + pub fn to_screen(&self, to_screen: &RectTransform) -> Self { + let mut points = [Pos2::default(); 3]; + for (i, origin_point) in self.points.iter().enumerate() { + points[i] = to_screen * *origin_point; } QuadraticBezierShape { points, closed: self.closed, - fill: self.fill.clone(), - stroke: self.stroke.clone(), + fill: self.fill, + stroke: self.stroke, } } - /// Convert the quadratic Bezier curve to one PathShape. + /// Convert the quadratic Bezier 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_pathshape(&self, tolerance: Option) -> PathShape { let points = self.flatten(tolerance); - PathShape{ + PathShape { points, closed: self.closed, - fill: self.fill.clone(), - stroke: self.stroke.clone(), + fill: self.fill, + stroke: self.stroke, } } /// bounding box of the quadratic bezier shape @@ -394,26 +430,36 @@ impl QuadraticBezierShape { (self.points[2].y, self.points[0].y) }; - quadratic_for_each_local_extremum(self.points[0].x, self.points[1].x, self.points[2].x, &mut |t|{ - let x = self.sample(t).x; - if x < min_x { - min_x = x; - } - if x > max_x { - max_x = x; - } - }); + quadratic_for_each_local_extremum( + self.points[0].x, + self.points[1].x, + self.points[2].x, + &mut |t| { + let x = self.sample(t).x; + if x < min_x { + min_x = x; + } + if x > max_x { + max_x = x; + } + }, + ); + + quadratic_for_each_local_extremum( + self.points[0].y, + self.points[1].y, + self.points[2].y, + &mut |t| { + let y = self.sample(t).y; + if y < min_y { + min_y = y; + } + if y > max_y { + max_y = y; + } + }, + ); - quadratic_for_each_local_extremum(self.points[0].y, self.points[1].y, self.points[2].y, &mut |t|{ - let y = self.sample(t).y; - if y < min_y { - min_y = y; - } - if y > max_y { - max_y = y; - } - }); - Rect { min: Pos2 { x: min_x, y: min_y }, max: Pos2 { x: max_x, y: max_y }, @@ -422,8 +468,8 @@ impl QuadraticBezierShape { /// Calculate the point (x,y) at t based on the quadratic bezier curve equation. /// t is in [0.0,1.0] - /// https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B.C3.A9zier_curves - /// + /// [Bezier 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!( t >= 0.0 && t <= 1.0, @@ -443,11 +489,10 @@ impl QuadraticBezierShape { /// find a set of points that approximate the quadratic bezier 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{ - let tolerance =tolerance.unwrap_or( (self.points[0].x-self.points[2].x).abs()*0.001); - let mut result = Vec::new(); - result.push(self.points[0]); - self.for_each_flattened_with_t(tolerance, &mut |p,_t|{ + pub fn flatten(&self, tolerance: Option) -> Vec { + let tolerance = tolerance.unwrap_or((self.points[0].x - self.points[2].x).abs() * 0.001); + let mut result = vec![self.points[0]]; + self.for_each_flattened_with_t(tolerance, &mut |p, _t| { result.push(p); }); result @@ -473,11 +518,11 @@ impl QuadraticBezierShape { let count = params.count as u32; for index in 1..count { let t = params.t_at_iteration(index as f32); - - callback(self.sample(t),t); + + callback(self.sample(t), t); } - callback(self.sample(1.0),1.0); + callback(self.sample(1.0), 1.0); } } @@ -490,17 +535,16 @@ 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( +fn flatten_cubic_bezier_with_t( curve: &CubicBezierShape, tolerance: f32, callback: &mut F, -) -{ +) { // debug_assert!(tolerance >= S::EPSILON * S::EPSILON); let quadratics_tolerance = tolerance * 0.2; let flattening_tolerance = tolerance * 0.8; - let num_quadratics = curve.num_quadratics( quadratics_tolerance); + let num_quadratics = curve.num_quadratics(quadratics_tolerance); let step = 1.0 / num_quadratics as f32; let n = num_quadratics; let mut t0 = 0.0; @@ -551,8 +595,7 @@ impl FlatteningParameters { // Note, scale can be NaN, for example with straight lines. When it happens the NaN will // propagate to other parameters. We catch it all by setting the iteration count to zero // and leave the rest as garbage. - let scale = - cross.abs() / ((ddx * ddx + ddy * ddy).sqrt() * (parabola_to - parabola_from).abs()); + let scale = cross.abs() / (ddx.hypot(ddy) * (parabola_to - parabola_from).abs()); let integral_from = approx_parabola_integral(parabola_from); let integral_to = approx_parabola_integral(parabola_to); @@ -569,9 +612,7 @@ impl FlatteningParameters { // If count is NaN the curve can be approximated by a single straight line or a point. if !count.is_finite() { count = 0.0; - is_point = ((to.x - from.x) * (to.x - from.x) + (to.y - from.y) * (to.y - from.y)) - .sqrt() - < tolerance * tolerance; + is_point = (to.x - from.x).hypot(to.y - from.y) < tolerance * tolerance; } let integral_step = integral_diff / count; @@ -588,9 +629,7 @@ impl FlatteningParameters { fn t_at_iteration(&self, iteration: f32) -> f32 { let u = approx_parabola_inv_integral(self.integral_from + self.integral_step * iteration); - let t = (u - self.inv_integral_from) * self.div_inv_integral_diff; - - t + (u - self.inv_integral_from) * self.div_inv_integral_diff } } @@ -608,7 +647,7 @@ fn approx_parabola_inv_integral(x: f32) -> f32 { x * (1.0 - b + (b * b + quarter * x * x).sqrt()) } -fn single_curve_approximation(curve:&CubicBezierShape) -> QuadraticBezierShape { +fn single_curve_approximation(curve: &CubicBezierShape) -> QuadraticBezierShape { let c1_x = (curve.points[1].x * 3.0 - curve.points[0].x) * 0.5; let c1_y = (curve.points[1].y * 3.0 - curve.points[0].y) * 0.5; let c2_x = (curve.points[2].x * 3.0 - curve.points[3].x) * 0.5; @@ -625,7 +664,7 @@ fn single_curve_approximation(curve:&CubicBezierShape) -> QuadraticBezierShape { } } -fn quadratic_for_each_local_extremum(p0:f32,p1:f32,p2:f32, cb:&mut F){ +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: // p(t) = p0 + t(p1 - p0) + t^2(p2 - 2p1 + p0) // The derivative is: @@ -642,24 +681,23 @@ fn quadratic_for_each_local_extremum(p0:f32,p1:f32,p2:f32, cb:&mut if t > 0.0 && t < 1.0 { cb(t); } - } -fn cubic_for_each_local_extremum(p0:f32,p1:f32,p2:f32,p3:f32, cb:&mut F){ +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: // 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); + let a = 3.0 * (p3 + 3.0 * (p1 - p2) - p0); let b = 6.0 * (p2 - 2.0 * p1 + p0); - let c = 3.0 * (p1-p0); + let c = 3.0 * (p1 - p0); - let in_range = |t:f32| t<=1.0 && t>=0.0; + let in_range = |t: f32| t <= 1.0 && t >= 0.0; // linear situation if a == 0.0 { if b != 0.0 { - let t = - c / b; + let t = -c / b; if in_range(t) { cb(t); } @@ -699,7 +737,7 @@ mod tests { use super::*; #[test] - fn test_quadratic_bounding_box(){ + fn test_quadratic_bounding_box() { let curve = QuadraticBezierShape { points: [ Pos2 { x: 110.0, y: 170.0 }, @@ -711,18 +749,18 @@ mod tests { stroke: Default::default(), }; let bbox = curve.bounding_rect(); - assert!( (bbox.min.x-72.96).abs()<0.01); - assert!( (bbox.min.y-27.78).abs()<0.01); - - assert!( (bbox.max.x-180.0).abs() < 0.01); - assert!( (bbox.max.y-170.0).abs() < 0.01); + assert!((bbox.min.x - 72.96).abs() < 0.01); + assert!((bbox.min.y - 27.78).abs() < 0.01); + + assert!((bbox.max.x - 180.0).abs() < 0.01); + assert!((bbox.max.y - 170.0).abs() < 0.01); let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.1, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.1, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 26); let curve = QuadraticBezierShape { @@ -736,23 +774,23 @@ mod tests { stroke: Default::default(), }; let bbox = curve.bounding_rect(); - assert!( (bbox.min.x-10.0).abs()<0.01); - assert!( (bbox.min.y-10.0).abs()<0.01); - - assert!( (bbox.max.x-130.42).abs() < 0.01); - assert!( (bbox.max.y-170.0).abs() < 0.01); + assert!((bbox.min.x - 10.0).abs() < 0.01); + assert!((bbox.min.y - 10.0).abs() < 0.01); + + assert!((bbox.max.x - 130.42).abs() < 0.01); + assert!((bbox.max.y - 170.0).abs() < 0.01); let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.1, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.1, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 25); } #[test] - fn test_quadratic_dfferent_tolerance(){ + fn test_quadratic_dfferent_tolerance() { let curve = QuadraticBezierShape { points: [ Pos2 { x: 110.0, y: 170.0 }, @@ -765,38 +803,38 @@ mod tests { }; let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(1.0, &mut |pos,_t| { + curve.for_each_flattened_with_t(1.0, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 9); let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.1, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.1, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 25); let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 77); let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.001, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.001, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 240); } #[test] - fn test_cubic_bounding_box(){ + fn test_cubic_bounding_box() { let curve = CubicBezierShape { points: [ Pos2::new(10.0, 10.0), @@ -830,9 +868,9 @@ mod tests { let bbox = curve.bounding_rect(); assert_eq!(bbox.min.x, 10.0); assert_eq!(bbox.min.y, 10.0); - assert!( (bbox.max.x-206.50).abs() < 0.01); - assert!( (bbox.max.y-148.48).abs() < 0.01); - + assert!((bbox.max.x - 206.50).abs() < 0.01); + assert!((bbox.max.y - 148.48).abs() < 0.01); + let curve = CubicBezierShape { points: [ Pos2::new(110.0, 170.0), @@ -846,11 +884,11 @@ mod tests { }; let bbox = curve.bounding_rect(); - assert!( (bbox.min.x-86.71).abs()<0.01); - assert!( (bbox.min.y-30.0).abs()<0.01); - - assert!( (bbox.max.x-199.27).abs() < 0.01); - assert!( (bbox.max.y-170.0).abs() < 0.01); + assert!((bbox.min.x - 86.71).abs() < 0.01); + assert!((bbox.min.y - 30.0).abs() < 0.01); + + assert!((bbox.max.x - 199.27).abs() < 0.01); + assert!((bbox.max.y - 170.0).abs() < 0.01); } #[test] fn test_cubic_different_tolerance_flattening() { @@ -868,42 +906,42 @@ mod tests { let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(1.0, &mut |pos,_t| { + curve.for_each_flattened_with_t(1.0, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 10); let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.5, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.5, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 13); let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.1, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.1, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 28); let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 83); let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.001, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.001, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 248); } @@ -923,10 +961,10 @@ mod tests { let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 117); let curve = CubicBezierShape { @@ -943,10 +981,10 @@ mod tests { let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 91); let curve = CubicBezierShape { @@ -963,10 +1001,10 @@ mod tests { let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 75); let curve = CubicBezierShape { @@ -983,10 +1021,10 @@ mod tests { let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 100); let curve = CubicBezierShape { @@ -1003,10 +1041,10 @@ mod tests { let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 71); let curve = CubicBezierShape { @@ -1023,10 +1061,10 @@ mod tests { let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 88); } @@ -1045,42 +1083,42 @@ mod tests { let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(1.0, &mut |pos,_t| { + curve.for_each_flattened_with_t(1.0, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 9); let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.5, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.5, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 11); let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.1, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.1, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 24); let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.01, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 72); let mut result = Vec::new(); result.push(curve.points[0]); //add the start point - curve.for_each_flattened_with_t(0.001, &mut |pos,_t| { + curve.for_each_flattened_with_t(0.001, &mut |pos, _t| { result.push(pos); }); - + assert_eq!(result.len(), 223); } } diff --git a/epaint/src/tessellator.rs b/epaint/src/tessellator.rs index 5c5fd8ae513..04137960b4d 100644 --- a/epaint/src/tessellator.rs +++ b/epaint/src/tessellator.rs @@ -742,7 +742,7 @@ impl Tessellator { let points = quadratic_shape.flatten(Some(options.bezier_tolerence)); self.tessellate_bezier_complete( - points, + &points, quadratic_shape.fill, quadratic_shape.closed, quadratic_shape.stroke, @@ -767,7 +767,7 @@ impl Tessellator { for points in points_vec { self.tessellate_bezier_complete( - points, + &points, cubic_shape.fill, cubic_shape.closed, cubic_shape.stroke, @@ -778,7 +778,7 @@ impl Tessellator { fn tessellate_bezier_complete( &mut self, - points: Vec, + points: &[Pos2], fill: Color32, closed: bool, stroke: Stroke, @@ -786,9 +786,9 @@ impl Tessellator { ) { self.scratchpad_path.clear(); if closed { - self.scratchpad_path.add_line_loop(&points); + self.scratchpad_path.add_line_loop(points); } else { - self.scratchpad_path.add_open_points(&points); + self.scratchpad_path.add_open_points(points); } if fill != Color32::TRANSPARENT { crate::epaint_assert!( From 634edc95fc2316e0b14273e72be34ddf258a7ec0 Mon Sep 17 00:00:00 2001 From: Xu Desheng <8742037+xudesheng@users.noreply.github.com> Date: Mon, 31 Jan 2022 00:07:56 -0500 Subject: [PATCH 3/4] changes based on Mingun's review comments --- egui_demo_lib/src/apps/demo/paint_bezier.rs | 39 ++++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/egui_demo_lib/src/apps/demo/paint_bezier.rs b/egui_demo_lib/src/apps/demo/paint_bezier.rs index bd66de5096e..3d1cc5473c3 100644 --- a/egui_demo_lib/src/apps/demo/paint_bezier.rs +++ b/egui_demo_lib/src/apps/demo/paint_bezier.rs @@ -5,17 +5,30 @@ use egui::*; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct PaintBezier { - bezier: usize, // current bezier curve degree, it can be 3,4, - bezier_backup: usize, //track the bezier degree before change in order to clean the remaining points. - points: Vec, //points already clicked. once it reaches the 'bezier' degree, it will be pushed into the 'shapes' - backup_points: Vec, //track last points set in order to draw auxiliary lines. - q_shapes: Vec, //shapes already drawn. once it reaches the 'bezier' degree, it will be pushed into the 'shapes' - c_shapes: Vec, // since `Shape` can't be 'serilized', we can't use Shape as variable type. + /// 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. stroke: Stroke, + /// Fill for bezier 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. bounding_box_stroke: Stroke, } @@ -23,7 +36,7 @@ impl Default for PaintBezier { fn default() -> Self { Self { bezier: 4, // default bezier degree, a cubic bezier curve - bezier_backup: 4, + degree_backup: 4, points: Default::default(), backup_points: Default::default(), q_shapes: Default::default(), @@ -73,16 +86,16 @@ impl PaintBezier { ui.separator(); ui.vertical(|ui| { if ui.radio_value(&mut self.bezier, 3, "Quadratic").clicked() - && self.bezier_backup != self.bezier + && self.degree_backup != self.bezier { self.points.clear(); - self.bezier_backup = self.bezier; + self.degree_backup = self.bezier; }; if ui.radio_value(&mut self.bezier, 4, "Cubic").clicked() - && self.bezier_backup != self.bezier + && self.degree_backup != self.bezier { self.points.clear(); - self.bezier_backup = self.bezier; + self.degree_backup = self.bezier; }; // ui.radio_value(self.bezier, 5, "Quintic"); ui.label("Click 3 or 4 points to build a bezier curve!"); @@ -184,8 +197,8 @@ impl PaintBezier { } } -// 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. +/// 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, From ffc309f1afe050a6be893e452ced660e1dd1ad22 Mon Sep 17 00:00:00 2001 From: Xu Desheng <8742037+xudesheng@users.noreply.github.com> Date: Mon, 31 Jan 2022 13:43:52 -0500 Subject: [PATCH 4/4] fix clippy warnings from the test code in bezier.rs --- epaint/src/bezier.rs | 67 +++++++++++++++----------------------------- 1 file changed, 22 insertions(+), 45 deletions(-) diff --git a/epaint/src/bezier.rs b/epaint/src/bezier.rs index 1c9913f239c..b17cf9123bd 100644 --- a/epaint/src/bezier.rs +++ b/epaint/src/bezier.rs @@ -755,8 +755,7 @@ mod tests { assert!((bbox.max.x - 180.0).abs() < 0.01); assert!((bbox.max.y - 170.0).abs() < 0.01); - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.1, &mut |pos, _t| { result.push(pos); }); @@ -780,8 +779,7 @@ mod tests { assert!((bbox.max.x - 130.42).abs() < 0.01); assert!((bbox.max.y - 170.0).abs() < 0.01); - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.1, &mut |pos, _t| { result.push(pos); }); @@ -801,32 +799,28 @@ mod tests { fill: Default::default(), stroke: Default::default(), }; - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(1.0, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 9); - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.1, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 25); - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 77); - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.001, &mut |pos, _t| { result.push(pos); }); @@ -904,40 +898,35 @@ mod tests { stroke: Default::default(), }; - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(1.0, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 10); - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.5, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 13); - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.1, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 28); - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 83); - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.001, &mut |pos, _t| { result.push(pos); }); @@ -959,8 +948,7 @@ mod tests { stroke: Default::default(), }; - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); @@ -979,8 +967,7 @@ mod tests { stroke: Default::default(), }; - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); @@ -999,8 +986,7 @@ mod tests { stroke: Default::default(), }; - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); @@ -1019,8 +1005,7 @@ mod tests { stroke: Default::default(), }; - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); @@ -1039,8 +1024,7 @@ mod tests { stroke: Default::default(), }; - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); @@ -1059,8 +1043,7 @@ mod tests { stroke: Default::default(), }; - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); @@ -1081,40 +1064,34 @@ mod tests { stroke: Default::default(), }; - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(1.0, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 9); - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.5, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 11); - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.1, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 24); - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 72); - - let mut result = Vec::new(); - result.push(curve.points[0]); //add the start point + let mut result = vec![curve.points[0]]; //add the start point curve.for_each_flattened_with_t(0.001, &mut |pos, _t| { result.push(pos); });