From 4e71905fecc3822624cc46e57a2d544665e89690 Mon Sep 17 00:00:00 2001 From: xudesheng Date: Mon, 24 Jan 2022 10:58:28 -0500 Subject: [PATCH 1/3] add support to Bezier Curve --- egui/src/introspection.rs | 1 + .../src/apps/demo/demo_app_windows.rs | 4 + egui_demo_lib/src/apps/demo/mod.rs | 1 + egui_demo_lib/src/apps/demo/paint_bezier.rs | 182 ++++ epaint/src/lib.rs | 4 +- epaint/src/shape.rs | 914 ++++++++++++++++++ epaint/src/shape_transform.rs | 8 + epaint/src/stats.rs | 7 + epaint/src/tessellator.rs | 85 ++ 9 files changed, 1205 insertions(+), 1 deletion(-) create mode 100644 egui_demo_lib/src/apps/demo/paint_bezier.rs diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index 9ec2a97c105..10355303b9f 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -142,6 +142,7 @@ impl Widget for &mut epaint::TessellationOptions { debug_paint_clip_rects, debug_paint_text_rects, debug_ignore_clip_rects, + bezier_flattern_tolerence:_, } = self; ui.checkbox(anti_alias, "Antialias") .on_hover_text("Turn off for small performance gain."); 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..c4755219b63 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,10 @@ 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..9907c5ef37e 100644 --- a/egui_demo_lib/src/apps/demo/mod.rs +++ b/egui_demo_lib/src/apps/demo/mod.rs @@ -26,6 +26,7 @@ pub mod toggle_switch; pub mod widget_gallery; pub mod window_options; pub mod window_with_panels; +pub mod paint_bezier; pub use { app::DemoApp, demo_app_windows::DemoWindows, misc_demo_window::MiscDemoWindow, 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..88a7b2ef894 --- /dev/null +++ b/egui_demo_lib/src/apps/demo/paint_bezier.rs @@ -0,0 +1,182 @@ +use egui::*; +use egui::emath::RectTransform; +use egui::{epaint::{QuadraticBezierShape, CubicBezierShape,CircleShape}}; + +#[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. + quadratic_shapes: Vec, //shapes already drawn. once it reaches the 'bezier' degree, it will be pushed into the 'shapes' + cubic_shapes: Vec, + aux_stroke: Stroke, + stroke: Stroke, +} + +impl Default for PaintBezier { + fn default() -> Self { + Self { + bezier: 4, // default bezier degree, a cubic bezier curve + bezier_backup: 4, + points: Default::default(), + backup_points: Default::default(), + quadratic_shapes: Default::default(), + cubic_shapes: Default::default(), + aux_stroke: Stroke::new(1.0, Color32::RED), + stroke: Stroke::new(1.0, Color32::LIGHT_BLUE), + } + } +} + +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.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.separator(); + ui.vertical(|ui|{ + if ui.button("Clear Painting").clicked() { + self.points.clear(); + self.backup_points.clear(); + self.quadratic_shapes.clear(); + self.cubic_shapes.clear(); + } + ui.label("Click 3 or 4 points to build a bezier curve!"); + }) + + }) + .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 { + 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 points: Vec = points.iter().map(|p| to_screen * *p).collect(); + let stroke = self.stroke.clone(); + let quadratic = QuadraticBezierShape::from_points_stroke(points, stroke); + self.quadratic_shapes.push(quadratic); + }, + 4 => { + let points: Vec = points.iter().map(|p| to_screen * *p).collect(); + let stroke = self.stroke.clone(); + let cubic = CubicBezierShape::from_points_stroke(points, stroke); + self.cubic_shapes.push(cubic); + }, + _ => { + todo!(); + } + } + + // self.points.clear(); + } + + response.mark_changed(); + } + } + let mut shapes = Vec::new(); + for shape in self.quadratic_shapes.iter() { + shapes.push(shape.clone().into()); + } + for shape in self.cubic_shapes.iter() { + shapes.push(shape.clone().into()); + } + 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 + } +} + +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/lib.rs b/epaint/src/lib.rs index 547d534074b..118aafebfb4 100644 --- a/epaint/src/lib.rs +++ b/epaint/src/lib.rs @@ -108,7 +108,9 @@ pub use { image::{AlphaImage, ColorImage, ImageData}, mesh::{Mesh, Mesh16, Vertex}, shadow::Shadow, - shape::{CircleShape, PathShape, RectShape, Shape, TextShape}, + shape::{ + CircleShape, CubicBezierShape, PathShape, QuadraticBezierShape, RectShape, Shape, TextShape, + }, stats::PaintStats, stroke::Stroke, tessellator::{tessellate_shapes, TessellationOptions, Tessellator}, diff --git a/epaint/src/shape.rs b/epaint/src/shape.rs index 792bfebc9b5..366cf4d9d61 100644 --- a/epaint/src/shape.rs +++ b/epaint/src/shape.rs @@ -1,3 +1,5 @@ +use std::ops::Range; + use crate::{ text::{Fonts, Galley, TextStyle}, Color32, Mesh, Stroke, @@ -26,6 +28,9 @@ pub enum Shape { Rect(RectShape), Text(TextShape), Mesh(Mesh), + // https://github.com/emilk/egui/issues/1120 + QuadraticBezier(QuadraticBezierShape), + CubicBezier(CubicBezierShape), } /// ## Constructors @@ -187,6 +192,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; + } + } } } } @@ -464,3 +479,902 @@ fn dashes_from_line( position_on_segment -= segment_length; }); } + +// ---------------------------------------------------------------------------- + +/// 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, stroke: impl Into) -> Self { + crate::epaint_assert!( + points.len() == 4, + "Cubic needs 4 points" + ); + Self { + points:points.try_into().unwrap(), + closed: false, + fill: Default::default(), + stroke: stroke.into(), + } + } + + /// 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 + } + + /// 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 + } + // 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, 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: false, + fill: Default::default(), + stroke: stroke.into(), + } + } + + /// 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(); + 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/ +pub 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); + } +} \ No newline at end of file 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..d46cb900661 100644 --- a/epaint/src/stats.rs +++ b/epaint/src/stats.rs @@ -211,6 +211,13 @@ impl PaintStats { Shape::Mesh(mesh) => { self.shape_mesh += AllocInfo::from_mesh(mesh); } + Shape::CubicBezier(_) => { + // todo!("CubicBezier"); + // panic!("CubicBezier"); + } + Shape::QuadraticBezier(_) => { + // todo!("QuadraticBezier"); + } } } diff --git a/epaint/src/tessellator.rs b/epaint/src/tessellator.rs index c2301a88de8..4da472a687b 100644 --- a/epaint/src/tessellator.rs +++ b/epaint/src/tessellator.rs @@ -286,6 +286,9 @@ pub struct TessellationOptions { /// If true, no clipping will be done. pub debug_ignore_clip_rects: bool, + + //to add this since it will cause a lot of breaking changes. + pub bezier_flattern_tolerence: f32, } impl Default for TessellationOptions { @@ -299,6 +302,8 @@ impl Default for TessellationOptions { debug_paint_text_rects: false, debug_paint_clip_rects: false, debug_ignore_clip_rects: false, + + bezier_flattern_tolerence: 0.1, //need a helper function to determine the best value automatically } } } @@ -710,9 +715,89 @@ 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_flattern_tolerence)); + + self.scratchpad_path.clear(); + if quadratic_shape.closed { + self.scratchpad_path.add_line_loop(&points); + } else { + self.scratchpad_path.add_open_points(&points); + } + if quadratic_shape.fill != Color32::TRANSPARENT { + crate::epaint_assert!( + quadratic_shape.closed, + "You asked to fill a path that is not closed. That makes no sense." + ); + self.scratchpad_path + .fill(quadratic_shape.fill, &self.options, out); + } + let typ = if quadratic_shape.closed { + PathType::Closed + } else { + PathType::Open + }; + self.scratchpad_path + .stroke(typ, quadratic_shape.stroke, &self.options, out); } + pub(crate) fn tessellate_cubic_bezier( + &mut self, + bezier_shape: CubicBezierShape, + out: &mut Mesh, + ) { + let options = &self.options; + let clip_rect = self.clip_rect; + if options.coarse_tessellation_culling + && !bezier_shape.bounding_rect().intersects(clip_rect) + { + return; + } + + let points = bezier_shape.flatten(Some(options.bezier_flattern_tolerence)); + + self.scratchpad_path.clear(); + if bezier_shape.closed { + self.scratchpad_path.add_line_loop(&points); + } else { + self.scratchpad_path.add_open_points(&points); + } + if bezier_shape.fill != Color32::TRANSPARENT { + crate::epaint_assert!( + bezier_shape.closed, + "You asked to fill a path that is not closed. That makes no sense." + ); + self.scratchpad_path + .fill(bezier_shape.fill, &self.options, out); + } + let typ = if bezier_shape.closed { + PathType::Closed + } else { + PathType::Open + }; + self.scratchpad_path + .stroke(typ, bezier_shape.stroke, &self.options, out); + } pub(crate) fn tessellate_path(&mut self, path_shape: PathShape, out: &mut Mesh) { if path_shape.points.len() < 2 { return; From 8f729bcc4eb2af98d91efb43380bf25bc62861be Mon Sep 17 00:00:00 2001 From: xudesheng Date: Thu, 27 Jan 2022 23:38:28 -0500 Subject: [PATCH 2/3] fixes for #1155 PR review --- egui/src/context.rs | 23 + egui/src/introspection.rs | 1 + .../src/apps/demo/demo_app_windows.rs | 3 - egui_demo_lib/src/apps/demo/paint_bezier.rs | 103 +- epaint/src/bezier.rs | 1086 +++++++++++++++++ epaint/src/lib.rs | 4 +- epaint/src/shape.rs | 902 +------------- epaint/src/stats.rs | 10 +- epaint/src/tessellator.rs | 67 +- 9 files changed, 1230 insertions(+), 969 deletions(-) create mode 100644 epaint/src/bezier.rs diff --git a/egui/src/context.rs b/egui/src/context.rs index 8b56d26bf67..4c1d34427fa 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -563,6 +563,29 @@ impl Context { std::sync::Arc::make_mut(&mut self.memory().options.style).visuals = visuals; } + /// The `tolerance` will be used in Cubic Bezier Curve, Quadratic Bezier Curve flattening. + /// It is the maximum distance between the original curve and the flattened curve. + /// The larger the tolerance, the less points will be generated. + /// The default value is 0.1. + /// The smaller the tolerance, the more points will be generated. + /// But practically, it should be kept in the range of [0.001, 2.0]. + /// Example: + /// ``` + /// let (mut response, painter) = + /// ui.allocate_painter(ui.available_size_before_wrap(), Sense::click()); + /// painter.ctx().set_tolerance(1.0); + /// ``` + pub fn set_tolerance(&self, tolerance:f32){ + (&mut self.memory().options.tessellation_options).bezier_flattern_tolerence = tolerance; + } + + /// The `epsilon` will be used when comparing two floats. + /// For example, when checking the curve crossed the base line or not, the `epsilon` is used. + /// The default value is 1.0e-5 for f32. + pub fn set_epsilon(&self, epsilon:f32){ + (&mut self.memory().options.tessellation_options).epsilon = epsilon; + } + /// The number of physical pixels for each logical point. #[inline(always)] pub fn pixels_per_point(&self) -> f32 { diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index 10355303b9f..0f0360a29a0 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -143,6 +143,7 @@ impl Widget for &mut epaint::TessellationOptions { debug_paint_text_rects, debug_ignore_clip_rects, bezier_flattern_tolerence:_, + epsilon: _, } = self; ui.checkbox(anti_alias, "Antialias") .on_hover_text("Turn off for small performance gain."); 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 c4755219b63..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,10 +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/paint_bezier.rs b/egui_demo_lib/src/apps/demo/paint_bezier.rs index 88a7b2ef894..0dfb4ef0293 100644 --- a/egui_demo_lib/src/apps/demo/paint_bezier.rs +++ b/egui_demo_lib/src/apps/demo/paint_bezier.rs @@ -6,26 +6,36 @@ use egui::{epaint::{QuadraticBezierShape, CubicBezierShape,CircleShape}}; #[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. - quadratic_shapes: Vec, //shapes already drawn. once it reaches the 'bezier' degree, it will be pushed into the 'shapes' - cubic_shapes: Vec, + 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(), - quadratic_shapes: Default::default(), - cubic_shapes: 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), } } } @@ -36,10 +46,34 @@ impl PaintBezier { 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(); @@ -52,18 +86,14 @@ impl PaintBezier { self.bezier_backup = self.bezier; } }; - // ui.radio_value(self.bezier, 5, "Quintic"); - }); - ui.separator(); - ui.vertical(|ui|{ + 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.quadratic_shapes.clear(); - self.cubic_shapes.clear(); + self.q_shapes.clear(); + self.c_shapes.clear(); } - ui.label("Click 3 or 4 points to build a bezier curve!"); }) }) @@ -98,6 +128,7 @@ impl PaintBezier { let (mut response, painter) = ui.allocate_painter(ui.available_size_before_wrap(), Sense::click()); + painter.ctx().set_tolerance(self.tolerance); let to_screen = emath::RectTransform::from_to( Rect::from_min_size(Pos2::ZERO, response.rect.square_proportions()), response.rect, @@ -113,36 +144,47 @@ impl PaintBezier { let points = self.points.drain(..).collect::>(); match points.len(){ 3 => { - let points: Vec = points.iter().map(|p| to_screen * *p).collect(); - let stroke = self.stroke.clone(); - let quadratic = QuadraticBezierShape::from_points_stroke(points, stroke); - self.quadratic_shapes.push(quadratic); + let quadratic = QuadraticBezierShape::from_points_stroke( + points, + self.closed, + self.fill.clone(), + self.stroke.clone() + ); + self.q_shapes.push(quadratic); }, 4 => { - let points: Vec = points.iter().map(|p| to_screen * *p).collect(); - let stroke = self.stroke.clone(); - let cubic = CubicBezierShape::from_points_stroke(points, stroke); - self.cubic_shapes.push(cubic); + let cubic = CubicBezierShape::from_points_stroke( + points, + self.closed, + self.fill.clone(), + self.stroke.clone() + ); + self.c_shapes.push(cubic); }, _ => { todo!(); } } - - // self.points.clear(); } response.mark_changed(); } } let mut shapes = Vec::new(); - for shape in self.quadratic_shapes.iter() { - shapes.push(shape.clone().into()); + 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.cubic_shapes.iter() { - shapes.push(shape.clone().into()); + 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{ @@ -151,11 +193,20 @@ impl PaintBezier { 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" + "✔ Bezier Curve" } fn show(&mut self, ctx: &Context, open: &mut bool) { 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 118aafebfb4..3c2254ebfef 100644 --- a/epaint/src/lib.rs +++ b/epaint/src/lib.rs @@ -93,6 +93,7 @@ mod mesh; pub mod mutex; mod shadow; mod shape; +mod bezier; pub mod shape_transform; pub mod stats; mod stroke; @@ -108,8 +109,9 @@ pub use { image::{AlphaImage, ColorImage, ImageData}, mesh::{Mesh, Mesh16, Vertex}, shadow::Shadow, + bezier:: {CubicBezierShape,QuadraticBezierShape}, shape::{ - CircleShape, CubicBezierShape, PathShape, QuadraticBezierShape, RectShape, Shape, TextShape, + CircleShape, PathShape, RectShape, Shape, TextShape, }, stats::PaintStats, stroke::Stroke, diff --git a/epaint/src/shape.rs b/epaint/src/shape.rs index 366cf4d9d61..d8060bce0a9 100644 --- a/epaint/src/shape.rs +++ b/epaint/src/shape.rs @@ -1,11 +1,11 @@ -use std::ops::Range; - use crate::{ text::{Fonts, Galley, TextStyle}, Color32, Mesh, Stroke, }; use emath::*; +use crate::{QuadraticBezierShape,CubicBezierShape}; + /// A paint primitive such as a circle or a piece of text. /// Coordinates are all screen space points (not physical pixels). #[must_use = "Add a Shape to a Painter"] @@ -480,901 +480,3 @@ fn dashes_from_line( }); } -// ---------------------------------------------------------------------------- - -/// 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, stroke: impl Into) -> Self { - crate::epaint_assert!( - points.len() == 4, - "Cubic needs 4 points" - ); - Self { - points:points.try_into().unwrap(), - closed: false, - fill: Default::default(), - stroke: stroke.into(), - } - } - - /// 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 - } - - /// 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 - } - // 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, 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: false, - fill: Default::default(), - stroke: stroke.into(), - } - } - - /// 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(); - 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/ -pub 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); - } -} \ No newline at end of file diff --git a/epaint/src/stats.rs b/epaint/src/stats.rs index d46cb900661..c60369a2ae8 100644 --- a/epaint/src/stats.rs +++ b/epaint/src/stats.rs @@ -195,7 +195,8 @@ 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); @@ -211,13 +212,6 @@ impl PaintStats { Shape::Mesh(mesh) => { self.shape_mesh += AllocInfo::from_mesh(mesh); } - Shape::CubicBezier(_) => { - // todo!("CubicBezier"); - // panic!("CubicBezier"); - } - Shape::QuadraticBezier(_) => { - // todo!("QuadraticBezier"); - } } } diff --git a/epaint/src/tessellator.rs b/epaint/src/tessellator.rs index 4da472a687b..518635d72a3 100644 --- a/epaint/src/tessellator.rs +++ b/epaint/src/tessellator.rs @@ -287,8 +287,11 @@ pub struct TessellationOptions { /// If true, no clipping will be done. pub debug_ignore_clip_rects: bool, - //to add this since it will cause a lot of breaking changes. + /// The maximum distance between the original curve and the flattened curve. pub bezier_flattern_tolerence: f32, + + /// The default value will be 1.0e-5, it will be used during float compare. + pub epsilon:f32, } impl Default for TessellationOptions { @@ -304,6 +307,7 @@ impl Default for TessellationOptions { debug_ignore_clip_rects: false, bezier_flattern_tolerence: 0.1, //need a helper function to determine the best value automatically + epsilon: 1.0e-5, } } } @@ -738,66 +742,67 @@ impl Tessellator { let points = quadratic_shape.flatten(Some(options.bezier_flattern_tolerence)); - self.scratchpad_path.clear(); - if quadratic_shape.closed { - self.scratchpad_path.add_line_loop(&points); - } else { - self.scratchpad_path.add_open_points(&points); - } - if quadratic_shape.fill != Color32::TRANSPARENT { - crate::epaint_assert!( - quadratic_shape.closed, - "You asked to fill a path that is not closed. That makes no sense." - ); - self.scratchpad_path - .fill(quadratic_shape.fill, &self.options, out); - } - let typ = if quadratic_shape.closed { - PathType::Closed - } else { - PathType::Open - }; - self.scratchpad_path - .stroke(typ, quadratic_shape.stroke, &self.options, out); + self.tessellate_bezier_complete( + points, + quadratic_shape.fill, + quadratic_shape.closed, + quadratic_shape.stroke, + out, + ); + } pub(crate) fn tessellate_cubic_bezier( &mut self, - bezier_shape: CubicBezierShape, + cubic_shape: CubicBezierShape, out: &mut Mesh, ) { let options = &self.options; let clip_rect = self.clip_rect; if options.coarse_tessellation_culling - && !bezier_shape.bounding_rect().intersects(clip_rect) + && !cubic_shape.bounding_rect().intersects(clip_rect) { return; } - let points = bezier_shape.flatten(Some(options.bezier_flattern_tolerence)); + let points_vec = cubic_shape.flatten_closed(Some(options.bezier_flattern_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 bezier_shape.closed { + if closed { self.scratchpad_path.add_line_loop(&points); } else { self.scratchpad_path.add_open_points(&points); } - if bezier_shape.fill != Color32::TRANSPARENT { + if fill != Color32::TRANSPARENT { crate::epaint_assert!( - bezier_shape.closed, + closed, "You asked to fill a path that is not closed. That makes no sense." ); self.scratchpad_path - .fill(bezier_shape.fill, &self.options, out); + .fill(fill, &self.options, out); } - let typ = if bezier_shape.closed { + let typ = if closed { PathType::Closed } else { PathType::Open }; self.scratchpad_path - .stroke(typ, bezier_shape.stroke, &self.options, out); + .stroke(typ, stroke, &self.options, out); } + pub(crate) fn tessellate_path(&mut self, path_shape: PathShape, out: &mut Mesh) { if path_shape.points.len() < 2 { return; From 1673f680b9cb0edc85d87455c97b8ccd24cadeed Mon Sep 17 00:00:00 2001 From: xudesheng Date: Thu, 27 Jan 2022 23:55:38 -0500 Subject: [PATCH 3/3] add lyon_geom to the credit --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5932266943e..35e57581edf 100644 --- a/README.md +++ b/README.md @@ -370,6 +370,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