Skip to content

Commit 39a7b76

Browse files
bakayu0HyperCubeKeavon
authored
Add snap and lock angle modifiers for handle dragging to the Path tool (#2160)
* added snap and lock angle to path tool * fixed breakage of `tab` and `space` functionality - Previous implementation broke functionality of using Tab to swap the being-dragged handle to its opposing handle, Now fixed. - Previous implementation broke functionality of using space to drag the manipulator group (anchor + handles) while dragging a handle, Now fixed. * fixed the angle snapping and locking when used together Now, if `shift` is used to snap to a 15° increment, then `ctrl` is used to preserve the angle, releasing the `shift` key will still preserve the angle. * Fix snapping angle logic * Improve transforms * added functionality for `alt` key Now, temporarily converts selected handles to colinear if they are not already colinear. * Revert "added functionality for `alt` key" This reverts commit f12ba6f. * Code review --------- Co-authored-by: hypercube <[email protected]> Co-authored-by: Keavon Chambers <[email protected]>
1 parent 606be8a commit 39a7b76

File tree

3 files changed

+166
-15
lines changed

3 files changed

+166
-15
lines changed

editor/src/consts.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 6.;
6262
pub const SELECTION_THRESHOLD: f64 = 10.;
6363
pub const HIDE_HANDLE_DISTANCE: f64 = 3.;
6464
pub const INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE: f64 = 50.;
65+
pub const HANDLE_ROTATE_SNAP_ANGLE: f64 = 15.;
6566

6667
// Pen tool
6768
pub const CREATE_CURVE_THRESHOLD: f64 = 5.;

editor/src/messages/input_mapper/input_mappings.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,16 +206,16 @@ pub fn input_mappings() -> Mapping {
206206
// PathToolMessage
207207
entry!(KeyDown(Delete); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath),
208208
entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath),
209-
entry!(KeyDown(Delete); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::BreakPath),
210-
entry!(KeyDown(Backspace); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::BreakPath),
209+
entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
210+
entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
211211
entry!(KeyDown(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles),
212212
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { direct_insert_without_sliding: Control, extend_selection: Shift }),
213213
entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick),
214214
entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape),
215215
entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }),
216216
entry!(KeyDown(KeyR); action_dispatch=PathToolMessage::GRS { key: KeyR }),
217217
entry!(KeyDown(KeyS); action_dispatch=PathToolMessage::GRS { key: KeyS }),
218-
entry!(PointerMove; refresh_keys=[KeyC, Shift, Alt, Space], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space}),
218+
entry!(PointerMove; refresh_keys=[KeyC, Space, Control, Shift, Alt], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space, snap_angle: Shift, lock_angle: Control }),
219219
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
220220
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors),
221221
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::DeselectAllPoints),

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

Lines changed: 162 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
use super::tool_prelude::*;
2-
use crate::consts::{COLOR_OVERLAY_YELLOW, DRAG_THRESHOLD, INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE, SELECTION_THRESHOLD, SELECTION_TOLERANCE};
2+
use crate::consts::{COLOR_OVERLAY_YELLOW, DRAG_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE, SELECTION_THRESHOLD, SELECTION_TOLERANCE};
33
use crate::messages::portfolio::document::overlays::utility_functions::path_overlays;
44
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
55
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
66
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
77
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
88
use crate::messages::tool::common_functionality::shape_editor::{ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedPointsInfo, ShapeState};
9-
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager};
9+
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
1010

1111
use graphene_core::renderer::Quad;
1212
use graphene_core::vector::ManipulatorPointId;
@@ -59,11 +59,15 @@ pub enum PathToolMessage {
5959
equidistant: Key,
6060
toggle_colinear: Key,
6161
move_anchor_with_handles: Key,
62+
snap_angle: Key,
63+
lock_angle: Key,
6264
},
6365
PointerOutsideViewport {
6466
equidistant: Key,
6567
toggle_colinear: Key,
6668
move_anchor_with_handles: Key,
69+
snap_angle: Key,
70+
lock_angle: Key,
6771
},
6872
RightClick,
6973
SelectAllAnchors,
@@ -294,6 +298,7 @@ struct PathToolData {
294298
saved_points_before_anchor_select_toggle: Vec<ManipulatorPointId>,
295299
select_anchor_toggled: bool,
296300
dragging_state: DraggingState,
301+
angle: f64,
297302
}
298303

