Add Path tool support for Alt-dragging an anchor to pull out a fresh equidistant handle pair (#2496)

* Added initial logic for dragging

* Alt drag stop makes opposite handle back to its position

* Implement new requested behaviour

* Fix sharp point bug

* Apply suggestions from code review

* Add hints

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Adesh Gupta 2025-04-17 11:35:45 +05:30 committed by GitHub
parent fa21385d2c
commit ab39f3f837
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 123 additions and 13 deletions

View File

@ -212,7 +212,7 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
entry!(KeyDownNoRepeat(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles),
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { direct_insert_without_sliding: Control, extend_selection: Shift, lasso_select: Control }),
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { direct_insert_without_sliding: Control, extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt }),
entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick),
entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape),
entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }),

View File

@ -742,6 +742,7 @@ impl ShapeState {
delta: DVec2,
equidistant: bool,
in_viewport_space: bool,
was_alt_dragging: bool,
opposite_handle_position: Option<DVec2>,
responses: &mut VecDeque<Message>,
) {
@ -810,9 +811,11 @@ impl ShapeState {
let length = opposing_handle.copied().unwrap_or_else(|| transform.transform_vector2(other_position - anchor_position).length());
direction.map_or(other_position - anchor_position, |direction| transform.inverse().transform_vector2(-direction * length))
};
let modification_type = other.set_relative_position(new_relative);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
if !was_alt_dragging {
let modification_type = other.set_relative_position(new_relative);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
}
}
}

View File

