From 827d8d6a1c077635a11b7d8974f95af8e9826d9d Mon Sep 17 00:00:00 2001 From: Enrico Lumetti Date: Wed, 24 Feb 2021 15:51:56 +0100 Subject: [PATCH] Reintroduce stroke eraser, copying code from PR #7 --- src/canvas.rs | 101 ++++++++++++++++++++++++++++++++++++++++--- src/main.rs | 5 ++- src/tool.rs | 40 +++++++++++++++-- src/widget/canvas.rs | 15 +++++-- 4 files changed, 148 insertions(+), 13 deletions(-) diff --git a/src/canvas.rs b/src/canvas.rs index 5cdd865..0c9ea65 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +use std::vec::Vec; + use im::Vector; use serde::de::{Deserializer, SeqAccess, Visitor}; @@ -25,12 +27,58 @@ pub struct Path { pub kurbo_path: druid::kurbo::BezPath, } -pub type Canvas = Vector; - #[derive(Serialize, Deserialize)] #[serde(remote = "druid::Color")] enum ColorDef { - Rgba32(u32) + Rgba32(u32), +} + +struct Interval { + start_pos: f64, + end_pos: f64, +} + +impl Interval { + fn new(x1: f64, x2: f64) -> Interval { + Interval { + start_pos: x1.min(x2), + end_pos: x1.max(x2), + } + } + + fn new_ordered(start: f64, end: f64) -> Interval { + Interval { + start_pos: start, + end_pos: end, + } + } + + fn overlaps(&self, other: &Self) -> bool { + self.start_pos < other.end_pos && other.start_pos < self.end_pos + } +} + +// O(1) complexity +// TODO(francesco): Implement this in a way that is not so flamboyant +// TODO(francesco): Implement intersection test for bezier curves +fn segment_intersects_rect(segment: &druid::kurbo::PathSeg, rect: druid::Rect) -> bool { + match segment { + druid::kurbo::PathSeg::Line(line) => { + // A Segment intersects the rect + // if their projections on the x and y line both overlap + let line_x_proj = Interval::new(line.p0.x, line.p1.x); + let line_y_proj = Interval::new(line.p0.y, line.p1.y); + + let rect_x_proj = Interval::new_ordered(rect.min_x(), rect.max_x()); + let rect_y_proj = Interval::new_ordered(rect.min_y(), rect.max_y()); + + line_x_proj.overlaps(&rect_x_proj) && line_y_proj.overlaps(&rect_y_proj) + } + + _ => { + unimplemented!(); + } + } } #[derive(Debug, Clone, druid::Data, Serialize, Deserialize)] @@ -46,7 +94,9 @@ pub enum CanvasElement { impl CanvasElement { pub fn bounding_box(&self) -> druid::Rect { match self { - CanvasElement::Freehand { path, thickness, .. } => { + CanvasElement::Freehand { + path, thickness, .. + } => { use druid::kurbo::Shape; path.kurbo_path .bounding_box() @@ -54,9 +104,25 @@ impl CanvasElement { } } } + + pub fn intersects_rect(&self, rect: druid::Rect) -> bool { + match self { + CanvasElement::Freehand { path, .. } => { + // TODO(enrico) this doesn't handle thickness + path.kurbo_path + .segments() + .any(|segment| segment_intersects_rect(&segment, rect)) + } + } + } + pub fn draw(&self, ctx: &mut druid::PaintCtx) { match self { - CanvasElement::Freehand { path, thickness, stroke_color } => { + CanvasElement::Freehand { + path, + thickness, + stroke_color, + } => { use druid::RenderContext; ctx.stroke(&path.kurbo_path, &*stroke_color, *thickness); } @@ -70,6 +136,29 @@ impl CanvasElement { } } +#[derive(Clone, druid::Data)] +pub struct Canvas { + pub elements: Vector, +} + +impl Canvas { + /// Find all CanvasElement that intersect with rect + pub fn find_intersections(&self, rect: druid::Rect) -> Vec { + let mut found_elements = Vec::::new(); + + for (i, elem) in self.elements.iter().enumerate() { + // Check if the element intersects the eraser rect + if elem.bounding_box().intersect(rect).area() > 0.0 { + if elem.intersects_rect(rect) { + found_elements.push(i); + } + } + } + + found_elements + } +} + impl Serialize for Path { fn serialize(&self, serializer: S) -> Result where @@ -92,7 +181,7 @@ impl<'de> Deserialize<'de> for Path { { struct PathVisitor; - impl<'de> Visitor<'de> for PathVisitor{ + impl<'de> Visitor<'de> for PathVisitor { type Value = Path; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { diff --git a/src/main.rs b/src/main.rs index cec11e8..eb54e93 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,7 @@ use druid::{ use im::Vector; +use stiletto::canvas::Canvas; use stiletto::history::VersionedCanvas; use stiletto::tool::{CanvasToolCtx, CanvasToolParams}; use stiletto::widget::{build_simple_tool_widget, CanvasState, CanvasToolIconState, CanvasWidget}; @@ -51,7 +52,9 @@ pub fn main() { }; let canvas_data = StilettoState { canvas: CanvasState { - versioned_canvas: VersionedCanvas::new(vector![]), + versioned_canvas: VersionedCanvas::new(Canvas { + elements: vector![], + }), tool_ctx: CanvasToolCtx::new(default_pen_params.clone()), }, tool_icons: vector![ diff --git a/src/tool.rs b/src/tool.rs index 78b1b7d..d22870e 100644 --- a/src/tool.rs +++ b/src/tool.rs @@ -36,7 +36,11 @@ pub enum CanvasToolType { #[derive(Clone, Data)] pub enum CanvasToolState { Idle, - DrawingFreehand { pen_params: CanvasToolParams, current_path: CanvasElement }, + DrawingFreehand { + pen_params: CanvasToolParams, + current_path: CanvasElement, + }, + Erasing, } #[derive(Clone, Data)] @@ -71,6 +75,36 @@ impl CanvasToolCtx { ) { match self.initial_params.tool_type() { CanvasToolType::Pen => self.handle_pen_event(&ctx, &event, &mut vcanvas, &env), + CanvasToolType::Eraser => self.handle_erase_event(&ctx, &event, &mut vcanvas, &env), + } + } + + pub fn handle_erase_event( + &mut self, + _ctx: &EventCtx, + event: &Event, + vcanvas: &mut VersionedCanvas, + _env: &Env, + ) { + match (&mut self.state, event) { + (CanvasToolState::Idle, Event::MouseDown(_)) => { + self.state = CanvasToolState::Erasing; + } + (CanvasToolState::Erasing, Event::MouseMove(mouse_event)) => { + let eraser_rect = druid::Rect::from_center_size(mouse_event.pos, (5.0, 5.0)); + let elements = vcanvas.get().find_intersections(eraser_rect); + + if !elements.is_empty() { + vcanvas.update(|canvas: &mut Canvas| { + for i in elements { + canvas.elements.remove(i); + } + }); + } + } + (CanvasToolState::Erasing, Event::MouseUp(_)) => { + self.state = CanvasToolState::Idle; + } _ => {} } } @@ -86,7 +120,7 @@ impl CanvasToolCtx { (CanvasToolState::Idle, Event::MouseDown(mouse_event)) => { let mut kurbo_path = BezPath::new(); kurbo_path.move_to((mouse_event.pos.x, mouse_event.pos.y)); - if let CanvasToolParams::Pen{ thickness, color } = &self.initial_params { + if let CanvasToolParams::Pen { thickness, color } = &self.initial_params { self.state = CanvasToolState::DrawingFreehand { pen_params: self.initial_params.clone(), current_path: CanvasElement::Freehand { @@ -114,7 +148,7 @@ impl CanvasToolCtx { vcanvas.update(move |canvas: &mut Canvas| { let current_state = std::mem::replace(&mut self.state, CanvasToolState::Idle); if let CanvasToolState::DrawingFreehand { current_path, .. } = current_state { - canvas.push_back(current_path); + canvas.elements.push_back(current_path); } }); } diff --git a/src/widget/canvas.rs b/src/widget/canvas.rs index 9918f26..242d601 100644 --- a/src/widget/canvas.rs +++ b/src/widget/canvas.rs @@ -16,6 +16,7 @@ use im::Vector; +use crate::canvas::Canvas; use crate::history::VersionedCanvas; use crate::tool::CanvasToolCtx; use crate::DocumentSnapshot; @@ -44,12 +45,20 @@ impl CanvasState { pub fn get_document_snapshot(&self) -> DocumentSnapshot { DocumentSnapshot { - canvas_elements: self.versioned_canvas.get().iter().cloned().collect(), + canvas_elements: self + .versioned_canvas + .get() + .elements + .iter() + .cloned() + .collect(), } } pub fn set_from_snapshot(&mut self, snapshot: DocumentSnapshot) { - self.versioned_canvas = VersionedCanvas::new(Vector::from(snapshot.canvas_elements)); + self.versioned_canvas = VersionedCanvas::new(Canvas { + elements: Vector::from(snapshot.canvas_elements), + }); } pub fn set_tool_ctx(&mut self, ctx: CanvasToolCtx) { @@ -121,7 +130,7 @@ impl Widget for CanvasWidget { // and we only want to clear this widget's area. ctx.fill(rect, &Color::WHITE); - for element in data.versioned_canvas.get().iter() { + for element in data.versioned_canvas.get().elements.iter() { element.draw(ctx); } if data.tool_ctx.needs_repaint() {