299304
impl PathToolData {
@@ -466,13 +471,114 @@ impl PathToolData {
466471
false
467472
}
468473

469-
fn drag(&mut self, equidistant: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
470-
// Move the selected points with the mouse
471-
let previous_mouse = document.metadata().document_to_viewport.transform_point2(self.previous_mouse_position);
472-
let snapped_delta = shape_editor.snap(&mut self.snap_manager, &self.snap_cache, document, input, previous_mouse);
474+
/// Attempts to get a single selected handle. Also retrieves the position of the anchor it is connected to. Used for the purpose of snapping the angle.
475+
fn try_get_selected_handle_and_anchor(&self, shape_editor: &ShapeState, document: &DocumentMessageHandler) -> Option<(DVec2, DVec2)> {
476+
// Only count selections of a single layer
477+
let (layer, selection) = shape_editor.selected_shape_state.iter().next()?;
478+
479+
// Do not allow selections of multiple points to count
480+
if selection.selected_points_count() != 1 {
481+
return None;
482+
}
483+
484+
// Only count selected handles
485+
let selected_handle = selection.selected().next()?.as_handle()?;
486+
487+
let layer_to_document = document.metadata().transform_to_document(*layer);
488+
let vector_data = document.network_interface.compute_modified_vector(*layer)?;
489+
490+
let handle_position_local = selected_handle.to_manipulator_point().get_position(&vector_data)?;
491+
let anchor_id = selected_handle.to_manipulator_point().get_anchor(&vector_data)?;
492+
let anchor_position_local = vector_data.point_domain.position_from_id(anchor_id)?;
493+
494+
let handle_position_document = layer_to_document.transform_point2(handle_position_local);
495+
let anchor_position_document = layer_to_document.transform_point2(anchor_position_local);
496+
497+
Some((handle_position_document, anchor_position_document))
498+
}
499+
500+
fn calculate_handle_angle(&mut self, handle_vector: DVec2, lock_angle: bool, snap_angle: bool) -> f64 {
501+
let mut handle_angle = -handle_vector.angle_to(DVec2::X);
502+
503+
// When the angle is locked we use the old angle
504+
if lock_angle {
505+
handle_angle = self.angle
506+
}
507+
508+
// Round the angle to the closest increment
509+
if snap_angle {
510+
let snap_resolution = HANDLE_ROTATE_SNAP_ANGLE.to_radians();
511+
handle_angle = (handle_angle / snap_resolution).round() * snap_resolution;
512+
}
513+
514+
// Cache the old handle angle for the lock angle.
515+
self.angle = handle_angle;
516+
517+
handle_angle
518+
}
519+
520+
fn apply_snapping(
521+
&mut self,
522+
handle_direction: DVec2,
523+
new_handle_position: DVec2,
524+
anchor_position: DVec2,
525+
using_angle_constraints: bool,
526+
handle_position: DVec2,
527+
document: &DocumentMessageHandler,
528+
input: &InputPreprocessorMessageHandler,
529+
) -> DVec2 {
530+
let snap_data = SnapData::new(document, input);
531+
let snap_point = SnapCandidatePoint::handle_neighbors(new_handle_position, [anchor_position]);
532+
533+
let snap_result = match using_angle_constraints {
534+
true => {
535+
let snap_constraint = SnapConstraint::Line {
536+
origin: anchor_position,
537+
direction: handle_direction.normalize_or_zero(),
538+
};
539+
self.snap_manager.constrained_snap(&snap_data, &snap_point, snap_constraint, Default::default())
540+
}
541+
false => self.snap_manager.free_snap(&snap_data, &snap_point, Default::default()),
542+
};
543+
544+
self.snap_manager.update_indicator(snap_result.clone());
545+
546+
document.metadata().document_to_viewport.transform_vector2(snap_result.snapped_point_document - handle_position)
547+
}
548+
549+
fn drag(
550+
&mut self,
551+
equidistant: bool,
552+
lock_angle: bool,
553+
snap_angle: bool,
554+
shape_editor: &mut ShapeState,
555+
document: &DocumentMessageHandler,
556+
input: &InputPreprocessorMessageHandler,
557+
responses: &mut VecDeque<Message>,
558+
) {
559+
let document_to_viewport = document.metadata().document_to_viewport;
560+
let previous_mouse = document_to_viewport.transform_point2(self.previous_mouse_position);
561+
let current_mouse = input.mouse.position;
562+
let raw_delta = document_to_viewport.inverse().transform_vector2(current_mouse - previous_mouse);
563+
564+
let snapped_delta = if let Some((handle_pos, anchor_pos)) = self.try_get_selected_handle_and_anchor(shape_editor, document) {
565+
let cursor_pos = handle_pos + raw_delta;
566+
567+
let handle_angle = self.calculate_handle_angle(cursor_pos - anchor_pos, lock_angle, snap_angle);
568+
569+
let constrained_direction = DVec2::new(handle_angle.cos(), handle_angle.sin());
570+
let projected_length = (cursor_pos - anchor_pos).dot(constrained_direction);
571+
let constrained_target = anchor_pos + constrained_direction * projected_length;
572+
let constrained_delta = constrained_target - handle_pos;
573+
574+
self.apply_snapping(constrained_direction, handle_pos + constrained_delta, anchor_pos, lock_angle || snap_angle, handle_pos, document, input)
575+
} else {
576+
shape_editor.snap(&mut self.snap_manager, &self.snap_cache, document, input, previous_mouse)
577+
};
578+
473579
let handle_lengths = if equidistant { None } else { self.opposing_handle_lengths.take() };
474580
shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, responses, true);
475-
self.previous_mouse_position += document.metadata().document_to_viewport.inverse().transform_vector2(snapped_delta);
581+
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta);
476582
}
477583
}
478584

