Skip to content

Commit e2a1423

Browse files
authored
Fix the Auto-Tangents node for linear polylines, and track colinear handles in manipulator data (#3972)
* Fix the Auto-Tangents node for linear polylines, and track colinear handles in manipulator data * Simplify
1 parent 71ff4c9 commit e2a1423

File tree

1 file changed

+73
-35
lines changed

1 file changed

+73
-35
lines changed

node-graph/nodes/vector/src/vector_nodes.rs

Lines changed: 73 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
2121
use vector_types::vector::algorithms::offset_subpath::offset_bezpath;
2222
use vector_types::vector::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
2323
use 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
};
2727
use vector_types::vector::style::{Fill, Gradient, GradientStops, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
2828
use 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

Comments
 (0)