Skip to content

Commit 8a8e496

Browse files
0SlowPoke0Keavon
andauthored
Implement angle locking when Ctrl is pressed over an adjacent anchor (#2663)
* Implement angle lock from adjacent anchors * Reset offset state and added comments * Code review * fix selecting correct handle to lock * Update comment --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
1 parent c467833 commit 8a8e496

File tree

2 files changed

+134
-19
lines changed

2 files changed

+134
-19
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageH
6767
Quad::from_box([DVec2::ZERO, far])
6868
}
6969

70-
pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector_data: &VectorData, pen_tool: bool) -> Option<f64> {
70+
pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector_data: &VectorData, prefer_handle_direction: bool) -> Option<f64> {
7171
let is_start = |point: PointId, segment: SegmentId| vector_data.segment_start_from_id(segment) == Some(point);
7272
let anchor_position = vector_data.point_domain.position_from_id(anchor)?;
7373
let end_handle = ManipulatorPointId::EndHandle(segment).get_position(vector_data);
@@ -81,12 +81,12 @@ pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector_data:
8181

8282
let required_handle = if is_start(anchor, segment) {
8383
start_handle
84-
.filter(|&handle| pen_tool && handle != anchor_position)
84+
.filter(|&handle| prefer_handle_direction && handle != anchor_position)
8585
.or(end_handle.filter(|&handle| Some(handle) != start_point))
8686
.or(start_point)
8787
} else {
8888
end_handle
89-
.filter(|&handle| pen_tool && handle != anchor_position)
89+
.filter(|&handle| prefer_handle_direction && handle != anchor_position)
9090
.or(start_handle.filter(|&handle| Some(handle) != start_point))
9191
.or(start_point)
9292
};

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

Lines changed: 131 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ struct PathToolData {
379379
alt_dragging_from_anchor: bool,
380380
angle_locked: bool,
381381
temporary_colinear_handles: bool,
382+
adjacent_anchor_offset: Option<DVec2>,
382383
}
383384

384385
impl PathToolData {
@@ -726,18 +727,39 @@ impl PathToolData {
726727
) -> f64 {
727728
let current_angle = -handle_vector.angle_to(DVec2::X);
728729

729-
if let Some(vector_data) = shape_editor
730+
if let Some((vector_data, layer)) = shape_editor
730731
.selected_shape_state
731732
.iter()
732733
.next()
733-
.and_then(|(layer, _)| document.network_interface.compute_modified_vector(*layer))
734+
.and_then(|(layer, _)| document.network_interface.compute_modified_vector(*layer).map(|vector_data| (vector_data, layer)))
734735
{
736+
let adjacent_anchor = check_handle_over_adjacent_anchor(handle_id, &vector_data);
737+
let mut required_angle = None;
738+
739+
// If the handle is dragged over one of its adjacent anchors while holding down the Ctrl key, compute the angle based on the tangent formed with the neighboring anchor points.
740+
if adjacent_anchor.is_some() && lock_angle && !self.angle_locked {
741+
let anchor = handle_id.get_anchor(&vector_data);
742+
let (angle, anchor_position) = calculate_adjacent_anchor_tangent(handle_id, anchor, adjacent_anchor, &vector_data);
743+
744+
let layer_to_document = document.metadata().transform_to_document(*layer);
745+
746+
self.adjacent_anchor_offset = handle_id
747+
.get_anchor_position(&vector_data)
748+
.and_then(|handle_anchor| anchor_position.map(|adjacent_anchor| layer_to_document.transform_point2(adjacent_anchor) - layer_to_document.transform_point2(handle_anchor)));
749+
750+
required_angle = angle;
751+
}
752+
753+
// If the handle is dragged near its adjacent anchors while holding down the Ctrl key, compute the angle using the tangent direction of neighboring segments.
735754
if relative_vector.length() < 25. && lock_angle && !self.angle_locked {
736-
if let Some(angle) = calculate_lock_angle(self, shape_editor, responses, document, &vector_data, handle_id, tangent_to_neighboring_tangents) {
737-
self.angle = angle;
738-
self.angle_locked = true;
739-
return angle;
740-
}
755+
required_angle = calculate_lock_angle(self, shape_editor, responses, document, &vector_data, handle_id, tangent_to_neighboring_tangents);
756+
}
757+
758+
// Finalize and apply angle locking if a valid target angle was determined.
759+
if let Some(angle) = required_angle {
760+
self.angle = angle;
761+
self.angle_locked = true;
762+
return angle;
741763
}
742764
}
743765

@@ -885,27 +907,36 @@ impl PathToolData {
885907
let current_mouse = input.mouse.position;
886908
let raw_delta = document_to_viewport.inverse().transform_vector2(current_mouse - previous_mouse);
887909

888-
let snapped_delta = if let Some((handle_pos, anchor_pos, handle_id)) = self.try_get_selected_handle_and_anchor(shape_editor, document) {
889-
let cursor_pos = handle_pos + raw_delta;
910+
let snapped_delta = if let Some((handle_position, anchor_position, handle_id)) = self.try_get_selected_handle_and_anchor(shape_editor, document) {
911+
let cursor_position = handle_position + raw_delta;
890912

891913
let handle_angle = self.calculate_handle_angle(
892914
shape_editor,
893915
document,
894916
responses,
895-
handle_pos - anchor_pos,
896-
cursor_pos - anchor_pos,
917+
handle_position - anchor_position,
918+
cursor_position - anchor_position,
897919
handle_id,
898920
lock_angle,
899921
snap_angle,
900922
equidistant,
901923
);
902924

925+
let adjacent_anchor_offset = self.adjacent_anchor_offset.unwrap_or(DVec2::ZERO);
903926
let constrained_direction = DVec2::new(handle_angle.cos(), handle_angle.sin());
904-
let projected_length = (cursor_pos - anchor_pos).dot(constrained_direction);
905-
let constrained_target = anchor_pos + constrained_direction * projected_length;
906-
let constrained_delta = constrained_target - handle_pos;
907-
908-
self.apply_snapping(constrained_direction, handle_pos + constrained_delta, anchor_pos, lock_angle || snap_angle, handle_pos, document, input)
927+
let projected_length = (cursor_position - anchor_position - adjacent_anchor_offset).dot(constrained_direction);
928+
let constrained_target = anchor_position + adjacent_anchor_offset + constrained_direction * projected_length;
929+
let constrained_delta = constrained_target - handle_position;
930+
931+
self.apply_snapping(
932+
constrained_direction,
933+
handle_position + constrained_delta,
934+
anchor_position + adjacent_anchor_offset,
935+
lock_angle || snap_angle,
936+
handle_position,
937+
document,
938+
input,
939+
)
909940
} else {
910941
shape_editor.snap(&mut self.snap_manager, &self.snap_cache, document, input, previous_mouse)
911942
};
@@ -1265,6 +1296,7 @@ impl Fsm for PathToolFsmState {
12651296

12661297
if !lock_angle_state {
12671298
tool_data.angle_locked = false;
1299+
tool_data.adjacent_anchor_offset = None;
12681300
}
12691301

12701302
if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, tool_action_data.shape_editor, tool_action_data.document, responses) {
@@ -1311,6 +1343,10 @@ impl Fsm for PathToolFsmState {
13111343
tool_data.saved_points_before_anchor_convert_smooth_sharp.clear();
13121344
}
13131345

1346+
if tool_data.adjacent_anchor_offset.is_some() {
1347+
tool_data.adjacent_anchor_offset = None;
1348+
}
1349+
13141350
// If there is a point nearby, then remove the overlay
13151351
if shape_editor
13161352
.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD)
@@ -1882,3 +1918,82 @@ fn calculate_lock_angle(
18821918
}
18831919
}
18841920
}
1921+
1922+
fn check_handle_over_adjacent_anchor(handle_id: ManipulatorPointId, vector_data: &VectorData) -> Option<PointId> {
1923+
let Some((anchor, handle_position)) = handle_id.get_anchor(&vector_data).zip(handle_id.get_position(vector_data)) else {
1924+
return None;
1925+
};
1926+
1927+
let check_if_close = |point_id: &PointId| {
1928+
let Some(anchor_position) = vector_data.point_domain.position_from_id(*point_id) else {
1929+
return false;
1930+
};
1931+
(anchor_position - handle_position).length() < 10.
1932+
};
1933+
1934+
vector_data.connected_points(anchor).find(|point| check_if_close(point))
1935+
}
1936+
fn calculate_adjacent_anchor_tangent(
1937+
currently_dragged_handle: ManipulatorPointId,
1938+
anchor: Option<PointId>,
1939+
adjacent_anchor: Option<PointId>,
1940+
vector_data: &VectorData,
1941+
) -> (Option<f64>, Option<DVec2>) {
1942+
// Early return if no anchor or no adjacent anchors
1943+
1944+
let Some((dragged_handle_anchor, adjacent_anchor)) = anchor.zip(adjacent_anchor) else {
1945+
return (None, None);
1946+
};
1947+
let adjacent_anchor_position = vector_data.point_domain.position_from_id(adjacent_anchor);
1948+
1949+
let handles: Vec<_> = vector_data.all_connected(adjacent_anchor).filter(|handle| handle.length(vector_data) > 1e-6).collect();
1950+
1951+
match handles.len() {
1952+
0 => {
1953+
// Find non-shared segments
1954+
let non_shared_segment: Vec<_> = vector_data
1955+
.segment_bezier_iter()
1956+
.filter_map(|(segment_id, _, start, end)| {
1957+
let touches_adjacent = start == adjacent_anchor || end == adjacent_anchor;
1958+
let shares_with_dragged = start == dragged_handle_anchor || end == dragged_handle_anchor;
1959+
1960+
if touches_adjacent && !shares_with_dragged { Some(segment_id) } else { None }
1961+
})
1962+
.collect();
1963+
1964+
match non_shared_segment.first() {
1965+
Some(&segment) => {
1966+
let angle = calculate_segment_angle(adjacent_anchor, segment, vector_data, true);
1967+
(angle, adjacent_anchor_position)
1968+
}
1969+
None => (None, None),
1970+
}
1971+
}
1972+
1973+
1 => {
1974+
let segment = handles[0].segment;
1975+
let angle = calculate_segment_angle(adjacent_anchor, segment, vector_data, true);
1976+
(angle, adjacent_anchor_position)
1977+
}
1978+
1979+
2 => {
1980+
// Use the angle formed by the handle of the shared segment relative to its associated anchor point.
1981+
let Some(shared_segment_handle) = handles
1982+
.iter()
1983+
.find(|handle| handle.opposite().to_manipulator_point() == currently_dragged_handle)
1984+
.map(|handle| handle.to_manipulator_point())
1985+
else {
1986+
return (None, None);
1987+
};
1988+
1989+
let angle = shared_segment_handle
1990+
.get_position(&vector_data)
1991+
.zip(adjacent_anchor_position)
1992+
.map(|(handle, anchor)| -(handle - anchor).angle_to(DVec2::X));
1993+
1994+
(angle, adjacent_anchor_position)
1995+
}
1996+
1997+
_ => (None, None),
1998+
}
1999+
}

0 commit comments

Comments
 (0)