Add Path tool feature for angle locking upon pressing Ctrl while dragging handle over anchor (#2612)

* almost_fixed

* fix need to refactor

* fixed issed need to refactor

* refactor-done fixed issue

* move function to common_functionality

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0SlowPoke0 2025-04-24 09:09:14 +05:30 committed by GitHub
parent 3d37ef79ac
commit d39308c048
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 150 additions and 38 deletions

View file

@ -729,7 +729,9 @@ impl ShapeState {
let length = transform.transform_vector2(unselected_position - anchor).length();
let position = transform.inverse().transform_vector2(direction * length);
let modification_type = unselected_handle.set_relative_position(position);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
if (anchor - selected_position).length() > 1e-6 {
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
}
// If both handles are selected, average the angles of the handles
else {
@ -775,6 +777,7 @@ impl ShapeState {
in_viewport_space: bool,
was_alt_dragging: bool,
opposite_handle_position: Option<DVec2>,
skip_opposite_handle: bool,
responses: &mut VecDeque<Message>,
) {
for (&layer, state) in &self.selected_shape_state {
@ -816,6 +819,11 @@ impl ShapeState {
responses.add(GraphOperationMessage::Vector { layer, modification_type });
let Some(other) = vector_data.other_colinear_handle(handle) else { continue };
if skip_opposite_handle {
continue;
}
if state.is_selected(other.to_manipulator_point()) {
// If two colinear handles are being dragged at the same time but not the anchor, it is necessary to break the colinear state.
let handles = [handle, other];

View file

@ -4,7 +4,7 @@ use crate::messages::tool::common_functionality::graph_modification_utils::get_t
use glam::DVec2;
use graphene_core::renderer::Quad;
use graphene_core::text::{FontCache, load_face};
use graphene_std::vector::PointId;
use graphene_std::vector::{ManipulatorPointId, PointId, SegmentId, VectorData};
/// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable.
pub fn should_extend(
@ -66,3 +66,30 @@ pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageH
Quad::from_box([DVec2::ZERO, far])
}
pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector_data: &VectorData, pen_tool: bool) -> Option<f64> {
let is_start = |point: PointId, segment: SegmentId| vector_data.segment_start_from_id(segment) == Some(point);
let anchor_position = vector_data.point_domain.position_from_id(anchor)?;
let end_handle = ManipulatorPointId::EndHandle(segment).get_position(vector_data);
let start_handle = ManipulatorPointId::PrimaryHandle(segment).get_position(vector_data);
let start_point = if is_start(anchor, segment) {
vector_data.segment_end_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id))
} else {
vector_data.segment_start_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id))
};
let required_handle = if is_start(anchor, segment) {
start_handle
.filter(|&handle| pen_tool && handle != anchor_position)
.or(end_handle.filter(|&handle| Some(handle) != start_point))
.or(start_point)
} else {
end_handle
.filter(|&handle| pen_tool && handle != anchor_position)
.or(start_handle.filter(|&handle| Some(handle) != start_point))
.or(start_point)
};
required_handle.map(|handle| -(handle - anchor_position).angle_to(DVec2::X))
}

View file

@ -15,6 +15,7 @@ use crate::messages::tool::common_functionality::shape_editor::{
ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedPointsInfo, SelectionChange, SelectionShape, SelectionShapeType, ShapeState,
};
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
use crate::messages::tool::common_functionality::utility_functions::calculate_segment_angle;
use graphene_core::renderer::Quad;
use graphene_core::vector::{ManipulatorPointId, PointId, VectorModificationType};
use graphene_std::vector::{HandleId, NoHashBuilder, SegmentId, VectorData};
@ -380,6 +381,8 @@ struct PathToolData {
snapping_axis: Option<Axis>,
alt_clicked_on_anchor: bool,
alt_dragging_from_anchor: bool,
angle_locked: bool,
temporary_colinear_handles: bool,
}
impl PathToolData {
@ -727,11 +730,38 @@ impl PathToolData {
Some((handle_position_document, anchor_position_document, handle_id))
}
fn calculate_handle_angle(&mut self, handle_vector: DVec2, handle_id: ManipulatorPointId, lock_angle: bool, snap_angle: bool) -> f64 {
#[allow(clippy::too_many_arguments)]
fn calculate_handle_angle(
&mut self,
shape_editor: &mut ShapeState,
document: &DocumentMessageHandler,
responses: &mut VecDeque<Message>,
relative_vector: DVec2,
handle_vector: DVec2,
handle_id: ManipulatorPointId,
lock_angle: bool,
snap_angle: bool,
) -> f64 {
let current_angle = -handle_vector.angle_to(DVec2::X);
if let Some(vector_data) = shape_editor
.selected_shape_state
.iter()
.next()
.and_then(|(layer, _)| document.network_interface.compute_modified_vector(*layer))
{
if relative_vector.length() < 25. && lock_angle && !self.angle_locked {
if let Some(angle) = calculate_lock_angle(self, shape_editor, responses, document, &vector_data, handle_id) {
self.angle = angle;
return angle;
}
}
}
// When the angle is locked we use the old angle
if self.current_selected_handle_id == Some(handle_id) && lock_angle {
self.angle_locked = true;
return self.angle;
}
@ -785,7 +815,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, false, None, responses);
shape_editor.move_selected_points(None, document, opposite_delta, false, true, false, None, false, responses);
// Calculate the projected delta and shift the points along that delta
let delta = current_mouse - drag_start;
@ -797,7 +827,7 @@ impl PathToolData {
_ => DVec2::new(delta.x, 0.),
};
shape_editor.move_selected_points(None, document, projected_delta, false, true, false, None, responses);
shape_editor.move_selected_points(None, document, projected_delta, false, true, false, None, false, responses);
}
fn stop_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
@ -813,12 +843,12 @@ impl PathToolData {
_ => DVec2::new(opposite_delta.x, 0.),
};
shape_editor.move_selected_points(None, document, opposite_projected_delta, false, true, false, None, responses);
shape_editor.move_selected_points(None, document, opposite_projected_delta, false, true, false, None, false, 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, false, None, responses);
shape_editor.move_selected_points(None, document, delta, false, true, false, None, false, responses);
self.snapping_axis = None;
}
@ -872,7 +902,7 @@ impl PathToolData {
let snapped_delta = if let Some((handle_pos, anchor_pos, handle_id)) = 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, handle_id, lock_angle, snap_angle);
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);
let constrained_direction = DVec2::new(handle_angle.cos(), handle_angle.sin());
let projected_length = (cursor_pos - anchor_pos).dot(constrained_direction);
@ -931,7 +961,14 @@ impl PathToolData {
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);
let mut skip_opposite = false;
if self.temporary_colinear_handles && !lock_angle {
shape_editor.disable_colinear_handles_state_on_selected(&document.network_interface, responses);
self.temporary_colinear_handles = false;
skip_opposite = true;
}
shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, skip_opposite, responses);
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta);
} else {
let Some(axis) = self.snapping_axis else { return };
@ -940,7 +977,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, false, opposite, responses);
shape_editor.move_selected_points(handle_lengths, document, projected_delta, equidistant, true, false, opposite, false, responses);
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta);
}
@ -1238,6 +1275,10 @@ impl Fsm for PathToolFsmState {
let lock_angle_state = input.keyboard.get(lock_angle as usize);
let snap_angle_state = input.keyboard.get(snap_angle as usize);
if !lock_angle_state {
tool_data.angle_locked = false;
}
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,
@ -1412,6 +1453,10 @@ impl Fsm for PathToolFsmState {
}
}
if tool_data.temporary_colinear_handles {
tool_data.temporary_colinear_handles = false;
}
if tool_data.handle_drag_toggle && drag_occurred {
shape_editor.deselect_all_points();
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag);
@ -1511,6 +1556,7 @@ impl Fsm for PathToolFsmState {
false,
false,
tool_data.opposite_handle_position,
false,
responses,
);
@ -1749,3 +1795,52 @@ fn get_selection_status(network_interface: &NodeNetworkInterface, shape_state: &
SelectionStatus::None
}
fn calculate_lock_angle(
tool_data: &mut PathToolData,
shape_state: &mut ShapeState,
responses: &mut VecDeque<Message>,
document: &DocumentMessageHandler,
vector_data: &VectorData,
handle_id: ManipulatorPointId,
) -> Option<f64> {
let anchor = handle_id.get_anchor(vector_data)?;
let anchor_position = vector_data.point_domain.position_from_id(anchor);
let current_segment = handle_id.get_segment();
let points_connected = vector_data.connected_count(anchor);
let (anchor_position, segment) = anchor_position.zip(current_segment)?;
if points_connected == 1 {
calculate_segment_angle(anchor, segment, vector_data, false)
} else {
let opposite_handle = handle_id
.get_handle_pair(vector_data)
.iter()
.flatten()
.find(|&h| h.to_manipulator_point() != handle_id)
.copied()
.map(|h| h.to_manipulator_point());
let opposite_handle_position = opposite_handle.and_then(|h| h.get_position(vector_data)).filter(|pos| (pos - anchor_position).length() > 1e-6);
if let Some(opposite_pos) = opposite_handle_position {
if !vector_data.colinear_manipulators.iter().flatten().map(|h| h.to_manipulator_point()).any(|h| h == handle_id) {
shape_state.convert_selected_manipulators_to_colinear_handles(responses, document);
tool_data.temporary_colinear_handles = true;
}
Some(-(opposite_pos - anchor_position).angle_to(DVec2::X))
} else {
let angle_1 = vector_data
.adjacent_segment(&handle_id)
.and_then(|(_, adjacent_segment)| calculate_segment_angle(anchor, adjacent_segment, vector_data, false));
let angle_2 = calculate_segment_angle(anchor, segment, vector_data, false);
match (angle_1, angle_2) {
(Some(angle_1), Some(angle_2)) => Some((angle_1 + angle_2) / 2.0),
(Some(angle_1), None) => Some(angle_1),
(None, Some(angle_2)) => Some(angle_2),
(None, None) => None,
}
}
}
}

