Compare commits
4 Commits
bbd221e489
...
2aaf3f7cc9
| Author | SHA1 | Date |
|---|---|---|
|
|
2aaf3f7cc9 | |
|
|
d0f40530ff | |
|
|
114ea2309e | |
|
|
4b1dd4caed |
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,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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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()
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue