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
This commit is contained in:
Keavon Chambers 2026-03-28 18:26:08 -07:00 committed by GitHub
parent 71ff4c937f
commit e2a142333f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 73 additions and 35 deletions

View File

@ -21,8 +21,8 @@ use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
use vector_types::vector::algorithms::offset_subpath::offset_bezpath;
use vector_types::vector::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
use vector_types::vector::misc::{
CentroidType, ExtrudeJoiningAlgorithm, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, handles_to_segment, is_linear,
point_to_dvec2, segment_to_handles,
CentroidType, ExtrudeJoiningAlgorithm, HandleId, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, handles_to_segment,
is_linear, point_to_dvec2, segment_to_handles,
};
use vector_types::vector::style::{Fill, Gradient, GradientStops, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
@ -900,80 +900,118 @@ async fn auto_tangents(
}
let mut new_manipulators_list = Vec::with_capacity(manipulators_list.len());
// Track which manipulator indices were given auto-tangent (colinear) handles
let mut auto_tangented = vec![false; manipulators_list.len()];
let is_closed = subpath.closed();
for i in 0..manipulators_list.len() {
let curr = &manipulators_list[i];
let current = &manipulators_list[i];
let is_endpoint = !is_closed && (i == 0 || i == manipulators_list.len() - 1);
if preserve_existing {
// Check if this point has handles that are meaningfully different from the anchor
let has_handles = (curr.in_handle.is_some() && !curr.in_handle.unwrap().abs_diff_eq(curr.anchor, 1e-5))
|| (curr.out_handle.is_some() && !curr.out_handle.unwrap().abs_diff_eq(curr.anchor, 1e-5));
let has_handles = (current.in_handle.is_some() && !current.in_handle.unwrap().abs_diff_eq(current.anchor, 1e-5))
|| (current.out_handle.is_some() && !current.out_handle.unwrap().abs_diff_eq(current.anchor, 1e-5));
// If the point already has handles, or if it's an endpoint of an open path, keep it as is.
if has_handles || (!is_closed && (i == 0 || i == manipulators_list.len() - 1)) {
new_manipulators_list.push(*curr);
// If the point already has handles, keep it as is
if has_handles {
new_manipulators_list.push(*current);
continue;
}
}
// If spread is 0, remove handles for this point, making it a sharp corner.
// If spread is 0, remove handles for this point, making it a sharp corner
if spread == 0. {
new_manipulators_list.push(ManipulatorGroup {
anchor: curr.anchor,
anchor: current.anchor,
in_handle: None,
out_handle: None,
id: curr.id,
id: current.id,
});
continue;
}
// Endpoints of open paths get zero-length cubic handles so adjacent segments remain cubic (not quadratic)
if is_endpoint {
new_manipulators_list.push(ManipulatorGroup {
anchor: current.anchor,
in_handle: Some(current.anchor),
out_handle: Some(current.anchor),
id: current.id,
});
continue;
}
// Get previous and next points for auto-tangent calculation
let prev_idx = if i == 0 { if is_closed { manipulators_list.len() - 1 } else { i } } else { i - 1 };
let next_idx = if i == manipulators_list.len() - 1 { if is_closed { 0 } else { i } } else { i + 1 };
let prev_index = if i == 0 { manipulators_list.len() - 1 } else { i - 1 };
let next_index = if i == manipulators_list.len() - 1 { 0 } else { i + 1 };
let prev = manipulators_list[prev_idx].anchor;
let curr_pos = curr.anchor;
let next = manipulators_list[next_idx].anchor;
let current_position = current.anchor;
let delta_prev = manipulators_list[prev_index].anchor - current_position;
let delta_next = manipulators_list[next_index].anchor - current_position;
// Calculate directions from current point to adjacent points
let dir_prev = (prev - curr_pos).normalize_or_zero();
let dir_next = (next - curr_pos).normalize_or_zero();
// Calculate normalized directions and distances to adjacent points
let distance_prev = delta_prev.length();
let distance_next = delta_next.length();
// Check if we have valid directions (e.g., points are not coincident)
if dir_prev.length_squared() < 1e-5 || dir_next.length_squared() < 1e-5 {
if distance_prev < 1e-5 || distance_next < 1e-5 {
// Fallback: keep the original manipulator group (which has no active handles here)
new_manipulators_list.push(*curr);
new_manipulators_list.push(*current);
continue;
}
// Calculate handle direction (colinear, pointing along the line from prev to next)
// Original logic: (dir_prev - dir_next) is equivalent to (prev - curr) - (next - curr) = prev - next
// The handle_dir will be along the line connecting prev and next, or perpendicular if they are coincident.
let mut handle_dir = (dir_prev - dir_next).try_normalize().unwrap_or_else(|| dir_prev.perp());
let direction_prev = delta_prev / distance_prev;
let direction_next = delta_next / distance_next;
// Ensure consistent orientation of the handle_dir
// This makes the `+ handle_dir` for in_handle and `- handle_dir` for out_handle consistent
if dir_prev.dot(handle_dir) < 0. {
handle_dir = -handle_dir;
// Calculate handle direction as the bisector of the two normalized directions.
// This ensures the in and out handles are colinear (180° apart) through the anchor.
let mut handle_direction = (direction_prev - direction_next).try_normalize().unwrap_or_else(|| direction_prev.perp());
// Ensure consistent orientation of the handle direction.
// This makes the `+ handle_direction` for in_handle and `- handle_direction` for out_handle consistent.
if direction_prev.dot(handle_direction) < 0. {
handle_direction = -handle_direction;
}
// Calculate handle lengths: 1/3 of distance to adjacent points, scaled by spread
let in_length = (curr_pos - prev).length() / 3. * spread;
let out_length = (next - curr_pos).length() / 3. * spread;
let in_length = distance_prev / 3. * spread;
let out_length = distance_next / 3. * spread;
// Create new manipulator group with calculated auto-tangents
new_manipulators_list.push(ManipulatorGroup {
anchor: curr_pos,
in_handle: Some(curr_pos + handle_dir * in_length),
out_handle: Some(curr_pos - handle_dir * out_length),
id: curr.id,
anchor: current_position,
in_handle: Some(current_position + handle_direction * in_length),
out_handle: Some(current_position - handle_direction * out_length),
id: current.id,
});
auto_tangented[i] = true;
}
// Record segment count before appending so we can find the new segment IDs
let segment_offset = result.segment_domain.ids().len();
let mut softened_bezpath = bezpath_from_manipulator_groups(&new_manipulators_list, is_closed);
softened_bezpath.apply_affine(Affine::new(transform.inverse().to_cols_array()));
result.append_bezpath(softened_bezpath);
// Mark auto-tangented points as having colinear handles
let segment_ids = result.segment_domain.ids();
let num_manipulators = new_manipulators_list.len();
for (i, _) in auto_tangented.iter().enumerate().filter(|&(_, &tangented)| tangented) {
// For interior point i, the incoming segment is segment_offset + (i - 1) and outgoing is segment_offset + i.
// For closed paths, point 0's incoming segment is the last one (segment_offset + num_manipulators - 1).
// For open paths, endpoints are never auto-tangented (the `is_endpoint` check above ensures that),
// so `i == 0` and `i == num_manipulators - 1` only occur here when the path is closed
let in_segment_index = if i == 0 { segment_offset + num_manipulators - 1 } else { segment_offset + i - 1 };
let out_segment_index = if i == num_manipulators - 1 { segment_offset } else { segment_offset + i };
if in_segment_index < segment_ids.len() && out_segment_index < segment_ids.len() {
result
.colinear_manipulators
.push([HandleId::end(segment_ids[in_segment_index]), HandleId::primary(segment_ids[out_segment_index])]);
}
}
}
TableRow {