@ -15,8 +15,8 @@ use crate::messages::tool::common_functionality::shape_editor::{
};
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
use graphene_core::renderer::Quad;
use graphene_core::vector::{ManipulatorPointId, PointId};
use graphene_std::vector::{NoHashBuilder, SegmentId};
use graphene_core::vector::{ManipulatorPointId, PointId, VectorModificationType};
use graphene_std::vector::{HandleId, NoHashBuilder, SegmentId, VectorData};
use std::vec;
#[derive(Default)]
@ -65,6 +65,7 @@ pub enum PathToolMessage {
direct_insert_without_sliding: Key,
extend_selection: Key,
lasso_select: Key,
handle_drag_from_anchor: Key,
},
NudgeSelectedPoints {
delta_x: f64,
@ -375,6 +376,8 @@ struct PathToolData {
angle: f64,
opposite_handle_position: Option<DVec2>,
snapping_axis: Option<Axis>,
alt_clicked_on_anchor: bool,
alt_dragging_from_anchor: bool,
}
impl PathToolData {
@ -489,6 +492,7 @@ impl PathToolData {
extend_selection: bool,
direct_insert_without_sliding: bool,
lasso_select: bool,
handle_drag_from_anchor: bool,
) -> PathToolFsmState {
self.double_click_handled = false;
self.opposing_handle_lengths = None;
@ -516,6 +520,31 @@ impl PathToolData {
self.saved_points_before_handle_drag = old_selection;
}
if handle_drag_from_anchor {
if let Some((layer, point)) = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) {
// Check that selected point is an anchor
if let (Some(point_id), Some(vector_data)) = (point.as_anchor(), document.network_interface.compute_modified_vector(layer)) {
let handles = vector_data.all_connected(point_id).collect::<Vec<_>>();
self.alt_clicked_on_anchor = true;
for handle in &handles {
let modification_type = handle.set_relative_position(DVec2::ZERO);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
for &handles in &vector_data.colinear_manipulators {
if handles.contains(&handle) {
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
}
}
let manipulator_point_id = handles[0].to_manipulator_point();
shape_editor.deselect_all_points();
shape_editor.select_points_by_manipulator_id(&vec![manipulator_point_id]);
responses.add(PathToolMessage::SelectedPointUpdated);
}
}
}
self.start_dragging_point(selected_points, input, document, shape_editor);
responses.add(OverlaysMessage::Draw);
}
@ -744,7 +773,7 @@ impl PathToolData {
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);
shape_editor.move_selected_points(None, document, opposite_delta, false, true, false, None, responses);
// Calculate the projected delta and shift the points along that delta
let delta = current_mouse - drag_start;
@ -756,7 +785,7 @@ impl PathToolData {
_ => DVec2::new(delta.x, 0.),
};
shape_editor.move_selected_points(None, document, projected_delta, false, true, None, responses);
shape_editor.move_selected_points(None, document, projected_delta, false, true, false, None, responses);
}
fn stop_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
@ -772,16 +801,33 @@ impl PathToolData {
_ => DVec2::new(opposite_delta.x, 0.),
};
shape_editor.move_selected_points(None, document, opposite_projected_delta, false, true, None, responses);
shape_editor.move_selected_points(None, document, opposite_projected_delta, false, true, false, 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);
shape_editor.move_selected_points(None, document, delta, false, true, false, None, responses);
self.snapping_axis = None;
}
fn get_normalized_tangent(&mut self, point: PointId, segment: SegmentId, vector_data: &VectorData) -> Option<DVec2> {
let other_point = vector_data.other_point(segment, point)?;
let position = ManipulatorPointId::Anchor(point).get_position(vector_data)?;
let mut handles = vector_data.all_connected(other_point);
let other_handle = handles.find(|handle| handle.segment == segment)?;
let target_position = if other_handle.length(vector_data) == 0. {
ManipulatorPointId::Anchor(other_point).get_position(vector_data)?
} else {
other_handle.to_manipulator_point().get_position(vector_data)?
};
let tangent_vector = target_position - position;
tangent_vector.try_normalize()
}
#[allow(clippy::too_many_arguments)]
fn drag(
&mut self,
@ -829,9 +875,51 @@ 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 };
let unsnapped_delta = current_mouse - previous_mouse;
let mut was_alt_dragging = false;
if self.snapping_axis.is_none() {
shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, opposite, responses);
if self.alt_clicked_on_anchor && !self.alt_dragging_from_anchor && self.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
// Checking which direction the dragging begins
self.alt_dragging_from_anchor = true;
let Some(layer) = document.network_interface.selected_nodes().selected_layers(document.metadata()).next() else {
return;
};
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { return };
let Some(point_id) = shape_editor.selected_points().next().unwrap().get_anchor(&vector_data) else {
return;
};
if vector_data.connected_count(point_id) == 2 {
let connected_segments: Vec<HandleId> = vector_data.all_connected(point_id).collect();
let segment1 = connected_segments[0];
let Some(tangent1) = self.get_normalized_tangent(point_id, segment1.segment, &vector_data) else {
return;
};
let segment2 = connected_segments[1];
let Some(tangent2) = self.get_normalized_tangent(point_id, segment2.segment, &vector_data) else {
return;
};
let delta = input.mouse.position - self.drag_start_pos;
let handle = if delta.dot(tangent1) >= delta.dot(tangent2) {
segment1.to_manipulator_point()
} else {
segment2.to_manipulator_point()
};
// Now change the selection to this handle
shape_editor.deselect_all_points();
shape_editor.select_points_by_manipulator_id(&vec![handle]);
responses.add(PathToolMessage::SelectionChanged);
}
}
if self.alt_dragging_from_anchor && !equidistant && self.alt_clicked_on_anchor {
was_alt_dragging = true;
self.alt_dragging_from_anchor = false;
self.alt_clicked_on_anchor = false;
}
shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, responses);
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta);
} else {
let Some(axis) = self.snapping_axis else { return };
@ -840,7 +928,7 @@ impl PathToolData {
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);
shape_editor.move_selected_points(handle_lengths, document, projected_delta, equidistant, true, false, opposite, responses);
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta);
}
@ -1024,16 +1112,27 @@ impl Fsm for PathToolFsmState {
direct_insert_without_sliding,
extend_selection,
lasso_select,
handle_drag_from_anchor,
},
) => {
let extend_selection = input.keyboard.get(extend_selection as usize);
let lasso_select = input.keyboard.get(lasso_select as usize);
let direct_insert_without_sliding = input.keyboard.get(direct_insert_without_sliding as usize);
let handle_drag_from_anchor = input.keyboard.get(handle_drag_from_anchor as usize);
tool_data.selection_mode = None;
tool_data.lasso_polygon.clear();
tool_data.mouse_down(shape_editor, document, input, responses, extend_selection, direct_insert_without_sliding, lasso_select)
tool_data.mouse_down(
shape_editor,
document,
input,
responses,
extend_selection,
direct_insert_without_sliding,
lasso_select,
handle_drag_from_anchor,
)
}
(
PathToolFsmState::Drawing { selection_shape },
@ -1295,6 +1394,9 @@ impl Fsm for PathToolFsmState {
tool_data.handle_drag_toggle = false;
}
tool_data.alt_dragging_from_anchor = false;
tool_data.alt_clicked_on_anchor = false;
if tool_data.select_anchor_toggled {
shape_editor.deselect_all_points();
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_select_toggle);
@ -1385,6 +1487,7 @@ impl Fsm for PathToolFsmState {
(delta_x, delta_y).into(),
true,
false,
false,
tool_data.opposite_handle_position,
responses,
);
@ -1446,7 +1549,11 @@ impl Fsm for PathToolFsmState {
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Control], "Lasso").prepend_plus()]),
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]),
// TODO: Only show if at least one anchor is selected, and dynamically show either "Smooth" or "Sharp" based on the current state
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDouble, "Make Anchor Smooth/Sharp")]),
HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDouble, "Convert Anchor Point"),
HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "To Sharp"),
HintInfo::keys_and_mouse([Key::Alt], MouseMotion::LmbDrag, "To Smooth"),
]),
// TODO: Only show the following hints if at least one point is selected
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]),
HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]),