Skip to content

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

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 13 commits into from
Apr 17, 2025
2 changes: 1 addition & 1 deletion editor/src/messages/input_mapper/input_mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand Down
7 changes: 5 additions & 2 deletions editor/src/messages/tool/common_functionality/shape_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
) {
Expand Down Expand Up @@ -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 });
}
}
}
}
Expand Down
127 changes: 117 additions & 10 deletions editor/src/messages/tool/tool_messages/path_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand All @@ -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>) {
Expand All @@ -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,
Expand Down Expand Up @@ -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 };
Expand All @@ -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);
}

Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1385,6 +1487,7 @@ impl Fsm for PathToolFsmState {
(delta_x, delta_y).into(),
true,
false,
false,
tool_data.opposite_handle_position,
responses,
);
Expand Down Expand Up @@ -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")]),
Expand Down
Loading