parent
4e99d8f409
commit
1f03f53dc0
11 changed files with 1488 additions and 2 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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;
|
||||
|
|
254
egui_demo_lib/src/apps/demo/paint_bezier.rs
Normal file
254
egui_demo_lib/src/apps/demo/paint_bezier.rs
Normal 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
1101
epaint/src/bezier.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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},
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,9 +718,93 @@ 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) {
|
||||
if path_shape.points.len() < 2 {
|
||||
return;
|
||||
|
|
Loading…
Reference in a new issue