Skip to content

Commit 6e7f218

Browse files
0SlowPoke0Keavon
andauthored
Add Path tool support for Ctrl-dragging to pull out zero-length handles with angle locking (#2620)
* implement check-drag and angle-lock * track bool * flip-smooth-sharp * fixed bugs * fixed flip-smooth jump bug and random angle locking bug * ctrl-alt 90 case * aligned flip-smooth sharp and fixed arbitrary handle-length when flipped * code-review change * 0.5 instead of 0.8 --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
1 parent 54b4ef1 commit 6e7f218

File tree

4 files changed

+114
-35
lines changed

4 files changed

+114
-35
lines changed

editor/src/consts.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ pub const HIDE_HANDLE_DISTANCE: f64 = 3.;
102102
pub const HANDLE_ROTATE_SNAP_ANGLE: f64 = 15.;
103103
pub const SEGMENT_INSERTION_DISTANCE: f64 = 7.5;
104104
pub const SEGMENT_OVERLAY_SIZE: f64 = 10.;
105+
pub const HANDLE_LENGTH_FACTOR: f64 = 0.5;
105106

106107
// PEN TOOL
107108
pub const CREATE_CURVE_THRESHOLD: f64 = 5.;

editor/src/messages/input_mapper/input_mappings.rs

Lines changed: 1 addition & 1 deletion
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 { extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt }),
215+
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt, drag_restore_handle: Control }),
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

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use super::graph_modification_utils::{self, merge_layers};
22
use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint};
3+
use super::utility_functions::calculate_segment_angle;
4+
use crate::consts::HANDLE_LENGTH_FACTOR;
35
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
46
use crate::messages::portfolio::document::utility_types::misc::{PathSnapSource, SnapSource};
57
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
@@ -728,23 +730,33 @@ impl ShapeState {
728730
return;
729731
};
730732
let handles = vector_data.all_connected(point_id).take(2).collect::<Vec<_>>();
733+
let non_zero_handles = handles.iter().filter(|handle| handle.length(vector_data) > 1e-6).count();
734+
let handle_segments = handles.iter().map(|handles| handles.segment).collect::<Vec<_>>();
731735

732736
// Grab the next and previous manipulator groups by simply looking at the next / previous index
733737
let points = handles.iter().map(|handle| vector_data.other_point(handle.segment, point_id));
734738
let anchor_positions = points
735739
.map(|point| point.and_then(|point| ManipulatorPointId::Anchor(point).get_position(vector_data)))
736740
.collect::<Vec<_>>();
737741

738-
// Use the position relative to the anchor
739-
let mut directions = anchor_positions
740-
.iter()
741-
.map(|position| position.map(|position| (position - anchor_position)).and_then(DVec2::try_normalize));
742+
let mut segment_angle = 0.;
743+
let mut segment_count = 0.;
744+
745+
for segment in &handle_segments {
746+
let Some(angle) = calculate_segment_angle(point_id, *segment, vector_data, false) else {
747+
continue;
748+
};
749+
segment_angle += angle;
750+
segment_count += 1.;
751+
}
742752

