Skip to content

Commit ab39f3f

Browse files
4adexKeavon
andauthored
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>
1 parent fa21385 commit ab39f3f

File tree

3 files changed

+123
-13
lines changed

3 files changed

+123
-13
lines changed

editor/src/messages/input_mapper/input_mappings.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ pub fn input_mappings() -> Mapping {
212212
entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
213213
entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
214214
entry!(KeyDownNoRepeat(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles),
215-
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { direct_insert_without_sliding: Control, extend_selection: Shift, lasso_select: Control }),
215+
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { direct_insert_without_sliding: Control, extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt }),
216216
entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick),
217217
entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape),
218218
entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }),

editor/src/messages/tool/common_functionality/shape_editor.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,7 @@ impl ShapeState {
742742
delta: DVec2,
743743
equidistant: bool,
744744
in_viewport_space: bool,
745+
was_alt_dragging: bool,
745746
opposite_handle_position: Option<DVec2>,
746747
responses: &mut VecDeque<Message>,
747748
) {
@@ -810,9 +811,11 @@ impl ShapeState {
810811
let length = opposing_handle.copied().unwrap_or_else(|| transform.transform_vector2(other_position - anchor_position).length());
811812
direction.map_or(other_position - anchor_position, |direction| transform.inverse().transform_vector2(-direction * length))
812813
};
813-
let modification_type = other.set_relative_position(new_relative);
814814

815-
responses.add(GraphOperationMessage::Vector { layer, modification_type });
815+
if !was_alt_dragging {
816+
let modification_type = other.set_relative_position(new_relative);
817+
responses.add(GraphOperationMessage::Vector { layer, modification_type });
818+
}
816819
}
817820
}
818821
}

editor/src/messages/tool/tool_messages/path_tool.rs

+117-10
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ use crate::messages::tool::common_functionality::shape_editor::{
1515
};
1616
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
1717
use graphene_core::renderer::Quad;
18-
use graphene_core::vector::{ManipulatorPointId, PointId};
19-
use graphene_std::vector::{NoHashBuilder, SegmentId};
18+
use graphene_core::vector::{ManipulatorPointId, PointId, VectorModificationType};
19+
use graphene_std::vector::{HandleId, NoHashBuilder, SegmentId, VectorData};
2020
use std::vec;
2121

