Implement fill tool (#254)

* Implement fill tool

* Add fill tool shortcut

* Add getters and setters to styles

* Make fill tool act on the topmost layer clicked

* Refactor fill operation

* Refactor and unify selection tolerance

* Add mark_as_dirty function

* Fix getter names
This commit is contained in:
Henry Sloan 2021-07-14 03:49:12 -04:00 committed by Keavon Chambers
parent 57b8ee0e86
commit 363b9c7ffa
13 changed files with 96 additions and 5 deletions

View File

@ -112,7 +112,7 @@
<Separator :type="SeparatorType.Section" :direction="SeparatorDirection.Vertical" />
<ShelfItem :icon="'TextTool'" title="Text Tool (T)" :active="activeTool === 'Text'" @click="'tool not implemented' || selectTool('Text')" />
<ShelfItem :icon="'FillTool'" title="Fill Tool (F)" :active="activeTool === 'Fill'" @click="'tool not implemented' || selectTool('Fill')" />
<ShelfItem :icon="'FillTool'" title="Fill Tool (F)" :active="activeTool === 'Fill'" @click="selectTool('Fill')" />
<ShelfItem :icon="'GradientTool'" title="Gradient Tool (H)" :active="activeTool === 'Gradient'" @click="'tool not implemented' || selectTool('Gradient')" />
<Separator :type="SeparatorType.Section" :direction="SeparatorDirection.Vertical" />

View File

@ -227,6 +227,16 @@ impl Document {
Ok(())
}
fn mark_as_dirty(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
let mut root = &mut self.root;
root.cache_dirty = true;
for id in path {
root = root.as_folder_mut()?.layer_mut(*id).ok_or(DocumentError::LayerNotFound)?;
root.cache_dirty = true;
}
Ok(())
}
/// Mutate the document by applying the `operation` to it. If the operation necessitates a
/// reaction from the frontend, responses may be returned.
pub fn handle_operation(&mut self, operation: Operation) -> Result<Option<Vec<DocumentResponse>>, DocumentError> {
@ -361,6 +371,12 @@ impl Document {
let path = path.as_slice()[..path.len() - 1].to_vec();
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path }])
}
Operation::FillLayer { path, color } => {
let layer = self.layer_mut(path).unwrap();
layer.style.set_fill(layers::style::Fill::new(*color));
self.mark_as_dirty(path)?;
Some(vec![DocumentResponse::DocumentChanged])
}
};
if !matches!(
operation,

View File

@ -23,8 +23,6 @@ use crate::LayerId;
pub use folder::Folder;
use serde::{Deserialize, Serialize};
pub const SELECTION_TOLERANCE: f64 = 5.0;
pub trait LayerData {
fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle);
fn to_kurbo_path(&self, transform: glam::DAffine2, style: style::PathStyle) -> BezPath;

View File

@ -56,6 +56,24 @@ impl PathStyle {
pub fn new(stroke: Option<Stroke>, fill: Option<Fill>) -> Self {
Self { stroke, fill }
}
pub fn fill(&self) -> Option<Fill> {
self.fill
}
pub fn stroke(&self) -> Option<Stroke> {
self.stroke
}
pub fn set_fill(&mut self, fill: Fill) {
self.fill = Some(fill);
}
pub fn set_stroke(&mut self, stroke: Stroke) {
self.stroke = Some(stroke);
}
pub fn clear_fill(&mut self) {
self.fill = None;
}
pub fn clear_stroke(&mut self) {
self.stroke = None;
}
pub fn render(&self) -> String {
format!(
"{}{}",

View File

@ -1,4 +1,5 @@
use crate::{
color::Color,
layers::{style, Layer},
LayerId,
};
@ -71,4 +72,8 @@ pub enum Operation {
ToggleVisibility {
path: Vec<LayerId>,
},
FillLayer {
path: Vec<LayerId>,
color: Color,
},
}

View File

@ -10,3 +10,5 @@ pub const WHEEL_ZOOM_RATE: f64 = 1. / 600.;
pub const MOUSE_ZOOM_RATE: f64 = 1. / 400.;
pub const ROTATE_SNAP_INTERVAL: f64 = 15.;
pub const SELECTION_TOLERANCE: f64 = 5.0;

View File

@ -169,7 +169,10 @@ impl Default for Mapping {
entry! {action=PenMessage::Confirm, key_down=Rmb},
entry! {action=PenMessage::Confirm, key_down=KeyEscape},
entry! {action=PenMessage::Confirm, key_down=KeyEnter},
// Fill
entry! {action=FillMessage::MouseDown, key_down=Lmb},
// Tool Actions
entry! {action=ToolMessage::SelectTool(ToolType::Fill), key_down=KeyF},
entry! {action=ToolMessage::SelectTool(ToolType::Rectangle), key_down=KeyM},
entry! {action=ToolMessage::SelectTool(ToolType::Ellipse), key_down=KeyE},
entry! {action=ToolMessage::SelectTool(ToolType::Select), key_down=KeyV},

View File

@ -60,6 +60,7 @@ pub mod message_prelude {
pub use super::tool::tool_messages::*;
pub use super::tool::tools::crop::{CropMessage, CropMessageDiscriminant};
pub use super::tool::tools::eyedropper::{EyedropperMessage, EyedropperMessageDiscriminant};
pub use super::tool::tools::fill::{FillMessage, FillMessageDiscriminant};
pub use super::tool::tools::line::{LineMessage, LineMessageDiscriminant};
pub use super::tool::tools::navigate::{NavigateMessage, NavigateMessageDiscriminant};
pub use super::tool::tools::path::{PathMessage, PathMessageDiscriminant};

View File

@ -83,6 +83,7 @@ impl Default for ToolFsmState {
Line => line::Line,
Shape => shape::Shape,
Ellipse => ellipse::Ellipse,
Fill => fill::Fill,
},
},
document_tool_data: DocumentToolData {

View File

@ -17,6 +17,8 @@ pub enum ToolMessage {
SwapColors,
ResetColors,
#[child]
Fill(FillMessage),
#[child]
Rectangle(RectangleMessage),
#[child]
Ellipse(EllipseMessage),
@ -89,6 +91,7 @@ impl MessageHandler<ToolMessage, (&SvgDocument, &InputPreprocessor)> for ToolMes
}
message => {
let tool_type = match message {
Fill(_) => ToolType::Fill,
Rectangle(_) => ToolType::Rectangle,
Ellipse(_) => ToolType::Ellipse,
Shape(_) => ToolType::Shape,

View File

@ -0,0 +1,43 @@
use crate::consts::SELECTION_TOLERANCE;
use crate::message_prelude::*;
use crate::tool::ToolActionHandlerData;
use document_core::Operation;
use glam::DVec2;
#[derive(Default)]
pub struct Fill;
#[impl_message(Message, ToolMessage, Fill)]
#[derive(PartialEq, Clone, Debug)]
pub enum FillMessage {
MouseDown,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Fill {
fn process_action(&mut self, _action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
let mouse_pos = data.2.mouse.position;
let (x, y) = (mouse_pos.x as f64, mouse_pos.y as f64);
let (point_1, point_2) = (
DVec2::new(x - SELECTION_TOLERANCE, y - SELECTION_TOLERANCE),
DVec2::new(x + SELECTION_TOLERANCE, y + SELECTION_TOLERANCE),
);
let quad = [
DVec2::new(point_1.x, point_1.y),
DVec2::new(point_2.x, point_1.y),
DVec2::new(point_2.x, point_2.y),
DVec2::new(point_1.x, point_2.y),
];
if let Some(path) = data.0.intersects_quad_root(quad).last() {
responses.push_back(
Operation::FillLayer {
path: path.to_vec(),
color: data.1.primary_color,
}
.into(),
);
}
}
advertise_actions!(FillMessageDiscriminant; MouseDown);
}

View File

@ -1,5 +1,6 @@
// already implemented
pub mod ellipse;
pub mod fill;
pub mod line;
pub mod pen;
pub mod rectangle;

View File

@ -1,13 +1,13 @@
use document_core::color::Color;
use document_core::layers::style;
use document_core::layers::style::Fill;
use document_core::layers::style::Stroke;
use document_core::layers::{style, SELECTION_TOLERANCE};
use document_core::Operation;
use glam::{DAffine2, DVec2};
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{message_prelude::*, SvgDocument};
use crate::{consts::SELECTION_TOLERANCE, message_prelude::*, SvgDocument};
#[derive(Default)]
pub struct Select {