Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 73 additions & 35 deletions node-graph/nodes/vector/src/vector_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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 {
Expand Down
Loading