Skip to content

Commit d39308c

Browse files
0SlowPoke0Keavon
andauthored
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>
1 parent 3d37ef7 commit d39308c

File tree

5 files changed

+150
-38
lines changed

5 files changed

+150
-38
lines changed

editor/src/messages/tool/common_functionality/shape_editor.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,9 @@ impl ShapeState {
729729
let length = transform.transform_vector2(unselected_position - anchor).length();
730730
let position = transform.inverse().transform_vector2(direction * length);
731731
let modification_type = unselected_handle.set_relative_position(position);
732-
responses.add(GraphOperationMessage::Vector { layer, modification_type });
732+
if (anchor - selected_position).length() > 1e-6 {
733+
responses.add(GraphOperationMessage::Vector { layer, modification_type });
734+
}
733735
}
734736
// If both handles are selected, average the angles of the handles
735737
else {
@@ -775,6 +777,7 @@ impl ShapeState {
775777
in_viewport_space: bool,
776778
was_alt_dragging: bool,
777779
opposite_handle_position: Option<DVec2>,
780+
skip_opposite_handle: bool,
778781
responses: &mut VecDeque<Message>,
779782
) {
780783
for (&layer, state) in &self.selected_shape_state {
@@ -816,6 +819,11 @@ impl ShapeState {
816819
responses.add(GraphOperationMessage::Vector { layer, modification_type });
817820

818821
let Some(other) = vector_data.other_colinear_handle(handle) else { continue };
822+
823+
if skip_opposite_handle {
824+
continue;
825+
}
826+
819827
if state.is_selected(other.to_manipulator_point()) {
820828
// If two colinear handles are being dragged at the same time but not the anchor, it is necessary to break the colinear state.
821829
let handles = [handle, other];

editor/src/messages/tool/common_functionality/utility_functions.rs

+28-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::messages::tool::common_functionality::graph_modification_utils::get_t
44
use glam::DVec2;
55
use graphene_core::renderer::Quad;
66
use graphene_core::text::{FontCache, load_face};
7-
use graphene_std::vector::PointId;
7+
use graphene_std::vector::{ManipulatorPointId, PointId, SegmentId, VectorData};
88

99
/// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable.
1010
pub fn should_extend(
@@ -66,3 +66,30 @@ pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageH
6666

6767
Quad::from_box([DVec2::ZERO, far])
6868
}
69+
70+
pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector_data: &VectorData, pen_tool: bool) -> Option<f64> {
71+
let is_start = |point: PointId, segment: SegmentId| vector_data.segment_start_from_id(segment) == Some(point);
72+
let anchor_position = vector_data.point_domain.position_from_id(anchor)?;
73+
let end_handle = ManipulatorPointId::EndHandle(segment).get_position(vector_data);
74+
let start_handle = ManipulatorPointId::PrimaryHandle(segment).get_position(vector_data);
75+
76+
let start_point = if is_start(anchor, segment) {
77+
vector_data.segment_end_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id))
78+
} else {
79+
vector_data.segment_start_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id))
80+
};
81+
82+
let required_handle = if is_start(anchor, segment) {
83+
start_handle
84+
.filter(|&handle| pen_tool && handle != anchor_position)
85+
.or(end_handle.filter(|&handle| Some(handle) != start_point))
86+
.or(start_point)
87+
} else {
88+
end_handle
89+
.filter(|&handle| pen_tool && handle != anchor_position)
90+
.or(start_handle.filter(|&handle| Some(handle) != start_point))
91+
.or(start_point)
92+
};
93+
94+
required_handle.map(|handle| -(handle - anchor_position).angle_to(DVec2::X))
95+
}

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

+103-8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::messages::tool::common_functionality::shape_editor::{
1515
ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedPointsInfo, SelectionChange, SelectionShape, SelectionShapeType, ShapeState,
1616
};
1717
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
18+
use crate::messages::tool::common_functionality::utility_functions::calculate_segment_angle;
1819
use graphene_core::renderer::Quad;
1920
use graphene_core::vector::{ManipulatorPointId, PointId, VectorModificationType};
2021
use graphene_std::vector::{HandleId, NoHashBuilder, SegmentId, VectorData};
@@ -380,6 +381,8 @@ struct PathToolData {
380381
snapping_axis: Option<Axis>,
381382
alt_clicked_on_anchor: bool,
382383
alt_dragging_from_anchor: bool,
384+
angle_locked: bool,
385+
temporary_colinear_handles: bool,
383386
}
384387

