Reintroduce stroke eraser, copying code from PR #7

This commit is contained in:
Enrico Lumetti 2021-02-24 15:51:56 +01:00
parent 0b74394042
commit 827d8d6a1c
4 changed files with 148 additions and 13 deletions

View File

@ -14,6 +14,8 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::vec::Vec;
use im::Vector; use im::Vector;
use serde::de::{Deserializer, SeqAccess, Visitor}; use serde::de::{Deserializer, SeqAccess, Visitor};
@ -25,12 +27,58 @@ pub struct Path {
pub kurbo_path: druid::kurbo::BezPath, pub kurbo_path: druid::kurbo::BezPath,
} }
pub type Canvas = Vector<CanvasElement>;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(remote = "druid::Color")] #[serde(remote = "druid::Color")]
enum ColorDef { 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)] #[derive(Debug, Clone, druid::Data, Serialize, Deserialize)]
@ -46,7 +94,9 @@ pub enum CanvasElement {
impl CanvasElement { impl CanvasElement {
pub fn bounding_box(&self) -> druid::Rect { pub fn bounding_box(&self) -> druid::Rect {
match self { match self {
CanvasElement::Freehand { path, thickness, .. } => { CanvasElement::Freehand {
path, thickness, ..
} => {
use druid::kurbo::Shape; use druid::kurbo::Shape;
path.kurbo_path path.kurbo_path
.bounding_box() .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) { pub fn draw(&self, ctx: &mut druid::PaintCtx) {
match self { match self {
CanvasElement::Freehand { path, thickness, stroke_color } => { CanvasElement::Freehand {
path,
thickness,
stroke_color,
} => {
use druid::RenderContext; use druid::RenderContext;
ctx.stroke(&path.kurbo_path, &*stroke_color, *thickness); ctx.stroke(&path.kurbo_path, &*stroke_color, *thickness);
} }
@ -70,6 +136,29 @@ impl CanvasElement {
} }
} }
#[derive(Clone, druid::Data)]
pub struct Canvas {
pub elements: Vector<CanvasElement>,
}
impl Canvas {
/// Find all CanvasElement that intersect with rect
pub fn find_intersections(&self, rect: druid::Rect) -> Vec<usize> {
let mut found_elements = Vec::<usize>::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 { impl Serialize for Path {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
@ -92,7 +181,7 @@ impl<'de> Deserialize<'de> for Path {
{ {
struct PathVisitor; struct PathVisitor;
impl<'de> Visitor<'de> for PathVisitor{ impl<'de> Visitor<'de> for PathVisitor {
type Value = Path; type Value = Path;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {

View File

@ -29,6 +29,7 @@ use druid::{
use im::Vector; use im::Vector;
use stiletto::canvas::Canvas;
use stiletto::history::VersionedCanvas; use stiletto::history::VersionedCanvas;
use stiletto::tool::{CanvasToolCtx, CanvasToolParams}; use stiletto::tool::{CanvasToolCtx, CanvasToolParams};
use stiletto::widget::{build_simple_tool_widget, CanvasState, CanvasToolIconState, CanvasWidget}; use stiletto::widget::{build_simple_tool_widget, CanvasState, CanvasToolIconState, CanvasWidget};
@ -51,7 +52,9 @@ pub fn main() {
}; };
let canvas_data = StilettoState { let canvas_data = StilettoState {
canvas: CanvasState { canvas: CanvasState {
versioned_canvas: VersionedCanvas::new(vector![]), versioned_canvas: VersionedCanvas::new(Canvas {
elements: vector![],
}),
tool_ctx: CanvasToolCtx::new(default_pen_params.clone()), tool_ctx: CanvasToolCtx::new(default_pen_params.clone()),
}, },
tool_icons: vector![ tool_icons: vector![

View File

@ -36,7 +36,11 @@ pub enum CanvasToolType {
#[derive(Clone, Data)] #[derive(Clone, Data)]
pub enum CanvasToolState { pub enum CanvasToolState {
Idle, Idle,
DrawingFreehand { pen_params: CanvasToolParams, current_path: CanvasElement }, DrawingFreehand {
pen_params: CanvasToolParams,
current_path: CanvasElement,
},
Erasing,
} }
#[derive(Clone, Data)] #[derive(Clone, Data)]
@ -71,6 +75,36 @@ impl CanvasToolCtx {
) { ) {
match self.initial_params.tool_type() { match self.initial_params.tool_type() {
CanvasToolType::Pen => self.handle_pen_event(&ctx, &event, &mut vcanvas, &env), 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)) => { (CanvasToolState::Idle, Event::MouseDown(mouse_event)) => {
let mut kurbo_path = BezPath::new(); let mut kurbo_path = BezPath::new();
kurbo_path.move_to((mouse_event.pos.x, mouse_event.pos.y)); 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 { self.state = CanvasToolState::DrawingFreehand {
pen_params: self.initial_params.clone(), pen_params: self.initial_params.clone(),
current_path: CanvasElement::Freehand { current_path: CanvasElement::Freehand {
@ -114,7 +148,7 @@ impl CanvasToolCtx {
vcanvas.update(move |canvas: &mut Canvas| { vcanvas.update(move |canvas: &mut Canvas| {
let current_state = std::mem::replace(&mut self.state, CanvasToolState::Idle); let current_state = std::mem::replace(&mut self.state, CanvasToolState::Idle);
if let CanvasToolState::DrawingFreehand { current_path, .. } = current_state { if let CanvasToolState::DrawingFreehand { current_path, .. } = current_state {
canvas.push_back(current_path); canvas.elements.push_back(current_path);
} }
}); });
} }

View File

@ -16,6 +16,7 @@
use im::Vector; use im::Vector;
use crate::canvas::Canvas;
use crate::history::VersionedCanvas; use crate::history::VersionedCanvas;
use crate::tool::CanvasToolCtx; use crate::tool::CanvasToolCtx;
use crate::DocumentSnapshot; use crate::DocumentSnapshot;
@ -44,12 +45,20 @@ impl CanvasState {
pub fn get_document_snapshot(&self) -> DocumentSnapshot { pub fn get_document_snapshot(&self) -> DocumentSnapshot {
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) { 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) { pub fn set_tool_ctx(&mut self, ctx: CanvasToolCtx) {
@ -121,7 +130,7 @@ impl Widget<CanvasState> for CanvasWidget {
// and we only want to clear this widget's area. // and we only want to clear this widget's area.
ctx.fill(rect, &Color::WHITE); 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); element.draw(ctx);
} }
if data.tool_ctx.needs_repaint() { if data.tool_ctx.needs_repaint() {