diff --git a/src/canvas.rs b/src/canvas.rs
index 5cdd865..0c9ea65 100644
--- a/src/canvas.rs
+++ b/src/canvas.rs
@@ -14,6 +14,8 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+use std::vec::Vec;
+
use im::Vector;
use serde::de::{Deserializer, SeqAccess, Visitor};
@@ -25,12 +27,58 @@ pub struct Path {
pub kurbo_path: druid::kurbo::BezPath,
}
-pub type Canvas = Vector;
-
#[derive(Serialize, Deserialize)]
#[serde(remote = "druid::Color")]
enum ColorDef {
- Rgba32(u32)
+ Rgba32(u32),
+}
+
+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(francesco): Implement this in a way that is not so flamboyant
+// TODO(francesco): 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) => {
+ // 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);
+
+ 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());
+
+ line_x_proj.overlaps(&rect_x_proj) && line_y_proj.overlaps(&rect_y_proj)
+ }
+
+ _ => {
+ unimplemented!();
+ }
+ }
}
#[derive(Debug, Clone, druid::Data, Serialize, Deserialize)]
@@ -46,7 +94,9 @@ pub enum CanvasElement {
impl CanvasElement {
pub fn bounding_box(&self) -> druid::Rect {
match self {
- CanvasElement::Freehand { path, thickness, .. } => {
+ CanvasElement::Freehand {
+ path, thickness, ..
+ } => {
use druid::kurbo::Shape;
path.kurbo_path
.bounding_box()
@@ -54,9 +104,25 @@ impl CanvasElement {
}
}
}
+
+ pub fn intersects_rect(&self, rect: druid::Rect) -> bool {
+ match self {
+ CanvasElement::Freehand { path, .. } => {
+ // TODO(enrico) this doesn't handle thickness
+ path.kurbo_path
+ .segments()
+ .any(|segment| segment_intersects_rect(&segment, rect))
+ }
+ }
+ }
+
pub fn draw(&self, ctx: &mut druid::PaintCtx) {
match self {
- CanvasElement::Freehand { path, thickness, stroke_color } => {
+ CanvasElement::Freehand {
+ path,
+ thickness,
+ stroke_color,
+ } => {
use druid::RenderContext;
ctx.stroke(&path.kurbo_path, &*stroke_color, *thickness);
}
@@ -70,6 +136,29 @@ impl CanvasElement {
}
}
+#[derive(Clone, druid::Data)]
+pub struct Canvas {
+ pub elements: Vector,
+}
+
+impl Canvas {
+ /// Find all CanvasElement that intersect with rect
+ pub fn find_intersections(&self, rect: druid::Rect) -> Vec {
+ let mut found_elements = Vec::::new();
+
+ for (i, elem) in self.elements.iter().enumerate() {
+ // Check if the element intersects the eraser rect
+ if elem.bounding_box().intersect(rect).area() > 0.0 {
+ if elem.intersects_rect(rect) {
+ found_elements.push(i);
+ }
+ }
+ }
+
+ found_elements
+ }
+}
+
impl Serialize for Path {
fn serialize(&self, serializer: S) -> Result
where
@@ -92,7 +181,7 @@ impl<'de> Deserialize<'de> for Path {
{
struct PathVisitor;
- impl<'de> Visitor<'de> for PathVisitor{
+ impl<'de> Visitor<'de> for PathVisitor {
type Value = Path;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
diff --git a/src/main.rs b/src/main.rs
index cec11e8..eb54e93 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -29,6 +29,7 @@ use druid::{
use im::Vector;
+use stiletto::canvas::Canvas;
use stiletto::history::VersionedCanvas;
use stiletto::tool::{CanvasToolCtx, CanvasToolParams};
use stiletto::widget::{build_simple_tool_widget, CanvasState, CanvasToolIconState, CanvasWidget};
@@ -51,7 +52,9 @@ pub fn main() {
};
let canvas_data = StilettoState {
canvas: CanvasState {
- versioned_canvas: VersionedCanvas::new(vector![]),
+ versioned_canvas: VersionedCanvas::new(Canvas {
+ elements: vector![],
+ }),
tool_ctx: CanvasToolCtx::new(default_pen_params.clone()),
},
tool_icons: vector![
diff --git a/src/tool.rs b/src/tool.rs
index 78b1b7d..d22870e 100644
--- a/src/tool.rs
+++ b/src/tool.rs
@@ -36,7 +36,11 @@ pub enum CanvasToolType {
#[derive(Clone, Data)]
pub enum CanvasToolState {
Idle,
- DrawingFreehand { pen_params: CanvasToolParams, current_path: CanvasElement },
+ DrawingFreehand {
+ pen_params: CanvasToolParams,
+ current_path: CanvasElement,
+ },
+ Erasing,
}
#[derive(Clone, Data)]
@@ -71,6 +75,36 @@ impl CanvasToolCtx {
) {
match self.initial_params.tool_type() {
CanvasToolType::Pen => self.handle_pen_event(&ctx, &event, &mut vcanvas, &env),
+ CanvasToolType::Eraser => self.handle_erase_event(&ctx, &event, &mut vcanvas, &env),
+ }
+ }
+
+ pub fn handle_erase_event(
+ &mut self,
+ _ctx: &EventCtx,
+ event: &Event,
+ vcanvas: &mut VersionedCanvas,
+ _env: &Env,
+ ) {
+ match (&mut self.state, event) {
+ (CanvasToolState::Idle, Event::MouseDown(_)) => {
+ self.state = CanvasToolState::Erasing;
+ }
+ (CanvasToolState::Erasing, Event::MouseMove(mouse_event)) => {
+ let eraser_rect = druid::Rect::from_center_size(mouse_event.pos, (5.0, 5.0));
+ let elements = vcanvas.get().find_intersections(eraser_rect);
+
+ if !elements.is_empty() {
+ vcanvas.update(|canvas: &mut Canvas| {
+ for i in elements {
+ canvas.elements.remove(i);
+ }
+ });
+ }
+ }
+ (CanvasToolState::Erasing, Event::MouseUp(_)) => {
+ self.state = CanvasToolState::Idle;
+ }
_ => {}
}
}
@@ -86,7 +120,7 @@ impl CanvasToolCtx {
(CanvasToolState::Idle, Event::MouseDown(mouse_event)) => {
let mut kurbo_path = BezPath::new();
kurbo_path.move_to((mouse_event.pos.x, mouse_event.pos.y));
- if let CanvasToolParams::Pen{ thickness, color } = &self.initial_params {
+ if let CanvasToolParams::Pen { thickness, color } = &self.initial_params {
self.state = CanvasToolState::DrawingFreehand {
pen_params: self.initial_params.clone(),
current_path: CanvasElement::Freehand {
@@ -114,7 +148,7 @@ impl CanvasToolCtx {
vcanvas.update(move |canvas: &mut Canvas| {
let current_state = std::mem::replace(&mut self.state, CanvasToolState::Idle);
if let CanvasToolState::DrawingFreehand { current_path, .. } = current_state {
- canvas.push_back(current_path);
+ canvas.elements.push_back(current_path);
}
});
}
diff --git a/src/widget/canvas.rs b/src/widget/canvas.rs
index 9918f26..242d601 100644
--- a/src/widget/canvas.rs
+++ b/src/widget/canvas.rs
@@ -16,6 +16,7 @@
use im::Vector;
+use crate::canvas::Canvas;
use crate::history::VersionedCanvas;
use crate::tool::CanvasToolCtx;
use crate::DocumentSnapshot;
@@ -44,12 +45,20 @@ impl CanvasState {
pub fn get_document_snapshot(&self) -> DocumentSnapshot {
DocumentSnapshot {
- canvas_elements: self.versioned_canvas.get().iter().cloned().collect(),
+ canvas_elements: self
+ .versioned_canvas
+ .get()
+ .elements
+ .iter()
+ .cloned()
+ .collect(),
}
}
pub fn set_from_snapshot(&mut self, snapshot: DocumentSnapshot) {
- self.versioned_canvas = VersionedCanvas::new(Vector::from(snapshot.canvas_elements));
+ self.versioned_canvas = VersionedCanvas::new(Canvas {
+ elements: Vector::from(snapshot.canvas_elements),
+ });
}
pub fn set_tool_ctx(&mut self, ctx: CanvasToolCtx) {
@@ -121,7 +130,7 @@ impl Widget for CanvasWidget {
// and we only want to clear this widget's area.
ctx.fill(rect, &Color::WHITE);
- for element in data.versioned_canvas.get().iter() {
+ for element in data.versioned_canvas.get().elements.iter() {
element.draw(ctx);
}
if data.tool_ctx.needs_repaint() {