@@ -574,6 +680,8 @@ impl Fsm for PathToolFsmState {
574680
equidistant,
575681
toggle_colinear,
576682
move_anchor_with_handles,
683+
snap_angle,
684+
lock_angle,
577685
},
578686
) => {
579687
tool_data.previous_mouse_position = input.mouse.position;
@@ -585,12 +693,16 @@ impl Fsm for PathToolFsmState {
585693
equidistant,
586694
toggle_colinear,
587695
move_anchor_with_handles,
696+
snap_angle,
697+
lock_angle,
588698
}
589699
.into(),
590700
PathToolMessage::PointerMove {
591701
equidistant,
592702
toggle_colinear,
593703
move_anchor_with_handles,
704+
snap_angle,
705+
lock_angle,
594706
}
595707
.into(),
596708
];
@@ -604,6 +716,8 @@ impl Fsm for PathToolFsmState {
604716
equidistant,
605717
toggle_colinear,
606718
move_anchor_with_handles,
719+
snap_angle,
720+
lock_angle,
607721
},
608722
) => {
609723
if tool_data.selection_status.is_none() {
@@ -631,8 +745,19 @@ impl Fsm for PathToolFsmState {
631745

632746
let toggle_colinear_state = input.keyboard.get(toggle_colinear as usize);
633747
let equidistant_state = input.keyboard.get(equidistant as usize);
634-
if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, shape_editor, document, responses) {
635-
tool_data.drag(equidistant_state, shape_editor, document, input, responses);
748+
let lock_angle_state = input.keyboard.get(lock_angle as usize);
749+
let snap_angle_state = input.keyboard.get(snap_angle as usize);
750+
751+
if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, tool_action_data.shape_editor, tool_action_data.document, responses) {
752+
tool_data.drag(
753+
equidistant_state,
754+
lock_angle_state,
755+
snap_angle_state,
756+
tool_action_data.shape_editor,
757+
tool_action_data.document,
758+
input,
759+
responses,
760+
);
636761
}
637762

638763
// Auto-panning
@@ -641,12 +766,16 @@ impl Fsm for PathToolFsmState {
641766
toggle_colinear,
642767
equidistant,
643768
move_anchor_with_handles,
769+
snap_angle,
770+
lock_angle,
644771
}
645772
.into(),
646773
PathToolMessage::PointerMove {
647774
toggle_colinear,
648775
equidistant,
649776
move_anchor_with_handles,
777+
snap_angle,
778+
lock_angle,
650779
}
651780
.into(),
652781
];
@@ -662,11 +791,19 @@ impl Fsm for PathToolFsmState {
662791

663792
PathToolFsmState::DrawingBox
664793
}
665-
(PathToolFsmState::Dragging(dragging_state), PathToolMessage::PointerOutsideViewport { equidistant, .. }) => {
794+
(
795+
PathToolFsmState::Dragging(dragging_state),
796+
PathToolMessage::PointerOutsideViewport {
797+
equidistant, snap_angle, lock_angle, ..
798+
},
799+
) => {
666800
// Auto-panning
667801
if tool_data.auto_panning.shift_viewport(input, responses).is_some() {
668802
let equidistant = input.keyboard.get(equidistant as usize);
669-
tool_data.drag(equidistant, shape_editor, document, input, responses);
803+
let snap_angle = input.keyboard.get(snap_angle as usize);
804+
let lock_angle = input.keyboard.get(lock_angle as usize);
805+
806+
tool_data.drag(equidistant, lock_angle, snap_angle, shape_editor, document, input, responses);
670807
}
671808

672809
PathToolFsmState::Dragging(dragging_state)
@@ -677,6 +814,8 @@ impl Fsm for PathToolFsmState {
677814
equidistant,
678815
toggle_colinear,
679816
move_anchor_with_handles,
817+
snap_angle,
818+
lock_angle,
680819
},
681820
) => {
682821
// Auto-panning
@@ -685,12 +824,16 @@ impl Fsm for PathToolFsmState {
685824
equidistant,
686825
toggle_colinear,
687826
move_anchor_with_handles,
827+
snap_angle,
828+
lock_angle,
688829
}
689830
.into(),
690831
PathToolMessage::PointerMove {
691832
equidistant,
692833
toggle_colinear,
693834
move_anchor_with_handles,
835+
snap_angle,
836+
lock_angle,
694837
}
695838
.into(),
696839
];
@@ -890,7 +1033,12 @@ impl Fsm for PathToolFsmState {
8901033

8911034
let drag_anchor = HintInfo::keys([Key::Space], "Drag Anchor");
8921035
let point_select_state_hint_group = match dragging_state.point_select_state {
893-
PointSelectState::HandleNoPair => vec![drag_anchor],
1036+
PointSelectState::HandleNoPair => {
1037+
let mut hints = vec![drag_anchor];
1038+
hints.push(HintInfo::keys([Key::Shift], "Snap 15°"));
1039+
hints.push(HintInfo::keys([Key::Control], "Lock Angle"));
1040+
hints
1041+
}
8941042
PointSelectState::HandleWithPair => {
8951043
let mut hints = vec![drag_anchor];
8961044
hints.push(HintInfo::keys([Key::Tab], "Swap Selected Handles"));
@@ -905,6 +1053,8 @@ impl Fsm for PathToolFsmState {
9051053
if colinear != ManipulatorAngle::Free {
9061054
hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles"));
9071055
}
1056+
hints.push(HintInfo::keys([Key::Shift], "Snap 15°"));
1057+
hints.push(HintInfo::keys([Key::Control], "Lock Angle"));
9081058
hints
9091059
}
9101060
PointSelectState::Anchor => Vec::new(),

0 commit comments

Comments
 (0)