Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bezier curve #1155

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
xudesheng marked this conversation as resolved.
Show resolved Hide resolved

/// The number of physical pixels for each logical point.
#[inline(always)]
pub fn pixels_per_point(&self) -> f32 {
Expand Down
2 changes: 2 additions & 0 deletions egui/src/introspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ impl Widget for &mut epaint::TessellationOptions {
debug_paint_clip_rects,
debug_paint_text_rects,
debug_ignore_clip_rects,
bezier_flattern_tolerence:_,
xudesheng marked this conversation as resolved.
Show resolved Hide resolved
epsilon: _,
} = self;
ui.checkbox(anti_alias, "Antialias")
.on_hover_text("Turn off for small performance gain.");
Expand Down
1 change: 1 addition & 0 deletions egui_demo_lib/src/apps/demo/demo_app_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
1 change: 1 addition & 0 deletions egui_demo_lib/src/apps/demo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
233 changes: 233 additions & 0 deletions egui_demo_lib/src/apps/demo/paint_bezier.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
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,
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<Pos2>, //points already clicked. once it reaches the 'bezier' degree, it will be pushed into the 'shapes'
backup_points:Vec<Pos2>, //track last points set in order to draw auxiliary lines.
q_shapes: Vec<QuadraticBezierShape>, //shapes already drawn. once it reaches the 'bezier' degree, it will be pushed into the 'shapes'
c_shapes: Vec<CubicBezierShape>, // since `Shape` can't be 'serilized', we can't use Shape as variable type.
aux_stroke: Stroke,
stroke: Stroke,
fill: Color32,
closed: bool,
show_bounding_box: bool,
bounding_box_stroke: Stroke,
}

impl Default for PaintBezier {
fn default() -> Self {
Self {
bezier: 4, // default bezier degree, a cubic bezier curve
tolerance: 1.0, // default tolerance 1.0
bezier_backup: 4,
points: Default::default(),
backup_points: Default::default(),
q_shapes: Default::default(),
c_shapes: Default::default(),
aux_stroke: Stroke::new(1.0, Color32::RED),
stroke: Stroke::new(1.0, Color32::LIGHT_BLUE),
fill: Default::default(),
closed: false,
show_bounding_box: false,
bounding_box_stroke: Stroke::new(1.0, Color32::LIGHT_GREEN),
}
}
}

impl PaintBezier {
pub fn ui_control(&mut self, ui: &mut egui::Ui) -> egui::Response {
ui.horizontal(|ui| {
ui.vertical(|ui| {
egui::stroke_ui(ui, &mut self.stroke, "Curve Stroke");
egui::stroke_ui(ui, &mut self.aux_stroke, "Auxiliary Stroke");
ui.horizontal(|ui|{
ui.label("Fill Color:");
if ui.color_edit_button_srgba(&mut self.fill).changed(){
if self.fill != Color32::TRANSPARENT{
self.closed = true;
}
}
if ui.checkbox(&mut self.closed, "Closed").clicked() {
if !self.closed{
self.fill=Color32::TRANSPARENT;
}
}
})
});

ui.separator();
ui.vertical(|ui|{
ui.add(egui::Slider::new(&mut self.tolerance, 0.0001..=10.0)
.logarithmic(true)
.show_value(true)
.text("Tolerance:"));
ui.checkbox(&mut self.show_bounding_box, "Bounding Box");

egui::stroke_ui(ui, &mut self.bounding_box_stroke, "Bounding Box Stroke");
});
ui.separator();
ui.vertical(|ui|{

if ui.radio_value(&mut self.bezier, 3, "Quadratic").clicked(){
if self.bezier_backup != self.bezier{
self.points.clear();
self.bezier_backup = self.bezier;
}
};
if ui.radio_value(&mut self.bezier, 4, "Cubic").clicked(){
if self.bezier_backup != self.bezier{
self.points.clear();
self.bezier_backup = self.bezier;
}
};
// ui.radio_value(self.bezier, 5, "Quintic");
ui.label("Click 3 or 4 points to build a bezier curve!");
if ui.button("Clear Painting").clicked() {
self.points.clear();
self.backup_points.clear();
self.q_shapes.clear();
self.c_shapes.clear();
}
})

})
.response
}

// an internal function to create auxiliary lines around the current bezier curve
// or to auxiliary lines (points) before the points meet the bezier curve requirements.
fn build_auxiliary_line(&self,points:&[Pos2],to_screen:&RectTransform,aux_stroke:&Stroke)->Vec<Shape>{
let mut shapes = Vec::new();
if points.len()>=2 {
let points:Vec<Pos2> = 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());

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,
);
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::<Vec<_>>();
match points.len(){
3 => {
let quadratic = QuadraticBezierShape::from_points_stroke(
points,
self.closed,
self.fill.clone(),
self.stroke.clone()
);
self.q_shapes.push(quadratic);
},
4 => {
let cubic = CubicBezierShape::from_points_stroke(
points,
self.closed,
self.fill.clone(),
self.stroke.clone()
);
self.c_shapes.push(cubic);
},
_ => {
todo!();
}
}
}

response.mark_changed();
}
}
let mut shapes = Vec::new();
for shape in self.q_shapes.iter(){
shapes.push(shape.to_screen(&to_screen).into());
if self.show_bounding_box{
shapes.push(self.build_bounding_box(shape.bounding_rect(),&to_screen));
}
}
for shape in self.c_shapes.iter(){
shapes.push(shape.to_screen(&to_screen).into());
if self.show_bounding_box{
shapes.push(self.build_bounding_box(shape.bounding_rect(),&to_screen));
}
}
painter.extend(shapes);
xudesheng marked this conversation as resolved.
Show resolved Hide resolved

if self.points.len()>0{
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hot tip: configure your editor to run cargo fmt on each save. It is a life-changer :)

painter.extend(self.build_auxiliary_line(&self.points,&to_screen,&self.aux_stroke));
}else if self.backup_points.len()>0{
painter.extend(self.build_auxiliary_line(&self.backup_points,&to_screen,&self.aux_stroke));
}

response
}

pub fn build_bounding_box(&self, bbox:Rect, to_screen:&RectTransform)->Shape{
let bbox = Rect{
min: to_screen * bbox.min,
max: to_screen * bbox.max,
};
let bbox_shape = epaint::RectShape::stroke(bbox, 0.0,self.bounding_box_stroke.clone());
bbox_shape.into()
}
}

impl super::Demo for PaintBezier {
fn name(&self) -> &'static str {
"✔ Bezier Curve"
}

fn show(&mut self, ctx: &Context, open: &mut bool) {
use super::View as _;
Window::new(self.name())
.open(open)
.default_size(vec2(512.0, 512.0))
.vscroll(false)
.show(ctx, |ui| self.ui(ui));
}
}

impl super::View for PaintBezier {
fn ui(&mut self, ui: &mut Ui) {
// ui.vertical_centered(|ui| {
// ui.add(crate::__egui_github_link_file!());
// });
self.ui_control(ui);

Frame::dark_canvas(ui.style()).show(ui, |ui| {
self.ui_content(ui);
});
}
}
Loading