Skip to content

Commit

Permalink
Add Bezier Shapes #1120 (#1178)
Browse files Browse the repository at this point in the history
  • Loading branch information
xudesheng committed Jan 31, 2022
1 parent 4e99d8f commit 1f03f53
Show file tree
Hide file tree
Showing 11 changed files with 1,488 additions and 2 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions egui/src/introspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
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 @@ -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;
Expand Down
254 changes: 254 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,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<Pos2>,
/// Track last points set in order to draw auxiliary lines.
backup_points: Vec<Pos2>,
/// Quadratic shapes already drawn.
q_shapes: Vec<QuadraticBezierShape>,
/// Cubic shapes already drawn.
/// Since `Shape` can't be 'serialized', we can't use Shape as variable type.
c_shapes: Vec<CubicBezierShape>,
/// 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::<Vec<_>>();
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<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));
}
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);
});
}
}

0 comments on commit 1f03f53

Please sign in to comment.