Skip to content
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

added snap and lock angle feature to path tool #2160

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions editor/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 6.;
pub const SELECTION_THRESHOLD: f64 = 10.;
pub const HIDE_HANDLE_DISTANCE: f64 = 3.;
pub const INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE: f64 = 50.;
pub const HANDLE_ROTATE_SNAP_ANGLE: f64 = 15.;

// Pen tool
pub const CREATE_CURVE_THRESHOLD: f64 = 5.;
Expand Down
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 @@ -215,7 +215,7 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }),
entry!(KeyDown(KeyR); action_dispatch=PathToolMessage::GRS { key: KeyR }),
entry!(KeyDown(KeyS); action_dispatch=PathToolMessage::GRS { key: KeyS }),
entry!(PointerMove; refresh_keys=[KeyC, Shift, Alt, Space], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space}),
entry!(PointerMove; refresh_keys=[KeyC, Shift, Alt, Control, Space], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space, snap_angle: Shift, lock_angle: Control}),
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors),
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::DeselectAllPoints),
Expand Down
191 changes: 179 additions & 12 deletions editor/src/messages/tool/tool_messages/path_tool.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use super::tool_prelude::*;
use crate::consts::{COLOR_OVERLAY_YELLOW, DRAG_THRESHOLD, INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE, SELECTION_THRESHOLD, SELECTION_TOLERANCE};
use crate::consts::{COLOR_OVERLAY_YELLOW, DRAG_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE, SELECTION_THRESHOLD, SELECTION_TOLERANCE};
use crate::messages::portfolio::document::overlays::utility_functions::path_overlays;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::shape_editor::{ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedPointsInfo, ShapeState};
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager};
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};

