Add Bezier Shapes #1120 (#1178)

This commit is contained in:
Xu Desheng 2022-01-31 14:26:31 -05:00 committed by GitHub
parent 4e99d8f409
commit 1f03f53dc0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1488 additions and 2 deletions

View file

@ -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

View file

@ -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,

View file

@ -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()),

View file

@ -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;

View file

@ -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);
});
}
}

1101
epaint/src/bezier.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -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},

View file

@ -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;
}
}
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}

View file

@ -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) {