diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 3d903ddb7d..af194fec22 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -7,6 +7,7 @@ use crate::messages::portfolio::document::overlays::utility_functions::{path_ove use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles, OverlayContext}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; +use crate::messages::portfolio::document::utility_types::transformation::Axis; use crate::messages::preferences::SelectionMode; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::shape_editor::{ @@ -373,6 +374,7 @@ struct PathToolData { current_selected_handle_id: Option, angle: f64, opposite_handle_position: Option, + snapping_axis: Option, } impl PathToolData { @@ -736,6 +738,50 @@ impl PathToolData { document.metadata().document_to_viewport.transform_vector2(snap_result.snapped_point_document - handle_position) } + fn start_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + // Find the negative delta to take the point to the drag start position + let current_mouse = input.mouse.position; + let drag_start = self.drag_start_pos; + let opposite_delta = drag_start - current_mouse; + + shape_editor.move_selected_points(None, document, opposite_delta, false, true, None, responses); + + // Calculate the projected delta and shift the points along that delta + let delta = current_mouse - drag_start; + let axis = if delta.x.abs() >= delta.y.abs() { Axis::X } else { Axis::Y }; + self.snapping_axis = Some(axis); + let projected_delta = match axis { + Axis::X => DVec2::new(delta.x, 0.), + Axis::Y => DVec2::new(0., delta.y), + _ => DVec2::new(delta.x, 0.), + }; + + shape_editor.move_selected_points(None, document, projected_delta, false, true, None, responses); + } + + fn stop_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + // Calculate the negative delta of the selection and move it back to the drag start + let current_mouse = input.mouse.position; + let drag_start = self.drag_start_pos; + + let opposite_delta = drag_start - current_mouse; + let Some(axis) = self.snapping_axis else { return }; + let opposite_projected_delta = match axis { + Axis::X => DVec2::new(opposite_delta.x, 0.), + Axis::Y => DVec2::new(0., opposite_delta.y), + _ => DVec2::new(opposite_delta.x, 0.), + }; + + shape_editor.move_selected_points(None, document, opposite_projected_delta, false, true, None, responses); + + // Calculate what actually would have been the original delta for the point, and apply that + let delta = current_mouse - drag_start; + + shape_editor.move_selected_points(None, document, delta, false, true, None, responses); + + self.snapping_axis = None; + } + #[allow(clippy::too_many_arguments)] fn drag( &mut self, @@ -747,6 +793,19 @@ impl PathToolData { input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) { + // First check if selection is not just a single handle point + let selected_points = shape_editor.selected_points(); + let single_handle_selected = selected_points.count() == 1 + && shape_editor + .selected_points() + .any(|point| matches!(point, ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_))); + + if snap_angle && self.snapping_axis.is_none() && !single_handle_selected { + self.start_snap_along_axis(shape_editor, document, input, responses); + } else if !snap_angle && self.snapping_axis.is_some() { + self.stop_snap_along_axis(shape_editor, document, input, responses); + } + let document_to_viewport = document.metadata().document_to_viewport; let previous_mouse = document_to_viewport.transform_point2(self.previous_mouse_position); let current_mouse = input.mouse.position; @@ -769,8 +828,31 @@ impl PathToolData { let handle_lengths = if equidistant { None } else { self.opposing_handle_lengths.take() }; let opposite = if lock_angle { None } else { self.opposite_handle_position }; - shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, opposite, responses); - self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta); + let unsnapped_delta = current_mouse - previous_mouse; + + if self.snapping_axis.is_none() { + shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, opposite, responses); + self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta); + } else { + let Some(axis) = self.snapping_axis else { return }; + let projected_delta = match axis { + Axis::X => DVec2::new(unsnapped_delta.x, 0.), + Axis::Y => DVec2::new(0., unsnapped_delta.y), + _ => DVec2::new(unsnapped_delta.x, 0.), + }; + shape_editor.move_selected_points(handle_lengths, document, projected_delta, equidistant, true, opposite, responses); + self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta); + } + + if snap_angle && self.snapping_axis.is_some() { + let Some(current_axis) = self.snapping_axis else { return }; + let total_delta = self.drag_start_pos - input.mouse.position; + + if (total_delta.x.abs() > total_delta.y.abs() && current_axis == Axis::Y) || (total_delta.y.abs() > total_delta.x.abs() && current_axis == Axis::X) { + self.stop_snap_along_axis(shape_editor, document, input, responses); + self.start_snap_along_axis(shape_editor, document, input, responses); + } + } } } @@ -872,6 +954,28 @@ impl Fsm for PathToolFsmState { } Self::Dragging(_) => { tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); + + // Draw the snapping axis lines + if tool_data.snapping_axis.is_some() { + let Some(axis) = tool_data.snapping_axis else { return self }; + let origin = tool_data.drag_start_pos; + let viewport_diagonal = input.viewport_bounds.size().length(); + + let mut faded_blue = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.25).rgba_hex(); + faded_blue.insert(0, '#'); + let other = faded_blue.as_str(); + + match axis { + Axis::Y => { + overlay_context.line(origin - DVec2::Y * viewport_diagonal, origin + DVec2::Y * viewport_diagonal, Some(COLOR_OVERLAY_BLUE)); + overlay_context.line(origin - DVec2::X * viewport_diagonal, origin + DVec2::X * viewport_diagonal, Some(other)); + } + Axis::X | Axis::Both => { + overlay_context.line(origin - DVec2::X * viewport_diagonal, origin + DVec2::X * viewport_diagonal, Some(COLOR_OVERLAY_BLUE)); + overlay_context.line(origin - DVec2::Y * viewport_diagonal, origin + DVec2::Y * viewport_diagonal, Some(other)); + } + } + } } Self::InsertPoint => { let state = tool_data.update_insertion(shape_editor, document, responses, input); @@ -1065,19 +1169,10 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Drawing { selection_shape: selection_type } } - ( - PathToolFsmState::Dragging(dragging_state), - PathToolMessage::PointerOutsideViewport { - equidistant, snap_angle, lock_angle, .. - }, - ) => { + (PathToolFsmState::Dragging(dragging_state), PathToolMessage::PointerOutsideViewport { .. }) => { // Auto-panning - if tool_data.auto_panning.shift_viewport(input, responses).is_some() { - let equidistant = input.keyboard.get(equidistant as usize); - let snap_angle = input.keyboard.get(snap_angle as usize); - let lock_angle = input.keyboard.get(lock_angle as usize); - - tool_data.drag(equidistant, lock_angle, snap_angle, shape_editor, document, input, responses); + if let Some(offset) = tool_data.auto_panning.shift_viewport(input, responses) { + tool_data.drag_start_pos += offset; } PathToolFsmState::Dragging(dragging_state) @@ -1223,6 +1318,10 @@ impl Fsm for PathToolFsmState { shape_editor.deselect_all_points(); } + if tool_data.snapping_axis.is_some() { + tool_data.snapping_axis = None; + } + responses.add(DocumentMessage::EndTransaction); responses.add(PathToolMessage::SelectedPointUpdated); tool_data.snap_manager.cleanup(responses);