2222
#[derive(Default)]
@@ -65,6 +65,7 @@ pub enum PathToolMessage {
6565
direct_insert_without_sliding: Key,
6666
extend_selection: Key,
6767
lasso_select: Key,
68+
handle_drag_from_anchor: Key,
6869
},
6970
NudgeSelectedPoints {
7071
delta_x: f64,
@@ -375,6 +376,8 @@ struct PathToolData {
375376
angle: f64,
376377
opposite_handle_position: Option<DVec2>,
377378
snapping_axis: Option<Axis>,
379+
alt_clicked_on_anchor: bool,
380+
alt_dragging_from_anchor: bool,
378381
}
379382

380383
impl PathToolData {
@@ -489,6 +492,7 @@ impl PathToolData {
489492
extend_selection: bool,
490493
direct_insert_without_sliding: bool,
491494
lasso_select: bool,
495+
handle_drag_from_anchor: bool,
492496
) -> PathToolFsmState {
493497
self.double_click_handled = false;
494498
self.opposing_handle_lengths = None;
@@ -516,6 +520,31 @@ impl PathToolData {
516520
self.saved_points_before_handle_drag = old_selection;
517521
}
518522

523+
if handle_drag_from_anchor {
524+
if let Some((layer, point)) = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) {
525+
// Check that selected point is an anchor
526+
if let (Some(point_id), Some(vector_data)) = (point.as_anchor(), document.network_interface.compute_modified_vector(layer)) {
527+
let handles = vector_data.all_connected(point_id).collect::<Vec<_>>();
528+
self.alt_clicked_on_anchor = true;
529+
for handle in &handles {
530+
let modification_type = handle.set_relative_position(DVec2::ZERO);
531+
responses.add(GraphOperationMessage::Vector { layer, modification_type });
532+
for &handles in &vector_data.colinear_manipulators {
533+
if handles.contains(&handle) {
534+
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
535+
responses.add(GraphOperationMessage::Vector { layer, modification_type });
536+
}
537+
}
538+
}
539+
540+
let manipulator_point_id = handles[0].to_manipulator_point();
541+
shape_editor.deselect_all_points();
542+
shape_editor.select_points_by_manipulator_id(&vec![manipulator_point_id]);
543+
responses.add(PathToolMessage::SelectedPointUpdated);
544+
}
545+
}
546+
}
547+
519548
self.start_dragging_point(selected_points, input, document, shape_editor);
520549
responses.add(OverlaysMessage::Draw);
521550
}
@@ -744,7 +773,7 @@ impl PathToolData {
744773
let drag_start = self.drag_start_pos;
745774
let opposite_delta = drag_start - current_mouse;
746775

747-
shape_editor.move_selected_points(None, document, opposite_delta, false, true, None, responses);
776+
shape_editor.move_selected_points(None, document, opposite_delta, false, true, false, None, responses);
748777

749778
// Calculate the projected delta and shift the points along that delta
750779
let delta = current_mouse - drag_start;
@@ -756,7 +785,7 @@ impl PathToolData {
756785
_ => DVec2::new(delta.x, 0.),
757786
};
758787

759-
shape_editor.move_selected_points(None, document, projected_delta, false, true, None, responses);
788+
shape_editor.move_selected_points(None, document, projected_delta, false, true, false, None, responses);
760789
}
761790

762791
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 {
772801
_ => DVec2::new(opposite_delta.x, 0.),
773802
};
774803

775-
shape_editor.move_selected_points(None, document, opposite_projected_delta, false, true, None, responses);
804+
shape_editor.move_selected_points(None, document, opposite_projected_delta, false, true, false, None, responses);
776805

777806
// Calculate what actually would have been the original delta for the point, and apply that
778807
let delta = current_mouse - drag_start;
779808

780-
shape_editor.move_selected_points(None, document, delta, false, true, None, responses);
809+
shape_editor.move_selected_points(None, document, delta, false, true, false, None, responses);
781810

782811
self.snapping_axis = None;
783812
}
784813

814+
fn get_normalized_tangent(&mut self, point: PointId, segment: SegmentId, vector_data: &VectorData) -> Option<DVec2> {
815+
let other_point = vector_data.other_point(segment, point)?;
816+
let position = ManipulatorPointId::Anchor(point).get_position(vector_data)?;
817+
818+
let mut handles = vector_data.all_connected(other_point);
819+
let other_handle = handles.find(|handle| handle.segment == segment)?;
820+
821+
let target_position = if other_handle.length(vector_data) == 0. {
822+
ManipulatorPointId::Anchor(other_point).get_position(vector_data)?
823+
} else {
824+
other_handle.to_manipulator_point().get_position(vector_data)?
825+
};
826+
827+
let tangent_vector = target_position - position;
828+
tangent_vector.try_normalize()
829+
}
830+
785831
#[allow(clippy::too_many_arguments)]
786832
fn drag(
787833
&mut self,
@@ -829,9 +875,51 @@ impl PathToolData {
829875
let handle_lengths = if equidistant { None } else { self.opposing_handle_lengths.take() };
830876
let opposite = if lock_angle { None } else { self.opposite_handle_position };
831877
let unsnapped_delta = current_mouse - previous_mouse;
878+
let mut was_alt_dragging = false;
832879

833880
if self.snapping_axis.is_none() {
834-
shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, opposite, responses);
881+
if self.alt_clicked_on_anchor && !self.alt_dragging_from_anchor && self.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
882+
// Checking which direction the dragging begins
883+
self.alt_dragging_from_anchor = true;
884+
let Some(layer) = document.network_interface.selected_nodes().selected_layers(document.metadata()).next() else {
885+
return;
886+
};
887+
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { return };
888+
let Some(point_id) = shape_editor.selected_points().next().unwrap().get_anchor(&vector_data) else {
889+
return;
890+
};
891+
892+
if vector_data.connected_count(point_id) == 2 {
893+
let connected_segments: Vec<HandleId> = vector_data.all_connected(point_id).collect();
894+
let segment1 = connected_segments[0];
895+
let Some(tangent1) = self.get_normalized_tangent(point_id, segment1.segment, &vector_data) else {
896+
return;
897+
};
898+
let segment2 = connected_segments[1];
899+
let Some(tangent2) = self.get_normalized_tangent(point_id, segment2.segment, &vector_data) else {
900+
return;
901+
};
902+
903+
let delta = input.mouse.position - self.drag_start_pos;
904+
let handle = if delta.dot(tangent1) >= delta.dot(tangent2) {
905+
segment1.to_manipulator_point()
906+
} else {
907+
segment2.to_manipulator_point()
908+
};
909+
910+
// Now change the selection to this handle
911+
shape_editor.deselect_all_points();
912+
shape_editor.select_points_by_manipulator_id(&vec![handle]);
913+
responses.add(PathToolMessage::SelectionChanged);
914+
}
915+
}
916+
917+
if self.alt_dragging_from_anchor && !equidistant && self.alt_clicked_on_anchor {
918+
was_alt_dragging = true;
919+
self.alt_dragging_from_anchor = false;
920+
self.alt_clicked_on_anchor = false;
921+
}
922+
shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, responses);
835923
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta);
836924
} else {
837925
let Some(axis) = self.snapping_axis else { return };
@@ -840,7 +928,7 @@ impl PathToolData {
840928
Axis::Y => DVec2::new(0., unsnapped_delta.y),
841929
_ => DVec2::new(unsnapped_delta.x, 0.),
842930
};
843-
shape_editor.move_selected_points(handle_lengths, document, projected_delta, equidistant, true, opposite, responses);
931+
shape_editor.move_selected_points(handle_lengths, document, projected_delta, equidistant, true, false, opposite, responses);
844932
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta);
845933
}
846934

