diff --git a/src/canvas.rs b/src/canvas.rs index c5c4423..01b843e 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -1,6 +1,32 @@ +use druid::kurbo::BezPath; + +// O(1) complexity +// TODO: This is only a toy implementation, this is not at all correct! +fn segment_intersects_rect(segment: &druid::kurbo::PathSeg, rect: druid::Rect) -> bool { + match segment { + druid::kurbo::PathSeg::Line(line) => rect.contains(line.p0) || rect.contains(line.p1), + + druid::kurbo::PathSeg::Quad(quad) => { + rect.contains(quad.p0) || rect.contains(quad.p1) || rect.contains(quad.p2) + } + + druid::kurbo::PathSeg::Cubic(cubic) => { + rect.contains(cubic.p0) || rect.contains(cubic.p1) || rect.contains(cubic.p2) || rect.contains(cubic.p3) + } + } +} + + #[derive(Clone, druid::Data)] pub struct Path { - pub kurbo_path: druid::kurbo::BezPath, + pub kurbo_path: BezPath, +} + +impl Path { + fn intersects_rect(&self, rect: druid::Rect) -> bool { + // The path intersects the rect if any of its segments does + self.kurbo_path.segments().any(|segment| segment_intersects_rect(&segment, rect)) + } } #[derive(Clone, druid::Data)] @@ -36,55 +62,139 @@ impl CanvasElement { } } -// O(1) complexity -// TODO: This is only a toy implementation, this is not at all correct! -fn segment_intersects_rect(segment: &druid::kurbo::PathSeg, rect: druid::Rect) -> bool { - match segment { - druid::kurbo::PathSeg::Line(line) => rect.contains(line.p0) || rect.contains(line.p1), - - druid::kurbo::PathSeg::Quad(quad) => { - rect.contains(quad.p0) || rect.contains(quad.p1) || rect.contains(quad.p2) - } - - druid::kurbo::PathSeg::Cubic(cubic) => { - rect.contains(cubic.p0) || rect.contains(cubic.p1) || rect.contains(cubic.p2) || rect.contains(cubic.p3) - } - } -} - // A canvas contains all elements to be drawn #[derive(Clone, druid::Data)] pub struct Canvas { pub elements: im::Vector, + pub version_id: usize } impl Canvas { pub fn new() -> Canvas { Canvas { elements: im::vector![], - } - } - // O(n) complexity, where n is the number of segments belonging to paths - // that intersect the eraser rectangle - pub fn erase(&mut self, eraser_rect: druid::Rect) { - for elem in self.elements.iter_mut() { - if elem.bounding_box().intersect(eraser_rect).area() > 0.0 { - // TODO: if a part of the path is erased, split the path into subpaths - match elem { - CanvasElement::Freehand { path, thickness: _ } => { - // Remove segments intersecting the eraser - let new_segments = (*path) - .kurbo_path - .segments() - .filter(|path| !segment_intersects_rect(path, eraser_rect)); - path.kurbo_path = druid::kurbo::BezPath::from_path_segments(new_segments); - } - } - } + version_id: 0, } } + pub fn unchanged(&self, other: &Canvas) -> bool { + self.version_id == other.version_id + } + + // Completely erase all strokes that intersect the rect + pub fn erase_strokes(&mut self, rect: druid::Rect) { + // TODO: Improve efficiency + + // Linear search through all elements in the canvas + // to find those whose bounding box intersects the eraser rect + // (note that this doesn't imply that the element itself intersects the eraser rect, + // but it helps filtering out elements that are too far away from the eraser) + let mut new_elements = im::Vector::new(); + + for elem in self.elements.iter() { + // Check if the element intersects the eraser rect + if elem.bounding_box().intersect(rect).area() > 0.0 { + // Check the element type, we only erase Freehand paths + match elem { + CanvasElement::Freehand {path, ..} => { + // Check whether the path intersects the rect, + // if so, remove it. + if !path.intersects_rect(rect) { + new_elements.push_back(elem.clone()); + } else { + self.version_id += 1; + } + } + } + } else { + new_elements.push_back(elem.clone()); + } + } + + // Update elements list + self.elements = new_elements; + } + + // O(n) complexity, where n is the number of segments belonging to paths + // that intersect the eraser rectangle + // Erase all portions of paths lying in the rect + pub fn erase_paths(&mut self, rect: druid::Rect) { + // TODO: Improve efficiency + + // Linear search through all elements in the canvas + // to find those whose bounding box intersects the eraser rect + // (note that this doesn't imply that the element itself intersects the eraser rect, + // but it helps filtering out elements that are too far away from the eraser) + let mut new_elements = im::Vector::new(); + + for elem in self.elements.iter() { + // Check if the element intersects the eraser rect + if elem.bounding_box().intersect(rect).area() > 0.0 { + // Check the element type, we only erase Freehand paths + match elem { + CanvasElement::Freehand {path, thickness} => { + // Remove segments intersecting the eraser + // And return the remaining subpaths + let (path_changed, new_paths) = Canvas::erase_segments_from_freehand_path(&path, rect); + + if path_changed { + // There has been some change, update the canvas + self.version_id += 1; + for path in new_paths { + new_elements.push_back(CanvasElement::Freehand { + path: path, + thickness: *thickness, + }); + } + } + } + } + } else { + new_elements.push_back(elem.clone()); + } + } + + // Update elements list + self.elements = new_elements; + } + + // Erase segments from a path, and returns the new subpaths that are left + fn erase_segments_from_freehand_path(path: &Path, rect: druid::Rect) -> (bool, im::Vector) { + + // Vector of pieces + let mut pieces = im::Vector::new(); + // Current subpath + let mut curr_piece = im::Vector::new(); + + // Flag indicating whether the segment has been modified + let mut erased = false; + + for segment in path.kurbo_path.segments() { + if segment_intersects_rect(&segment, rect) { + erased = true; + // We found a segment to erase, so split the path here + // If the path is not empty, add it to the pieces vector + if !curr_piece.is_empty() { + pieces.push_back(curr_piece); + // Recreate accumulator + curr_piece = im::Vector::new(); + } + } else { + // Add segment to curr piece + curr_piece.push_back(segment); + } + } + + // We end up with `pieces`, a vector of non empty paths + let paths = pieces.iter().map(|segments| Path { + kurbo_path: BezPath::from_path_segments(segments.iter().cloned()), + }).collect(); + + (erased, paths) + } + pub fn add_element(&mut self, element: CanvasElement) { + self.version_id += 1; self.elements.push_back(element); } } diff --git a/src/canvas_tools.rs b/src/canvas_tools.rs index 5b4fca3..c106679 100644 --- a/src/canvas_tools.rs +++ b/src/canvas_tools.rs @@ -13,6 +13,10 @@ pub enum CanvasTool { Eraser { eraser_rect: Option, }, + + StrokeEraser { + eraser_rect: Option, + }, } impl CanvasTool { @@ -26,13 +30,20 @@ impl CanvasTool { CanvasTool::Eraser { eraser_rect: None } } + pub fn new_stroke_eraser() -> CanvasTool { + CanvasTool::StrokeEraser { eraser_rect: None } + } + pub fn handle_event(&mut self, event: &Event, canvas: &mut VersionedCanvas) { match self { CanvasTool::Pen { current_element } => { CanvasTool::pen_handle_event(current_element, event, canvas) } CanvasTool::Eraser { eraser_rect } => { - CanvasTool::eraser_handle_event(eraser_rect, event, canvas) + CanvasTool::eraser_handle_event(eraser_rect, event, canvas, false) + } + CanvasTool::StrokeEraser { eraser_rect } => { + CanvasTool::eraser_handle_event(eraser_rect, event, canvas, true) } } } @@ -65,10 +76,8 @@ impl CanvasTool { Event::MouseUp(_) => { if current_element.is_some() { if let Some(current_element) = current_element.take() { - canvas.update(move |canvas: &Canvas| -> Canvas { - let mut new_canvas = canvas.clone(); - new_canvas.add_element(current_element); - return new_canvas; + canvas.update(move |canvas: &mut Canvas| { + canvas.add_element(current_element) }); } } @@ -81,17 +90,19 @@ impl CanvasTool { eraser_rect: &mut Option, event: &Event, canvas: &mut VersionedCanvas, + is_stroke_eraser: bool ) { match event { Event::MouseDown(mouse_event) => { - let rect = - druid::Rect::from_center_size(mouse_event.pos, druid::Size::new(10.0, 10.0)); + let rect = druid::Rect::from_center_size(mouse_event.pos, druid::Size::new(10.0, 10.0)); *eraser_rect = Some(rect); // Create a new undo version each time the mouse is down - canvas.update(|canvas: &Canvas| -> Canvas { - let mut new_canvas = canvas.clone(); - new_canvas.erase(rect); - return new_canvas; + canvas.update(|canvas: &mut Canvas| { + if is_stroke_eraser { + canvas.erase_strokes(rect) + } else { + canvas.erase_paths(rect) + } }); } Event::MouseMove(mouse_event) => { @@ -105,7 +116,11 @@ impl CanvasTool { // So we make irreversible changes as long as // the mouse is pressed. canvas.irreversible_update(|canvas: &mut Canvas| { - canvas.erase(rect); + if is_stroke_eraser { + canvas.erase_strokes(rect) + } else { + canvas.erase_paths(rect) + } }); } } diff --git a/src/main.rs b/src/main.rs index 876a7ea..1043dff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,20 +27,20 @@ use stiletto::versioned_canvas::VersionedCanvas; struct CanvasData { #[lens(name = "current_tool_lens")] current_tool: CanvasTool, - elements: VersionedCanvas, + canvas: VersionedCanvas, } impl CanvasData { fn perform_undo(&mut self) { - self.elements.undo(); + self.canvas.undo(); } fn perform_redo(&mut self) { - self.elements.redo(); + self.canvas.redo(); } fn handle_tool_event(&mut self, event: &Event) { - self.current_tool.handle_event(event, &mut self.elements); + self.current_tool.handle_event(event, &mut self.canvas); } } @@ -90,14 +90,17 @@ impl Widget for CanvasWidget { ctx.request_paint(); } } - (CanvasTool::Eraser { eraser_rect: _ }, CanvasTool::Eraser { eraser_rect }) => { - // We just stopped erasing, no need to repaint + (CanvasTool::Eraser{..}, CanvasTool::Eraser{ eraser_rect }) => { if let Some(rect) = eraser_rect { ctx.request_paint_rect(*rect); } else { ctx.request_paint(); } } + (CanvasTool::StrokeEraser{..}, CanvasTool::StrokeEraser{..}) => { + ctx.request_paint(); + } + _ => { // we just changed the canvas tool, there is no need to repaint return; @@ -132,7 +135,7 @@ impl Widget for CanvasWidget { // and we only want to clear this widget's area. ctx.fill(rect, &Color::WHITE); - for element in data.elements.get().elements.iter() { + for element in data.canvas.get().elements.iter() { element.draw(ctx); } @@ -153,6 +156,7 @@ impl Widget for CanvasWidget { enum ToolType { Pen, Eraser, + StrokeEraser, } // Make a CanvasTool of a given type @@ -160,6 +164,7 @@ fn update_canvas_tool_from_type(tool: &mut CanvasTool, new_type: ToolType) { match new_type { ToolType::Pen => *tool = CanvasTool::new_pen(), ToolType::Eraser => *tool = CanvasTool::new_eraser(), + ToolType::StrokeEraser => *tool = CanvasTool::new_stroke_eraser(), } } @@ -168,17 +173,13 @@ fn canvas_tool_type(tool: &CanvasTool) -> ToolType { match tool { CanvasTool::Pen { .. } => ToolType::Pen, CanvasTool::Eraser { .. } => ToolType::Eraser, + CanvasTool::StrokeEraser { .. } => ToolType::StrokeEraser, } } fn build_ui() -> impl Widget { - use druid::widget::{Align, Button, CrossAxisAlignment, Flex, RadioGroup, SizedBox}; + use druid::widget::{Align, Button, CrossAxisAlignment, Flex, Radio, SizedBox}; - let radio_group = RadioGroup::new(vec![("Pen", ToolType::Pen), ("Eraser", ToolType::Eraser)]) - .lens(CanvasData::current_tool_lens.map(canvas_tool_type, update_canvas_tool_from_type)); - // ^ We use a lens to make the radiogroup act only on the tool portion of data. - // Actually we must also perform a conversion, between ToolType and CanvasTool, - // that's why we perform a map on the lens. let toolbar = Flex::row() .cross_axis_alignment(CrossAxisAlignment::Center) @@ -189,7 +190,20 @@ fn build_ui() -> impl Widget { .with_child( Button::new("Redo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_redo()), ) - .with_child(radio_group); + .with_child( + Radio::new("Pen", ToolType::Pen).lens(CanvasData::current_tool_lens.map(canvas_tool_type, update_canvas_tool_from_type)) + ) + .with_child( + Radio::new("Eraser", ToolType::Eraser).lens(CanvasData::current_tool_lens.map(canvas_tool_type, update_canvas_tool_from_type)) + ) + .with_child( + Radio::new("Stroke Eraser", ToolType::StrokeEraser).lens(CanvasData::current_tool_lens.map(canvas_tool_type, update_canvas_tool_from_type)) + ); + // ^ We use a lens to make the radiogroup act only on the tool portion of data. + // Actually we must also perform a conversion, between ToolType and CanvasTool, + // that's why we perform a map on the lens. + + Flex::column() .cross_axis_alignment(CrossAxisAlignment::Center) @@ -206,7 +220,7 @@ pub fn main() { ); let canvas_data = CanvasData { current_tool: CanvasTool::new_pen(), - elements: VersionedCanvas::new(Canvas::new()), + canvas: VersionedCanvas::new(Canvas::new()), }; AppLauncher::with_window(window) .use_simple_logger() diff --git a/src/versioned_canvas.rs b/src/versioned_canvas.rs index fc96ed8..2996409 100644 --- a/src/versioned_canvas.rs +++ b/src/versioned_canvas.rs @@ -50,19 +50,19 @@ impl VersionedCanvas { // Copy on Write semantics of im::Vector. // Is this a good solution? Does this work correctly? THINK ABOUT THIS pub fn update(&mut self, update_fn: impl FnOnce(&mut Canvas)) { - // This is a linear history, - // so we first check if there are newer versions, if so - // this means we are in the past, so a change in the past destroys the future - // and creates a new future. - if self.has_newer_versions() { - self.versions = self.versions.take(self.curr_version + 1); - } - let mut new_version = self.get().clone(); update_fn(&mut new_version); // Only push new version if there has been an actual change in the vector - if !new_version.elements.ptr_eq(&self.get().elements) { + if !new_version.unchanged(&self.get()) { + // This is a linear history, + // so we first check if there are newer versions, if so + // this means we are in the past, so a change in the past destroys the future + // and creates a new future. + if self.has_newer_versions() { + self.versions = self.versions.take(self.curr_version + 1); + } + self.versions.push_back(new_version); self.curr_version = self.curr_version + 1; } @@ -70,14 +70,19 @@ impl VersionedCanvas { // Do inplace update, which will be irreversible pub fn irreversible_update(&mut self, update_fn: impl FnOnce(&mut Canvas)) { - // This is a linear history, - // so we first check if there are newer versions, if so - // this means we are in the past, so a change in the past destroys the future - // and creates a new future. - if self.has_newer_versions() { - self.versions = self.versions.take(self.curr_version + 1); - } + let old_version = self.get().version_id; update_fn(self.versions.back_mut().unwrap()); + + // Check whether there has been a new change + if old_version != self.get().version_id { + // This is a linear history, + // so we first check if there are newer versions, if so + // this means we are in the past, so a change in the past destroys the future + // and creates a new future. + if self.has_newer_versions() { + self.versions = self.versions.take(self.curr_version + 1); + } + } } }