Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 18, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 113 additions & 14 deletions editor/src/messages/tool/tool_messages/path_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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);
}
}
}
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down
Loading