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:
parent
d0f40530ff
commit
2aaf3f7cc9
182
src/canvas.rs
182
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<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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
44
src/main.rs
44
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<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()
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue