From 57ecdac1ec0659433a038ee8dfbf14c707d44f6c Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Sat, 23 Aug 2025 02:12:32 +0530 Subject: [PATCH 1/5] added operation tool --- .../messages/input_mapper/input_mappings.rs | 8 + .../graph_operation_message.rs | 3 + .../graph_operation_message_handler.rs | 5 + editor/src/messages/prelude.rs | 1 + editor/src/messages/tool/tool_message.rs | 3 + editor/src/messages/tool/tool_messages/mod.rs | 1 + .../tool/tool_messages/operation_tool.rs | 295 ++++++++++++++++++ editor/src/messages/tool/utility_types.rs | 4 + 8 files changed, 320 insertions(+) create mode 100644 editor/src/messages/tool/tool_messages/operation_tool.rs diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 211104bc21..87c146d4f8 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::Confirm), + 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..6251e74124 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,9 @@ pub enum GraphOperationMessage { layer: LayerNodeIdentifier, fill: Fill, }, + RepeatSet { + layer: LayerNodeIdentifier, + }, 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..fd7bcddb23 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::RepeatSet { layer } => { + if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { + modify_inputs.create_node("Repeat"); + } + } 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/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/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..e4c06616cb --- /dev/null +++ b/editor/src/messages/tool/tool_messages/operation_tool.rs @@ -0,0 +1,295 @@ +use super::tool_prelude::*; +use crate::consts::{DEFAULT_STROKE_WIDTH, DRAG_THRESHOLD, PATH_JOIN_THRESHOLD, SNAP_POINT_TOLERANCE}; +use crate::messages::input_mapper::utility_types::input_mouse::MouseKeys; +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_std::Color; +use graphene_std::vector::{PointId, SegmentId, VectorModificationType}; + +#[derive(Default, ExtractField)] +pub struct OperationTool { + fsm_state: OperationToolFsmState, + tool_data: OperationToolData, + options: OperationOptions, +} + +pub struct OperationOptions { + line_weight: f64, + fill: ToolColorOptions, + stroke: ToolColorOptions, +} + +impl Default for OperationOptions { + fn default() -> Self { + Self { + line_weight: DEFAULT_STROKE_WIDTH, + fill: ToolColorOptions::new_none(), + stroke: ToolColorOptions::new_primary(), + } + } +} + +#[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, + MergeEndpoints, + PointerMove, + PointerOutsideViewport, + Undo, + UpdateOptions { options: OperationOptionsUpdate }, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +enum OperationToolFsmState { + #[default] + Ready, + Drawing, +} + +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum OperationOptionsUpdate { + FillColor(Option), + FillColorType(ToolColorType), + LineWeight(f64), + StrokeColor(Option), + StrokeColorType(ToolColorType), + WorkingColors(Option, Option), +} + +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_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| { + OperationToolMessage::UpdateOptions { + options: OperationOptionsUpdate::LineWeight(number_input.value.unwrap()), + } + .into() + }) + .widget_holder() +} + +impl LayoutHolder for OperationTool { + fn layout(&self) -> Layout { + let mut widgets = self.options.fill.create_widgets( + "Fill", + true, + |_| { + OperationToolMessage::UpdateOptions { + options: OperationOptionsUpdate::FillColor(None), + } + .into() + }, + |color_type: ToolColorType| { + WidgetCallback::new(move |_| { + OperationToolMessage::UpdateOptions { + options: OperationOptionsUpdate::FillColorType(color_type.clone()), + } + .into() + }) + }, + |color: &ColorInput| { + OperationToolMessage::UpdateOptions { + options: OperationOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb())), + } + .into() + }, + ); + + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + + widgets.append(&mut self.options.stroke.create_widgets( + "Stroke", + true, + |_| { + OperationToolMessage::UpdateOptions { + options: OperationOptionsUpdate::StrokeColor(None), + } + .into() + }, + |color_type: ToolColorType| { + WidgetCallback::new(move |_| { + OperationToolMessage::UpdateOptions { + options: OperationOptionsUpdate::StrokeColorType(color_type.clone()), + } + .into() + }) + }, + |color: &ColorInput| { + OperationToolMessage::UpdateOptions { + options: OperationOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb())), + } + .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 }])) + } +} + +#[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::LineWeight(line_weight) => self.options.line_weight = line_weight, + OperationOptionsUpdate::FillColor(color) => { + self.options.fill.custom_color = color; + self.options.fill.color_type = ToolColorType::Custom; + } + OperationOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type, + OperationOptionsUpdate::StrokeColor(color) => { + self.options.stroke.custom_color = color; + self.options.stroke.color_type = ToolColorType::Custom; + } + OperationOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type, + OperationOptionsUpdate::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 { + 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)] +struct OperationToolData {} + +impl OperationToolData { + fn cleanup(&mut 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, + global_tool_data, + input, + shape_editor, + preferences, + .. + } = tool_action_data; + + let ToolMessage::Operation(event) = event else { return self }; + match (self, event) { + (_, OperationToolMessage::Overlays { context: mut overlay_context }) => self, + (OperationToolFsmState::Ready, OperationToolMessage::DragStart) => { + let Some(layer) = document.click(&input) else { return self }; + responses.add(GraphOperationMessage::RepeatSet { layer }); + responses.add(NodeGraphMessage::RunDocumentGraph); + OperationToolFsmState::Drawing + } + (OperationToolFsmState::Drawing, OperationToolMessage::DragStop) => OperationToolFsmState::Drawing, + (OperationToolFsmState::Drawing, OperationToolMessage::PointerMove) => OperationToolFsmState::Drawing, + (_, OperationToolMessage::PointerMove) => { + log::info!("hello"); + self + } + + (OperationToolFsmState::Drawing, OperationToolMessage::PointerOutsideViewport) => OperationToolFsmState::Drawing, + (state, OperationToolMessage::PointerOutsideViewport) => state, + (OperationToolFsmState::Drawing, OperationToolMessage::Abort) => 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, From dabdef7bed21c3b9d74ba8221ffe8092541bd1e3 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Sun, 24 Aug 2025 22:02:20 +0530 Subject: [PATCH 2/5] multi-layer drag and repeat --- .../graph_operation_message.rs | 5 +- .../graph_operation_message_handler.rs | 4 +- .../document/graph_operation/utility_types.rs | 11 +++ .../shapes/shape_utility.rs | 12 +++ .../tool/tool_messages/operation_tool.rs | 78 ++++++++++++++++--- 5 files changed, 96 insertions(+), 14 deletions(-) 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 6251e74124..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,8 +22,11 @@ pub enum GraphOperationMessage { layer: LayerNodeIdentifier, fill: Fill, }, - RepeatSet { + CircularRepeatSet { layer: LayerNodeIdentifier, + angle: f64, + radius: f64, + count: u32, }, BlendingFillSet { layer: LayerNodeIdentifier, 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 fd7bcddb23..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,9 +37,9 @@ impl MessageHandler> for modify_inputs.fill_set(fill); } } - GraphOperationMessage::RepeatSet { layer } => { + GraphOperationMessage::CircularRepeatSet { layer, angle, radius, count } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { - modify_inputs.create_node("Repeat"); + modify_inputs.circular_repeat_set(angle, radius, count); } } GraphOperationMessage::BlendingFillSet { layer, 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/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_messages/operation_tool.rs b/editor/src/messages/tool/tool_messages/operation_tool.rs index e4c06616cb..5228edb6aa 100644 --- a/editor/src/messages/tool/tool_messages/operation_tool.rs +++ b/editor/src/messages/tool/tool_messages/operation_tool.rs @@ -8,6 +8,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye 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::shapes::shape_utility::{extract_circular_repeat_parameters, extract_star_parameters}; 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}; @@ -49,7 +50,6 @@ pub enum OperationToolMessage { Confirm, DragStart, DragStop, - MergeEndpoints, PointerMove, PointerOutsideViewport, Undo, @@ -222,10 +222,19 @@ impl ToolTransition for OperationTool { } #[derive(Clone, Debug, Default)] -struct OperationToolData {} +struct OperationToolData { + drag_start: DVec2, + clicked_layer: LayerNodeIdentifier, + layers_dragging: Vec<(LayerNodeIdentifier, f64)>, + initial_center: DVec2, +} impl OperationToolData { - fn cleanup(&mut self) {} + fn cleanup(&mut self) { + self.layers_dragging.clear(); + } + + fn modify_circular_repeat(&mut self) {} } impl Fsm for OperationToolFsmState { @@ -251,19 +260,66 @@ impl Fsm for OperationToolFsmState { let ToolMessage::Operation(event) = event else { return self }; match (self, event) { - (_, OperationToolMessage::Overlays { context: mut overlay_context }) => self, + (_, OperationToolMessage::Overlays { context: mut overlay_context }) => {} (OperationToolFsmState::Ready, OperationToolMessage::DragStart) => { + let selected_layers = document + .network_interface + .selected_nodes() + .selected_layers(document.metadata()) + .collect::>(); let Some(layer) = document.click(&input) else { return self }; - responses.add(GraphOperationMessage::RepeatSet { layer }); - responses.add(NodeGraphMessage::RunDocumentGraph); + let viewport = document.metadata().transform_to_viewport(layer); + + if selected_layers.contains(&layer) { + // store all + tool_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)); + (*layer, radius) + }) + .collect::>(); + } else { + // deselect all the layer and store the clicked layer for repeat and dragging + + responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }); + let (_angle_offset, radius, _count) = extract_circular_repeat_parameters(Some(layer), document).unwrap_or((0.0, 0.0, 6)); + tool_data.layers_dragging = vec![(layer, radius)]; + } + tool_data.drag_start = input.mouse.position; + tool_data.clicked_layer = layer; + tool_data.initial_center = viewport.transform_point2(DVec2::ZERO); + OperationToolFsmState::Drawing } - (OperationToolFsmState::Drawing, OperationToolMessage::DragStop) => OperationToolFsmState::Drawing, - (OperationToolFsmState::Drawing, OperationToolMessage::PointerMove) => OperationToolFsmState::Drawing, - (_, OperationToolMessage::PointerMove) => { - log::info!("hello"); - self + (OperationToolFsmState::Drawing, OperationToolMessage::DragStop) => { + tool_data.cleanup(); + 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; + }; + + let viewport = document.metadata().transform_to_viewport(tool_data.clicked_layer); + let center = viewport.transform_point2(DVec2::ZERO); + let sign = (input.mouse.position - tool_data.initial_center).dot(tool_data.drag_start - tool_data.initial_center).signum(); + let delta = viewport.inverse().transform_vector2(input.mouse.position - tool_data.drag_start).length() * sign; + log::info!("{:?}", delta); + for (layer, initial_radius) in &tool_data.layers_dragging { + responses.add(GraphOperationMessage::CircularRepeatSet { + layer: *layer, + angle: 0., + radius: initial_radius + delta, + count: 6, + }); + } + responses.add(NodeGraphMessage::RunDocumentGraph); + + OperationToolFsmState::Drawing } + (_, OperationToolMessage::PointerMove) => self, (OperationToolFsmState::Drawing, OperationToolMessage::PointerOutsideViewport) => OperationToolFsmState::Drawing, (state, OperationToolMessage::PointerOutsideViewport) => state, From 40db3238f3c1ed427de2e0c0465d6496f8924e74 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Tue, 26 Aug 2025 02:39:33 +0530 Subject: [PATCH 3/5] multi layer click and repeat --- .../tool/tool_messages/operation_tool.rs | 125 ++++++++++++------ 1 file changed, 86 insertions(+), 39 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/operation_tool.rs b/editor/src/messages/tool/tool_messages/operation_tool.rs index 5228edb6aa..63fb65d9ad 100644 --- a/editor/src/messages/tool/tool_messages/operation_tool.rs +++ b/editor/src/messages/tool/tool_messages/operation_tool.rs @@ -1,19 +1,10 @@ use super::tool_prelude::*; -use crate::consts::{DEFAULT_STROKE_WIDTH, DRAG_THRESHOLD, PATH_JOIN_THRESHOLD, SNAP_POINT_TOLERANCE}; -use crate::messages::input_mapper::utility_types::input_mouse::MouseKeys; -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::consts::DEFAULT_STROKE_WIDTH; 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::shapes::shape_utility::{extract_circular_repeat_parameters, extract_star_parameters}; -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 crate::messages::tool::common_functionality::shapes::shape_utility::extract_circular_repeat_parameters; use graphene_std::Color; -use graphene_std::vector::{PointId, SegmentId, VectorModificationType}; #[derive(Default, ExtractField)] pub struct OperationTool { @@ -224,7 +215,7 @@ impl ToolTransition for OperationTool { #[derive(Clone, Debug, Default)] struct OperationToolData { drag_start: DVec2, - clicked_layer: LayerNodeIdentifier, + clicked_layer_radius: (LayerNodeIdentifier, f64), layers_dragging: Vec<(LayerNodeIdentifier, f64)>, initial_center: DVec2, } @@ -233,8 +224,6 @@ impl OperationToolData { fn cleanup(&mut self) { self.layers_dragging.clear(); } - - fn modify_circular_repeat(&mut self) {} } impl Fsm for OperationToolFsmState { @@ -246,54 +235,95 @@ impl Fsm for OperationToolFsmState { event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionMessageContext, - tool_options: &Self::ToolOptions, + _tool_options: &Self::ToolOptions, responses: &mut VecDeque, ) -> Self { - let ToolActionMessageContext { - document, - global_tool_data, - input, - shape_editor, - preferences, - .. - } = tool_action_data; + let ToolActionMessageContext { document, input, .. } = tool_action_data; let ToolMessage::Operation(event) = event else { return self }; match (self, event) { - (_, OperationToolMessage::Overlays { context: mut overlay_context }) => {} + (_, OperationToolMessage::Overlays { context: mut overlay_context }) => { + match self { + OperationToolFsmState::Ready => { + for layer in document.network_interface.selected_nodes().selected_layers(document.metadata()) { + let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue }; + let viewport = document.metadata().transform_to_viewport(layer); + let center = viewport.transform_point2(DVec2::ZERO); + if center.distance(input.mouse.position) < 5. { + overlay_context.circle(center, 3., None, None); + } + + overlay_context.outline_vector(&vector, viewport); + } + if let Some(layer) = document.click(&input) { + let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return self }; + let viewport = document.metadata().transform_to_viewport(layer); + let center = viewport.transform_point2(DVec2::ZERO); + if center.distance(input.mouse.position) < 5. { + overlay_context.circle(center, 3., None, None); + } + + overlay_context.outline_vector(&vector, viewport); + } + } + _ => { + for layer in tool_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); + } + } + } + + self + } (OperationToolFsmState::Ready, OperationToolMessage::DragStart) => { let selected_layers = document .network_interface .selected_nodes() .selected_layers(document.metadata()) .collect::>(); - let Some(layer) = document.click(&input) else { return self }; - let viewport = document.metadata().transform_to_viewport(layer); + let Some(clicked_layer) = document.click(&input) else { return self }; + responses.add(DocumentMessage::StartTransaction); + let viewport = document.metadata().transform_to_viewport(clicked_layer); + let center = viewport.transform_point2(DVec2::ZERO); - if selected_layers.contains(&layer) { + if center.distance(input.mouse.position) > 5. { + return self; + }; + + if selected_layers.contains(&clicked_layer) { // store all tool_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.clicked_layer_radius = (*layer, radius) + } (*layer, radius) }) .collect::>(); } else { // deselect all the layer and store the clicked layer for repeat and dragging - responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }); - let (_angle_offset, radius, _count) = extract_circular_repeat_parameters(Some(layer), document).unwrap_or((0.0, 0.0, 6)); - tool_data.layers_dragging = vec![(layer, radius)]; + 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.clicked_layer_radius = (clicked_layer, radius); + tool_data.layers_dragging = vec![(clicked_layer, radius)]; } tool_data.drag_start = input.mouse.position; - tool_data.clicked_layer = layer; tool_data.initial_center = viewport.transform_point2(DVec2::ZERO); 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) => { @@ -302,16 +332,27 @@ impl Fsm for OperationToolFsmState { return self; }; - let viewport = document.metadata().transform_to_viewport(tool_data.clicked_layer); - let center = viewport.transform_point2(DVec2::ZERO); - let sign = (input.mouse.position - tool_data.initial_center).dot(tool_data.drag_start - tool_data.initial_center).signum(); - let delta = viewport.inverse().transform_vector2(input.mouse.position - tool_data.drag_start).length() * sign; - log::info!("{:?}", delta); + let (_clicked_layer, clicked_radius) = tool_data.clicked_layer_radius; + let viewport = document.metadata().transform_to_viewport(tool_data.clicked_layer_radius.0); + let sign = (input.mouse.position - tool_data.initial_center).dot(viewport.transform_vector2(DVec2::Y)).signum(); + let delta = document + .metadata() + .downstream_transform_to_viewport(tool_data.clicked_layer_radius.0) + .inverse() + .transform_vector2(input.mouse.position - tool_data.initial_center) + .length() * sign; + for (layer, initial_radius) in &tool_data.layers_dragging { + 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: initial_radius + delta, + radius: new_radius, count: 6, }); } @@ -319,11 +360,17 @@ impl Fsm for OperationToolFsmState { OperationToolFsmState::Drawing } - (_, OperationToolMessage::PointerMove) => self, + (_, OperationToolMessage::PointerMove) => { + responses.add(OverlaysMessage::Draw); + self + } (OperationToolFsmState::Drawing, OperationToolMessage::PointerOutsideViewport) => OperationToolFsmState::Drawing, (state, OperationToolMessage::PointerOutsideViewport) => state, - (OperationToolFsmState::Drawing, OperationToolMessage::Abort) => OperationToolFsmState::Ready, + (OperationToolFsmState::Drawing, OperationToolMessage::Abort) => { + responses.add(DocumentMessage::AbortTransaction); + OperationToolFsmState::Ready + } (_, OperationToolMessage::WorkingColorChanged) => self, _ => self, } From 2aa95d6ee87b7870f47739a480a5a08adf139ca5 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Tue, 26 Aug 2025 12:20:39 +0530 Subject: [PATCH 4/5] added operation type widget --- .../tool/tool_messages/operation_tool.rs | 122 ++++-------------- 1 file changed, 27 insertions(+), 95 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/operation_tool.rs b/editor/src/messages/tool/tool_messages/operation_tool.rs index 63fb65d9ad..e96f2fc442 100644 --- a/editor/src/messages/tool/tool_messages/operation_tool.rs +++ b/editor/src/messages/tool/tool_messages/operation_tool.rs @@ -1,10 +1,7 @@ use super::tool_prelude::*; -use crate::consts::DEFAULT_STROKE_WIDTH; 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::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::shapes::shape_utility::extract_circular_repeat_parameters; -use graphene_std::Color; #[derive(Default, ExtractField)] pub struct OperationTool { @@ -14,17 +11,13 @@ pub struct OperationTool { } pub struct OperationOptions { - line_weight: f64, - fill: ToolColorOptions, - stroke: ToolColorOptions, + operation_type: OperationType, } impl Default for OperationOptions { fn default() -> Self { Self { - line_weight: DEFAULT_STROKE_WIDTH, - fill: ToolColorOptions::new_none(), - stroke: ToolColorOptions::new_primary(), + operation_type: OperationType::CircularRepeat, } } } @@ -56,12 +49,7 @@ enum OperationToolFsmState { #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] pub enum OperationOptionsUpdate { - FillColor(Option), - FillColorType(ToolColorType), - LineWeight(f64), - StrokeColor(Option), - StrokeColorType(ToolColorType), - WorkingColors(Option, Option), + OperationType(OperationType), } impl ToolMetadata for OperationTool { @@ -76,76 +64,29 @@ impl ToolMetadata for OperationTool { } } -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| { +fn create_operation_type_option_widget(operation_type: OperationType) -> WidgetHolder { + let entries = vec![vec![ + MenuListEntry::new("Repeat").label("Repeat").on_commit(move |_| { OperationToolMessage::UpdateOptions { - options: OperationOptionsUpdate::LineWeight(number_input.value.unwrap()), + options: OperationOptionsUpdate::OperationType(OperationType::Repeat), } .into() - }) - .widget_holder() + }), + MenuListEntry::new("Repeat").label("Circular Repeat").on_commit(move |_| { + OperationToolMessage::UpdateOptions { + options: OperationOptionsUpdate::OperationType(OperationType::CircularRepeat), + } + .into() + }), + ]]; + DropdownInput::new(entries).selected_index(Some(operation_type as u32)).widget_holder() } impl LayoutHolder for OperationTool { fn layout(&self) -> Layout { - let mut widgets = self.options.fill.create_widgets( - "Fill", - true, - |_| { - OperationToolMessage::UpdateOptions { - options: OperationOptionsUpdate::FillColor(None), - } - .into() - }, - |color_type: ToolColorType| { - WidgetCallback::new(move |_| { - OperationToolMessage::UpdateOptions { - options: OperationOptionsUpdate::FillColorType(color_type.clone()), - } - .into() - }) - }, - |color: &ColorInput| { - OperationToolMessage::UpdateOptions { - options: OperationOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb())), - } - .into() - }, - ); - - widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - - widgets.append(&mut self.options.stroke.create_widgets( - "Stroke", - true, - |_| { - OperationToolMessage::UpdateOptions { - options: OperationOptionsUpdate::StrokeColor(None), - } - .into() - }, - |color_type: ToolColorType| { - WidgetCallback::new(move |_| { - OperationToolMessage::UpdateOptions { - options: OperationOptionsUpdate::StrokeColorType(color_type.clone()), - } - .into() - }) - }, - |color: &ColorInput| { - OperationToolMessage::UpdateOptions { - options: OperationOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb())), - } - .into() - }, - )); - widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - widgets.push(create_weight_widget(self.options.line_weight)); + let mut widgets = vec![]; + + widgets.push(create_operation_type_option_widget(self.options.operation_type)); Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) } @@ -159,23 +100,7 @@ impl<'a> MessageHandler> for Oper return; }; match options { - OperationOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, - OperationOptionsUpdate::FillColor(color) => { - self.options.fill.custom_color = color; - self.options.fill.color_type = ToolColorType::Custom; - } - OperationOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type, - OperationOptionsUpdate::StrokeColor(color) => { - self.options.stroke.custom_color = color; - self.options.stroke.color_type = ToolColorType::Custom; - } - OperationOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type, - OperationOptionsUpdate::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; - } + OperationOptionsUpdate::OperationType(operation_type) => self.options.operation_type = operation_type, } self.send_layout(responses, LayoutTarget::ToolOptions); @@ -396,3 +321,10 @@ impl Fsm for OperationToolFsmState { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); } } + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum OperationType { + #[default] + CircularRepeat = 0, + Repeat = 1, +} From 26984093150114d936d4079194fb30295b46aaa9 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Wed, 27 Aug 2025 19:45:06 +0530 Subject: [PATCH 5/5] separate operation into different files --- .../messages/input_mapper/input_mappings.rs | 2 +- .../messages/tool/common_functionality/mod.rs | 1 + .../operations/circular_repeat.rs | 143 ++++++++++++++++++ .../common_functionality/operations/mod.rs | 1 + .../tool/tool_messages/operation_tool.rs | 142 ++++------------- 5 files changed, 177 insertions(+), 112 deletions(-) create mode 100644 editor/src/messages/tool/common_functionality/operations/circular_repeat.rs create mode 100644 editor/src/messages/tool/common_functionality/operations/mod.rs diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 87c146d4f8..9cc018ba23 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -176,7 +176,7 @@ pub fn input_mappings() -> Mapping { 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::Confirm), + entry!(KeyDown(Escape); action_dispatch=OperationToolMessage::Abort), entry!(KeyDown(Enter); action_dispatch=OperationToolMessage::Confirm), // // ShapeToolMessage 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/tool_messages/operation_tool.rs b/editor/src/messages/tool/tool_messages/operation_tool.rs index e96f2fc442..9db24457d0 100644 --- a/editor/src/messages/tool/tool_messages/operation_tool.rs +++ b/editor/src/messages/tool/tool_messages/operation_tool.rs @@ -1,7 +1,13 @@ use super::tool_prelude::*; 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::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 { @@ -41,7 +47,7 @@ pub enum OperationToolMessage { } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -enum OperationToolFsmState { +pub enum OperationToolFsmState { #[default] Ready, Drawing, @@ -66,15 +72,15 @@ impl ToolMetadata for OperationTool { fn create_operation_type_option_widget(operation_type: OperationType) -> WidgetHolder { let entries = vec![vec![ - MenuListEntry::new("Repeat").label("Repeat").on_commit(move |_| { + MenuListEntry::new("Circular Repeat").label("Circular Repeat").on_commit(move |_| { OperationToolMessage::UpdateOptions { - options: OperationOptionsUpdate::OperationType(OperationType::Repeat), + options: OperationOptionsUpdate::OperationType(OperationType::CircularRepeat), } .into() }), - MenuListEntry::new("Repeat").label("Circular Repeat").on_commit(move |_| { + MenuListEntry::new("Repeat").label("Repeat").on_commit(move |_| { OperationToolMessage::UpdateOptions { - options: OperationOptionsUpdate::OperationType(OperationType::CircularRepeat), + options: OperationOptionsUpdate::OperationType(OperationType::Repeat), } .into() }), @@ -138,16 +144,14 @@ impl ToolTransition for OperationTool { } #[derive(Clone, Debug, Default)] -struct OperationToolData { - drag_start: DVec2, - clicked_layer_radius: (LayerNodeIdentifier, f64), - layers_dragging: Vec<(LayerNodeIdentifier, f64)>, - initial_center: DVec2, +pub struct OperationToolData { + pub drag_start: DVec2, + pub circular_operation_data: CircularRepeatOperationData, } impl OperationToolData { fn cleanup(&mut self) { - self.layers_dragging.clear(); + CircularRepeatOperation::cleanup(self); } } @@ -160,7 +164,7 @@ impl Fsm for OperationToolFsmState { event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionMessageContext, - _tool_options: &Self::ToolOptions, + tool_options: &Self::ToolOptions, responses: &mut VecDeque, ) -> Self { let ToolActionMessageContext { document, input, .. } = tool_action_data; @@ -168,78 +172,20 @@ impl Fsm for OperationToolFsmState { let ToolMessage::Operation(event) = event else { return self }; match (self, event) { (_, OperationToolMessage::Overlays { context: mut overlay_context }) => { - match self { - OperationToolFsmState::Ready => { - for layer in document.network_interface.selected_nodes().selected_layers(document.metadata()) { - let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue }; - let viewport = document.metadata().transform_to_viewport(layer); - let center = viewport.transform_point2(DVec2::ZERO); - if center.distance(input.mouse.position) < 5. { - overlay_context.circle(center, 3., None, None); - } - - overlay_context.outline_vector(&vector, viewport); - } - if let Some(layer) = document.click(&input) { - let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return self }; - let viewport = document.metadata().transform_to_viewport(layer); - let center = viewport.transform_point2(DVec2::ZERO); - if center.distance(input.mouse.position) < 5. { - overlay_context.circle(center, 3., None, None); - } - - overlay_context.outline_vector(&vector, viewport); - } - } - _ => { - for layer in tool_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); - } - } + match tool_options.operation_type { + OperationType::CircularRepeat => CircularRepeatOperation::overlays(&self, tool_data, document, input, &mut overlay_context), + _ => {} } self } (OperationToolFsmState::Ready, OperationToolMessage::DragStart) => { - let selected_layers = document - .network_interface - .selected_nodes() - .selected_layers(document.metadata()) - .collect::>(); - let Some(clicked_layer) = document.click(&input) else { return self }; - responses.add(DocumentMessage::StartTransaction); - let viewport = document.metadata().transform_to_viewport(clicked_layer); - let center = viewport.transform_point2(DVec2::ZERO); - - if center.distance(input.mouse.position) > 5. { - return self; - }; - - if selected_layers.contains(&clicked_layer) { - // store all - tool_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.clicked_layer_radius = (*layer, radius) - } - (*layer, radius) - }) - .collect::>(); - } else { - // deselect all the layer and store the clicked layer for repeat and dragging - - 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.clicked_layer_radius = (clicked_layer, radius); - tool_data.layers_dragging = vec![(clicked_layer, radius)]; + match tool_options.operation_type { + OperationType::CircularRepeat => { + CircularRepeatOperation::create_node(tool_data, document, responses, input); + } + OperationType::Repeat => {} } - tool_data.drag_start = input.mouse.position; - tool_data.initial_center = viewport.transform_point2(DVec2::ZERO); OperationToolFsmState::Drawing } @@ -257,31 +203,12 @@ impl Fsm for OperationToolFsmState { return self; }; - let (_clicked_layer, clicked_radius) = tool_data.clicked_layer_radius; - let viewport = document.metadata().transform_to_viewport(tool_data.clicked_layer_radius.0); - let sign = (input.mouse.position - tool_data.initial_center).dot(viewport.transform_vector2(DVec2::Y)).signum(); - let delta = document - .metadata() - .downstream_transform_to_viewport(tool_data.clicked_layer_radius.0) - .inverse() - .transform_vector2(input.mouse.position - tool_data.initial_center) - .length() * sign; - - for (layer, initial_radius) in &tool_data.layers_dragging { - 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, - }); + match tool_options.operation_type { + OperationType::CircularRepeat => { + CircularRepeatOperation::update_shape(tool_data, document, responses, input); + } + OperationType::Repeat => {} } - responses.add(NodeGraphMessage::RunDocumentGraph); OperationToolFsmState::Drawing } @@ -321,10 +248,3 @@ impl Fsm for OperationToolFsmState { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); } } - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)] -pub enum OperationType { - #[default] - CircularRepeat = 0, - Repeat = 1, -}