diff --git a/editor/src/communication/dispatcher.rs b/editor/src/communication/dispatcher.rs index 9a70db03..1bd55b63 100644 --- a/editor/src/communication/dispatcher.rs +++ b/editor/src/communication/dispatcher.rs @@ -255,7 +255,7 @@ mod test { style: Default::default(), }); - editor.handle_message(Operation::AddPen { + editor.handle_message(Operation::AddPolyline { path: vec![folder_id, PEN_INDEX as u64], insert_index: 0, transform: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index cf5eaa0f..1b492a5c 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -94,6 +94,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}, + // Freehand + entry! {action=FreehandMessage::PointerMove, message=InputMapperMessage::PointerMove}, + entry! {action=FreehandMessage::DragStart, key_down=Lmb}, + entry! {action=FreehandMessage::DragStop, key_up=Lmb}, // Fill entry! {action=FillMessage::LeftMouseDown, key_down=Lmb}, entry! {action=FillMessage::RightMouseDown, key_down=Rmb}, @@ -104,6 +108,7 @@ impl Default for Mapping { entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Fill }, key_down=KeyF}, entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Path }, key_down=KeyA}, entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Pen }, key_down=KeyP}, + entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Freehand }, key_down=KeyN}, entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Line }, key_down=KeyL}, entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Rectangle }, key_down=KeyM}, entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Ellipse }, key_down=KeyE}, diff --git a/editor/src/lib.rs b/editor/src/lib.rs index 1b3501b8..37443ceb 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -73,6 +73,7 @@ pub mod message_prelude { pub use crate::viewport_tools::tools::ellipse::{EllipseMessage, EllipseMessageDiscriminant}; pub use crate::viewport_tools::tools::eyedropper::{EyedropperMessage, EyedropperMessageDiscriminant}; pub use crate::viewport_tools::tools::fill::{FillMessage, FillMessageDiscriminant}; + pub use crate::viewport_tools::tools::freehand::{FreehandMessage, FreehandMessageDiscriminant}; pub use crate::viewport_tools::tools::line::{LineMessage, LineMessageDiscriminant}; pub use crate::viewport_tools::tools::navigate::{NavigateMessage, NavigateMessageDiscriminant}; pub use crate::viewport_tools::tools::path::{PathMessage, PathMessageDiscriminant}; diff --git a/editor/src/viewport_tools/tool.rs b/editor/src/viewport_tools/tool.rs index 71513883..a955e405 100644 --- a/editor/src/viewport_tools/tool.rs +++ b/editor/src/viewport_tools/tool.rs @@ -87,7 +87,7 @@ impl Default for ToolFsmState { // Relight => relight::Relight, Path => path::Path, Pen => pen::Pen, - // Freehand => freehand::Freehand, + Freehand => freehand::Freehand, // Spline => spline::Spline, Line => line::Line, Rectangle => rectangle::Rectangle, @@ -219,7 +219,7 @@ impl ToolType { ToolType::Relight => ToolOptions::Relight {}, ToolType::Path => ToolOptions::Path {}, ToolType::Pen => ToolOptions::Pen { weight: 5 }, - ToolType::Freehand => ToolOptions::Freehand {}, + ToolType::Freehand => ToolOptions::Freehand { weight: 5 }, ToolType::Spline => ToolOptions::Spline {}, ToolType::Line => ToolOptions::Line { weight: 5 }, ToolType::Rectangle => ToolOptions::Rectangle {}, @@ -264,15 +264,26 @@ pub fn standard_tool_message(tool: ToolType, message_type: StandardToolMessageTy }, StandardToolMessageType::Abort => match tool { ToolType::Select => Some(SelectMessage::Abort.into()), - ToolType::Path => Some(PathMessage::Abort.into()), + // ToolType::Crop => Some(CropMessage::Abort.into()), ToolType::Navigate => Some(NavigateMessage::Abort.into()), + ToolType::Eyedropper => Some(EyedropperMessage::Abort.into()), + // ToolType::Text => Some(TextMessage::Abort.into()), + ToolType::Fill => Some(FillMessage::Abort.into()), + // ToolType::Gradient => Some(GradientMessage::Abort.into()), + // ToolType::Brush => Some(BrushMessage::Abort.into()), + // ToolType::Heal => Some(HealMessage::Abort.into()), + // ToolType::Clone => Some(CloneMessage::Abort.into()), + // ToolType::Patch => Some(PatchMessage::Abort.into()), + // ToolType::BlurSharpen => Some(BlurSharpenMessage::Abort.into()), + // ToolType::Relight => Some(RelightMessage::Abort.into()), + ToolType::Path => Some(PathMessage::Abort.into()), ToolType::Pen => Some(PenMessage::Abort.into()), + ToolType::Freehand => Some(FreehandMessage::Abort.into()), + // ToolType::Spline => Some(SplineMessage::Abort.into()), ToolType::Line => Some(LineMessage::Abort.into()), ToolType::Rectangle => Some(RectangleMessage::Abort.into()), ToolType::Ellipse => Some(EllipseMessage::Abort.into()), ToolType::Shape => Some(ShapeMessage::Abort.into()), - ToolType::Eyedropper => Some(EyedropperMessage::Abort.into()), - ToolType::Fill => Some(FillMessage::Abort.into()), _ => None, }, } @@ -297,7 +308,7 @@ pub fn message_to_tool_type(message: &ToolMessage) -> ToolType { // Relight(_) => ToolType::Relight, Path(_) => ToolType::Path, Pen(_) => ToolType::Pen, - // Freehand(_) => ToolType::Freehand, + Freehand(_) => ToolType::Freehand, // Spline(_) => ToolType::Spline, Line(_) => ToolType::Line, Rectangle(_) => ToolType::Rectangle, diff --git a/editor/src/viewport_tools/tool_message.rs b/editor/src/viewport_tools/tool_message.rs index 1826ddf8..b801a0be 100644 --- a/editor/src/viewport_tools/tool_message.rs +++ b/editor/src/viewport_tools/tool_message.rs @@ -13,22 +13,43 @@ pub enum ToolMessage { // Sub-messages #[remain::unsorted] #[child] + Select(SelectMessage), + #[remain::unsorted] + #[child] Crop(CropMessage), #[remain::unsorted] #[child] - Ellipse(EllipseMessage), + Navigate(NavigateMessage), #[remain::unsorted] #[child] Eyedropper(EyedropperMessage), + // #[remain::unsorted] + // #[child] + // Text(TextMessage), #[remain::unsorted] #[child] Fill(FillMessage), - #[remain::unsorted] - #[child] - Line(LineMessage), - #[remain::unsorted] - #[child] - Navigate(NavigateMessage), + // #[remain::unsorted] + // #[child] + // Gradient(GradientMessage), + // #[remain::unsorted] + // #[child] + // Brush(BrushMessage), + // #[remain::unsorted] + // #[child] + // Heal(HealMessage), + // #[remain::unsorted] + // #[child] + // Clone(CloneMessage), + // #[remain::unsorted] + // #[child] + // Patch(PatchMessage), + // #[remain::unsorted] + // #[child] + // Detail(DetailMessage), + // #[remain::unsorted] + // #[child] + // Relight(RelightMessage), #[remain::unsorted] #[child] Path(PathMessage), @@ -37,10 +58,19 @@ pub enum ToolMessage { Pen(PenMessage), #[remain::unsorted] #[child] + Freehand(FreehandMessage), + // #[remain::unsorted] + // #[child] + // Spline(SplineMessage), + #[remain::unsorted] + #[child] + Line(LineMessage), + #[remain::unsorted] + #[child] Rectangle(RectangleMessage), #[remain::unsorted] #[child] - Select(SelectMessage), + Ellipse(EllipseMessage), #[remain::unsorted] #[child] Shape(ShapeMessage), diff --git a/editor/src/viewport_tools/tool_options.rs b/editor/src/viewport_tools/tool_options.rs index 19638850..4ca47bac 100644 --- a/editor/src/viewport_tools/tool_options.rs +++ b/editor/src/viewport_tools/tool_options.rs @@ -17,7 +17,7 @@ pub enum ToolOptions { Relight {}, Path {}, Pen { weight: u32 }, - Freehand {}, + Freehand { weight: u32 }, Spline {}, Line { weight: u32 }, Rectangle {}, diff --git a/editor/src/viewport_tools/tools/freehand.rs b/editor/src/viewport_tools/tools/freehand.rs new file mode 100644 index 00000000..3b80f90b --- /dev/null +++ b/editor/src/viewport_tools/tools/freehand.rs @@ -0,0 +1,190 @@ +use crate::document::DocumentMessageHandler; +use crate::frontend::utility_types::MouseCursorIcon; +use crate::input::keyboard::MouseMotion; +use crate::input::InputPreprocessorMessageHandler; +use crate::message_prelude::*; +use crate::misc::{HintData, HintGroup, HintInfo}; +use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType}; +use crate::viewport_tools::tool_options::ToolOptions; + +use graphene::layers::style; +use graphene::Operation; + +use glam::{DAffine2, DVec2}; +use serde::{Deserialize, Serialize}; + +#[derive(Default)] +pub struct Freehand { + fsm_state: FreehandToolFsmState, + data: FreehandToolData, +} + +#[remain::sorted] +#[impl_message(Message, ToolMessage, Freehand)] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] +pub enum FreehandMessage { + // Standard messages + #[remain::unsorted] + Abort, + + // Tool-specific messages + DragStart, + DragStop, + PointerMove, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum FreehandToolFsmState { + Ready, + Drawing, +} + +impl<'a> MessageHandler> for Freehand { + fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { + if action == ToolMessage::UpdateHints { + self.fsm_state.update_hints(responses); + return; + } + + if action == ToolMessage::UpdateCursor { + self.fsm_state.update_cursor(responses); + return; + } + + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + + if self.fsm_state != new_state { + self.fsm_state = new_state; + self.fsm_state.update_hints(responses); + self.fsm_state.update_cursor(responses); + } + } + + fn actions(&self) -> ActionList { + use FreehandToolFsmState::*; + + match self.fsm_state { + Ready => actions!(FreehandMessageDiscriminant; DragStart, DragStop, Abort), + Drawing => actions!(FreehandMessageDiscriminant; DragStop, PointerMove, Abort), + } + } +} + +impl Default for FreehandToolFsmState { + fn default() -> Self { + FreehandToolFsmState::Ready + } +} +#[derive(Clone, Debug, Default)] +struct FreehandToolData { + points: Vec, + weight: u32, + path: Option>, +} + +impl Fsm for FreehandToolFsmState { + type ToolData = FreehandToolData; + + fn transition( + self, + event: ToolMessage, + document: &DocumentMessageHandler, + tool_data: &DocumentToolData, + data: &mut Self::ToolData, + input: &InputPreprocessorMessageHandler, + responses: &mut VecDeque, + ) -> Self { + use FreehandMessage::*; + use FreehandToolFsmState::*; + + let transform = document.graphene_document.root.transform; + + if let ToolMessage::Freehand(event) = event { + match (self, event) { + (Ready, DragStart) => { + responses.push_back(DocumentMessage::StartTransaction.into()); + responses.push_back(DocumentMessage::DeselectAllLayers.into()); + data.path = Some(vec![generate_uuid()]); + + let pos = transform.inverse().transform_point2(input.mouse.position); + + data.points.push(pos); + + data.weight = match tool_data.tool_options.get(&ToolType::Freehand) { + Some(&ToolOptions::Freehand { weight }) => weight, + _ => 5, + }; + + responses.push_back(make_operation(data, tool_data)); + + Drawing + } + (Drawing, PointerMove) => { + let pos = transform.inverse().transform_point2(input.mouse.position); + + if data.points.last() != Some(&pos) { + data.points.push(pos); + } + + responses.push_back(remove_preview(data)); + responses.push_back(make_operation(data, tool_data)); + + Drawing + } + (Drawing, DragStop) | (Drawing, Abort) => { + if data.points.len() >= 2 { + responses.push_back(DocumentMessage::DeselectAllLayers.into()); + responses.push_back(remove_preview(data)); + responses.push_back(make_operation(data, tool_data)); + responses.push_back(DocumentMessage::CommitTransaction.into()); + } else { + responses.push_back(DocumentMessage::AbortTransaction.into()); + } + + data.path = None; + data.points.clear(); + + Ready + } + _ => self, + } + } else { + self + } + } + + fn update_hints(&self, responses: &mut VecDeque) { + let hint_data = match self { + FreehandToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::LmbDrag), + label: String::from("Draw Polyline"), + plus: false, + }])]), + FreehandToolFsmState::Drawing => HintData(vec![]), + }; + + responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); + } + + fn update_cursor(&self, responses: &mut VecDeque) { + responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into()); + } +} + +fn remove_preview(data: &FreehandToolData) -> Message { + Operation::DeleteLayer { path: data.path.clone().unwrap() }.into() +} + +fn make_operation(data: &FreehandToolData, tool_data: &DocumentToolData) -> Message { + let points: Vec<(f64, f64)> = data.points.iter().map(|p| (p.x, p.y)).collect(); + + Operation::AddPolyline { + path: data.path.clone().unwrap(), + insert_index: -1, + transform: DAffine2::IDENTITY.to_cols_array(), + points, + style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, data.weight as f32)), Some(style::Fill::none())), + } + .into() +} diff --git a/editor/src/viewport_tools/tools/mod.rs b/editor/src/viewport_tools/tools/mod.rs index ea454d46..72124355 100644 --- a/editor/src/viewport_tools/tools/mod.rs +++ b/editor/src/viewport_tools/tools/mod.rs @@ -2,6 +2,7 @@ pub mod crop; pub mod ellipse; pub mod eyedropper; pub mod fill; +pub mod freehand; pub mod line; pub mod navigate; pub mod path; diff --git a/editor/src/viewport_tools/tools/pen.rs b/editor/src/viewport_tools/tools/pen.rs index 336b4626..2cc84fa8 100644 --- a/editor/src/viewport_tools/tools/pen.rs +++ b/editor/src/viewport_tools/tools/pen.rs @@ -84,7 +84,6 @@ struct PenToolData { next_point: DVec2, weight: u32, path: Option>, - layer_exists: bool, snap_handler: SnapHandler, } @@ -111,7 +110,6 @@ impl Fsm for PenToolFsmState { responses.push_back(DocumentMessage::StartTransaction.into()); responses.push_back(DocumentMessage::DeselectAllLayers.into()); data.path = Some(vec![generate_uuid()]); - data.layer_exists = false; data.snap_handler.start_snap(document, document.visible_layers()); let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); @@ -220,7 +218,7 @@ fn make_operation(data: &PenToolData, tool_data: &DocumentToolData, show_preview points.push((data.next_point.x, data.next_point.y)) } - Operation::AddPen { + Operation::AddPolyline { path: data.path.clone().unwrap(), insert_index: -1, transform: DAffine2::IDENTITY.to_cols_array(), diff --git a/frontend/src/components/panels/Document.vue b/frontend/src/components/panels/Document.vue index bba1ff01..2b3d08ed 100644 --- a/frontend/src/components/panels/Document.vue +++ b/frontend/src/components/panels/Document.vue @@ -100,7 +100,7 @@ - + diff --git a/frontend/src/components/widgets/options/ToolOptions.vue b/frontend/src/components/widgets/options/ToolOptions.vue index fcc7845f..48ef61e1 100644 --- a/frontend/src/components/widgets/options/ToolOptions.vue +++ b/frontend/src/components/widgets/options/ToolOptions.vue @@ -165,7 +165,7 @@ export default defineComponent({ Relight: [], Path: [], Pen: [{ kind: "NumberInput", optionPath: ["weight"], props: { min: 1, isInteger: true, unit: " px", label: "Weight" } }], - Freehand: [], + Freehand: [{ kind: "NumberInput", optionPath: ["weight"], props: { min: 1, isInteger: true, unit: " px", label: "Weight" } }], Spline: [], Line: [{ kind: "NumberInput", optionPath: ["weight"], props: { min: 1, isInteger: true, unit: " px", label: "Weight" } }], Rectangle: [], diff --git a/graphene/src/document.rs b/graphene/src/document.rs index 14b3d7b6..a7c77c9e 100644 --- a/graphene/src/document.rs +++ b/graphene/src/document.rs @@ -474,7 +474,7 @@ impl Document { Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }]].concat()) } - Operation::AddPen { + Operation::AddPolyline { path, insert_index, points, diff --git a/graphene/src/operation.rs b/graphene/src/operation.rs index 087a0430..7393c496 100644 --- a/graphene/src/operation.rs +++ b/graphene/src/operation.rs @@ -45,7 +45,7 @@ pub enum Operation { transform: [f64; 6], style: style::PathStyle, }, - AddPen { + AddPolyline { path: Vec, transform: [f64; 6], insert_index: isize,