diff --git a/README.md b/README.md index a4e8d166d85..c17460d4a9a 100644 --- a/README.md +++ b/README.md @@ -374,6 +374,8 @@ Notable contributions by: egui is licensed under [MIT](LICENSE-MIT) OR [Apache-2.0](LICENSE-APACHE). +* The flattening algorithm for the cubic bezier curve and quadratic bezier curve is from [lyon_geom](https://docs.rs/lyon_geom/latest/lyon_geom/) + Default fonts: * `emoji-icon-font.ttf`: [Copyright (c) 2014 John Slegers](/~https://github.com/jslegers/emoji-icon-font) , MIT License diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index eb6b7c4ab4f..615ac8d2641 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -144,9 +144,17 @@ impl Widget for &mut epaint::TessellationOptions { debug_paint_clip_rects, debug_paint_text_rects, debug_ignore_clip_rects, + bezier_tolerence, + epsilon: _, } = self; ui.checkbox(anti_alias, "Antialias") .on_hover_text("Turn off for small performance gain."); + ui.add( + crate::widgets::Slider::new(bezier_tolerence, 0.0001..=10.0) + .logarithmic(true) + .show_value(true) + .text("Spline Tolerance"), + ); ui.collapsing("debug", |ui| { ui.checkbox( coarse_tessellation_culling, diff --git a/egui_demo_lib/src/apps/demo/demo_app_windows.rs b/egui_demo_lib/src/apps/demo/demo_app_windows.rs index b905ca34904..6c4b69e7cfb 100644 --- a/egui_demo_lib/src/apps/demo/demo_app_windows.rs +++ b/egui_demo_lib/src/apps/demo/demo_app_windows.rs @@ -25,6 +25,7 @@ impl Default for Demos { Box::new(super::MiscDemoWindow::default()), Box::new(super::multi_touch::MultiTouch::default()), Box::new(super::painting::Painting::default()), + Box::new(super::paint_bezier::PaintBezier::default()), Box::new(super::plot_demo::PlotDemo::default()), Box::new(super::scrolling::Scrolling::default()), Box::new(super::sliders::Sliders::default()), diff --git a/egui_demo_lib/src/apps/demo/mod.rs b/egui_demo_lib/src/apps/demo/mod.rs index 79ef343f748..61118f7d7ee 100644 --- a/egui_demo_lib/src/apps/demo/mod.rs +++ b/egui_demo_lib/src/apps/demo/mod.rs @@ -15,6 +15,7 @@ pub mod font_book; pub mod layout_test; pub mod misc_demo_window; pub mod multi_touch; +pub mod paint_bezier; pub mod painting; pub mod password; pub mod plot_demo; diff --git a/egui_demo_lib/src/apps/demo/paint_bezier.rs b/egui_demo_lib/src/apps/demo/paint_bezier.rs new file mode 100644 index 00000000000..3d1cc5473c3 --- /dev/null +++ b/egui_demo_lib/src/apps/demo/paint_bezier.rs @@ -0,0 +1,254 @@ +use egui::emath::RectTransform; +use egui::epaint::{CircleShape, CubicBezierShape, QuadraticBezierShape}; +use egui::*; + +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct PaintBezier { + /// Current bezier curve degree, it can be 3, 4. + bezier: usize, + /// Track the bezier degree before change in order to clean the remaining points. + degree_backup: usize, + /// Points already clicked. once it reaches the 'bezier' degree, it will be pushed into the 'shapes' + points: Vec, + /// Track last points set in order to draw auxiliary lines. + backup_points: Vec, + /// Quadratic shapes already drawn. + q_shapes: Vec, + /// Cubic shapes already drawn. + /// Since `Shape` can't be 'serialized', we can't use Shape as variable type. + c_shapes: Vec, + /// Stroke for auxiliary lines. + aux_stroke: Stroke, + /// Stroke for bezier curve. + stroke: Stroke, + /// Fill for bezier curve. + fill: Color32, + /// The curve should be closed or not. + closed: bool, + /// Display the bounding box or not. + show_bounding_box: bool, + /// Storke for the bounding box. + bounding_box_stroke: Stroke, +} + +impl Default for PaintBezier { + fn default() -> Self { + Self { + bezier: 4, // default bezier degree, a cubic bezier curve + degree_backup: 4, + points: Default::default(), + backup_points: Default::default(), + q_shapes: Default::default(), + c_shapes: Default::default(), + aux_stroke: Stroke::new(1.0, Color32::RED), + stroke: Stroke::new(1.0, Color32::LIGHT_BLUE), + fill: Default::default(), + closed: false, + show_bounding_box: false, + bounding_box_stroke: Stroke::new(1.0, Color32::LIGHT_GREEN), + } + } +} + +impl PaintBezier { + pub fn ui_control(&mut self, ui: &mut egui::Ui) -> egui::Response { + ui.horizontal(|ui| { + ui.vertical(|ui| { + egui::stroke_ui(ui, &mut self.stroke, "Curve Stroke"); + egui::stroke_ui(ui, &mut self.aux_stroke, "Auxiliary Stroke"); + ui.horizontal(|ui| { + ui.label("Fill Color:"); + if ui.color_edit_button_srgba(&mut self.fill).changed() + && self.fill != Color32::TRANSPARENT + { + self.closed = true; + } + if ui.checkbox(&mut self.closed, "Closed").clicked() && !self.closed { + self.fill = Color32::TRANSPARENT; + } + }); + egui::stroke_ui(ui, &mut self.bounding_box_stroke, "Bounding Box Stroke"); + }); + + ui.separator(); + ui.vertical(|ui| { + { + let mut tessellation_options = *(ui.ctx().tessellation_options()); + let tessellation_options = &mut tessellation_options; + tessellation_options.ui(ui); + let mut new_tessellation_options = ui.ctx().tessellation_options(); + *new_tessellation_options = *tessellation_options; + } + + ui.checkbox(&mut self.show_bounding_box, "Bounding Box"); + }); + ui.separator(); + ui.vertical(|ui| { + if ui.radio_value(&mut self.bezier, 3, "Quadratic").clicked() + && self.degree_backup != self.bezier + { + self.points.clear(); + self.degree_backup = self.bezier; + }; + if ui.radio_value(&mut self.bezier, 4, "Cubic").clicked() + && self.degree_backup != self.bezier + { + self.points.clear(); + self.degree_backup = self.bezier; + }; + // ui.radio_value(self.bezier, 5, "Quintic"); + ui.label("Click 3 or 4 points to build a bezier curve!"); + if ui.button("Clear Painting").clicked() { + self.points.clear(); + self.backup_points.clear(); + self.q_shapes.clear(); + self.c_shapes.clear(); + } + }) + }) + .response + } + + pub fn ui_content(&mut self, ui: &mut Ui) -> egui::Response { + let (mut response, painter) = + ui.allocate_painter(ui.available_size_before_wrap(), Sense::click()); + + let to_screen = emath::RectTransform::from_to( + Rect::from_min_size(Pos2::ZERO, response.rect.square_proportions()), + response.rect, + ); + let from_screen = to_screen.inverse(); + + if response.clicked() { + if let Some(pointer_pos) = response.interact_pointer_pos() { + let canvas_pos = from_screen * pointer_pos; + self.points.push(canvas_pos); + if self.points.len() >= self.bezier { + self.backup_points = self.points.clone(); + let points = self.points.drain(..).collect::>(); + match points.len() { + 3 => { + let quadratic = QuadraticBezierShape::from_points_stroke( + points, + self.closed, + self.fill, + self.stroke, + ); + self.q_shapes.push(quadratic); + } + 4 => { + let cubic = CubicBezierShape::from_points_stroke( + points, + self.closed, + self.fill, + self.stroke, + ); + self.c_shapes.push(cubic); + } + _ => { + unreachable!(); + } + } + } + + response.mark_changed(); + } + } + let mut shapes = Vec::new(); + for shape in self.q_shapes.iter() { + shapes.push(shape.to_screen(&to_screen).into()); + if self.show_bounding_box { + shapes.push(self.build_bounding_box(shape.bounding_rect(), &to_screen)); + } + } + for shape in self.c_shapes.iter() { + shapes.push(shape.to_screen(&to_screen).into()); + if self.show_bounding_box { + shapes.push(self.build_bounding_box(shape.bounding_rect(), &to_screen)); + } + } + painter.extend(shapes); + + if !self.points.is_empty() { + painter.extend(build_auxiliary_line( + &self.points, + &to_screen, + &self.aux_stroke, + )); + } else if !self.backup_points.is_empty() { + painter.extend(build_auxiliary_line( + &self.backup_points, + &to_screen, + &self.aux_stroke, + )); + } + + 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); + bbox_shape.into() + } +} + +/// An internal function to create auxiliary lines around the current bezier curve +/// or to auxiliary lines (points) before the points meet the bezier curve requirements. +fn build_auxiliary_line( + points: &[Pos2], + to_screen: &RectTransform, + aux_stroke: &Stroke, +) -> Vec { + let mut shapes = Vec::new(); + if points.len() >= 2 { + let points: Vec = points.iter().map(|p| to_screen * *p).collect(); + shapes.push(egui::Shape::line(points, *aux_stroke)); + } + for point in points.iter() { + let center = to_screen * *point; + let radius = aux_stroke.width * 3.0; + let circle = CircleShape { + center, + radius, + fill: aux_stroke.color, + stroke: *aux_stroke, + }; + + shapes.push(circle.into()); + } + + shapes +} + +impl super::Demo for PaintBezier { + fn name(&self) -> &'static str { + "✔ Bezier Curve" + } + + fn show(&mut self, ctx: &Context, open: &mut bool) { + use super::View as _; + Window::new(self.name()) + .open(open) + .default_size(vec2(512.0, 512.0)) + .vscroll(false) + .show(ctx, |ui| self.ui(ui)); + } +} + +impl super::View for PaintBezier { + fn ui(&mut self, ui: &mut Ui) { + // ui.vertical_centered(|ui| { + // ui.add(crate::__egui_github_link_file!()); + // }); + self.ui_control(ui); + + Frame::dark_canvas(ui.style()).show(ui, |ui| { + self.ui_content(ui); + }); + } +} diff --git a/epaint/src/bezier.rs b/epaint/src/bezier.rs new file mode 100644 index 00000000000..b17cf9123bd --- /dev/null +++ b/epaint/src/bezier.rs @@ -0,0 +1,1101 @@ +#![allow(clippy::many_single_char_names)] +use std::ops::Range; + +use crate::{shape::Shape, Color32, PathShape, Stroke}; +use emath::*; + +// ---------------------------------------------------------------------------- + +/// How to paint a cubic Bezier curve on screen. +/// The definition: [Bezier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). +/// This implementation is only for cubic Bezier curve, or the Bezier curve of degree 3. +#[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, origin_point) in self.points.iter().enumerate() { + points[i] = to_screen * *origin_point; + } + CubicBezierShape { + points, + closed: self.closed, + fill: self.fill, + stroke: self.stroke, + } + } + + /// Convert the cubic Bezier curve to one or two `PathShapes`. + /// When the curve is closed and it has to intersect with the base line, it will be converted into two shapes. + /// Otherwise, it will be converted into one shape. + /// The `tolerance` will be used to control the max distance between the curve and the base line. + /// The `epsilon` is used when comparing two floats. + pub fn to_pathshapes(&self, tolerance: Option, epsilon: Option) -> Vec { + 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, + stroke: self.stroke, + }; + 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 convex line. + /// If the curve has more than two intersection points with the base line, the "fill" will be a problem. + /// We need to find out where is the 3rd t value (0 0, there will be one real root, two complex roots + /// when p = 0, there will be two real roots, when p=q=0, there will be three real roots but all 0. + /// when p < 0, there will be three unique real roots. this is what we need. (x1, x2, x3) + /// t = x + b / (3 * a), then we have: t1, t2, t3. + /// the one between 0.0 and 1.0 is what we need. + /// <`https://baike.baidu.com/item/%E4%B8%80%E5%85%83%E4%B8%89%E6%AC%A1%E6%96%B9%E7%A8%8B/8388473 /`> + /// + pub fn find_cross_t(&self, epsilon: f32) -> Option { + 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); + + 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 t1 = 2.0 * r.cbrt() * theta.cos() + h; + let t2 = 2.0 * r.cbrt() * (theta + 120.0 * std::f32::consts::PI / 180.0).cos() + h; + let t3 = 2.0 * r.cbrt() * (theta + 240.0 * std::f32::consts::PI / 180.0).cos() + h; + + if t1 > epsilon && t1 < 1.0 - epsilon { + return Some(t1); + } + if t2 > epsilon && t2 < 1.0 - epsilon { + return Some(t2); + } + if t3 > epsilon && t3 < 1.0 - epsilon { + return Some(t3); + } + None + } + + /// Calculate the point (x,y) at t based on the cubic bezier curve equation. + /// t is in [0.0,1.0] + /// [Bezier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves) + /// + 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![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); + match cross { + Some(cross) => { + if self.closed { + self.for_each_flattened_with_t(tolerance, &mut |p, t| { + if t < cross { + first_half.push(p); + } else { + if !flipped { + // when just crossed the base line, flip the order of the points + // add the cross point to the first half as the last point + // and add the cross point to the second half as the first point + flipped = true; + let cross_point = self.sample(cross); + first_half.push(cross_point); + second_half.push(cross_point); + } + second_half.push(p); + } + }); + } else { + self.for_each_flattened_with_t(tolerance, &mut |p, _t| { + first_half.push(p); + }); + } + } + None => { + self.for_each_flattened_with_t(tolerance, &mut |p, _t| { + first_half.push(p); + }); + } + } + + result.push(first_half); + if !second_half.is_empty() { + 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, origin_point) in self.points.iter().enumerate() { + points[i] = to_screen * *origin_point; + } + QuadraticBezierShape { + points, + closed: self.closed, + fill: self.fill, + stroke: self.stroke, + } + } + + /// 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, + stroke: self.stroke, + } + } + /// 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] + /// [Bezier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B.C3.A9zier_curves) + /// + pub fn sample(&self, t: f32) -> Pos2 { + crate::epaint_assert!( + t >= 0.0 && t <= 1.0, + "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![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.hypot(ddy) * (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).hypot(to.y - from.y) < 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); + (u - self.inv_integral_from) * self.div_inv_integral_diff + } +} + +/// 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![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![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![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![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![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![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![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![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![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![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![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![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![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![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![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![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![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![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![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![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![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![curve.points[0]]; //add the start point + curve.for_each_flattened_with_t(0.001, &mut |pos, _t| { + result.push(pos); + }); + + assert_eq!(result.len(), 223); + } +} diff --git a/epaint/src/lib.rs b/epaint/src/lib.rs index 2207398d974..48df065bb69 100644 --- a/epaint/src/lib.rs +++ b/epaint/src/lib.rs @@ -87,6 +87,7 @@ #![allow(clippy::float_cmp)] #![allow(clippy::manual_range_contains)] +mod bezier; pub mod color; pub mod image; mod mesh; @@ -104,6 +105,7 @@ pub mod textures; pub mod util; pub use { + bezier::{CubicBezierShape, QuadraticBezierShape}, color::{Color32, Rgba}, image::{AlphaImage, ColorImage, ImageData, ImageDelta}, mesh::{Mesh, Mesh16, Vertex}, diff --git a/epaint/src/shape.rs b/epaint/src/shape.rs index 84a363cfea2..4bdf242a466 100644 --- a/epaint/src/shape.rs +++ b/epaint/src/shape.rs @@ -2,6 +2,7 @@ use crate::{ text::{FontId, Fonts, Galley}, Color32, Mesh, Stroke, }; +use crate::{CubicBezierShape, QuadraticBezierShape}; use emath::*; /// A paint primitive such as a circle or a piece of text. @@ -26,6 +27,8 @@ pub enum Shape { Rect(RectShape), Text(TextShape), Mesh(Mesh), + QuadraticBezier(QuadraticBezierShape), + CubicBezier(CubicBezierShape), } /// ## Constructors @@ -187,6 +190,16 @@ impl Shape { Shape::Mesh(mesh) => { mesh.translate(delta); } + Shape::QuadraticBezier(bezier_shape) => { + bezier_shape.points[0] += delta; + bezier_shape.points[1] += delta; + bezier_shape.points[2] += delta; + } + Shape::CubicBezier(cubie_curve) => { + for p in &mut cubie_curve.points { + *p += delta; + } + } } } } diff --git a/epaint/src/shape_transform.rs b/epaint/src/shape_transform.rs index b3ffc19d534..c033a8ee4b2 100644 --- a/epaint/src/shape_transform.rs +++ b/epaint/src/shape_transform.rs @@ -43,5 +43,13 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { adjust_color(&mut v.color); } } + Shape::QuadraticBezier(quatratic) => { + adjust_color(&mut quatratic.fill); + adjust_color(&mut quatratic.stroke.color); + } + Shape::CubicBezier(bezier) => { + adjust_color(&mut bezier.fill); + adjust_color(&mut bezier.stroke.color); + } } } diff --git a/epaint/src/stats.rs b/epaint/src/stats.rs index ad7a0c88389..322c0ddf13e 100644 --- a/epaint/src/stats.rs +++ b/epaint/src/stats.rs @@ -195,8 +195,12 @@ impl PaintStats { self.add(shape); } } - Shape::Noop | Shape::Circle { .. } | Shape::LineSegment { .. } | Shape::Rect { .. } => { - } + Shape::Noop + | Shape::Circle { .. } + | Shape::LineSegment { .. } + | Shape::Rect { .. } + | Shape::CubicBezier(_) + | Shape::QuadraticBezier(_) => {} Shape::Path(path_shape) => { self.shape_path += AllocInfo::from_slice(&path_shape.points); } diff --git a/epaint/src/tessellator.rs b/epaint/src/tessellator.rs index c2301a88de8..04137960b4d 100644 --- a/epaint/src/tessellator.rs +++ b/epaint/src/tessellator.rs @@ -286,6 +286,12 @@ pub struct TessellationOptions { /// If true, no clipping will be done. pub debug_ignore_clip_rects: bool, + + /// The maximum distance between the original curve and the flattened curve. + pub bezier_tolerence: f32, + + /// The default value will be 1.0e-5, it will be used during float compare. + pub epsilon: f32, } impl Default for TessellationOptions { @@ -299,6 +305,8 @@ impl Default for TessellationOptions { debug_paint_text_rects: false, debug_paint_clip_rects: false, debug_ignore_clip_rects: false, + bezier_tolerence: 0.1, + epsilon: 1.0e-5, } } } @@ -710,7 +718,91 @@ impl Tessellator { } self.tessellate_text(tex_size, text_shape, out); } + Shape::QuadraticBezier(quadratic_shape) => { + self.tessellate_quadratic_bezier(quadratic_shape, out); + } + Shape::CubicBezier(cubic_shape) => self.tessellate_cubic_bezier(cubic_shape, out), + } + } + + pub(crate) fn tessellate_quadratic_bezier( + &mut self, + quadratic_shape: QuadraticBezierShape, + out: &mut Mesh, + ) { + let options = &self.options; + let clip_rect = self.clip_rect; + + if options.coarse_tessellation_culling + && !quadratic_shape.bounding_rect().intersects(clip_rect) + { + return; } + + let points = quadratic_shape.flatten(Some(options.bezier_tolerence)); + + self.tessellate_bezier_complete( + &points, + quadratic_shape.fill, + quadratic_shape.closed, + quadratic_shape.stroke, + out, + ); + } + + pub(crate) fn tessellate_cubic_bezier( + &mut self, + cubic_shape: CubicBezierShape, + out: &mut Mesh, + ) { + let options = &self.options; + let clip_rect = self.clip_rect; + if options.coarse_tessellation_culling && !cubic_shape.bounding_rect().intersects(clip_rect) + { + return; + } + + let points_vec = + cubic_shape.flatten_closed(Some(options.bezier_tolerence), Some(options.epsilon)); + + for points in points_vec { + self.tessellate_bezier_complete( + &points, + cubic_shape.fill, + cubic_shape.closed, + cubic_shape.stroke, + out, + ); + } + } + + fn tessellate_bezier_complete( + &mut self, + points: &[Pos2], + fill: Color32, + closed: bool, + stroke: Stroke, + out: &mut Mesh, + ) { + self.scratchpad_path.clear(); + if closed { + self.scratchpad_path.add_line_loop(points); + } else { + self.scratchpad_path.add_open_points(points); + } + if fill != Color32::TRANSPARENT { + crate::epaint_assert!( + closed, + "You asked to fill a path that is not closed. That makes no sense." + ); + self.scratchpad_path.fill(fill, &self.options, out); + } + let typ = if closed { + PathType::Closed + } else { + PathType::Open + }; + self.scratchpad_path.stroke(typ, stroke, &self.options, out); } pub(crate) fn tessellate_path(&mut self, path_shape: PathShape, out: &mut Mesh) {