743-
// The direction of the handles is either the perpendicular vector to the sum of the anchors' positions or just the anchor's position (if only one)
744-
let mut handle_direction = match (directions.next().flatten(), directions.next().flatten()) {
745-
(Some(previous), Some(next)) => (previous - next).try_normalize().unwrap_or(next.perp()),
746-
(Some(val), None) | (None, Some(val)) => val,
747-
(None, None) => return,
753+
// For a non-endpoint anchor, handles are perpendicular to the average tangent of adjacent segments.(Refer:https://github.com/GraphiteEditor/Graphite/pull/2620#issuecomment-2881501494)
754+
let mut handle_direction = if segment_count > 1. {
755+
segment_angle = segment_angle / segment_count;
756+
segment_angle += std::f64::consts::FRAC_PI_2;
757+
DVec2::new(segment_angle.cos(), segment_angle.sin())
758+
} else {
759+
DVec2::new(segment_angle.cos(), segment_angle.sin())
748760
};
749761

750762
// Set the manipulator to have colinear handles
@@ -762,20 +774,41 @@ impl ShapeState {
762774
handle_direction *= -1.;
763775
}
764776

765-
// Push both in and out handles into the correct position
766-
for ((handle, sign), other_anchor) in handles.iter().zip([1., -1.]).zip(&anchor_positions) {
767-
// To find the length of the new tangent we just take the distance to the anchor and divide by 3 (pretty arbitrary)
768-
let Some(length) = other_anchor.map(|position| (position - anchor_position).length() / 3.) else {
769-
continue;
777+
if non_zero_handles != 0 {
778+
let [a, b] = handles.as_slice() else { return };
779+
let (non_zero_handle, zero_handle) = if a.length(vector_data) > 1e-6 { (a, b) } else { (b, a) };
780+
let Some(direction) = non_zero_handle
781+
.to_manipulator_point()
782+
.get_position(&vector_data)
783+
.and_then(|position| (position - anchor_position).try_normalize())
784+
else {
785+
return;
770786
};
771-
let new_position = handle_direction * length * sign;
772-
let modification_type = handle.set_relative_position(new_position);
787+
let new_position = -direction * non_zero_handle.length(vector_data);
788+
let modification_type = zero_handle.set_relative_position(new_position);
773789
responses.add(GraphOperationMessage::Vector { layer, modification_type });
790+
} else {
791+
// Push both in and out handles into the correct position
792+
for ((handle, sign), other_anchor) in handles.iter().zip([1., -1.]).zip(&anchor_positions) {
793+
let Some(anchor_vector) = other_anchor.map(|position| (position - anchor_position)) else {
794+
continue;
795+
};
796+
797+
let Some(unit_vector) = anchor_vector.try_normalize() else {
798+
continue;
799+
};
774800

775-
// Create the opposite handle if it doesn't exist (if it is not a cubic segment)
776-
if handle.opposite().to_manipulator_point().get_position(vector_data).is_none() {
777-
let modification_type = handle.opposite().set_relative_position(DVec2::ZERO);
801+
let projection = anchor_vector.length() * HANDLE_LENGTH_FACTOR * handle_direction.dot(unit_vector).abs();
802+
803+
let new_position = handle_direction * projection * sign;
804+
let modification_type = handle.set_relative_position(new_position);
778805
responses.add(GraphOperationMessage::Vector { layer, modification_type });
806+
807+
// Create the opposite handle if it doesn't exist (if it is not a cubic segment)
808+
if handle.opposite().to_manipulator_point().get_position(vector_data).is_none() {
809+
let modification_type = handle.opposite().set_relative_position(DVec2::ZERO);
810+
responses.add(GraphOperationMessage::Vector { layer, modification_type });
811+
}
779812
}
780813
}
781814
}
@@ -1503,13 +1536,13 @@ impl ShapeState {
15031536

15041537
let (id, anchor) = result?;
15051538
let handles = vector_data.all_connected(id);
1506-
let mut positions = handles
1539+
let positions = handles
15071540
.filter_map(|handle| handle.to_manipulator_point().get_position(&vector_data))
1508-
.filter(|&handle| !anchor.abs_diff_eq(handle, 1e-5));
1541+
.filter(|&handle| anchor.abs_diff_eq(handle, 1e-5))
1542+
.count();
15091543

15101544
// Check by comparing the handle positions to the anchor if this manipulator group is a point
1511-
let already_sharp = positions.next().is_none();
1512-
if already_sharp {
1545+
if positions != 0 {
15131546
self.convert_manipulator_handles_to_colinear(&vector_data, id, responses, layer);
15141547
} else {
15151548
for handle in vector_data.all_connected(id) {

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

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ pub enum PathToolMessage {
6767
extend_selection: Key,
6868
lasso_select: Key,
6969
handle_drag_from_anchor: Key,
70+
drag_restore_handle: Key,
7071
},
7172
NudgeSelectedPoints {
7273
delta_x: f64,
@@ -362,7 +363,6 @@ struct PathToolData {
362363
saved_points_before_handle_drag: Vec<ManipulatorPointId>,
363364
handle_drag_toggle: bool,
364365
dragging_state: DraggingState,
365-
current_selected_handle_id: Option<ManipulatorPointId>,
366366
angle: f64,
367367
opposite_handle_position: Option<DVec2>,
368368
last_clicked_point_was_selected: bool,
@@ -438,6 +438,7 @@ impl PathToolData {
438438
extend_selection: bool,
439439
lasso_select: bool,
440440
handle_drag_from_anchor: bool,
441+
drag_zero_handle: bool,
441442
) -> PathToolFsmState {
442443
self.double_click_handled = false;
443444
self.opposing_handle_lengths = None;
@@ -500,6 +501,24 @@ impl PathToolData {
500501
}
501502
}
502503

504+
if let Some((Some(point), Some(vector_data))) = shape_editor
505+
.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD)
506+
.and_then(|(layer, point)| Some((point.as_anchor(), document.network_interface.compute_modified_vector(layer))))
507+
{
508+
let handles = vector_data
509+
.all_connected(point)
510+
.filter(|handle| handle.length(&vector_data) < 1e-6)
511+
.map(|handle| handle.to_manipulator_point())
512+
.collect::<Vec<_>>();
513+
let endpoint = vector_data.extendable_points(false).any(|anchor| point == anchor);
514+
515+
if drag_zero_handle && (handles.len() == 1 && !endpoint) {
516+
shape_editor.deselect_all_points();
517+
shape_editor.select_points_by_manipulator_id(&handles);
518+
shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document);
519+
}
520+
}
521+
503522
self.start_dragging_point(selected_points, input, document, shape_editor);
504523
responses.add(OverlaysMessage::Draw);
505524
}
@@ -689,6 +708,7 @@ impl PathToolData {
689708
handle_id: ManipulatorPointId,
690709
lock_angle: bool,
691710
snap_angle: bool,
711+
tangent_to_neighboring_tangents: bool,
692712
) -> f64 {
693713
let current_angle = -handle_vector.angle_to(DVec2::X);
694714

@@ -699,17 +719,22 @@ impl PathToolData {
699719
.and_then(|(layer, _)| document.network_interface.compute_modified_vector(*layer))
700720
{
701721
if relative_vector.length() < 25. && lock_angle && !self.angle_locked {
702-
if let Some(angle) = calculate_lock_angle(self, shape_editor, responses, document, &vector_data, handle_id) {
722+
if let Some(angle) = calculate_lock_angle(self, shape_editor, responses, document, &vector_data, handle_id, tangent_to_neighboring_tangents) {
703723
self.angle = angle;
724+
self.angle_locked = true;
704725
return angle;
705726
}
706727
}
707728
}
708729

709-
// When the angle is locked we use the old angle
710-
711-
if self.current_selected_handle_id == Some(handle_id) && lock_angle {
730+
if lock_angle && !self.angle_locked {
712731
self.angle_locked = true;
732+
self.angle = -relative_vector.angle_to(DVec2::X);
733+
return -relative_vector.angle_to(DVec2::X);
734+
}
735+
736+
// When the angle is locked we use the old angle
737+
if self.angle_locked {
713738
return self.angle;
714739
}
715740

@@ -720,8 +745,6 @@ impl PathToolData {
720745
handle_angle = (handle_angle / snap_resolution).round() * snap_resolution;
721746
}
722747

723-
// Cache the angle and handle id for lock angle
724-
self.current_selected_handle_id = Some(handle_id);
725748
self.angle = handle_angle;
726749

727750
handle_angle
@@ -747,6 +770,7 @@ impl PathToolData {
747770
origin: anchor_position,
748771
direction: handle_direction.normalize_or_zero(),
749772
};
773+
750774
self.snap_manager.constrained_snap(&snap_data, &snap_point, snap_constraint, Default::default())
751775
}
752776
false => self.snap_manager.free_snap(&snap_data, &snap_point, Default::default()),
@@ -850,7 +874,17 @@ impl PathToolData {
850874
let snapped_delta = if let Some((handle_pos, anchor_pos, handle_id)) = self.try_get_selected_handle_and_anchor(shape_editor, document) {
851875
let cursor_pos = handle_pos + raw_delta;
852876

853-
let handle_angle = self.calculate_handle_angle(shape_editor, document, responses, handle_pos - anchor_pos, cursor_pos - anchor_pos, handle_id, lock_angle, snap_angle);
877+
let handle_angle = self.calculate_handle_angle(
878+
shape_editor,
879+
document,
880+
responses,
881+
handle_pos - anchor_pos,
882+
cursor_pos - anchor_pos,
883+
handle_id,
884+
lock_angle,
885+
snap_angle,
886+
equidistant,
887+
);
854888

855889
let constrained_direction = DVec2::new(handle_angle.cos(), handle_angle.sin());
856890
let projected_length = (cursor_pos - anchor_pos).dot(constrained_direction);
@@ -1109,17 +1143,18 @@ impl Fsm for PathToolFsmState {
11091143
extend_selection,
11101144
lasso_select,
11111145
handle_drag_from_anchor,
1112-
..
1146+
drag_restore_handle,
11131147
},
11141148
) => {
11151149
let extend_selection = input.keyboard.get(extend_selection as usize);
11161150
let lasso_select = input.keyboard.get(lasso_select as usize);
11171151
let handle_drag_from_anchor = input.keyboard.get(handle_drag_from_anchor as usize);
1152+
let drag_zero_handle = input.keyboard.get(drag_restore_handle as usize);
11181153

11191154
tool_data.selection_mode = None;
11201155
tool_data.lasso_polygon.clear();
11211156

1122-
tool_data.mouse_down(shape_editor, document, input, responses, extend_selection, lasso_select, handle_drag_from_anchor)
1157+
tool_data.mouse_down(shape_editor, document, input, responses, extend_selection, lasso_select, handle_drag_from_anchor, drag_zero_handle)
11231158
}
11241159
(
11251160
PathToolFsmState::Drawing { selection_shape },
@@ -1375,6 +1410,7 @@ impl Fsm for PathToolFsmState {
13751410
tool_data.saved_points_before_handle_drag.clear();
13761411
tool_data.handle_drag_toggle = false;
13771412
}
1413+
tool_data.angle_locked = false;
13781414
responses.add(DocumentMessage::AbortTransaction);
13791415
tool_data.snap_manager.cleanup(responses);
13801416
PathToolFsmState::Ready
@@ -1443,6 +1479,7 @@ impl Fsm for PathToolFsmState {
14431479

14441480
tool_data.alt_dragging_from_anchor = false;
14451481
tool_data.alt_clicked_on_anchor = false;
1482+
tool_data.angle_locked = false;
14461483

14471484
if tool_data.select_anchor_toggled {
14481485
shape_editor.deselect_all_points();
@@ -1775,6 +1812,7 @@ fn calculate_lock_angle(
17751812
document: &DocumentMessageHandler,
17761813
vector_data: &VectorData,
17771814
handle_id: ManipulatorPointId,
1815+
tangent_to_neighboring_tangents: bool,
17781816
) -> Option<f64> {
17791817
let anchor = handle_id.get_anchor(vector_data)?;
17801818
let anchor_position = vector_data.point_domain.position_from_id(anchor);
@@ -1808,7 +1846,14 @@ fn calculate_lock_angle(
18081846
let angle_2 = calculate_segment_angle(anchor, segment, vector_data, false);
18091847

18101848
match (angle_1, angle_2) {
1811-
(Some(angle_1), Some(angle_2)) => Some((angle_1 + angle_2) / 2.0),
1849+
(Some(angle_1), Some(angle_2)) => {
1850+
let angle = Some((angle_1 + angle_2) / 2.);
1851+
if tangent_to_neighboring_tangents {
1852+
angle.map(|angle| angle + std::f64::consts::FRAC_PI_2)
1853+
} else {
1854+
angle
1855+
}
1856+
}
18121857
(Some(angle_1), None) => Some(angle_1),
18131858
(None, Some(angle_2)) => Some(angle_2),
18141859
(None, None) => None,

0 commit comments

Comments
 (0)