Reintroduce stroke eraser, copying code from PR #7
This commit is contained in:
parent
0b74394042
commit
827d8d6a1c
101
src/canvas.rs
101
src/canvas.rs
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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![
|
||||||
|
|
|
||||||
40
src/tool.rs
40
src/tool.rs
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue