diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 8e33f194c7..45f8297bd7 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -272,14 +272,6 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(MouseRight); action_dispatch=FreehandToolMessage::Abort), entry!(KeyDown(Escape); action_dispatch=FreehandToolMessage::Abort), // - // SplineToolMessage - entry!(PointerMove; action_dispatch=SplineToolMessage::PointerMove), - entry!(KeyDown(MouseLeft); action_dispatch=SplineToolMessage::DragStart { append_to_selected: Shift }), - entry!(KeyUp(MouseLeft); action_dispatch=SplineToolMessage::DragStop), - entry!(KeyDown(MouseRight); action_dispatch=SplineToolMessage::Confirm), - entry!(KeyDown(Escape); action_dispatch=SplineToolMessage::Confirm), - entry!(KeyDown(Enter); action_dispatch=SplineToolMessage::Confirm), - // // FillToolMessage entry!(KeyDown(MouseLeft); action_dispatch=FillToolMessage::FillPrimaryColor), entry!(KeyDown(MouseLeft); modifiers=[Shift], action_dispatch=FillToolMessage::FillSecondaryColor), diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index 894ef6791c..1049a660dd 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -45,7 +45,6 @@ pub use crate::messages::tool::tool_messages::pen_tool::{PenToolMessage, PenTool pub use crate::messages::tool::tool_messages::polygon_tool::{PolygonToolMessage, PolygonToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::rectangle_tool::{RectangleToolMessage, RectangleToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::select_tool::{SelectToolMessage, SelectToolMessageDiscriminant}; -pub use crate::messages::tool::tool_messages::spline_tool::{SplineToolMessage, SplineToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::text_tool::{TextToolMessage, TextToolMessageDiscriminant}; // Helper diff --git a/editor/src/messages/tool/tool_message.rs b/editor/src/messages/tool/tool_message.rs index f812f63a22..4a3b913ece 100644 --- a/editor/src/messages/tool/tool_message.rs +++ b/editor/src/messages/tool/tool_message.rs @@ -31,8 +31,6 @@ pub enum ToolMessage { #[child] Freehand(FreehandToolMessage), #[child] - Spline(SplineToolMessage), - #[child] Line(LineToolMessage), #[child] Rectangle(RectangleToolMessage), diff --git a/editor/src/messages/tool/tool_messages/mod.rs b/editor/src/messages/tool/tool_messages/mod.rs index faa5840c54..5680eb111f 100644 --- a/editor/src/messages/tool/tool_messages/mod.rs +++ b/editor/src/messages/tool/tool_messages/mod.rs @@ -13,7 +13,6 @@ pub mod pen_tool; pub mod polygon_tool; pub mod rectangle_tool; pub mod select_tool; -pub mod spline_tool; pub mod text_tool; pub mod tool_prelude { diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 9d9011454d..d6fc08716f 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1,21 +1,28 @@ use super::tool_prelude::*; -use crate::consts::{DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE}; +use crate::consts::{DEFAULT_STROKE_WIDTH, DRAG_THRESHOLD, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE, PATH_JOIN_THRESHOLD, SNAP_POINT_TOLERANCE}; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; -use crate::messages::portfolio::document::overlays::utility_functions::path_overlays; +use crate::messages::portfolio::document::overlays::utility_functions::{path_endpoint_overlays, path_overlays}; use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles, OverlayContext}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; -use crate::messages::tool::common_functionality::graph_modification_utils::{self, merge_layers}; +use crate::messages::tool::common_functionality::graph_modification_utils::{self, find_spline, merge_layers, merge_points}; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend}; use bezier_rs::{Bezier, BezierHandles}; -use graph_craft::document::NodeId; +use graph_craft::document::{NodeId, NodeInput}; use graphene_core::vector::{PointId, VectorModificationType}; use graphene_core::Color; use graphene_std::vector::{HandleId, ManipulatorPointId, SegmentId, VectorData}; +use std::fmt; + +// TODO: refactor the code into new module for drawing a Path. +mod spline_mode; + +use spline_mode::*; + #[derive(Default)] pub struct PenTool { fsm_state: PenToolFsmState, @@ -28,6 +35,7 @@ pub struct PenOptions { fill: ToolColorOptions, stroke: ToolColorOptions, pen_overlay_mode: PenOverlayMode, + tool_mode: ToolMode, } impl Default for PenOptions { @@ -37,6 +45,7 @@ impl Default for PenOptions { fill: ToolColorOptions::new_secondary(), stroke: ToolColorOptions::new_primary(), pen_overlay_mode: PenOverlayMode::FrontierHandles, + tool_mode: ToolMode::Path, } } } @@ -62,10 +71,14 @@ pub enum PenToolMessage { Redo, Undo, UpdateOptions(PenOptionsUpdate), + ToolModeChanged, RecalculateLatestPointsPosition, RemovePreviousHandle, GRS { grab: Key, rotate: Key, scale: Key }, FinalPosition { final_position: DVec2 }, + + // Specific to the Spline mode. + SplineMergeEndpoints, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -75,6 +88,8 @@ enum PenToolFsmState { DraggingHandle(HandleMode), PlacingAnchor, GRSHandle, + SplineDrawing, + SplineMergingEndpoints, } #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] @@ -92,6 +107,25 @@ pub enum PenOptionsUpdate { StrokeColorType(ToolColorType), WorkingColors(Option, Option), OverlayModeType(PenOverlayMode), + ToolMode(ToolMode), +} + +impl PenTool { + fn tool_mode_widget(&self) -> WidgetHolder { + let tool_mode_entries = [ToolMode::Path, ToolMode::Spline] + .iter() + .map(|mode| { + MenuListEntry::new(format!("{mode:?}")) + .label(mode.to_string()) + .on_commit(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::ToolMode(*mode)).into()) + }) + .collect(); + + DropdownInput::new(vec![tool_mode_entries]) + .selected_index(Some((self.options.tool_mode) as u32)) + .tooltip("Select Spline to draw smooth curves or select Path to draw a path.") + .widget_holder() + } } impl ToolMetadata for PenTool { @@ -118,13 +152,19 @@ fn create_weight_widget(line_weight: f64) -> WidgetHolder { impl LayoutHolder for PenTool { fn layout(&self) -> Layout { - let mut widgets = self.options.fill.create_widgets( + let mut widgets = Vec::new(); + + widgets.push(self.tool_mode_widget()); + + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + + widgets.append(&mut self.options.fill.create_widgets( "Fill", true, |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColorType(color_type.clone())).into()), |color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value.as_solid())).into(), - ); + )); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); @@ -142,20 +182,22 @@ impl LayoutHolder for PenTool { widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - widgets.push( - RadioInput::new(vec![ - RadioEntryData::new("all") - .icon("HandleVisibilityAll") - .tooltip("Show all handles regardless of selection") - .on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::AllHandles)).into()), - RadioEntryData::new("frontier") - .icon("HandleVisibilityFrontier") - .tooltip("Show only handles at the frontiers of the segments connected to selected points") - .on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::FrontierHandles)).into()), - ]) - .selected_index(Some(self.options.pen_overlay_mode as u32)) - .widget_holder(), - ); + if self.options.tool_mode == ToolMode::Path { + widgets.push( + RadioInput::new(vec![ + RadioEntryData::new("all") + .icon("HandleVisibilityAll") + .tooltip("Show all handles regardless of selection") + .on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::AllHandles)).into()), + RadioEntryData::new("frontier") + .icon("HandleVisibilityFrontier") + .tooltip("Show only handles at the frontiers of the segments connected to selected points") + .on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::FrontierHandles)).into()), + ]) + .selected_index(Some(self.options.pen_overlay_mode as u32)) + .widget_holder(), + ); + } Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) } @@ -189,6 +231,10 @@ impl<'a> MessageHandler> for PenTool self.options.fill.primary_working_color = primary; self.options.fill.secondary_working_color = secondary; } + PenOptionsUpdate::ToolMode(tool_mode) => { + self.options.tool_mode = tool_mode; + responses.add(PenToolMessage::ToolModeChanged); + } } self.send_layout(responses, LayoutTarget::ToolOptions); @@ -214,6 +260,15 @@ impl<'a> MessageHandler> for PenTool RemovePreviousHandle, GRS, ), + PenToolFsmState::SplineDrawing => actions!(PenToolMessageDiscriminant; + DragStop, + PointerMove, + Confirm, + Abort, + ), + PenToolFsmState::SplineMergingEndpoints => actions!(PenToolMessageDiscriminant; + SplineMergeEndpoints, + ), } } } @@ -263,8 +318,26 @@ enum HandleMode { ColinearEquidistant, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum ToolMode { + #[default] + Path, + Spline, +} + +impl fmt::Display for ToolMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ToolMode::Path => write!(f, "Path"), + ToolMode::Spline => write!(f, "Spline"), + } + } +} + #[derive(Clone, Debug, Default)] struct PenToolData { + spline_mode_tool_data: SplineModeToolData, + snap_manager: SnapManager, latest_points: Vec, point_index: usize, @@ -910,6 +983,132 @@ impl Fsm for PenToolFsmState { let ToolMessage::Pen(event) = event else { return self }; match (self, event) { + (state, PenToolMessage::ToolModeChanged) => { + if !matches!(state, PenToolFsmState::Ready) { + responses.add(PenToolMessage::Abort); + responses.add(PenToolMessage::ToolModeChanged); + return state; + } + state + } + (PenToolFsmState::SplineDrawing, PenToolMessage::DragStop) => { + let tool_data = &mut tool_data.spline_mode_tool_data; + // The first DragStop event will be ignored to prevent insertion of new point. + if tool_data.extend { + tool_data.extend = false; + return PenToolFsmState::SplineDrawing; + } + if tool_data.current_layer.is_none() { + return PenToolFsmState::Ready; + }; + tool_data.next_point = tool_data.snapped_point(document, input).snapped_point_document; + if tool_data.points.last().map_or(true, |last_pos| last_pos.1.distance(tool_data.next_point) > DRAG_THRESHOLD) { + let preview_point = tool_data.preview_point; + extend_spline(tool_data, false, responses); + tool_data.preview_point = preview_point; + + if try_merging_latest_endpoint(document, tool_data, preferences).is_some() { + responses.add(PenToolMessage::Confirm); + } + } + + PenToolFsmState::SplineDrawing + } + ( + PenToolFsmState::SplineDrawing, + PenToolMessage::PointerMove { + snap_angle, + break_handle, + lock_angle, + colinear, + }, + ) => { + let tool_data = &mut tool_data.spline_mode_tool_data; + let Some(layer) = tool_data.current_layer else { return PenToolFsmState::Ready }; + let ignore = |cp: PointId| tool_data.preview_point.is_some_and(|pp| pp == cp) || tool_data.points.last().is_some_and(|(ep, _)| *ep == cp); + let join_point = closest_point(document, input.mouse.position, PATH_JOIN_THRESHOLD, vec![layer].into_iter(), ignore, preferences); + + // Endpoints snapping + if let Some((_, _, point)) = join_point { + tool_data.next_point = point; + tool_data.snap_manager.clear_indicator(); + } else { + let snapped_point = tool_data.snapped_point(document, input); + tool_data.next_point = snapped_point.snapped_point_document; + tool_data.snap_manager.update_indicator(snapped_point); + } + + extend_spline(tool_data, true, responses); + + // Auto-panning + let messages = [ + PenToolMessage::PointerOutsideViewport { + snap_angle, + break_handle, + lock_angle, + colinear, + } + .into(), + PenToolMessage::PointerMove { + snap_angle, + break_handle, + lock_angle, + colinear, + } + .into(), + ]; + tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); + + PenToolFsmState::SplineDrawing + } + (PenToolFsmState::SplineDrawing, PenToolMessage::PointerOutsideViewport { .. }) => { + let tool_data = &mut tool_data.spline_mode_tool_data; + // Auto-panning + let _ = tool_data.auto_panning.shift_viewport(input, responses); + + PenToolFsmState::SplineDrawing + } + (PenToolFsmState::SplineDrawing, PenToolMessage::Confirm) => { + let tool_data = &mut tool_data.spline_mode_tool_data; + if tool_data.points.len() >= 2 { + delete_preview(tool_data, responses); + } + responses.add(PenToolMessage::SplineMergeEndpoints); + PenToolFsmState::SplineMergingEndpoints + } + (PenToolFsmState::SplineDrawing, PenToolMessage::Abort) => { + responses.add(DocumentMessage::AbortTransaction); + PenToolFsmState::Ready + } + (PenToolFsmState::SplineMergingEndpoints, PenToolMessage::SplineMergeEndpoints) => { + let tool_data = &mut tool_data.spline_mode_tool_data; + let Some(current_layer) = tool_data.current_layer else { return PenToolFsmState::Ready }; + + if let Some(&layer) = tool_data.merge_layers.iter().last() { + merge_layers(document, current_layer, layer, responses); + tool_data.merge_layers.remove(&layer); + + responses.add(PenToolMessage::SplineMergeEndpoints); + return PenToolFsmState::SplineMergingEndpoints; + } + + let Some((start_endpoint, _)) = tool_data.points.first() else { return PenToolFsmState::Ready }; + let Some((last_endpoint, _)) = tool_data.points.last() else { return PenToolFsmState::Ready }; + + if let Some((position, second_endpoint)) = tool_data.merge_endpoints.pop() { + let first_endpoint = match position { + EndpointPosition::Start => *start_endpoint, + EndpointPosition::End => *last_endpoint, + }; + merge_points(document, current_layer, first_endpoint, second_endpoint, responses); + + responses.add(PenToolMessage::SplineMergeEndpoints); + return PenToolFsmState::SplineMergingEndpoints; + } + + responses.add(DocumentMessage::EndTransaction); + PenToolFsmState::Ready + } (PenToolFsmState::PlacingAnchor | PenToolFsmState::GRSHandle, PenToolMessage::GRS { grab, rotate, scale }) => { let Some(layer) = layer else { return PenToolFsmState::PlacingAnchor }; @@ -1049,6 +1248,12 @@ impl Fsm for PenToolFsmState { self } (_, PenToolMessage::Overlays(mut overlay_context)) => { + if tool_options.tool_mode == ToolMode::Spline { + let spline_tool_data = &mut tool_data.spline_mode_tool_data; + path_endpoint_overlays(document, shape_editor, &mut overlay_context, preferences); + spline_tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); + return self; + } let valid = |point: DVec2, handle: DVec2| point.distance_squared(handle) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE; let transform = document.metadata().document_to_viewport * transform; @@ -1152,6 +1357,76 @@ impl Fsm for PenToolFsmState { self } (PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => { + if tool_options.tool_mode == ToolMode::Spline { + let tool_data = &mut tool_data.spline_mode_tool_data; + responses.add(DocumentMessage::StartTransaction); + + tool_data.snap_manager.cleanup(responses); + tool_data.cleanup(); + tool_data.weight = tool_options.line_weight; + + let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); + let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); + let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); + + let layers = LayerNodeIdentifier::ROOT_PARENT + .descendants(document.metadata()) + .filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])); + + // Extend an endpoint of the selected path + if let Some((layer, point, position)) = should_extend(document, viewport, SNAP_POINT_TOLERANCE, layers, preferences) { + if find_spline(document, layer).is_some() { + // If the point is the part of Spline then we extend it. + tool_data.current_layer = Some(layer); + tool_data.points.push((point, position)); + tool_data.next_point = position; + tool_data.extend = true; + + extend_spline(tool_data, true, responses); + + return PenToolFsmState::SplineDrawing; + } else { + tool_data.merge_layers.insert(layer); + tool_data.merge_endpoints.push((EndpointPosition::Start, point)); + } + } + + let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap(); + let mut selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&document.network_interface); + let selected_layer = selected_layers_except_artboards.next().filter(|_| selected_layers_except_artboards.next().is_none()); + + let append_to_selected_layer = input.keyboard.key(append_to_selected); + + // Create new path in the selected layer when shift is down + if let (Some(layer), true) = (selected_layer, append_to_selected_layer) { + tool_data.current_layer = Some(layer); + + let transform = document.metadata().transform_to_viewport(layer); + let position = transform.inverse().transform_point2(input.mouse.position); + tool_data.next_point = position; + + return PenToolFsmState::SplineDrawing; + } + + responses.add(DocumentMessage::DeselectAllLayers); + + let parent = document.new_layer_bounding_artboard(input); + + let path_node_type = resolve_document_node_type("Path").expect("Path node does not exist"); + let path_node = path_node_type.default_node_template(); + let spline_node_type = resolve_document_node_type("Spline").expect("Spline node does not exist"); + let spline_node = spline_node_type.node_template_input_override([Some(NodeInput::node(NodeId(1), 0))]); + let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)]; + + let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses); + tool_options.fill.apply_fill(layer, responses); + tool_options.stroke.apply_stroke(tool_data.weight, layer, responses); + tool_data.current_layer = Some(layer); + + responses.add(Message::StartBuffer); + + return PenToolFsmState::SplineDrawing; + } responses.add(DocumentMessage::StartTransaction); tool_data.handle_mode = HandleMode::Free; @@ -1476,6 +1751,12 @@ impl Fsm for PenToolFsmState { dragging_hint_data.0.push(HintGroup(hold_group)); dragging_hint_data } + PenToolFsmState::SplineDrawing => HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), + HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Extend Spline")]), + HintGroup(vec![HintInfo::keys([Key::Enter], "End Spline")]), + ]), + PenToolFsmState::SplineMergingEndpoints => HintData(vec![]), }; responses.add(FrontendMessage::UpdateInputHints { hint_data }); diff --git a/editor/src/messages/tool/tool_messages/pen_tool/spline_mode.rs b/editor/src/messages/tool/tool_messages/pen_tool/spline_mode.rs new file mode 100644 index 0000000000..6eed25351e --- /dev/null +++ b/editor/src/messages/tool/tool_messages/pen_tool/spline_mode.rs @@ -0,0 +1,125 @@ +use super::super::tool_prelude::*; +use crate::consts::PATH_JOIN_THRESHOLD; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::tool::common_functionality::auto_panning::AutoPanning; +use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapData, SnapManager, SnapTypeConfiguration, SnappedPoint}; +use crate::messages::tool::common_functionality::utility_functions::closest_point; + +use graphene_std::vector::{PointId, SegmentId, VectorModificationType}; + +#[derive(Clone, Debug)] +pub(super) enum EndpointPosition { + Start, + End, +} + +#[derive(Clone, Debug, Default)] +pub(super) struct SplineModeToolData { + /// List of points inserted. + pub points: Vec<(PointId, DVec2)>, + /// Point to be inserted. + pub next_point: DVec2, + /// Point that was inserted temporarily to show preview. + pub preview_point: Option, + /// Segment that was inserted temporarily to show preview. + pub preview_segment: Option, + pub extend: bool, + pub weight: f64, + /// The layer we are editing. + pub current_layer: Option, + /// The layers to merge to the current layer before we merge endpoints in merge_endpoint field. + pub merge_layers: HashSet, + /// The endpoint IDs to merge with the spline's start/end endpoint after spline drawing is finished. + pub merge_endpoints: Vec<(EndpointPosition, PointId)>, + pub snap_manager: SnapManager, + pub auto_panning: AutoPanning, +} + +impl SplineModeToolData { + pub fn cleanup(&mut self) { + self.current_layer = None; + self.merge_layers = HashSet::new(); + self.merge_endpoints = Vec::new(); + self.preview_point = None; + self.preview_segment = None; + self.extend = false; + self.points = Vec::new(); + } + + /// Get the snapped point while ignoring current layer + pub fn snapped_point(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) -> SnappedPoint { + let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); + let ignore = if let Some(layer) = self.current_layer { vec![layer] } else { vec![] }; + let snap_data = SnapData::ignore(document, input, &ignore); + self.snap_manager.free_snap(&snap_data, &point, SnapTypeConfiguration::default()) + } +} + +pub(super) fn try_merging_latest_endpoint(document: &DocumentMessageHandler, tool_data: &mut SplineModeToolData, preferences: &PreferencesMessageHandler) -> Option<()> { + if tool_data.points.len() < 2 { + return None; + }; + let (last_endpoint, last_endpoint_position) = tool_data.points.last()?; + let preview_point = tool_data.preview_point; + let current_layer = tool_data.current_layer?; + + let layers = LayerNodeIdentifier::ROOT_PARENT + .descendants(document.metadata()) + .filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])); + + let exclude = |p: PointId| preview_point.is_some_and(|pp| pp == p) || *last_endpoint == p; + let position = document.metadata().transform_to_viewport(current_layer).transform_point2(*last_endpoint_position); + + let (layer, endpoint, _) = closest_point(document, position, PATH_JOIN_THRESHOLD, layers, exclude, preferences)?; + tool_data.merge_layers.insert(layer); + tool_data.merge_endpoints.push((EndpointPosition::End, endpoint)); + + Some(()) +} + +pub(super) fn extend_spline(tool_data: &mut SplineModeToolData, show_preview: bool, responses: &mut VecDeque) { + delete_preview(tool_data, responses); + + let Some(layer) = tool_data.current_layer else { return }; + + let next_point_pos = tool_data.next_point; + let next_point_id = PointId::generate(); + let modification_type = VectorModificationType::InsertPoint { + id: next_point_id, + position: next_point_pos, + }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + + if let Some((last_point_id, _)) = tool_data.points.last() { + let points = [*last_point_id, next_point_id]; + let id = SegmentId::generate(); + let modification_type = VectorModificationType::InsertSegment { id, points, handles: [None, None] }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + + if show_preview { + tool_data.preview_segment = Some(id); + } + } + + if show_preview { + tool_data.preview_point = Some(next_point_id); + } else { + tool_data.points.push((next_point_id, next_point_pos)); + } +} + +pub(super) fn delete_preview(tool_data: &mut SplineModeToolData, responses: &mut VecDeque) { + let Some(layer) = tool_data.current_layer else { return }; + + if let Some(id) = tool_data.preview_point { + let modification_type = VectorModificationType::RemovePoint { id }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + if let Some(id) = tool_data.preview_segment { + let modification_type = VectorModificationType::RemoveSegment { id }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + + tool_data.preview_point = None; + tool_data.preview_segment = None; +} diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs deleted file mode 100644 index 5878344e6b..0000000000 --- a/editor/src/messages/tool/tool_messages/spline_tool.rs +++ /dev/null @@ -1,531 +0,0 @@ -use super::tool_prelude::*; -use crate::consts::{DEFAULT_STROKE_WIDTH, DRAG_THRESHOLD, PATH_JOIN_THRESHOLD, SNAP_POINT_TOLERANCE}; -use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; -use crate::messages::portfolio::document::overlays::utility_functions::path_endpoint_overlays; -use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; -use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; -use crate::messages::tool::common_functionality::auto_panning::AutoPanning; -use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; -use crate::messages::tool::common_functionality::graph_modification_utils::{self, find_spline, merge_layers, merge_points}; -use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapData, SnapManager, SnapTypeConfiguration, SnappedPoint}; -use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend}; - -use graph_craft::document::{NodeId, NodeInput}; -use graphene_core::Color; -use graphene_std::vector::{PointId, SegmentId, VectorModificationType}; - -#[derive(Default)] -pub struct SplineTool { - fsm_state: SplineToolFsmState, - tool_data: SplineToolData, - options: SplineOptions, -} - -pub struct SplineOptions { - line_weight: f64, - fill: ToolColorOptions, - stroke: ToolColorOptions, -} - -impl Default for SplineOptions { - fn default() -> Self { - Self { - line_weight: DEFAULT_STROKE_WIDTH, - fill: ToolColorOptions::new_none(), - stroke: ToolColorOptions::new_primary(), - } - } -} - -#[impl_message(Message, ToolMessage, Spline)] -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] -pub enum SplineToolMessage { - // Standard messages - Overlays(OverlayContext), - CanvasTransformed, - Abort, - WorkingColorChanged, - - // Tool-specific messages - Confirm, - DragStart { append_to_selected: Key }, - DragStop, - MergeEndpoints, - PointerMove, - PointerOutsideViewport, - Undo, - UpdateOptions(SplineOptionsUpdate), -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -enum SplineToolFsmState { - #[default] - Ready, - Drawing, - MergingEndpoints, -} - -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] -pub enum SplineOptionsUpdate { - FillColor(Option), - FillColorType(ToolColorType), - LineWeight(f64), - StrokeColor(Option), - StrokeColorType(ToolColorType), - WorkingColors(Option, Option), -} - -impl ToolMetadata for SplineTool { - fn icon_name(&self) -> String { - "VectorSplineTool".into() - } - fn tooltip(&self) -> String { - "Spline Tool".into() - } - fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType { - ToolType::Spline - } -} - -fn create_weight_widget(line_weight: f64) -> WidgetHolder { - NumberInput::new(Some(line_weight)) - .unit(" px") - .label("Weight") - .min(0.) - .max((1_u64 << f64::MANTISSA_DIGITS) as f64) - .on_update(|number_input: &NumberInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) - .widget_holder() -} - -impl LayoutHolder for SplineTool { - fn layout(&self) -> Layout { - let mut widgets = self.options.fill.create_widgets( - "Fill", - true, - |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(None)).into(), - |color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColorType(color_type.clone())).into()), - |color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(color.value.as_solid())).into(), - ); - - widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - - widgets.append(&mut self.options.stroke.create_widgets( - "Stroke", - true, - |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(None)).into(), - |color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColorType(color_type.clone())).into()), - |color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(color.value.as_solid())).into(), - )); - widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - widgets.push(create_weight_widget(self.options.line_weight)); - - Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) - } -} - -impl<'a> MessageHandler> for SplineTool { - fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { - let ToolMessage::Spline(SplineToolMessage::UpdateOptions(action)) = message else { - self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true); - return; - }; - match action { - SplineOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, - SplineOptionsUpdate::FillColor(color) => { - self.options.fill.custom_color = color; - self.options.fill.color_type = ToolColorType::Custom; - } - SplineOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type, - SplineOptionsUpdate::StrokeColor(color) => { - self.options.stroke.custom_color = color; - self.options.stroke.color_type = ToolColorType::Custom; - } - SplineOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type, - SplineOptionsUpdate::WorkingColors(primary, secondary) => { - self.options.stroke.primary_working_color = primary; - self.options.stroke.secondary_working_color = secondary; - self.options.fill.primary_working_color = primary; - self.options.fill.secondary_working_color = secondary; - } - } - - self.send_layout(responses, LayoutTarget::ToolOptions); - } - - fn actions(&self) -> ActionList { - match self.fsm_state { - SplineToolFsmState::Ready => actions!(SplineToolMessageDiscriminant; - Undo, - DragStart, - DragStop, - PointerMove, - Confirm, - Abort, - ), - SplineToolFsmState::Drawing => actions!(SplineToolMessageDiscriminant; - DragStop, - PointerMove, - Confirm, - Abort, - ), - SplineToolFsmState::MergingEndpoints => actions!(SplineToolMessageDiscriminant; - MergeEndpoints, - ), - } - } -} - -impl ToolTransition for SplineTool { - fn event_to_message_map(&self) -> EventToMessageMap { - EventToMessageMap { - overlay_provider: Some(|overlay_context: OverlayContext| SplineToolMessage::Overlays(overlay_context).into()), - canvas_transformed: Some(SplineToolMessage::CanvasTransformed.into()), - tool_abort: Some(SplineToolMessage::Abort.into()), - working_color_changed: Some(SplineToolMessage::WorkingColorChanged.into()), - ..Default::default() - } - } -} - -#[derive(Clone, Debug)] -enum EndpointPosition { - Start, - End, -} - -#[derive(Clone, Debug, Default)] -struct SplineToolData { - /// List of points inserted. - points: Vec<(PointId, DVec2)>, - /// Point to be inserted. - next_point: DVec2, - /// Point that was inserted temporarily to show preview. - preview_point: Option, - /// Segment that was inserted temporarily to show preview. - preview_segment: Option, - extend: bool, - weight: f64, - /// The layer we are editing. - current_layer: Option, - /// The layers to merge to the current layer before we merge endpoints in merge_endpoint field. - merge_layers: HashSet, - /// The endpoint IDs to merge with the spline's start/end endpoint after spline drawing is finished. - merge_endpoints: Vec<(EndpointPosition, PointId)>, - snap_manager: SnapManager, - auto_panning: AutoPanning, -} - -impl SplineToolData { - fn cleanup(&mut self) { - self.current_layer = None; - self.merge_layers = HashSet::new(); - self.merge_endpoints = Vec::new(); - self.preview_point = None; - self.preview_segment = None; - self.extend = false; - self.points = Vec::new(); - } - - /// Get the snapped point while ignoring current layer - fn snapped_point(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) -> SnappedPoint { - let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); - let ignore = if let Some(layer) = self.current_layer { vec![layer] } else { vec![] }; - let snap_data = SnapData::ignore(document, input, &ignore); - self.snap_manager.free_snap(&snap_data, &point, SnapTypeConfiguration::default()) - } -} - -impl Fsm for SplineToolFsmState { - type ToolData = SplineToolData; - type ToolOptions = SplineOptions; - - fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque) -> Self { - let ToolActionHandlerData { - document, - global_tool_data, - input, - shape_editor, - preferences, - .. - } = tool_action_data; - - let ToolMessage::Spline(event) = event else { return self }; - match (self, event) { - (_, SplineToolMessage::CanvasTransformed) => self, - (_, SplineToolMessage::Overlays(mut overlay_context)) => { - path_endpoint_overlays(document, shape_editor, &mut overlay_context, preferences); - tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); - self - } - (SplineToolFsmState::MergingEndpoints, SplineToolMessage::MergeEndpoints) => { - let Some(current_layer) = tool_data.current_layer else { return SplineToolFsmState::Ready }; - - if let Some(&layer) = tool_data.merge_layers.iter().last() { - merge_layers(document, current_layer, layer, responses); - tool_data.merge_layers.remove(&layer); - - responses.add(SplineToolMessage::MergeEndpoints); - return SplineToolFsmState::MergingEndpoints; - } - - let Some((start_endpoint, _)) = tool_data.points.first() else { return SplineToolFsmState::Ready }; - let Some((last_endpoint, _)) = tool_data.points.last() else { return SplineToolFsmState::Ready }; - - if let Some((position, second_endpoint)) = tool_data.merge_endpoints.pop() { - let first_endpoint = match position { - EndpointPosition::Start => *start_endpoint, - EndpointPosition::End => *last_endpoint, - }; - merge_points(document, current_layer, first_endpoint, second_endpoint, responses); - - responses.add(SplineToolMessage::MergeEndpoints); - return SplineToolFsmState::MergingEndpoints; - } - - responses.add(DocumentMessage::EndTransaction); - SplineToolFsmState::Ready - } - (SplineToolFsmState::Ready, SplineToolMessage::DragStart { append_to_selected }) => { - responses.add(DocumentMessage::StartTransaction); - - tool_data.snap_manager.cleanup(responses); - tool_data.cleanup(); - tool_data.weight = tool_options.line_weight; - - let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); - let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); - let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); - - let layers = LayerNodeIdentifier::ROOT_PARENT - .descendants(document.metadata()) - .filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])); - - // Extend an endpoint of the selected path - if let Some((layer, point, position)) = should_extend(document, viewport, SNAP_POINT_TOLERANCE, layers, preferences) { - if find_spline(document, layer).is_some() { - // If the point is the part of Spline then we extend it. - tool_data.current_layer = Some(layer); - tool_data.points.push((point, position)); - tool_data.next_point = position; - tool_data.extend = true; - - extend_spline(tool_data, true, responses); - - return SplineToolFsmState::Drawing; - } else { - tool_data.merge_layers.insert(layer); - tool_data.merge_endpoints.push((EndpointPosition::Start, point)); - } - } - - let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap(); - let mut selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&document.network_interface); - let selected_layer = selected_layers_except_artboards.next().filter(|_| selected_layers_except_artboards.next().is_none()); - - let append_to_selected_layer = input.keyboard.key(append_to_selected); - - // Create new path in the selected layer when shift is down - if let (Some(layer), true) = (selected_layer, append_to_selected_layer) { - tool_data.current_layer = Some(layer); - - let transform = document.metadata().transform_to_viewport(layer); - let position = transform.inverse().transform_point2(input.mouse.position); - tool_data.next_point = position; - - return SplineToolFsmState::Drawing; - } - - responses.add(DocumentMessage::DeselectAllLayers); - - let parent = document.new_layer_bounding_artboard(input); - - let path_node_type = resolve_document_node_type("Path").expect("Path node does not exist"); - let path_node = path_node_type.default_node_template(); - let spline_node_type = resolve_document_node_type("Spline").expect("Spline node does not exist"); - let spline_node = spline_node_type.node_template_input_override([Some(NodeInput::node(NodeId(1), 0))]); - let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)]; - - let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses); - tool_options.fill.apply_fill(layer, responses); - tool_options.stroke.apply_stroke(tool_data.weight, layer, responses); - tool_data.current_layer = Some(layer); - - responses.add(Message::StartBuffer); - - SplineToolFsmState::Drawing - } - (SplineToolFsmState::Drawing, SplineToolMessage::DragStop) => { - // The first DragStop event will be ignored to prevent insertion of new point. - if tool_data.extend { - tool_data.extend = false; - return SplineToolFsmState::Drawing; - } - if tool_data.current_layer.is_none() { - return SplineToolFsmState::Ready; - }; - tool_data.next_point = tool_data.snapped_point(document, input).snapped_point_document; - if tool_data.points.last().map_or(true, |last_pos| last_pos.1.distance(tool_data.next_point) > DRAG_THRESHOLD) { - let preview_point = tool_data.preview_point; - extend_spline(tool_data, false, responses); - tool_data.preview_point = preview_point; - - if try_merging_lastest_endpoint(document, tool_data, preferences).is_some() { - responses.add(SplineToolMessage::Confirm); - } - } - - SplineToolFsmState::Drawing - } - (SplineToolFsmState::Drawing, SplineToolMessage::PointerMove) => { - let Some(layer) = tool_data.current_layer else { return SplineToolFsmState::Ready }; - let ignore = |cp: PointId| tool_data.preview_point.is_some_and(|pp| pp == cp) || tool_data.points.last().is_some_and(|(ep, _)| *ep == cp); - let join_point = closest_point(document, input.mouse.position, PATH_JOIN_THRESHOLD, vec![layer].into_iter(), ignore, preferences); - - // Endpoints snapping - if let Some((_, _, point)) = join_point { - tool_data.next_point = point; - tool_data.snap_manager.clear_indicator(); - } else { - let snapped_point = tool_data.snapped_point(document, input); - tool_data.next_point = snapped_point.snapped_point_document; - tool_data.snap_manager.update_indicator(snapped_point); - } - - extend_spline(tool_data, true, responses); - - // Auto-panning - let messages = [SplineToolMessage::PointerOutsideViewport.into(), SplineToolMessage::PointerMove.into()]; - tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); - - SplineToolFsmState::Drawing - } - (_, SplineToolMessage::PointerMove) => { - tool_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position); - responses.add(OverlaysMessage::Draw); - self - } - (SplineToolFsmState::Drawing, SplineToolMessage::PointerOutsideViewport) => { - // Auto-panning - let _ = tool_data.auto_panning.shift_viewport(input, responses); - - SplineToolFsmState::Drawing - } - (state, SplineToolMessage::PointerOutsideViewport) => { - // Auto-panning - let messages = [SplineToolMessage::PointerOutsideViewport.into(), SplineToolMessage::PointerMove.into()]; - tool_data.auto_panning.stop(&messages, responses); - - state - } - (SplineToolFsmState::Drawing, SplineToolMessage::Confirm) => { - if tool_data.points.len() >= 2 { - delete_preview(tool_data, responses); - } - responses.add(SplineToolMessage::MergeEndpoints); - SplineToolFsmState::MergingEndpoints - } - (SplineToolFsmState::Drawing, SplineToolMessage::Abort) => { - responses.add(DocumentMessage::AbortTransaction); - SplineToolFsmState::Ready - } - (_, SplineToolMessage::WorkingColorChanged) => { - responses.add(SplineToolMessage::UpdateOptions(SplineOptionsUpdate::WorkingColors( - Some(global_tool_data.primary_color), - Some(global_tool_data.secondary_color), - ))); - self - } - _ => self, - } - } - - fn update_hints(&self, responses: &mut VecDeque) { - let hint_data = match self { - SplineToolFsmState::Ready => HintData(vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::Lmb, "Draw Spline"), - HintInfo::keys([Key::Shift], "Append to Selected Layer").prepend_plus(), - ])]), - SplineToolFsmState::Drawing => HintData(vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), - HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Extend Spline")]), - HintGroup(vec![HintInfo::keys([Key::Enter], "End Spline")]), - ]), - SplineToolFsmState::MergingEndpoints => HintData(vec![]), - }; - - responses.add(FrontendMessage::UpdateInputHints { hint_data }); - } - - fn update_cursor(&self, responses: &mut VecDeque) { - responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); - } -} - -fn try_merging_lastest_endpoint(document: &DocumentMessageHandler, tool_data: &mut SplineToolData, preferences: &PreferencesMessageHandler) -> Option<()> { - if tool_data.points.len() < 2 { - return None; - }; - let (last_endpoint, last_endpoint_position) = tool_data.points.last()?; - let preview_point = tool_data.preview_point; - let current_layer = tool_data.current_layer?; - - let layers = LayerNodeIdentifier::ROOT_PARENT - .descendants(document.metadata()) - .filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])); - - let exclude = |p: PointId| preview_point.is_some_and(|pp| pp == p) || *last_endpoint == p; - let position = document.metadata().transform_to_viewport(current_layer).transform_point2(*last_endpoint_position); - - let (layer, endpoint, _) = closest_point(document, position, PATH_JOIN_THRESHOLD, layers, exclude, preferences)?; - tool_data.merge_layers.insert(layer); - tool_data.merge_endpoints.push((EndpointPosition::End, endpoint)); - - Some(()) -} - -fn extend_spline(tool_data: &mut SplineToolData, show_preview: bool, responses: &mut VecDeque) { - delete_preview(tool_data, responses); - - let Some(layer) = tool_data.current_layer else { return }; - - let next_point_pos = tool_data.next_point; - let next_point_id = PointId::generate(); - let modification_type = VectorModificationType::InsertPoint { - id: next_point_id, - position: next_point_pos, - }; - responses.add(GraphOperationMessage::Vector { layer, modification_type }); - - if let Some((last_point_id, _)) = tool_data.points.last() { - let points = [*last_point_id, next_point_id]; - let id = SegmentId::generate(); - let modification_type = VectorModificationType::InsertSegment { id, points, handles: [None, None] }; - responses.add(GraphOperationMessage::Vector { layer, modification_type }); - - if show_preview { - tool_data.preview_segment = Some(id); - } - } - - if show_preview { - tool_data.preview_point = Some(next_point_id); - } else { - tool_data.points.push((next_point_id, next_point_pos)); - } -} - -fn delete_preview(tool_data: &mut SplineToolData, responses: &mut VecDeque) { - let Some(layer) = tool_data.current_layer else { return }; - - if let Some(id) = tool_data.preview_point { - let modification_type = VectorModificationType::RemovePoint { id }; - responses.add(GraphOperationMessage::Vector { layer, modification_type }); - } - if let Some(id) = tool_data.preview_segment { - let modification_type = VectorModificationType::RemoveSegment { id }; - responses.add(GraphOperationMessage::Vector { layer, modification_type }); - } - - tool_data.preview_point = None; - tool_data.preview_segment = None; -} diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index d16f2cdb33..012533708c 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -368,7 +368,6 @@ fn list_tools_in_groups() -> Vec> { ToolAvailability::Available(Box::::default()), ToolAvailability::Available(Box::::default()), ToolAvailability::Available(Box::::default()), - ToolAvailability::Available(Box::::default()), ToolAvailability::Available(Box::::default()), ToolAvailability::Available(Box::::default()), ToolAvailability::Available(Box::::default()), @@ -403,7 +402,6 @@ pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType { ToolMessage::Path(_) => ToolType::Path, ToolMessage::Pen(_) => ToolType::Pen, ToolMessage::Freehand(_) => ToolType::Freehand, - ToolMessage::Spline(_) => ToolType::Spline, ToolMessage::Line(_) => ToolType::Line, ToolMessage::Rectangle(_) => ToolType::Rectangle, ToolMessage::Ellipse(_) => ToolType::Ellipse,