385388
impl PathToolData {
@@ -727,11 +730,38 @@ impl PathToolData {
727730
Some((handle_position_document, anchor_position_document, handle_id))
728731
}
729732

730-
fn calculate_handle_angle(&mut self, handle_vector: DVec2, handle_id: ManipulatorPointId, lock_angle: bool, snap_angle: bool) -> f64 {
733+
#[allow(clippy::too_many_arguments)]
734+
fn calculate_handle_angle(
735+
&mut self,
736+
shape_editor: &mut ShapeState,
737+
document: &DocumentMessageHandler,
738+
responses: &mut VecDeque<Message>,
739+
relative_vector: DVec2,
740+
handle_vector: DVec2,
741+
handle_id: ManipulatorPointId,
742+
lock_angle: bool,
743+
snap_angle: bool,
744+
) -> f64 {
731745
let current_angle = -handle_vector.angle_to(DVec2::X);
732746

747+
if let Some(vector_data) = shape_editor
748+
.selected_shape_state
749+
.iter()
750+
.next()
751+
.and_then(|(layer, _)| document.network_interface.compute_modified_vector(*layer))
752+
{
753+
if relative_vector.length() < 25. && lock_angle && !self.angle_locked {
754+
if let Some(angle) = calculate_lock_angle(self, shape_editor, responses, document, &vector_data, handle_id) {
755+
self.angle = angle;
756+
return angle;
757+
}
758+
}
759+
}
760+
733761
// When the angle is locked we use the old angle
762+
734763
if self.current_selected_handle_id == Some(handle_id) && lock_angle {
764+
self.angle_locked = true;
735765
return self.angle;
736766
}
737767

@@ -785,7 +815,7 @@ impl PathToolData {
785815
let drag_start = self.drag_start_pos;
786816
let opposite_delta = drag_start - current_mouse;
787817

788-
shape_editor.move_selected_points(None, document, opposite_delta, false, true, false, None, responses);
818+
shape_editor.move_selected_points(None, document, opposite_delta, false, true, false, None, false, responses);
789819

790820
// Calculate the projected delta and shift the points along that delta
791821
let delta = current_mouse - drag_start;
@@ -797,7 +827,7 @@ impl PathToolData {
797827
_ => DVec2::new(delta.x, 0.),
798828
};
799829

800-
shape_editor.move_selected_points(None, document, projected_delta, false, true, false, None, responses);
830+
shape_editor.move_selected_points(None, document, projected_delta, false, true, false, None, false, responses);
801831
}
802832

803833
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 {
813843
_ => DVec2::new(opposite_delta.x, 0.),
814844
};
815845

816-
shape_editor.move_selected_points(None, document, opposite_projected_delta, false, true, false, None, responses);
846+
shape_editor.move_selected_points(None, document, opposite_projected_delta, false, true, false, None, false, responses);
817847

818848
// Calculate what actually would have been the original delta for the point, and apply that
819849
let delta = current_mouse - drag_start;
820850

821-
shape_editor.move_selected_points(None, document, delta, false, true, false, None, responses);
851+
shape_editor.move_selected_points(None, document, delta, false, true, false, None, false, responses);
822852

823853
self.snapping_axis = None;
824854
}
@@ -872,7 +902,7 @@ impl PathToolData {
872902
let snapped_delta = if let Some((handle_pos, anchor_pos, handle_id)) = self.try_get_selected_handle_and_anchor(shape_editor, document) {
873903
let cursor_pos = handle_pos + raw_delta;
874904

875-
let handle_angle = self.calculate_handle_angle(cursor_pos - anchor_pos, handle_id, lock_angle, snap_angle);
905+
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);
876906

877907
let constrained_direction = DVec2::new(handle_angle.cos(), handle_angle.sin());
878908
let projected_length = (cursor_pos - anchor_pos).dot(constrained_direction);
@@ -931,7 +961,14 @@ impl PathToolData {
931961
self.alt_dragging_from_anchor = false;
932962
self.alt_clicked_on_anchor = false;
933963
}
934-
shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, responses);
964+
965+
let mut skip_opposite = false;
966+
if self.temporary_colinear_handles && !lock_angle {
967+
shape_editor.disable_colinear_handles_state_on_selected(&document.network_interface, responses);
968+
self.temporary_colinear_handles = false;
969+
skip_opposite = true;
970+
}
971+
shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, skip_opposite, responses);
935972
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta);
936973
} else {
937974
let Some(axis) = self.snapping_axis else { return };
@@ -940,7 +977,7 @@ impl PathToolData {
940977
Axis::Y => DVec2::new(0., unsnapped_delta.y),
941978
_ => DVec2::new(unsnapped_delta.x, 0.),
942979
};
943-
shape_editor.move_selected_points(handle_lengths, document, projected_delta, equidistant, true, false, opposite, responses);
980+
shape_editor.move_selected_points(handle_lengths, document, projected_delta, equidistant, true, false, opposite, false, responses);
944981
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta);
945982
}
946983

