@@ -21,8 +21,8 @@ use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
2121use vector_types:: vector:: algorithms:: offset_subpath:: offset_bezpath;
2222use vector_types:: vector:: algorithms:: spline:: { solve_spline_first_handle_closed, solve_spline_first_handle_open} ;
2323use vector_types:: vector:: misc:: {
24- CentroidType , ExtrudeJoiningAlgorithm , MergeByDistanceAlgorithm , PointSpacingType , RowsOrColumns , bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, handles_to_segment, is_linear ,
25- point_to_dvec2, segment_to_handles,
24+ CentroidType , ExtrudeJoiningAlgorithm , HandleId , MergeByDistanceAlgorithm , PointSpacingType , RowsOrColumns , bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, handles_to_segment,
25+ is_linear , point_to_dvec2, segment_to_handles,
2626} ;
2727use vector_types:: vector:: style:: { Fill , Gradient , GradientStops , PaintOrder , Stroke , StrokeAlign , StrokeCap , StrokeJoin } ;
2828use vector_types:: vector:: { FillId , PointId , RegionId , SegmentDomain , SegmentId , StrokeId , VectorExt } ;
@@ -900,80 +900,118 @@ async fn auto_tangents(
900900 }
901901
902902 let mut new_manipulators_list = Vec :: with_capacity ( manipulators_list. len ( ) ) ;
903+ // Track which manipulator indices were given auto-tangent (colinear) handles
904+ let mut auto_tangented = vec ! [ false ; manipulators_list. len( ) ] ;
903905 let is_closed = subpath. closed ( ) ;
904906
905907 for i in 0 ..manipulators_list. len ( ) {
906- let curr = & manipulators_list[ i] ;
908+ let current = & manipulators_list[ i] ;
909+ let is_endpoint = !is_closed && ( i == 0 || i == manipulators_list. len ( ) - 1 ) ;
907910
908911 if preserve_existing {
909912 // Check if this point has handles that are meaningfully different from the anchor
910- let has_handles = ( curr . in_handle . is_some ( ) && !curr . in_handle . unwrap ( ) . abs_diff_eq ( curr . anchor , 1e-5 ) )
911- || ( curr . out_handle . is_some ( ) && !curr . out_handle . unwrap ( ) . abs_diff_eq ( curr . anchor , 1e-5 ) ) ;
913+ let has_handles = ( current . in_handle . is_some ( ) && !current . in_handle . unwrap ( ) . abs_diff_eq ( current . anchor , 1e-5 ) )
914+ || ( current . out_handle . is_some ( ) && !current . out_handle . unwrap ( ) . abs_diff_eq ( current . anchor , 1e-5 ) ) ;
912915
913- // If the point already has handles, or if it's an endpoint of an open path, keep it as is.
914- if has_handles || ( !is_closed && ( i == 0 || i == manipulators_list . len ( ) - 1 ) ) {
915- new_manipulators_list. push ( * curr ) ;
916+ // If the point already has handles, keep it as is
917+ if has_handles {
918+ new_manipulators_list. push ( * current ) ;
916919 continue ;
917920 }
918921 }
919922
920- // If spread is 0, remove handles for this point, making it a sharp corner.
923+ // If spread is 0, remove handles for this point, making it a sharp corner
921924 if spread == 0. {
922925 new_manipulators_list. push ( ManipulatorGroup {
923- anchor : curr . anchor ,
926+ anchor : current . anchor ,
924927 in_handle : None ,
925928 out_handle : None ,
926- id : curr. id ,
929+ id : current. id ,
930+ } ) ;
931+ continue ;
932+ }
933+
934+ // Endpoints of open paths get zero-length cubic handles so adjacent segments remain cubic (not quadratic)
935+ if is_endpoint {
936+ new_manipulators_list. push ( ManipulatorGroup {
937+ anchor : current. anchor ,
938+ in_handle : Some ( current. anchor ) ,
939+ out_handle : Some ( current. anchor ) ,
940+ id : current. id ,
927941 } ) ;
928942 continue ;
929943 }
930944
931945 // Get previous and next points for auto-tangent calculation
932- let prev_idx = if i == 0 { if is_closed { manipulators_list. len ( ) - 1 } else { i } } else { i - 1 } ;
933- let next_idx = if i == manipulators_list. len ( ) - 1 { if is_closed { 0 } else { i } } else { i + 1 } ;
946+ let prev_index = if i == 0 { manipulators_list. len ( ) - 1 } else { i - 1 } ;
947+ let next_index = if i == manipulators_list. len ( ) - 1 { 0 } else { i + 1 } ;
934948
935- let prev = manipulators_list [ prev_idx ] . anchor ;
936- let curr_pos = curr . anchor ;
937- let next = manipulators_list[ next_idx ] . anchor ;
949+ let current_position = current . anchor ;
950+ let delta_prev = manipulators_list [ prev_index ] . anchor - current_position ;
951+ let delta_next = manipulators_list[ next_index ] . anchor - current_position ;
938952
939- // Calculate directions from current point to adjacent points
940- let dir_prev = ( prev - curr_pos ) . normalize_or_zero ( ) ;
941- let dir_next = ( next - curr_pos ) . normalize_or_zero ( ) ;
953+ // Calculate normalized directions and distances to adjacent points
954+ let distance_prev = delta_prev . length ( ) ;
955+ let distance_next = delta_next . length ( ) ;
942956
943957 // Check if we have valid directions (e.g., points are not coincident)
944- if dir_prev . length_squared ( ) < 1e-5 || dir_next . length_squared ( ) < 1e-5 {
958+ if distance_prev < 1e-5 || distance_next < 1e-5 {
945959 // Fallback: keep the original manipulator group (which has no active handles here)
946- new_manipulators_list. push ( * curr ) ;
960+ new_manipulators_list. push ( * current ) ;
947961 continue ;
948962 }
949963
950- // Calculate handle direction (colinear, pointing along the line from prev to next)
951- // Original logic: (dir_prev - dir_next) is equivalent to (prev - curr) - (next - curr) = prev - next
952- // The handle_dir will be along the line connecting prev and next, or perpendicular if they are coincident.
953- let mut handle_dir = ( dir_prev - dir_next) . try_normalize ( ) . unwrap_or_else ( || dir_prev. perp ( ) ) ;
964+ let direction_prev = delta_prev / distance_prev;
965+ let direction_next = delta_next / distance_next;
954966
955- // Ensure consistent orientation of the handle_dir
956- // This makes the `+ handle_dir` for in_handle and `- handle_dir` for out_handle consistent
957- if dir_prev. dot ( handle_dir) < 0. {
958- handle_dir = -handle_dir;
967+ // Calculate handle direction as the bisector of the two normalized directions.
968+ // This ensures the in and out handles are colinear (180° apart) through the anchor.
969+ let mut handle_direction = ( direction_prev - direction_next) . try_normalize ( ) . unwrap_or_else ( || direction_prev. perp ( ) ) ;
970+
971+ // Ensure consistent orientation of the handle direction.
972+ // This makes the `+ handle_direction` for in_handle and `- handle_direction` for out_handle consistent.
973+ if direction_prev. dot ( handle_direction) < 0. {
974+ handle_direction = -handle_direction;
959975 }
960976
961977 // Calculate handle lengths: 1/3 of distance to adjacent points, scaled by spread
962- let in_length = ( curr_pos - prev ) . length ( ) / 3. * spread;
963- let out_length = ( next - curr_pos ) . length ( ) / 3. * spread;
978+ let in_length = distance_prev / 3. * spread;
979+ let out_length = distance_next / 3. * spread;
964980
965981 // Create new manipulator group with calculated auto-tangents
966982 new_manipulators_list. push ( ManipulatorGroup {
967- anchor : curr_pos ,
968- in_handle : Some ( curr_pos + handle_dir * in_length) ,
969- out_handle : Some ( curr_pos - handle_dir * out_length) ,
970- id : curr . id ,
983+ anchor : current_position ,
984+ in_handle : Some ( current_position + handle_direction * in_length) ,
985+ out_handle : Some ( current_position - handle_direction * out_length) ,
986+ id : current . id ,
971987 } ) ;
988+ auto_tangented[ i] = true ;
972989 }
973990
991+ // Record segment count before appending so we can find the new segment IDs
992+ let segment_offset = result. segment_domain . ids ( ) . len ( ) ;
993+
974994 let mut softened_bezpath = bezpath_from_manipulator_groups ( & new_manipulators_list, is_closed) ;
975995 softened_bezpath. apply_affine ( Affine :: new ( transform. inverse ( ) . to_cols_array ( ) ) ) ;
976996 result. append_bezpath ( softened_bezpath) ;
997+
998+ // Mark auto-tangented points as having colinear handles
999+ let segment_ids = result. segment_domain . ids ( ) ;
1000+ let num_manipulators = new_manipulators_list. len ( ) ;
1001+ for ( i, _) in auto_tangented. iter ( ) . enumerate ( ) . filter ( |& ( _, & tangented) | tangented) {
1002+ // For interior point i, the incoming segment is segment_offset + (i - 1) and outgoing is segment_offset + i.
1003+ // For closed paths, point 0's incoming segment is the last one (segment_offset + num_manipulators - 1).
1004+ // For open paths, endpoints are never auto-tangented (the `is_endpoint` check above ensures that),
1005+ // so `i == 0` and `i == num_manipulators - 1` only occur here when the path is closed
1006+ let in_segment_index = if i == 0 { segment_offset + num_manipulators - 1 } else { segment_offset + i - 1 } ;
1007+ let out_segment_index = if i == num_manipulators - 1 { segment_offset } else { segment_offset + i } ;
1008+
1009+ if in_segment_index < segment_ids. len ( ) && out_segment_index < segment_ids. len ( ) {
1010+ result
1011+ . colinear_manipulators
1012+ . push ( [ HandleId :: end ( segment_ids[ in_segment_index] ) , HandleId :: primary ( segment_ids[ out_segment_index] ) ] ) ;
1013+ }
1014+ }
9771015 }
9781016
9791017 TableRow {
0 commit comments