diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 211104bc21..9cc018ba23 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -171,6 +171,14 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(MouseRight); action_dispatch=GradientToolMessage::Abort), entry!(KeyDown(Escape); action_dispatch=GradientToolMessage::Abort), // + // OperationToolMessage + entry!(PointerMove; action_dispatch=OperationToolMessage::PointerMove), + entry!(KeyDown(MouseLeft); action_dispatch=OperationToolMessage::DragStart), + entry!(KeyUp(MouseLeft); action_dispatch=OperationToolMessage::DragStop), + entry!(KeyDown(MouseRight); action_dispatch=OperationToolMessage::Confirm), + entry!(KeyDown(Escape); action_dispatch=OperationToolMessage::Abort), + entry!(KeyDown(Enter); action_dispatch=OperationToolMessage::Confirm), + // // ShapeToolMessage entry!(KeyDown(MouseLeft); action_dispatch=ShapeToolMessage::DragStart), entry!(KeyUp(MouseLeft); action_dispatch=ShapeToolMessage::DragStop), diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index 8ebde695b5..a304d0f4bc 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -22,6 +22,12 @@ pub enum GraphOperationMessage { layer: LayerNodeIdentifier, fill: Fill, }, + CircularRepeatSet { + layer: LayerNodeIdentifier, + angle: f64, + radius: f64, + count: u32, + }, BlendingFillSet { layer: LayerNodeIdentifier, fill: f64, diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 5bf6cce032..bb25241272 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -37,6 +37,11 @@ impl MessageHandler> for modify_inputs.fill_set(fill); } } + GraphOperationMessage::CircularRepeatSet { layer, angle, radius, count } => { + if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { + modify_inputs.circular_repeat_set(angle, radius, count); + } + } GraphOperationMessage::BlendingFillSet { layer, fill } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { modify_inputs.blending_fill_set(fill); diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 4079fefd32..49a0674723 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -394,6 +394,17 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.dash_offset), false), true); } + pub fn circular_repeat_set(&mut self, angle: f64, radius: f64, count: u32) { + let Some(circular_repeat_node_id) = self.existing_node_id("Circular Repeat", true) else { return }; + + let input_connector = InputConnector::node(circular_repeat_node_id, graphene_std::vector::circular_repeat::AngleOffsetInput::INDEX); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(angle), false), true); + let input_connector = InputConnector::node(circular_repeat_node_id, graphene_std::vector::circular_repeat::RadiusInput::INDEX); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(radius), false), true); + let input_connector = InputConnector::node(circular_repeat_node_id, graphene_std::vector::circular_repeat::CountInput::INDEX); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::U32(count), false), false); + } + /// Update the transform value of the upstream Transform node based a change to its existing value and the given parent transform. /// A new Transform node is created if one does not exist, unless it would be given the identity transform. pub fn transform_change_with_parent(&mut self, transform: DAffine2, transform_in: TransformIn, parent_transform: DAffine2, skip_rerender: bool) { diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index 7a696f9ede..16c9256e23 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -41,6 +41,7 @@ pub use crate::messages::tool::tool_messages::fill_tool::{FillToolMessage, FillT pub use crate::messages::tool::tool_messages::freehand_tool::{FreehandToolMessage, FreehandToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::gradient_tool::{GradientToolMessage, GradientToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::navigate_tool::{NavigateToolMessage, NavigateToolMessageDiscriminant}; +pub use crate::messages::tool::tool_messages::operation_tool::{OperationToolMessage, OperationToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::path_tool::{PathToolMessage, PathToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::pen_tool::{PenToolMessage, PenToolMessageDiscriminant}; pub use crate::messages::tool::tool_messages::select_tool::{SelectToolMessage, SelectToolMessageDiscriminant}; diff --git a/editor/src/messages/tool/common_functionality/mod.rs b/editor/src/messages/tool/common_functionality/mod.rs index ca3e629e19..5154848e9f 100644 --- a/editor/src/messages/tool/common_functionality/mod.rs +++ b/editor/src/messages/tool/common_functionality/mod.rs @@ -4,6 +4,7 @@ pub mod compass_rose; pub mod gizmos; pub mod graph_modification_utils; pub mod measure; +pub mod operations; pub mod pivot; pub mod resize; pub mod shape_editor; diff --git a/editor/src/messages/tool/common_functionality/operations/circular_repeat.rs b/editor/src/messages/tool/common_functionality/operations/circular_repeat.rs new file mode 100644 index 0000000000..2ba25d1af8 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/operations/circular_repeat.rs @@ -0,0 +1,143 @@ +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::shapes::shape_utility::extract_circular_repeat_parameters; +use crate::messages::tool::tool_messages::operation_tool::{OperationToolData, OperationToolFsmState}; +use crate::messages::tool::tool_messages::tool_prelude::*; +use std::collections::VecDeque; + +#[derive(Default)] +pub struct CircularRepeatOperation; + +#[derive(Clone, Debug, Default)] +pub struct CircularRepeatOperationData { + clicked_layer_radius: (LayerNodeIdentifier, f64), + layers_dragging: Vec<(LayerNodeIdentifier, f64)>, + initial_center: DVec2, +} + +impl CircularRepeatOperation { + pub fn create_node(tool_data: &mut OperationToolData, document: &DocumentMessageHandler, responses: &mut VecDeque, input: &InputPreprocessorMessageHandler) { + let selected_layers = document + .network_interface + .selected_nodes() + .selected_layers(document.metadata()) + .collect::>(); + + let Some(clicked_layer) = document.click(&input) else { return }; + responses.add(DocumentMessage::StartTransaction); + let viewport = document.metadata().transform_to_viewport(clicked_layer); + let center = viewport.transform_point2(DVec2::ZERO); + + // Only activate the operation if the click is close enough to the repeat center + if center.distance(input.mouse.position) > 5. { + return; + }; + + // If the clicked layer is part of the current selection, apply the operation to all selected layers + if selected_layers.contains(&clicked_layer) { + tool_data.circular_operation_data.layers_dragging = selected_layers + .iter() + .map(|layer| { + let (_angle_offset, radius, _count) = extract_circular_repeat_parameters(Some(*layer), document).unwrap_or((0.0, 0.0, 6)); + if *layer == clicked_layer { + tool_data.circular_operation_data.clicked_layer_radius = (*layer, radius) + } + (*layer, radius) + }) + .collect::>(); + } else { + // If the clicked layer is not in the selection, deselect all and only apply the operation to the clicked layer + responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![clicked_layer.to_node()] }); + + let (_angle_offset, radius, _count) = extract_circular_repeat_parameters(Some(clicked_layer), document).unwrap_or((0.0, 0.0, 6)); + + tool_data.circular_operation_data.clicked_layer_radius = (clicked_layer, radius); + tool_data.circular_operation_data.layers_dragging = vec![(clicked_layer, radius)]; + } + tool_data.drag_start = input.mouse.position; + tool_data.circular_operation_data.initial_center = viewport.transform_point2(DVec2::ZERO); + } + + pub fn update_shape(tool_data: &mut OperationToolData, document: &DocumentMessageHandler, responses: &mut VecDeque, input: &InputPreprocessorMessageHandler) { + let (_clicked_layer, clicked_radius) = tool_data.circular_operation_data.clicked_layer_radius; + + let viewport = document.metadata().transform_to_viewport(tool_data.circular_operation_data.clicked_layer_radius.0); + let sign = (input.mouse.position - tool_data.circular_operation_data.initial_center) + .dot(viewport.transform_vector2(DVec2::Y)) + .signum(); + + // Compute mouse movement in local space, ignoring the layer’s own transform + let delta = document + .metadata() + .downstream_transform_to_viewport(tool_data.circular_operation_data.clicked_layer_radius.0) + .inverse() + .transform_vector2(input.mouse.position - tool_data.circular_operation_data.initial_center) + .length() * sign; + + for (layer, initial_radius) in &tool_data.circular_operation_data.layers_dragging { + // If the layer’s sign differs from the clicked layer, invert delta to preserve consistent in/out dragging behavior + let new_radius = if initial_radius.signum() == clicked_radius.signum() { + *initial_radius + delta + } else { + *initial_radius + delta.signum() * -1. * delta.abs() + }; + + responses.add(GraphOperationMessage::CircularRepeatSet { + layer: *layer, + angle: 0., + radius: new_radius, + count: 6, + }); + } + + responses.add(NodeGraphMessage::RunDocumentGraph); + } + + pub fn overlays( + tool_state: &OperationToolFsmState, + tool_data: &mut OperationToolData, + document: &DocumentMessageHandler, + input: &InputPreprocessorMessageHandler, + overlay_context: &mut OverlayContext, + ) { + match tool_state { + OperationToolFsmState::Ready => { + // Draw overlays for all selected layers + for layer in document.network_interface.selected_nodes().selected_layers(document.metadata()) { + Self::draw_layer_overlay(layer, document, input, overlay_context) + } + + // Also highlight the hovered layer if it’s not selected + if let Some(layer) = document.click(&input) { + Self::draw_layer_overlay(layer, document, input, overlay_context); + } + } + _ => { + // While dragging, only draw overlays for the layers being modified + for layer in tool_data.circular_operation_data.layers_dragging.iter().map(|(l, _)| l) { + let Some(vector) = document.network_interface.compute_modified_vector(*layer) else { continue }; + let viewport = document.metadata().transform_to_viewport(*layer); + + overlay_context.outline_vector(&vector, viewport); + } + } + } + } + + fn draw_layer_overlay(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, overlay_context: &mut OverlayContext) { + if let Some(vector) = document.network_interface.compute_modified_vector(layer) { + let viewport = document.metadata().transform_to_viewport(layer); + let center = viewport.transform_point2(DVec2::ZERO); + // Show a small circle if the mouse is near the repeat center + if center.distance(input.mouse.position) < 5. { + overlay_context.circle(center, 3., None, None); + } + overlay_context.outline_vector(&vector, viewport); + } + } + + pub fn cleanup(tool_data: &mut OperationToolData) { + // Clear stored drag state at the end of the operation + tool_data.circular_operation_data.layers_dragging.clear(); + } +} diff --git a/editor/src/messages/tool/common_functionality/operations/mod.rs b/editor/src/messages/tool/common_functionality/operations/mod.rs new file mode 100644 index 0000000000..392f6fa3ae --- /dev/null +++ b/editor/src/messages/tool/common_functionality/operations/mod.rs @@ -0,0 +1 @@ +pub mod circular_repeat; diff --git a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs index 17c7f8e574..5dfad7d169 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -230,6 +230,18 @@ pub fn extract_star_parameters(layer: Option, document: &Do Some((sides, radius_1, radius_2)) } +pub fn extract_circular_repeat_parameters(layer: Option, document: &DocumentMessageHandler) -> Option<(f64, f64, u32)> { + let node_inputs = NodeGraphLayer::new(layer?, &document.network_interface).find_node_inputs("Circular Repeat")?; + + let (Some(&TaggedValue::F64(angle)), Some(&TaggedValue::F64(radius)), Some(&TaggedValue::U32(count))) = + (node_inputs.get(1)?.as_value(), node_inputs.get(2)?.as_value(), node_inputs.get(3)?.as_value()) + else { + return None; + }; + + Some((angle, radius, count)) +} + /// Extract the node input values of Polygon. /// Returns an option of (sides, radius). pub fn extract_polygon_parameters(layer: Option, document: &DocumentMessageHandler) -> Option<(u32, f64)> { diff --git a/editor/src/messages/tool/tool_message.rs b/editor/src/messages/tool/tool_message.rs index 70668a26df..62c2559fcb 100644 --- a/editor/src/messages/tool/tool_message.rs +++ b/editor/src/messages/tool/tool_message.rs @@ -22,6 +22,8 @@ pub enum ToolMessage { Fill(FillToolMessage), #[child] Gradient(GradientToolMessage), + #[child] + Operation(OperationToolMessage), #[child] Path(PathToolMessage), @@ -58,6 +60,7 @@ pub enum ToolMessage { ActivateToolEyedropper, ActivateToolFill, ActivateToolGradient, + ActivateToolOperation, // Vector tools ActivateToolPath, ActivateToolPen, diff --git a/editor/src/messages/tool/tool_messages/mod.rs b/editor/src/messages/tool/tool_messages/mod.rs index 6d29ad81a9..9ddc4a26ff 100644 --- a/editor/src/messages/tool/tool_messages/mod.rs +++ b/editor/src/messages/tool/tool_messages/mod.rs @@ -5,6 +5,7 @@ pub mod fill_tool; pub mod freehand_tool; pub mod gradient_tool; pub mod navigate_tool; +pub mod operation_tool; pub mod path_tool; pub mod pen_tool; pub mod select_tool; diff --git a/editor/src/messages/tool/tool_messages/operation_tool.rs b/editor/src/messages/tool/tool_messages/operation_tool.rs new file mode 100644 index 0000000000..9db24457d0 --- /dev/null +++ b/editor/src/messages/tool/tool_messages/operation_tool.rs @@ -0,0 +1,250 @@ +use super::tool_prelude::*; +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; +use crate::messages::tool::common_functionality::operations::circular_repeat::{CircularRepeatOperation, CircularRepeatOperationData}; + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum OperationType { + #[default] + CircularRepeat = 0, + Repeat, +} + +#[derive(Default, ExtractField)] +pub struct OperationTool { + fsm_state: OperationToolFsmState, + tool_data: OperationToolData, + options: OperationOptions, +} + +pub struct OperationOptions { + operation_type: OperationType, +} + +impl Default for OperationOptions { + fn default() -> Self { + Self { + operation_type: OperationType::CircularRepeat, + } + } +} + +#[impl_message(Message, ToolMessage, Operation)] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum OperationToolMessage { + // Standard messages + Overlays { context: OverlayContext }, + Abort, + WorkingColorChanged, + + // Tool-specific messages + Confirm, + DragStart, + DragStop, + PointerMove, + PointerOutsideViewport, + Undo, + UpdateOptions { options: OperationOptionsUpdate }, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum OperationToolFsmState { + #[default] + Ready, + Drawing, +} + +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum OperationOptionsUpdate { + OperationType(OperationType), +} + +impl ToolMetadata for OperationTool { + fn icon_name(&self) -> String { + "GeneralOperationTool".into() + } + fn tooltip(&self) -> String { + "Operation Tool".into() + } + fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType { + ToolType::Operation + } +} + +fn create_operation_type_option_widget(operation_type: OperationType) -> WidgetHolder { + let entries = vec![vec![ + MenuListEntry::new("Circular Repeat").label("Circular Repeat").on_commit(move |_| { + OperationToolMessage::UpdateOptions { + options: OperationOptionsUpdate::OperationType(OperationType::CircularRepeat), + } + .into() + }), + MenuListEntry::new("Repeat").label("Repeat").on_commit(move |_| { + OperationToolMessage::UpdateOptions { + options: OperationOptionsUpdate::OperationType(OperationType::Repeat), + } + .into() + }), + ]]; + DropdownInput::new(entries).selected_index(Some(operation_type as u32)).widget_holder() +} + +impl LayoutHolder for OperationTool { + fn layout(&self) -> Layout { + let mut widgets = vec![]; + + widgets.push(create_operation_type_option_widget(self.options.operation_type)); + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) + } +} + +#[message_handler_data] +impl<'a> MessageHandler> for OperationTool { + fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, context: &mut ToolActionMessageContext<'a>) { + let ToolMessage::Operation(OperationToolMessage::UpdateOptions { options }) = message else { + self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true); + return; + }; + match options { + OperationOptionsUpdate::OperationType(operation_type) => self.options.operation_type = operation_type, + } + + self.send_layout(responses, LayoutTarget::ToolOptions); + } + + fn actions(&self) -> ActionList { + match self.fsm_state { + OperationToolFsmState::Ready => actions!(OperationToolMessageDiscriminant; + Undo, + DragStart, + DragStop, + PointerMove, + Confirm, + Abort, + ), + OperationToolFsmState::Drawing => actions!(OperationToolMessageDiscriminant; + DragStop, + PointerMove, + Confirm, + Abort, + ), + } + } +} + +impl ToolTransition for OperationTool { + fn event_to_message_map(&self) -> EventToMessageMap { + EventToMessageMap { + overlay_provider: Some(|context: OverlayContext| OperationToolMessage::Overlays { context }.into()), + tool_abort: Some(OperationToolMessage::Abort.into()), + working_color_changed: Some(OperationToolMessage::WorkingColorChanged.into()), + ..Default::default() + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct OperationToolData { + pub drag_start: DVec2, + pub circular_operation_data: CircularRepeatOperationData, +} + +impl OperationToolData { + fn cleanup(&mut self) { + CircularRepeatOperation::cleanup(self); + } +} + +impl Fsm for OperationToolFsmState { + type ToolData = OperationToolData; + type ToolOptions = OperationOptions; + + fn transition( + self, + event: ToolMessage, + tool_data: &mut Self::ToolData, + tool_action_data: &mut ToolActionMessageContext, + tool_options: &Self::ToolOptions, + responses: &mut VecDeque, + ) -> Self { + let ToolActionMessageContext { document, input, .. } = tool_action_data; + + let ToolMessage::Operation(event) = event else { return self }; + match (self, event) { + (_, OperationToolMessage::Overlays { context: mut overlay_context }) => { + match tool_options.operation_type { + OperationType::CircularRepeat => CircularRepeatOperation::overlays(&self, tool_data, document, input, &mut overlay_context), + _ => {} + } + + self + } + (OperationToolFsmState::Ready, OperationToolMessage::DragStart) => { + match tool_options.operation_type { + OperationType::CircularRepeat => { + CircularRepeatOperation::create_node(tool_data, document, responses, input); + } + OperationType::Repeat => {} + } + + OperationToolFsmState::Drawing + } + (OperationToolFsmState::Drawing, OperationToolMessage::DragStop) => { + if tool_data.drag_start.distance(input.mouse.position) < 5. { + responses.add(DocumentMessage::AbortTransaction); + }; + tool_data.cleanup(); + responses.add(DocumentMessage::EndTransaction); + OperationToolFsmState::Ready + } + (OperationToolFsmState::Drawing, OperationToolMessage::PointerMove) => { + // Don't add the repeat node unless dragging more that 5 px + if tool_data.drag_start.distance(input.mouse.position) < 5. { + return self; + }; + + match tool_options.operation_type { + OperationType::CircularRepeat => { + CircularRepeatOperation::update_shape(tool_data, document, responses, input); + } + OperationType::Repeat => {} + } + + OperationToolFsmState::Drawing + } + (_, OperationToolMessage::PointerMove) => { + responses.add(OverlaysMessage::Draw); + self + } + + (OperationToolFsmState::Drawing, OperationToolMessage::PointerOutsideViewport) => OperationToolFsmState::Drawing, + (state, OperationToolMessage::PointerOutsideViewport) => state, + (OperationToolFsmState::Drawing, OperationToolMessage::Abort) => { + responses.add(DocumentMessage::AbortTransaction); + OperationToolFsmState::Ready + } + (_, OperationToolMessage::WorkingColorChanged) => self, + _ => self, + } + } + + fn update_hints(&self, responses: &mut VecDeque) { + let hint_data = match self { + OperationToolFsmState::Ready => HintData(vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::Lmb, "Draw Spline"), + HintInfo::keys([Key::Shift], "Append to Selected Layer").prepend_plus(), + ])]), + OperationToolFsmState::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")]), + ]), + }; + + responses.add(FrontendMessage::UpdateInputHints { hint_data }); + } + + fn update_cursor(&self, responses: &mut VecDeque) { + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); + } +} diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index b26e14d620..07893a348a 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -345,6 +345,7 @@ pub enum ToolType { Eyedropper, Fill, Gradient, + Operation, // Vector tool group Path, @@ -397,6 +398,7 @@ fn list_tools_in_groups() -> Vec> { ToolAvailability::Available(Box::::default()), ToolAvailability::Available(Box::::default()), ToolAvailability::Available(Box::::default()), + ToolAvailability::Available(Box::::default()), ], vec![ // Vector tool group @@ -431,6 +433,7 @@ pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType { ToolMessage::Eyedropper(_) => ToolType::Eyedropper, ToolMessage::Fill(_) => ToolType::Fill, ToolMessage::Gradient(_) => ToolType::Gradient, + ToolMessage::Operation(_) => ToolType::Operation, // Vector tool group ToolMessage::Path(_) => ToolType::Path, @@ -460,6 +463,7 @@ pub fn tool_type_to_activate_tool_message(tool_type: ToolType) -> ToolMessageDis ToolType::Eyedropper => ToolMessageDiscriminant::ActivateToolEyedropper, ToolType::Fill => ToolMessageDiscriminant::ActivateToolFill, ToolType::Gradient => ToolMessageDiscriminant::ActivateToolGradient, + ToolType::Operation => ToolMessageDiscriminant::ActivateToolOperation, // Vector tool group ToolType::Path => ToolMessageDiscriminant::ActivateToolPath,