Compare commits

...

4 Commits

Author SHA1 Message Date
Francesco Magliocca 2aaf3f7cc9 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
2020-11-09 22:42:03 +01:00
Francesco Magliocca d0f40530ff Document the update detection mechanism of VersionedCanvas::update 2020-11-09 21:07:37 +01:00
Francesco Magliocca 114ea2309e Make update interface slightly more usable 2020-11-09 21:06:05 +01:00
Francesco Magliocca 4b1dd4caed Add TODO messages and rename Canvas::push_back to Canvas::add_element 2020-11-09 18:54:11 +01:00
4 changed files with 235 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,53 +62,139 @@ impl CanvasElement {
}
}
// 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<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 {
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 push_back(&mut self, element: CanvasElement) {
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.push_back(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

@ -45,7 +45,16 @@ impl VersionedCanvas {
}
}
pub fn update(&mut self, update_fn: impl FnOnce(&Canvas) -> Canvas) {
// 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)) {
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
@ -53,13 +62,20 @@ 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;
}
}
// 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
@ -67,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());
}
}
}