Add Path tool support for dragging along an axis when Shift is held (#2449)

* Initial logic for snapping

* Solved switching of axes

* Solved conflict

* Fixed autopanning issue

* Autopanning issue

* cleared comments

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Adesh Gupta 2025-03-18 13:27:48 +05:30 committed by GitHub
parent 7e7e88f6fa
commit 4275eaf5bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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<ManipulatorPointId>,
angle: f64,
opposite_handle_position: Option<DVec2>,
snapping_axis: Option<Axis>,
}
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<Message>) {
// 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<Message>) {
// 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<Message>,
) {
// 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);