From be11d8e6eec123d90d2f6b24d8210f6262935086 Mon Sep 17 00:00:00 2001 From: Francesco Magliocca Date: Mon, 9 Nov 2020 16:28:31 +0100 Subject: [PATCH 1/8] Start dirty implementation of an eraser --- src/lib.rs | 56 +++++++++++++- src/main.rs | 205 ++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 194 insertions(+), 67 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bf2baa7..383c121 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,16 +36,61 @@ impl CanvasElement { } } -use im::Vector; +// O(1) complexity +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), + } +} // A canvas contains all elements to be drawn -pub type Canvas = Vector; +#[derive(Clone, druid::Data)] +pub struct Canvas { + pub elements: im::Vector, +} + +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 { + 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); + } + } + } + } + } + + pub fn push_back(&mut self, element: CanvasElement) { + self.elements.push_back(element); + } +} #[derive(Clone, druid::Data)] pub struct VersionedCanvas { // We internally guarantee that this vector // is never empty - versions: Vector, + versions: im::Vector, curr_version: usize, } @@ -59,7 +104,6 @@ impl VersionedCanvas { // Get current canvas version pub fn get(&self) -> &Canvas { - let focus = self.versions.focus(); self.versions.get(self.curr_version).unwrap() } @@ -71,6 +115,10 @@ impl VersionedCanvas { self.curr_version > 0 } + pub fn version(&self) -> usize { + self.curr_version + } + pub fn undo(&mut self) { if self.has_older_versions() { self.curr_version = self.curr_version - 1; diff --git a/src/main.rs b/src/main.rs index fa001d2..5721e1e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,34 +14,121 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use druid::im::{vector}; -use druid::kurbo::BezPath; +use druid::kurbo::{BezPath, Rect}; use druid::widget::prelude::*; use druid::{AppLauncher, Color, Data, Event, LocalizedString, WindowDesc}; use stiletto::{CanvasElement, VersionedCanvas, Canvas}; + +// Tools that can be used to interact with the canvas +#[derive(Clone, Data)] +enum CanvasTool { + Pen { current_element: Option }, + Eraser { eraser_rect: Option }, +} + +impl CanvasTool { + fn new_pen() -> CanvasTool { + CanvasTool::Pen { current_element: None } + } + fn new_eraser() -> CanvasTool { + CanvasTool::Eraser { eraser_rect: None } + } +} + #[derive(Clone, Data)] struct CanvasData { - current_element: Option, - //elements: Vector, + current_tool: CanvasTool, elements: VersionedCanvas, } impl CanvasData { - fn is_drawing(&self) -> bool { + /*fn is_drawing(&self) -> bool { self.current_element.is_some() + }*/ + + fn set_tool(&mut self, tool: CanvasTool) { + self.current_tool = tool; } fn perform_undo(&mut self) { - if !self.is_drawing() { - self.elements.undo(); - } + self.elements.undo(); } fn perform_redo(&mut self) { - if !self.is_drawing() { - self.elements.redo(); + self.elements.redo(); + } + + fn handle_tool_event(&mut self, event: &Event) { + match &mut self.current_tool { + CanvasTool::Pen { current_element } => { + match event { + Event::MouseDown(mouse_event) => { + let mut kurbo_path = BezPath::new(); + kurbo_path.move_to((mouse_event.pos.x, mouse_event.pos.y)); + *current_element = Some(CanvasElement::Freehand { + path: stiletto::Path { kurbo_path }, + thickness: 2.0, + }); + } + Event::MouseMove(mouse_event) => { + if current_element.is_some() { + if let Some(current_element) = current_element.as_mut() { + current_element + .get_path_mut() + .unwrap() + .kurbo_path + .line_to((mouse_event.pos.x, mouse_event.pos.y)); + } + } + } + Event::MouseUp(_) => { + if current_element.is_some() { + if let Some(current_element) = current_element.take() { + + self.elements.update(move |canvas: &Canvas| -> Canvas { + let mut new_canvas = canvas.clone(); + new_canvas.push_back(current_element); + return new_canvas; + }); + + } + } + } + _ => {} + } + }, + CanvasTool::Eraser { eraser_rect } => { + match event { + Event::MouseDown(mouse_event) => { + 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 + self.elements.update(|canvas: &Canvas| -> Canvas { + let mut new_canvas = canvas.clone(); + new_canvas.erase(rect); + return new_canvas; + }); + } + Event::MouseMove(mouse_event) => { + if eraser_rect.is_some() { + let rect = druid::Rect::from_center_size(mouse_event.pos, druid::Size::new(10.0, 10.0)); + *eraser_rect = Some(rect); + // We don't want too many levels of undoing + // So we make irreversible changes as long as + // the mouse is pressed. + self.elements.irreversible_update(|canvas: &mut Canvas| { + canvas.erase(rect); + }); + } + } + Event::MouseUp(_) => { + *eraser_rect = None; + } + _ => {} + } + }, } } } @@ -50,41 +137,8 @@ struct CanvasWidget; impl Widget for CanvasWidget { fn event(&mut self, _ctx: &mut EventCtx, event: &Event, data: &mut CanvasData, _env: &Env) { - match event { - Event::MouseDown(mouse_event) => { - let mut kurbo_path = BezPath::new(); - kurbo_path.move_to((mouse_event.pos.x, mouse_event.pos.y)); - data.current_element = Some(CanvasElement::Freehand { - path: stiletto::Path { kurbo_path }, - thickness: 2.0, - }); - } - Event::MouseMove(mouse_event) => { - if data.is_drawing() { - if let Some(current_element) = data.current_element.as_mut() { - current_element - .get_path_mut() - .unwrap() - .kurbo_path - .line_to((mouse_event.pos.x, mouse_event.pos.y)); - } - } - } - Event::MouseUp(_) => { - if data.is_drawing() { - if let Some(current_element) = data.current_element.take() { - - data.elements.update(move |canvas: &Canvas| -> Canvas { - let mut new_canvas = canvas.clone(); - new_canvas.push_back(current_element); - return new_canvas; - }); - - } - } - } - _ => {} - } + data.handle_tool_event(event); + } fn lifecycle( @@ -103,16 +157,34 @@ impl Widget for CanvasWidget { data: &CanvasData, _env: &Env, ) { - // the current_element is moved to the elements array, no need to repaint - if old_data.is_drawing() && !data.is_drawing() { - return; - } - if data.is_drawing() { - if let Some(e) = data.current_element.as_ref() { - ctx.request_paint_rect(e.bounding_box()); + + match (&old_data.current_tool, &data.current_tool) { + (CanvasTool::Pen{ current_element: old_element }, CanvasTool::Pen{ current_element: new_element }) => { + // the current_element is moved to the elements array, no need to repaint + if old_element.is_some() && !new_element.is_some() { + return + } + + if new_element.is_some() { + if let Some(e) = new_element.as_ref() { + ctx.request_paint_rect(e.bounding_box()); + } + } else { + ctx.request_paint(); + } + } + (CanvasTool::Eraser{ eraser_rect: _ }, CanvasTool::Eraser{ eraser_rect }) => { + // We just stopped erasing, no need to repaint + if let Some(rect) = eraser_rect { + ctx.request_paint_rect(*rect); + } else { + ctx.request_paint(); + } + } + _ => { + // we just changed the canvas tool, there is no need to repaint + return; } - } else { - ctx.request_paint(); } } @@ -143,24 +215,31 @@ 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().iter() { + for element in data.elements.get().elements.iter() { element.draw(ctx); } - if let Some(element) = &data.current_element { - element.draw(ctx); + + match &data.current_tool { + CanvasTool::Pen { current_element } => { + if let Some(element) = ¤t_element { + element.draw(ctx); + } + } + _ => {} } } } fn build_ui() -> impl Widget { use druid::widget::{Align, Button, CrossAxisAlignment, Flex, SizedBox}; + let toolbar = Flex::row() .cross_axis_alignment(CrossAxisAlignment::Center) .with_spacer(30.0) - .with_child( - Button::new("Undo").on_click(|_ctx: &mut EventCtx, data: &mut CanvasData, _env: &Env| data.perform_undo()) - ) - .with_child(Button::new("Redo").on_click(|_ctx: &mut EventCtx, data: &mut CanvasData, _env: &Env| data.perform_redo())); + .with_child(Button::new("Undo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_undo())) + .with_child(Button::new("Redo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_redo())) + .with_child(Button::new("Pen").on_click(|_ctx, data: &mut CanvasData, _env| data.set_tool(CanvasTool::new_pen()))) + .with_child(Button::new("Eraser").on_click(|_ctx, data: &mut CanvasData, _env| data.set_tool(CanvasTool::new_eraser()))); Flex::column() .cross_axis_alignment(CrossAxisAlignment::Center) @@ -176,8 +255,8 @@ pub fn main() { LocalizedString::new("custom-widget-demo-window-title").with_placeholder("Stiletto"), ); let canvas_data = CanvasData { - current_element: None, - elements: VersionedCanvas::new(vector![]), + current_tool: CanvasTool::new_pen(), + elements: VersionedCanvas::new(Canvas::new()), }; AppLauncher::with_window(window) .use_simple_logger() -- 2.39.5 From 8bd487dc11b420931ddf6b98d11a0147cc272a39 Mon Sep 17 00:00:00 2001 From: Francesco Magliocca Date: Mon, 9 Nov 2020 17:18:00 +0100 Subject: [PATCH 2/8] Use a RadioGroup to show which canvas tool is active --- src/main.rs | 47 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5721e1e..607a727 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,10 +16,15 @@ use druid::kurbo::{BezPath, Rect}; use druid::widget::prelude::*; -use druid::{AppLauncher, Color, Data, Event, LocalizedString, WindowDesc}; +use druid::{AppLauncher, Color, Data, Lens, Event, LocalizedString, WidgetExt, WindowDesc}; use stiletto::{CanvasElement, VersionedCanvas, Canvas}; +#[derive(Clone, Data, PartialEq)] +enum CanvasToolType { + Pen, + Eraser +} // Tools that can be used to interact with the canvas #[derive(Clone, Data)] @@ -35,6 +40,33 @@ impl CanvasTool { fn new_eraser() -> CanvasTool { CanvasTool::Eraser { eraser_rect: None } } + + fn tool_type(&self) -> CanvasToolType { + match self { + CanvasTool::Pen { current_element: _ } => CanvasToolType::Pen, + CanvasTool::Eraser { eraser_rect: _ } => CanvasToolType::Eraser, + } + } +} + +struct CanvasToolLens; + +impl Lens for CanvasToolLens { + fn with R>(&self, data: &CanvasData, f: F) -> R { + f(&data.current_tool.tool_type()) + } + + fn with_mut R>(&self, data: &mut CanvasData, f: F) -> R { + let mut curr_type = data.current_tool.tool_type(); + let result = f(&mut curr_type); + + match curr_type { + CanvasToolType::Pen => data.current_tool = CanvasTool::new_pen(), + CanvasToolType::Eraser => data.current_tool = CanvasTool::new_eraser(), + } + + result + } } #[derive(Clone, Data)] @@ -48,9 +80,7 @@ impl CanvasData { self.current_element.is_some() }*/ - fn set_tool(&mut self, tool: CanvasTool) { - self.current_tool = tool; - } + const CURRENT_TOOL_LENS: CanvasToolLens = CanvasToolLens; fn perform_undo(&mut self) { self.elements.undo(); @@ -133,6 +163,7 @@ impl CanvasData { } } + struct CanvasWidget; impl Widget for CanvasWidget { @@ -231,15 +262,17 @@ impl Widget for CanvasWidget { } fn build_ui() -> impl Widget { - use druid::widget::{Align, Button, CrossAxisAlignment, Flex, SizedBox}; + use druid::widget::{Align, Button, RadioGroup, CrossAxisAlignment, Flex, SizedBox}; + + let radio_group = RadioGroup::new(vec![("Pen", CanvasToolType::Pen), ("Eraser", CanvasToolType::Eraser)]); + let toolbar = Flex::row() .cross_axis_alignment(CrossAxisAlignment::Center) .with_spacer(30.0) .with_child(Button::new("Undo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_undo())) .with_child(Button::new("Redo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_redo())) - .with_child(Button::new("Pen").on_click(|_ctx, data: &mut CanvasData, _env| data.set_tool(CanvasTool::new_pen()))) - .with_child(Button::new("Eraser").on_click(|_ctx, data: &mut CanvasData, _env| data.set_tool(CanvasTool::new_eraser()))); + .with_child(radio_group.lens(CanvasData::CURRENT_TOOL_LENS)); Flex::column() .cross_axis_alignment(CrossAxisAlignment::Center) -- 2.39.5 From bbd221e489452a418937042ba9ee020a0a324ecd Mon Sep 17 00:00:00 2001 From: Francesco Magliocca Date: Mon, 9 Nov 2020 18:33:14 +0100 Subject: [PATCH 3/8] Polish code a bit --- src/canvas.rs | 88 ++++++++++++++++++ src/canvas_tools.rs | 118 ++++++++++++++++++++++++ src/lib.rs | 163 +------------------------------- src/main.rs | 199 ++++++++++++---------------------------- src/versioned_canvas.rs | 73 +++++++++++++++ 5 files changed, 340 insertions(+), 301 deletions(-) create mode 100644 src/canvas.rs create mode 100644 src/canvas_tools.rs create mode 100644 src/versioned_canvas.rs diff --git a/src/canvas.rs b/src/canvas.rs new file mode 100644 index 0000000..097cf4c --- /dev/null +++ b/src/canvas.rs @@ -0,0 +1,88 @@ +#[derive(Clone, druid::Data)] +pub struct Path { + pub kurbo_path: druid::kurbo::BezPath, +} + +#[derive(Clone, druid::Data)] +pub enum CanvasElement { + Freehand { path: Path, thickness: f64 }, +} + +impl CanvasElement { + pub fn bounding_box(&self) -> druid::Rect { + match self { + CanvasElement::Freehand { path, thickness } => { + use druid::kurbo::Shape; + path.kurbo_path + .bounding_box() + .inflate(*thickness, *thickness) + } + } + } + pub fn draw(&self, ctx: &mut druid::PaintCtx) { + match self { + CanvasElement::Freehand { path, thickness } => { + use druid::RenderContext; + let stroke_color = druid::Color::rgb8(0, 128, 0); + ctx.stroke(&path.kurbo_path, &stroke_color, *thickness); + } + } + } + + pub fn get_path_mut(&mut self) -> Option<&mut Path> { + match self { + CanvasElement::Freehand { path, .. } => Some(path), + } + } +} + +// O(1) complexity +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) + } + } +} + +// A canvas contains all elements to be drawn +#[derive(Clone, druid::Data)] +pub struct Canvas { + pub elements: im::Vector, +} + +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 { + 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); + } + } + } + } + } + + pub fn push_back(&mut self, element: CanvasElement) { + self.elements.push_back(element); + } +} diff --git a/src/canvas_tools.rs b/src/canvas_tools.rs new file mode 100644 index 0000000..ba26292 --- /dev/null +++ b/src/canvas_tools.rs @@ -0,0 +1,118 @@ +use crate::canvas::*; +use crate::versioned_canvas::VersionedCanvas; + +use druid::kurbo::BezPath; +use druid::{Data, Event, Rect}; + +// Tools that can be used to interact with the canvas +#[derive(Clone, Data)] +pub enum CanvasTool { + Pen { + current_element: Option, + }, + Eraser { + eraser_rect: Option, + }, +} + +impl CanvasTool { + pub fn new_pen() -> CanvasTool { + CanvasTool::Pen { + current_element: None, + } + } + + pub fn new_eraser() -> CanvasTool { + CanvasTool::Eraser { 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) + } + } + } + + fn pen_handle_event( + current_element: &mut Option, + event: &Event, + canvas: &mut VersionedCanvas, + ) { + match event { + Event::MouseDown(mouse_event) => { + let mut kurbo_path = BezPath::new(); + kurbo_path.move_to((mouse_event.pos.x, mouse_event.pos.y)); + *current_element = Some(CanvasElement::Freehand { + path: Path { kurbo_path }, + thickness: 2.0, + }); + } + Event::MouseMove(mouse_event) => { + if current_element.is_some() { + if let Some(current_element) = current_element.as_mut() { + current_element + .get_path_mut() + .unwrap() + .kurbo_path + .line_to((mouse_event.pos.x, mouse_event.pos.y)); + } + } + } + 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.push_back(current_element); + return new_canvas; + }); + } + } + } + _ => {} + } + } + + fn eraser_handle_event( + eraser_rect: &mut Option, + event: &Event, + canvas: &mut VersionedCanvas, + ) { + match event { + Event::MouseDown(mouse_event) => { + 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; + }); + } + Event::MouseMove(mouse_event) => { + if eraser_rect.is_some() { + let rect = druid::Rect::from_center_size( + mouse_event.pos, + druid::Size::new(10.0, 10.0), + ); + *eraser_rect = Some(rect); + // We don't want too many levels of undoing + // So we make irreversible changes as long as + // the mouse is pressed. + canvas.irreversible_update(|canvas: &mut Canvas| { + canvas.erase(rect); + }); + } + } + Event::MouseUp(_) => { + *eraser_rect = None; + } + _ => {} + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 383c121..44639cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,160 +1,3 @@ -#[derive(Clone, druid::Data)] -pub struct Path { - pub kurbo_path: druid::kurbo::BezPath, -} - -#[derive(Clone, druid::Data)] -pub enum CanvasElement { - Freehand { path: Path, thickness: f64 }, -} - -impl CanvasElement { - pub fn bounding_box(&self) -> druid::Rect { - match self { - CanvasElement::Freehand { path, thickness } => { - use druid::kurbo::Shape; - path.kurbo_path - .bounding_box() - .inflate(*thickness, *thickness) - } - } - } - pub fn draw(&self, ctx: &mut druid::PaintCtx) { - match self { - CanvasElement::Freehand { path, thickness } => { - use druid::RenderContext; - let stroke_color = druid::Color::rgb8(0, 128, 0); - ctx.stroke(&path.kurbo_path, &stroke_color, *thickness); - } - } - } - - pub fn get_path_mut(&mut self) -> Option<&mut Path> { - match self { - CanvasElement::Freehand { path, .. } => Some(path), - } - } -} - -// O(1) complexity -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), - } -} - -// A canvas contains all elements to be drawn -#[derive(Clone, druid::Data)] -pub struct Canvas { - pub elements: im::Vector, -} - -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 { - 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); - } - } - } - } - } - - pub fn push_back(&mut self, element: CanvasElement) { - self.elements.push_back(element); - } -} - -#[derive(Clone, druid::Data)] -pub struct VersionedCanvas { - // We internally guarantee that this vector - // is never empty - versions: im::Vector, - curr_version: usize, -} - -impl VersionedCanvas { - pub fn new(canvas: Canvas) -> VersionedCanvas { - VersionedCanvas { - versions: im::vector![canvas], - curr_version: 0, - } - } - - // Get current canvas version - pub fn get(&self) -> &Canvas { - self.versions.get(self.curr_version).unwrap() - } - - fn has_newer_versions(&self) -> bool { - self.curr_version + 1 < self.versions.len() - } - - fn has_older_versions(&self) -> bool { - self.curr_version > 0 - } - - pub fn version(&self) -> usize { - self.curr_version - } - - pub fn undo(&mut self) { - if self.has_older_versions() { - self.curr_version = self.curr_version - 1; - } - } - - pub fn redo(&mut self) { - if self.has_newer_versions() { - self.curr_version = self.curr_version + 1; - } - } - - pub fn update(&mut self, update_fn: impl FnOnce(&Canvas) -> 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 new_version = update_fn(self.get()); - self.versions.push_back(new_version); - self.curr_version = self.curr_version + 1; - } - - // 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); - } - - update_fn(self.versions.back_mut().unwrap()); - } -} - +pub mod canvas; +pub mod canvas_tools; +pub mod versioned_canvas; diff --git a/src/main.rs b/src/main.rs index 607a727..876a7ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,74 +14,23 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use druid::kurbo::{BezPath, Rect}; use druid::widget::prelude::*; -use druid::{AppLauncher, Color, Data, Lens, Event, LocalizedString, WidgetExt, WindowDesc}; +use druid::{ + AppLauncher, Color, Data, Event, Lens, LensExt, LocalizedString, WidgetExt, WindowDesc, +}; -use stiletto::{CanvasElement, VersionedCanvas, Canvas}; +use stiletto::canvas::Canvas; +use stiletto::canvas_tools::CanvasTool; +use stiletto::versioned_canvas::VersionedCanvas; -#[derive(Clone, Data, PartialEq)] -enum CanvasToolType { - Pen, - Eraser -} - -// Tools that can be used to interact with the canvas -#[derive(Clone, Data)] -enum CanvasTool { - Pen { current_element: Option }, - Eraser { eraser_rect: Option }, -} - -impl CanvasTool { - fn new_pen() -> CanvasTool { - CanvasTool::Pen { current_element: None } - } - fn new_eraser() -> CanvasTool { - CanvasTool::Eraser { eraser_rect: None } - } - - fn tool_type(&self) -> CanvasToolType { - match self { - CanvasTool::Pen { current_element: _ } => CanvasToolType::Pen, - CanvasTool::Eraser { eraser_rect: _ } => CanvasToolType::Eraser, - } - } -} - -struct CanvasToolLens; - -impl Lens for CanvasToolLens { - fn with R>(&self, data: &CanvasData, f: F) -> R { - f(&data.current_tool.tool_type()) - } - - fn with_mut R>(&self, data: &mut CanvasData, f: F) -> R { - let mut curr_type = data.current_tool.tool_type(); - let result = f(&mut curr_type); - - match curr_type { - CanvasToolType::Pen => data.current_tool = CanvasTool::new_pen(), - CanvasToolType::Eraser => data.current_tool = CanvasTool::new_eraser(), - } - - result - } -} - -#[derive(Clone, Data)] +#[derive(Clone, Data, Lens)] struct CanvasData { + #[lens(name = "current_tool_lens")] current_tool: CanvasTool, elements: VersionedCanvas, } impl CanvasData { - /*fn is_drawing(&self) -> bool { - self.current_element.is_some() - }*/ - - const CURRENT_TOOL_LENS: CanvasToolLens = CanvasToolLens; - fn perform_undo(&mut self) { self.elements.undo(); } @@ -91,85 +40,15 @@ impl CanvasData { } fn handle_tool_event(&mut self, event: &Event) { - match &mut self.current_tool { - CanvasTool::Pen { current_element } => { - match event { - Event::MouseDown(mouse_event) => { - let mut kurbo_path = BezPath::new(); - kurbo_path.move_to((mouse_event.pos.x, mouse_event.pos.y)); - *current_element = Some(CanvasElement::Freehand { - path: stiletto::Path { kurbo_path }, - thickness: 2.0, - }); - } - Event::MouseMove(mouse_event) => { - if current_element.is_some() { - if let Some(current_element) = current_element.as_mut() { - current_element - .get_path_mut() - .unwrap() - .kurbo_path - .line_to((mouse_event.pos.x, mouse_event.pos.y)); - } - } - } - Event::MouseUp(_) => { - if current_element.is_some() { - if let Some(current_element) = current_element.take() { - - self.elements.update(move |canvas: &Canvas| -> Canvas { - let mut new_canvas = canvas.clone(); - new_canvas.push_back(current_element); - return new_canvas; - }); - - } - } - } - _ => {} - } - }, - CanvasTool::Eraser { eraser_rect } => { - match event { - Event::MouseDown(mouse_event) => { - 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 - self.elements.update(|canvas: &Canvas| -> Canvas { - let mut new_canvas = canvas.clone(); - new_canvas.erase(rect); - return new_canvas; - }); - } - Event::MouseMove(mouse_event) => { - if eraser_rect.is_some() { - let rect = druid::Rect::from_center_size(mouse_event.pos, druid::Size::new(10.0, 10.0)); - *eraser_rect = Some(rect); - // We don't want too many levels of undoing - // So we make irreversible changes as long as - // the mouse is pressed. - self.elements.irreversible_update(|canvas: &mut Canvas| { - canvas.erase(rect); - }); - } - } - Event::MouseUp(_) => { - *eraser_rect = None; - } - _ => {} - } - }, - } + self.current_tool.handle_event(event, &mut self.elements); } } - struct CanvasWidget; impl Widget for CanvasWidget { fn event(&mut self, _ctx: &mut EventCtx, event: &Event, data: &mut CanvasData, _env: &Env) { data.handle_tool_event(event); - } fn lifecycle( @@ -188,12 +67,19 @@ impl Widget for CanvasWidget { data: &CanvasData, _env: &Env, ) { - + // TODO: Polish a bit this logic match (&old_data.current_tool, &data.current_tool) { - (CanvasTool::Pen{ current_element: old_element }, CanvasTool::Pen{ current_element: new_element }) => { + ( + CanvasTool::Pen { + current_element: old_element, + }, + CanvasTool::Pen { + current_element: new_element, + }, + ) => { // the current_element is moved to the elements array, no need to repaint if old_element.is_some() && !new_element.is_some() { - return + return; } if new_element.is_some() { @@ -204,7 +90,7 @@ impl Widget for CanvasWidget { ctx.request_paint(); } } - (CanvasTool::Eraser{ eraser_rect: _ }, CanvasTool::Eraser{ eraser_rect }) => { + (CanvasTool::Eraser { eraser_rect: _ }, CanvasTool::Eraser { eraser_rect }) => { // We just stopped erasing, no need to repaint if let Some(rect) = eraser_rect { ctx.request_paint_rect(*rect); @@ -261,18 +147,49 @@ impl Widget for CanvasWidget { } } -fn build_ui() -> impl Widget { - use druid::widget::{Align, Button, RadioGroup, CrossAxisAlignment, Flex, SizedBox}; +// Enum containing an entry for each canvas tool +// it is used to implement the radio group for selecting the tool +#[derive(Clone, Data, PartialEq)] +enum ToolType { + Pen, + Eraser, +} - let radio_group = RadioGroup::new(vec![("Pen", CanvasToolType::Pen), ("Eraser", CanvasToolType::Eraser)]); - +// Make a CanvasTool of a given type +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(), + } +} + +// Get a ToolType from a CanvasTool +fn canvas_tool_type(tool: &CanvasTool) -> ToolType { + match tool { + CanvasTool::Pen { .. } => ToolType::Pen, + CanvasTool::Eraser { .. } => ToolType::Eraser, + } +} + +fn build_ui() -> impl Widget { + use druid::widget::{Align, Button, CrossAxisAlignment, Flex, RadioGroup, 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) .with_spacer(30.0) - .with_child(Button::new("Undo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_undo())) - .with_child(Button::new("Redo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_redo())) - .with_child(radio_group.lens(CanvasData::CURRENT_TOOL_LENS)); + .with_child( + Button::new("Undo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_undo()), + ) + .with_child( + Button::new("Redo").on_click(|_ctx, data: &mut CanvasData, _env| data.perform_redo()), + ) + .with_child(radio_group); Flex::column() .cross_axis_alignment(CrossAxisAlignment::Center) diff --git a/src/versioned_canvas.rs b/src/versioned_canvas.rs new file mode 100644 index 0000000..e549baa --- /dev/null +++ b/src/versioned_canvas.rs @@ -0,0 +1,73 @@ +use crate::canvas::Canvas; + +#[derive(Clone, druid::Data)] +pub struct VersionedCanvas { + // We internally guarantee that this vector + // is never empty + versions: im::Vector, + curr_version: usize, +} + +impl VersionedCanvas { + pub fn new(canvas: Canvas) -> VersionedCanvas { + VersionedCanvas { + versions: im::vector![canvas], + curr_version: 0, + } + } + + // Get current canvas version + pub fn get(&self) -> &Canvas { + self.versions.get(self.curr_version).unwrap() + } + + fn has_newer_versions(&self) -> bool { + self.curr_version + 1 < self.versions.len() + } + + fn has_older_versions(&self) -> bool { + self.curr_version > 0 + } + + pub fn version(&self) -> usize { + self.curr_version + } + + pub fn undo(&mut self) { + if self.has_older_versions() { + self.curr_version = self.curr_version - 1; + } + } + + pub fn redo(&mut self) { + if self.has_newer_versions() { + self.curr_version = self.curr_version + 1; + } + } + + pub fn update(&mut self, update_fn: impl FnOnce(&Canvas) -> 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 new_version = update_fn(self.get()); + self.versions.push_back(new_version); + self.curr_version = self.curr_version + 1; + } + + // 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); + } + + update_fn(self.versions.back_mut().unwrap()); + } +} -- 2.39.5 From 4b1dd4caed461ddcaee3a24fef670859926ccb68 Mon Sep 17 00:00:00 2001 From: Francesco Magliocca Date: Mon, 9 Nov 2020 18:54:11 +0100 Subject: [PATCH 4/8] Add TODO messages and rename Canvas::push_back to Canvas::add_element --- src/canvas.rs | 6 ++++-- src/canvas_tools.rs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/canvas.rs b/src/canvas.rs index 097cf4c..c5c4423 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -37,6 +37,7 @@ 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), @@ -46,7 +47,7 @@ fn segment_intersects_rect(segment: &druid::kurbo::PathSeg, rect: druid::Rect) - } druid::kurbo::PathSeg::Cubic(cubic) => { - rect.contains(cubic.p0) || rect.contains(cubic.p1) || rect.contains(cubic.p2) + rect.contains(cubic.p0) || rect.contains(cubic.p1) || rect.contains(cubic.p2) || rect.contains(cubic.p3) } } } @@ -68,6 +69,7 @@ impl Canvas { 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 @@ -82,7 +84,7 @@ impl Canvas { } } - pub fn push_back(&mut self, element: CanvasElement) { + pub fn add_element(&mut self, element: CanvasElement) { self.elements.push_back(element); } } diff --git a/src/canvas_tools.rs b/src/canvas_tools.rs index ba26292..5b4fca3 100644 --- a/src/canvas_tools.rs +++ b/src/canvas_tools.rs @@ -67,7 +67,7 @@ impl CanvasTool { if let Some(current_element) = current_element.take() { canvas.update(move |canvas: &Canvas| -> Canvas { let mut new_canvas = canvas.clone(); - new_canvas.push_back(current_element); + new_canvas.add_element(current_element); return new_canvas; }); } -- 2.39.5 From 114ea2309e3fc03b683a992a73aa9944c80d7de5 Mon Sep 17 00:00:00 2001 From: Francesco Magliocca Date: Mon, 9 Nov 2020 21:06:05 +0100 Subject: [PATCH 5/8] Make update interface slightly more usable --- src/versioned_canvas.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/versioned_canvas.rs b/src/versioned_canvas.rs index e549baa..546f766 100644 --- a/src/versioned_canvas.rs +++ b/src/versioned_canvas.rs @@ -45,7 +45,7 @@ impl VersionedCanvas { } } - pub fn update(&mut self, update_fn: impl FnOnce(&Canvas) -> Canvas) { + 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 @@ -53,9 +53,15 @@ impl VersionedCanvas { if self.has_newer_versions() { self.versions = self.versions.take(self.curr_version + 1); } - let new_version = update_fn(self.get()); - self.versions.push_back(new_version); - self.curr_version = 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) { + self.versions.push_back(new_version); + self.curr_version = self.curr_version + 1; + } } // Do inplace update, which will be irreversible -- 2.39.5 From d0f40530ff673f9a10bdfb9c3bf6d1c0bae8533d Mon Sep 17 00:00:00 2001 From: Francesco Magliocca Date: Mon, 9 Nov 2020 21:07:37 +0100 Subject: [PATCH 6/8] Document the update detection mechanism of VersionedCanvas::update --- src/versioned_canvas.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/versioned_canvas.rs b/src/versioned_canvas.rs index 546f766..fc96ed8 100644 --- a/src/versioned_canvas.rs +++ b/src/versioned_canvas.rs @@ -45,6 +45,10 @@ impl VersionedCanvas { } } + // TODO: Right now the update function internally checks + // whether the canvas has been changed by leveraging the + // 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 -- 2.39.5 From 2aaf3f7cc996ba621423a78c6e368b131f6a5725 Mon Sep 17 00:00:00 2001 From: Francesco Magliocca Date: Mon, 9 Nov 2020 22:42:03 +0100 Subject: [PATCH 7/8] Implement Erasers and Fix VersionedCanvas Implement stroke eraser, fix simple eraser and make VersionedCanvas check that something has actually changed in the canvas before updating the history --- src/canvas.rs | 182 ++++++++++++++++++++++++++++++++-------- src/canvas_tools.rs | 39 ++++++--- src/main.rs | 44 ++++++---- src/versioned_canvas.rs | 37 ++++---- 4 files changed, 223 insertions(+), 79 deletions(-) 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); + } + } } } -- 2.39.5 From db8a0462a25753bb9b9bf05b9d83cfdddb18b7ff Mon Sep 17 00:00:00 2001 From: Francesco Magliocca Date: Mon, 9 Nov 2020 23:33:30 +0100 Subject: [PATCH 8/8] Start correct implementation of line-rect intersection test --- src/canvas.rs | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/canvas.rs b/src/canvas.rs index 01b843e..1ee8c13 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -1,17 +1,49 @@ use druid::kurbo::BezPath; +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: This is only a toy implementation, this is not at all correct! +// TODO: Implement this in a way that is not so flamboyant +// TODO: 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) => rect.contains(line.p0) || rect.contains(line.p1), + 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); - druid::kurbo::PathSeg::Quad(quad) => { - rect.contains(quad.p0) || rect.contains(quad.p1) || rect.contains(quad.p2) - } + 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()); - druid::kurbo::PathSeg::Cubic(cubic) => { - rect.contains(cubic.p0) || rect.contains(cubic.p1) || rect.contains(cubic.p2) || rect.contains(cubic.p3) + line_x_proj.overlaps(&rect_x_proj) && line_y_proj.overlaps(&rect_y_proj) + }, + + _ => { + unimplemented!(); } } } -- 2.39.5