use graphene_core::renderer::Quad;
use graphene_core::vector::ManipulatorPointId;
Expand Down Expand Up @@ -59,11 +59,15 @@ pub enum PathToolMessage {
equidistant: Key,
toggle_colinear: Key,
move_anchor_with_handles: Key,
snap_angle: Key,
lock_angle: Key,
},
PointerOutsideViewport {
equidistant: Key,
toggle_colinear: Key,
move_anchor_with_handles: Key,
snap_angle: Key,
lock_angle: Key,
},
RightClick,
SelectAllAnchors,
Expand Down Expand Up @@ -283,6 +287,8 @@ struct PathToolData {
drag_start_pos: DVec2,
previous_mouse_position: DVec2,
toggle_colinear_debounce: bool,
equidistant_colinear_debounce: bool,
original_handle_colinear: bool,
opposing_handle_lengths: Option<OpposingHandleLengths>,
/// Describes information about the selected point(s), if any, across one or multiple shapes and manipulator point types (anchor or handle).
/// The available information varies depending on whether `None`, `One`, or `Multiple` points are currently selected.
Expand All @@ -294,6 +300,7 @@ struct PathToolData {
saved_points_before_anchor_select_toggle: Vec<ManipulatorPointId>,
select_anchor_toggled: bool,
dragging_state: DraggingState,
angle: f64,
}

impl PathToolData {
Expand Down Expand Up @@ -466,13 +473,129 @@ impl PathToolData {
false
}

fn drag(&mut self, equidistant: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
// Move the selected points with the mouse
let previous_mouse = document.metadata().document_to_viewport.transform_point2(self.previous_mouse_position);
let snapped_delta = shape_editor.snap(&mut self.snap_manager, &self.snap_cache, document, input, previous_mouse);
/// Temporarily converts selected handles to colinear if they are not already colinear.
fn update_equidistant_handle_collinearity(&mut self, equidistant: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
match (equidistant, self.equidistant_colinear_debounce) {
(true, false) => {
let current_angle = shape_editor.selected_manipulator_angles(&document.network_interface);
self.original_handle_colinear = current_angle == ManipulatorAngle::Colinear;

if !self.original_handle_colinear {
shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document);
}
self.equidistant_colinear_debounce = true;
}
(false, true) => {
if !self.original_handle_colinear {
shape_editor.disable_colinear_handles_state_on_selected(&document.network_interface, responses);
}
self.equidistant_colinear_debounce = false;
}
_ => {}
}
}

/// 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.
fn try_get_selected_handle_and_anchor(&self, shape_editor: &ShapeState, document: &DocumentMessageHandler) -> Option<(DVec2, DVec2)> {
let (layer, selection) = shape_editor.selected_shape_state.iter().next()?; // Only count selections of a single layer
if selection.selected_points_count() != 1 {
return None; // Do not allow selections of multiple points to count.
}
let selected_handle = selection.selected().next()?.as_handle()?; // Only count selected handles
let layer_to_document = document.metadata().transform_to_document(*layer);
let vector_data = document.network_interface.compute_modified_vector(*layer)?;

let handle_position_local = selected_handle.to_manipulator_point().get_position(&vector_data)?;
let anchor_id = selected_handle.to_manipulator_point().get_anchor(&vector_data)?;
let anchor_position_local = vector_data.point_domain.position_from_id(anchor_id)?;

let handle_position_document = layer_to_document.transform_point2(handle_position_local);
let anchor_position_document = layer_to_document.transform_point2(anchor_position_local);

Some((handle_position_document, anchor_position_document))
}

fn calculate_handle_angle(&mut self, handle_vector: DVec2, lock_angle: bool, snap_angle: bool) -> f64 {
let mut handle_angle = -handle_vector.angle_to(DVec2::X);

if lock_angle {
// When the angle is locked we use the old angle
handle_angle = self.angle
}

if snap_angle {
// Round the angle to the closest increment
let snap_resolution = HANDLE_ROTATE_SNAP_ANGLE.to_radians();
handle_angle = (handle_angle / snap_resolution).round() * snap_resolution;
}

self.angle = handle_angle; // Cache the old handle angle for the lock angle.
handle_angle
}

fn apply_snapping(
&mut self,
handle_direction: DVec2,
new_handle_position: DVec2,
anchor_position: DVec2,
using_angle_constraints: bool,
handle_position: DVec2,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
) -> DVec2 {
let snap_point = SnapCandidatePoint::handle_neighbors(new_handle_position, [anchor_position]);

document.metadata().document_to_viewport.transform_vector2(if using_angle_constraints {
let direction = handle_direction.normalize_or_zero();
let snap_constraint = SnapConstraint::Line { origin: anchor_position, direction };
let snap_result = self
.snap_manager
.constrained_snap(&SnapData::new(document, input), &snap_point, snap_constraint, SnapTypeConfiguration::default());

self.snap_manager.update_indicator(snap_result.clone());
snap_result.snapped_point_document - handle_position
} else {
let snap_result = self.snap_manager.free_snap(&SnapData::new(document, input), &snap_point, SnapTypeConfiguration::default());
self.snap_manager.update_indicator(snap_result.clone());
snap_result.snapped_point_document - handle_position
})
}

fn drag(
&mut self,
equidistant: bool,
lock_angle: bool,
snap_angle: bool,
shape_editor: &mut ShapeState,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) {
let document_to_viewport = document.metadata().document_to_viewport;
let previous_mouse = document_to_viewport.transform_point2(self.previous_mouse_position);
let current_mouse = input.mouse.position;
let raw_delta = document_to_viewport.inverse().transform_vector2(current_mouse - previous_mouse);

let snapped_delta = if let Some((handle_pos, anchor_pos)) = self.try_get_selected_handle_and_anchor(shape_editor, document) {
let cursor_pos = handle_pos + raw_delta;

let handle_angle = self.calculate_handle_angle(cursor_pos - anchor_pos, lock_angle, snap_angle);

let constrained_direction = DVec2::new(handle_angle.cos(), handle_angle.sin());
let projected_length = (cursor_pos - anchor_pos).dot(constrained_direction);
let constrained_target = anchor_pos + constrained_direction * projected_length;
let constrained_delta = constrained_target - handle_pos;

self.apply_snapping(constrained_direction, handle_pos + constrained_delta, anchor_pos, lock_angle || snap_angle, handle_pos, document, input)
} else {
shape_editor.snap(&mut self.snap_manager, &self.snap_cache, document, input, previous_mouse)
};

self.update_equidistant_handle_collinearity(equidistant, shape_editor, document, responses);

let handle_lengths = if equidistant { None } else { self.opposing_handle_lengths.take() };
shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, responses, true);
self.previous_mouse_position += document.metadata().document_to_viewport.inverse().transform_vector2(snapped_delta);
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta);
}
}

