From 977e8bc27e916e0b169ca210ddbcb347fb48ca15 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 26 Mar 2026 15:43:31 -0700 Subject: [PATCH 1/2] Fix the Auto-Tangents node for linear polylines, and track colinear handles in manipulator data --- node-graph/nodes/vector/src/vector_nodes.rs | 114 ++++++++++++++------ 1 file changed, 79 insertions(+), 35 deletions(-) diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 3e72b77d4a..6a9cf01022 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -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,124 @@ 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). + let in_segment_index = if i == 0 { + if is_closed { segment_offset + num_manipulators - 1 } else { continue } + } else { + segment_offset + i - 1 + }; + let out_segment_index = if i == num_manipulators - 1 { + if is_closed { segment_offset } else { continue } + } 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 { From b03c5e20b06b36e74a54a3ed92ae166e0fcdb6a3 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 28 Mar 2026 18:23:28 -0700 Subject: [PATCH 2/2] Simplify --- node-graph/nodes/vector/src/vector_nodes.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 6a9cf01022..8c37b10c7a 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -1001,16 +1001,10 @@ async fn auto_tangents( 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). - let in_segment_index = if i == 0 { - if is_closed { segment_offset + num_manipulators - 1 } else { continue } - } else { - segment_offset + i - 1 - }; - let out_segment_index = if i == num_manipulators - 1 { - if is_closed { segment_offset } else { continue } - } else { - segment_offset + i - }; + // 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