@@ -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
384385impl 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