@@ -1238,6 +1275,10 @@ impl Fsm for PathToolFsmState {
12381275
let lock_angle_state = input.keyboard.get(lock_angle as usize);
12391276
let snap_angle_state = input.keyboard.get(snap_angle as usize);
12401277

1278+
if !lock_angle_state {
1279+
tool_data.angle_locked = false;
1280+
}
1281+
12411282
if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, tool_action_data.shape_editor, tool_action_data.document, responses) {
12421283
tool_data.drag(
12431284
equidistant_state,
@@ -1412,6 +1453,10 @@ impl Fsm for PathToolFsmState {
14121453
}
14131454
}
14141455

1456+
if tool_data.temporary_colinear_handles {
1457+
tool_data.temporary_colinear_handles = false;
1458+
}
1459+
14151460
if tool_data.handle_drag_toggle && drag_occurred {
14161461
shape_editor.deselect_all_points();
14171462
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag);
@@ -1511,6 +1556,7 @@ impl Fsm for PathToolFsmState {
15111556
false,
15121557
false,
15131558
tool_data.opposite_handle_position,
1559+
false,
15141560
responses,
15151561
);
15161562

@@ -1749,3 +1795,52 @@ fn get_selection_status(network_interface: &NodeNetworkInterface, shape_state: &
17491795

17501796
SelectionStatus::None
17511797
}
1798+
1799+
fn calculate_lock_angle(
1800+
tool_data: &mut PathToolData,
1801+
shape_state: &mut ShapeState,
1802+
responses: &mut VecDeque<Message>,
1803+
document: &DocumentMessageHandler,
1804+
vector_data: &VectorData,
1805+
handle_id: ManipulatorPointId,
1806+
) -> Option<f64> {
1807+
let anchor = handle_id.get_anchor(vector_data)?;
1808+
let anchor_position = vector_data.point_domain.position_from_id(anchor);
1809+
let current_segment = handle_id.get_segment();
1810+
let points_connected = vector_data.connected_count(anchor);
1811+
1812+
let (anchor_position, segment) = anchor_position.zip(current_segment)?;
1813+
if points_connected == 1 {
1814+
calculate_segment_angle(anchor, segment, vector_data, false)
1815+
} else {
1816+
let opposite_handle = handle_id
1817+
.get_handle_pair(vector_data)
1818+
.iter()
1819+
.flatten()
1820+
.find(|&h| h.to_manipulator_point() != handle_id)
1821+
.copied()
1822+
.map(|h| h.to_manipulator_point());
1823+
let opposite_handle_position = opposite_handle.and_then(|h| h.get_position(vector_data)).filter(|pos| (pos - anchor_position).length() > 1e-6);
1824+
1825+
if let Some(opposite_pos) = opposite_handle_position {
1826+
if !vector_data.colinear_manipulators.iter().flatten().map(|h| h.to_manipulator_point()).any(|h| h == handle_id) {
1827+
shape_state.convert_selected_manipulators_to_colinear_handles(responses, document);
1828+
tool_data.temporary_colinear_handles = true;
1829+
}
1830+
Some(-(opposite_pos - anchor_position).angle_to(DVec2::X))
1831+
} else {
1832+
let angle_1 = vector_data
1833+
.adjacent_segment(&handle_id)
1834+
.and_then(|(_, adjacent_segment)| calculate_segment_angle(anchor, adjacent_segment, vector_data, false));
1835+
1836+
let angle_2 = calculate_segment_angle(anchor, segment, vector_data, false);
1837+
1838+
match (angle_1, angle_2) {
1839+
(Some(angle_1), Some(angle_2)) => Some((angle_1 + angle_2) / 2.0),
1840+
(Some(angle_1), None) => Some(angle_1),
1841+
(None, Some(angle_2)) => Some(angle_2),
1842+
(None, None) => None,
1843+
}
1844+
}
1845+
}
1846+
}

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

+3-28
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::messages::tool::common_functionality::color_selector::{ToolColorOptio
1010
use crate::messages::tool::common_functionality::graph_modification_utils::{self, merge_layers};
1111
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
1212
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
13-
use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend};
13+
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, closest_point, should_extend};
1414
use bezier_rs::{Bezier, BezierHandles};
1515
use graph_craft::document::NodeId;
1616
use graphene_core::Color;
@@ -1295,31 +1295,8 @@ impl PenToolData {
12951295

12961296
match (self.handle_type, self.path_closed) {
12971297
(TargetHandle::FuturePreviewOutHandle, _) | (TargetHandle::PreviewInHandle, true) => {
1298-
let is_start = |point: PointId, segment: SegmentId| vector_data.segment_start_from_id(segment) == Some(point);
1299-
1300-
let end_handle = ManipulatorPointId::EndHandle(segment).get_position(vector_data);
1301-
let start_handle = ManipulatorPointId::PrimaryHandle(segment).get_position(vector_data);
1302-
1303-
let start_point = if is_start(anchor, segment) {
1304-
vector_data.segment_end_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id))
1305-
} else {
1306-
vector_data.segment_start_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id))
1307-
};
1308-
1309-
let required_handle = if is_start(anchor, segment) {
1310-
start_handle
1311-
.filter(|&handle| handle != anchor_position)
1312-
.or(end_handle.filter(|&handle| Some(handle) != start_point))
1313-
.or(start_point)
1314-
} else {
1315-
end_handle
1316-
.filter(|&handle| handle != anchor_position)
1317-
.or(start_handle.filter(|&handle| Some(handle) != start_point))
1318-
.or(start_point)
1319-
};
1320-
1321-
if let Some(required_handle) = required_handle {
1322-
self.angle = -(required_handle - anchor_position).angle_to(DVec2::X);
1298+
if let Some(required_handle) = calculate_segment_angle(anchor, segment, vector_data, true) {
1299+
self.angle = required_handle;
13231300
self.handle_mode = HandleMode::ColinearEquidistant;
13241301
}
13251302
}
@@ -1332,8 +1309,6 @@ impl PenToolData {
13321309
self.handle_mode = HandleMode::ColinearEquidistant;
13331310
}
13341311
}
1335-
1336-
// Closure to check if a point is the start or end of a segment
13371312
}
13381313

13391314
fn add_point_layer_position(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, viewport: DVec2) {

node-graph/gcore/src/vector/vector_data.rs

+7
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,13 @@ impl ManipulatorPointId {
561561
_ => None,
562562
}
563563
}
564+
565+
pub fn get_segment(self) -> Option<SegmentId> {
566+
match self {
567+
ManipulatorPointId::PrimaryHandle(segment) | ManipulatorPointId::EndHandle(segment) => Some(segment),
568+
_ => None,
569+
}
570+
}
564571
}
565572

566573
/// The type of handle found on a bézier curve.

0 commit comments

Comments
 (0)