Expand Down Expand Up @@ -574,6 +697,8 @@ impl Fsm for PathToolFsmState {
equidistant,
toggle_colinear,
move_anchor_with_handles,
snap_angle,
lock_angle,
},
) => {
tool_data.previous_mouse_position = input.mouse.position;
Expand All @@ -585,12 +710,16 @@ impl Fsm for PathToolFsmState {
equidistant,
toggle_colinear,
move_anchor_with_handles,
snap_angle,
lock_angle,
}
.into(),
PathToolMessage::PointerMove {
equidistant,
toggle_colinear,
move_anchor_with_handles,
snap_angle,
lock_angle,
}
.into(),
];
Expand All @@ -604,6 +733,8 @@ impl Fsm for PathToolFsmState {
equidistant,
toggle_colinear,
move_anchor_with_handles,
snap_angle,
lock_angle,
},
) => {
if tool_data.selection_status.is_none() {
Expand Down Expand Up @@ -631,8 +762,19 @@ impl Fsm for PathToolFsmState {

let toggle_colinear_state = input.keyboard.get(toggle_colinear as usize);
let equidistant_state = input.keyboard.get(equidistant as usize);
if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, shape_editor, document, responses) {
tool_data.drag(equidistant_state, shape_editor, document, input, responses);
let lock_angle_state = input.keyboard.get(lock_angle as usize);
let snap_angle_state = input.keyboard.get(snap_angle as usize);

if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, tool_action_data.shape_editor, tool_action_data.document, responses) {
tool_data.drag(
equidistant_state,
lock_angle_state,
snap_angle_state,
tool_action_data.shape_editor,
tool_action_data.document,
input,
responses,
);
}

// Auto-panning
Expand All @@ -641,12 +783,16 @@ impl Fsm for PathToolFsmState {
toggle_colinear,
equidistant,
move_anchor_with_handles,
snap_angle,
lock_angle,
}
.into(),
PathToolMessage::PointerMove {
toggle_colinear,
equidistant,
move_anchor_with_handles,
snap_angle,
lock_angle,
}
.into(),
];
Expand All @@ -662,11 +808,19 @@ impl Fsm for PathToolFsmState {

PathToolFsmState::DrawingBox
}
(PathToolFsmState::Dragging(dragging_state), PathToolMessage::PointerOutsideViewport { equidistant, .. }) => {
(
PathToolFsmState::Dragging(dragging_state),
PathToolMessage::PointerOutsideViewport {
equidistant, snap_angle, lock_angle, ..
},
) => {
// Auto-panning
if tool_data.auto_panning.shift_viewport(input, responses).is_some() {
let equidistant = input.keyboard.get(equidistant as usize);
tool_data.drag(equidistant, shape_editor, document, input, responses);
let snap_angle = input.keyboard.get(snap_angle as usize);
let lock_angle = input.keyboard.get(lock_angle as usize);

tool_data.drag(equidistant, lock_angle, snap_angle, shape_editor, document, input, responses);
}

PathToolFsmState::Dragging(dragging_state)
Expand All @@ -677,6 +831,8 @@ impl Fsm for PathToolFsmState {
equidistant,
toggle_colinear,
move_anchor_with_handles,
snap_angle,
lock_angle,
},
) => {
// Auto-panning
Expand All @@ -685,12 +841,16 @@ impl Fsm for PathToolFsmState {
equidistant,
toggle_colinear,
move_anchor_with_handles,
snap_angle,
lock_angle,
}
.into(),
PathToolMessage::PointerMove {
equidistant,
toggle_colinear,
move_anchor_with_handles,
snap_angle,
lock_angle,
}
.into(),
];
Expand Down Expand Up @@ -890,7 +1050,12 @@ impl Fsm for PathToolFsmState {

let drag_anchor = HintInfo::keys([Key::Space], "Drag Anchor");
let point_select_state_hint_group = match dragging_state.point_select_state {
PointSelectState::HandleNoPair => vec![drag_anchor],
PointSelectState::HandleNoPair => {
let mut hints = vec![drag_anchor];
hints.push(HintInfo::keys([Key::Shift], "Snap 15°"));
hints.push(HintInfo::keys([Key::Control], "Lock Angle"));
hints
}
PointSelectState::HandleWithPair => {
let mut hints = vec![drag_anchor];
hints.push(HintInfo::keys([Key::Tab], "Swap Selected Handles"));
Expand All @@ -905,6 +1070,8 @@ impl Fsm for PathToolFsmState {
if colinear != ManipulatorAngle::Free {
hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles"));
}
hints.push(HintInfo::keys([Key::Shift], "Snap 15°"));
hints.push(HintInfo::keys([Key::Control], "Lock Angle"));
hints
}
PointSelectState::Anchor => Vec::new(),
Expand Down
Loading