View file

@ -10,7 +10,7 @@ use crate::messages::tool::common_functionality::color_selector::{ToolColorOptio
use crate::messages::tool::common_functionality::graph_modification_utils::{self, merge_layers};
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend};
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, closest_point, should_extend};
use bezier_rs::{Bezier, BezierHandles};
use graph_craft::document::NodeId;
use graphene_core::Color;
@ -1295,31 +1295,8 @@ impl PenToolData {
match (self.handle_type, self.path_closed) {
(TargetHandle::FuturePreviewOutHandle, _) | (TargetHandle::PreviewInHandle, true) => {
let is_start = |point: PointId, segment: SegmentId| vector_data.segment_start_from_id(segment) == Some(point);
let end_handle = ManipulatorPointId::EndHandle(segment).get_position(vector_data);
let start_handle = ManipulatorPointId::PrimaryHandle(segment).get_position(vector_data);
let start_point = if is_start(anchor, segment) {
vector_data.segment_end_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id))
} else {
vector_data.segment_start_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id))
};
let required_handle = if is_start(anchor, segment) {
start_handle
.filter(|&handle| handle != anchor_position)
.or(end_handle.filter(|&handle| Some(handle) != start_point))
.or(start_point)
} else {
end_handle
.filter(|&handle| handle != anchor_position)
.or(start_handle.filter(|&handle| Some(handle) != start_point))
.or(start_point)
};
if let Some(required_handle) = required_handle {
self.angle = -(required_handle - anchor_position).angle_to(DVec2::X);
if let Some(required_handle) = calculate_segment_angle(anchor, segment, vector_data, true) {
self.angle = required_handle;
self.handle_mode = HandleMode::ColinearEquidistant;
}
}
@ -1332,8 +1309,6 @@ impl PenToolData {
self.handle_mode = HandleMode::ColinearEquidistant;
}
}
// Closure to check if a point is the start or end of a segment
}
fn add_point_layer_position(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, viewport: DVec2) {

View file

@ -561,6 +561,13 @@ impl ManipulatorPointId {
_ => None,
}
}
pub fn get_segment(self) -> Option<SegmentId> {
match self {
ManipulatorPointId::PrimaryHandle(segment) | ManipulatorPointId::EndHandle(segment) => Some(segment),
_ => None,
}
}
}
/// The type of handle found on a bézier curve.