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
This commit is contained in:
Francesco Magliocca 2020-11-09 22:42:03 +01:00
parent d0f40530ff
commit 2aaf3f7cc9
4 changed files with 223 additions and 79 deletions

View File

@ -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<CanvasElement>,
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<Path>) {
// 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);
}
}

View File

@ -13,6 +13,10 @@ pub enum CanvasTool {
Eraser {
eraser_rect: Option<Rect>,
},
StrokeEraser {
eraser_rect: Option<Rect>,
},
}
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<Rect>,
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)
}
});
}
}

View File

@ -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<CanvasData> 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<CanvasData> 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<CanvasData> 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<CanvasData> {
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<CanvasData> {
.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()

View File

@ -50,6 +50,11 @@ 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)) {
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.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
@ -58,11 +63,6 @@ impl VersionedCanvas {
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) {
self.versions.push_back(new_version);
self.curr_version = self.curr_version + 1;
}
@ -70,6 +70,12 @@ impl VersionedCanvas {
// Do inplace update, which will be irreversible
pub fn irreversible_update(&mut self, update_fn: impl FnOnce(&mut Canvas)) {
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
@ -77,7 +83,6 @@ impl VersionedCanvas {
if self.has_newer_versions() {
self.versions = self.versions.take(self.curr_version + 1);
}
update_fn(self.versions.back_mut().unwrap());
}
}
}