@@ -1024,16 +1112,27 @@ impl Fsm for PathToolFsmState {
10241112
direct_insert_without_sliding,
10251113
extend_selection,
10261114
lasso_select,
1115+
handle_drag_from_anchor,
10271116
},
10281117
) => {
10291118
let extend_selection = input.keyboard.get(extend_selection as usize);
10301119
let lasso_select = input.keyboard.get(lasso_select as usize);
10311120
let direct_insert_without_sliding = input.keyboard.get(direct_insert_without_sliding as usize);
1121+
let handle_drag_from_anchor = input.keyboard.get(handle_drag_from_anchor as usize);
10321122

10331123
tool_data.selection_mode = None;
10341124
tool_data.lasso_polygon.clear();
10351125

1036-
tool_data.mouse_down(shape_editor, document, input, responses, extend_selection, direct_insert_without_sliding, lasso_select)
1126+
tool_data.mouse_down(
1127+
shape_editor,
1128+
document,
1129+
input,
1130+
responses,
1131+
extend_selection,
1132+
direct_insert_without_sliding,
1133+
lasso_select,
1134+
handle_drag_from_anchor,
1135+
)
10371136
}
10381137
(
10391138
PathToolFsmState::Drawing { selection_shape },
@@ -1295,6 +1394,9 @@ impl Fsm for PathToolFsmState {
12951394
tool_data.handle_drag_toggle = false;
12961395
}
12971396

1397+
tool_data.alt_dragging_from_anchor = false;
1398+
tool_data.alt_clicked_on_anchor = false;
1399+
12981400
if tool_data.select_anchor_toggled {
12991401
shape_editor.deselect_all_points();
13001402
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_select_toggle);
@@ -1385,6 +1487,7 @@ impl Fsm for PathToolFsmState {
13851487
(delta_x, delta_y).into(),
13861488
true,
13871489
false,
1490+
false,
13881491
tool_data.opposite_handle_position,
13891492
responses,
13901493
);
@@ -1446,7 +1549,11 @@ impl Fsm for PathToolFsmState {
14461549
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Control], "Lasso").prepend_plus()]),
14471550
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]),
14481551
// TODO: Only show if at least one anchor is selected, and dynamically show either "Smooth" or "Sharp" based on the current state
1449-
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDouble, "Make Anchor Smooth/Sharp")]),
1552+
HintGroup(vec![
1553+
HintInfo::mouse(MouseMotion::LmbDouble, "Convert Anchor Point"),
1554+
HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "To Sharp"),
1555+
HintInfo::keys_and_mouse([Key::Alt], MouseMotion::LmbDrag, "To Smooth"),
1556+
]),
14501557
// TODO: Only show the following hints if at least one point is selected
14511558
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]),
14521559
HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]),

0 commit comments

Comments
 (0)